From d374299350c61ac83bcdb13a7520abfd67963417 Mon Sep 17 00:00:00 2001 From: Jarkko Pesonen <435495+jrkkp@users.noreply.github.com> Date: Mon, 26 Aug 2024 15:18:58 +0300 Subject: [PATCH 001/248] VKT(Backend): support for exam event open and close times --- .../oph/vkt/api/dto/PublicExamEventDTO.java | 4 ++- .../dto/clerk/ClerkExamEventCreateDTO.java | 3 +- .../vkt/api/dto/clerk/ClerkExamEventDTO.java | 3 +- .../clerk/ClerkExamEventDTOCommonFields.java | 3 +- .../api/dto/clerk/ClerkExamEventListDTO.java | 4 ++- .../dto/clerk/ClerkExamEventUpdateDTO.java | 3 +- .../main/java/fi/oph/vkt/model/ExamEvent.java | 6 +++- .../repository/ClerkExamEventProjection.java | 4 ++- .../vkt/repository/ExamEventRepository.java | 10 +++--- .../repository/PublicExamEventProjection.java | 4 ++- .../vkt/service/ClerkExamEventService.java | 1 + .../vkt/service/PublicEnrollmentService.java | 5 +-- .../vkt/service/PublicExamEventService.java | 5 ++- .../java/fi/oph/vkt/util/ExamEventUtil.java | 2 +- .../db/changelog/db.changelog-1.0.xml | 22 ++++++++++++ .../vkt/src/test/java/fi/oph/vkt/Factory.java | 3 +- .../service/ClerkExamEventServiceTest.java | 23 +++++++----- .../service/PublicEnrollmentServiceTest.java | 2 +- .../service/PublicExamEventServiceTest.java | 36 ++++++++++++------- 19 files changed, 103 insertions(+), 40 deletions(-) diff --git a/backend/vkt/src/main/java/fi/oph/vkt/api/dto/PublicExamEventDTO.java b/backend/vkt/src/main/java/fi/oph/vkt/api/dto/PublicExamEventDTO.java index 2e32b6160..cb60e37f8 100644 --- a/backend/vkt/src/main/java/fi/oph/vkt/api/dto/PublicExamEventDTO.java +++ b/backend/vkt/src/main/java/fi/oph/vkt/api/dto/PublicExamEventDTO.java @@ -2,6 +2,7 @@ import fi.oph.vkt.model.type.ExamLanguage; import java.time.LocalDate; +import java.time.LocalDateTime; import lombok.Builder; import lombok.NonNull; @@ -10,7 +11,8 @@ public record PublicExamEventDTO( @NonNull Long id, @NonNull ExamLanguage language, @NonNull LocalDate date, - @NonNull LocalDate registrationCloses, + @NonNull LocalDateTime registrationCloses, + @NonNull LocalDateTime registrationOpens, @NonNull Long openings, @NonNull Boolean hasCongestion ) {} diff --git a/backend/vkt/src/main/java/fi/oph/vkt/api/dto/clerk/ClerkExamEventCreateDTO.java b/backend/vkt/src/main/java/fi/oph/vkt/api/dto/clerk/ClerkExamEventCreateDTO.java index ec3bddfee..aba9e9e00 100644 --- a/backend/vkt/src/main/java/fi/oph/vkt/api/dto/clerk/ClerkExamEventCreateDTO.java +++ b/backend/vkt/src/main/java/fi/oph/vkt/api/dto/clerk/ClerkExamEventCreateDTO.java @@ -4,6 +4,7 @@ import fi.oph.vkt.model.type.ExamLevel; import jakarta.validation.constraints.NotNull; import java.time.LocalDate; +import java.time.LocalDateTime; import lombok.Builder; import lombok.NonNull; @@ -12,7 +13,7 @@ public record ClerkExamEventCreateDTO( @NonNull @NotNull ExamLanguage language, @NonNull @NotNull ExamLevel level, @NonNull @NotNull LocalDate date, - @NonNull @NotNull LocalDate registrationCloses, + @NonNull @NotNull LocalDateTime registrationCloses, @NonNull @NotNull Boolean isHidden, @NonNull @NotNull Long maxParticipants ) diff --git a/backend/vkt/src/main/java/fi/oph/vkt/api/dto/clerk/ClerkExamEventDTO.java b/backend/vkt/src/main/java/fi/oph/vkt/api/dto/clerk/ClerkExamEventDTO.java index 2cb3cd08f..285f6524b 100644 --- a/backend/vkt/src/main/java/fi/oph/vkt/api/dto/clerk/ClerkExamEventDTO.java +++ b/backend/vkt/src/main/java/fi/oph/vkt/api/dto/clerk/ClerkExamEventDTO.java @@ -4,6 +4,7 @@ import fi.oph.vkt.model.type.ExamLevel; import jakarta.validation.constraints.NotNull; import java.time.LocalDate; +import java.time.LocalDateTime; import java.util.List; import lombok.Builder; import lombok.NonNull; @@ -15,7 +16,7 @@ public record ClerkExamEventDTO( @NonNull @NotNull ExamLanguage language, @NonNull @NotNull ExamLevel level, @NonNull @NotNull LocalDate date, - @NonNull @NotNull LocalDate registrationCloses, + @NonNull @NotNull LocalDateTime registrationCloses, @NonNull @NotNull Boolean isHidden, @NonNull @NotNull Long maxParticipants, @NonNull @NotNull List enrollments diff --git a/backend/vkt/src/main/java/fi/oph/vkt/api/dto/clerk/ClerkExamEventDTOCommonFields.java b/backend/vkt/src/main/java/fi/oph/vkt/api/dto/clerk/ClerkExamEventDTOCommonFields.java index 713331255..2aaec62d7 100644 --- a/backend/vkt/src/main/java/fi/oph/vkt/api/dto/clerk/ClerkExamEventDTOCommonFields.java +++ b/backend/vkt/src/main/java/fi/oph/vkt/api/dto/clerk/ClerkExamEventDTOCommonFields.java @@ -3,12 +3,13 @@ import fi.oph.vkt.model.type.ExamLanguage; import fi.oph.vkt.model.type.ExamLevel; import java.time.LocalDate; +import java.time.LocalDateTime; public interface ClerkExamEventDTOCommonFields { ExamLanguage language(); ExamLevel level(); LocalDate date(); - LocalDate registrationCloses(); + LocalDateTime registrationCloses(); Boolean isHidden(); Long maxParticipants(); } diff --git a/backend/vkt/src/main/java/fi/oph/vkt/api/dto/clerk/ClerkExamEventListDTO.java b/backend/vkt/src/main/java/fi/oph/vkt/api/dto/clerk/ClerkExamEventListDTO.java index ef0b80a71..06fdcaa57 100644 --- a/backend/vkt/src/main/java/fi/oph/vkt/api/dto/clerk/ClerkExamEventListDTO.java +++ b/backend/vkt/src/main/java/fi/oph/vkt/api/dto/clerk/ClerkExamEventListDTO.java @@ -3,6 +3,7 @@ import fi.oph.vkt.model.type.ExamLanguage; import fi.oph.vkt.model.type.ExamLevel; import java.time.LocalDate; +import java.time.LocalDateTime; import lombok.Builder; import lombok.NonNull; @@ -12,7 +13,8 @@ public record ClerkExamEventListDTO( @NonNull ExamLanguage language, @NonNull ExamLevel level, @NonNull LocalDate date, - @NonNull LocalDate registrationCloses, + @NonNull LocalDateTime registrationCloses, + @NonNull LocalDateTime registrationOpens, @NonNull Long participants, @NonNull Long maxParticipants, @NonNull Boolean isUnusedSeats, diff --git a/backend/vkt/src/main/java/fi/oph/vkt/api/dto/clerk/ClerkExamEventUpdateDTO.java b/backend/vkt/src/main/java/fi/oph/vkt/api/dto/clerk/ClerkExamEventUpdateDTO.java index 303066fe9..a54a3ce4d 100644 --- a/backend/vkt/src/main/java/fi/oph/vkt/api/dto/clerk/ClerkExamEventUpdateDTO.java +++ b/backend/vkt/src/main/java/fi/oph/vkt/api/dto/clerk/ClerkExamEventUpdateDTO.java @@ -4,6 +4,7 @@ import fi.oph.vkt.model.type.ExamLevel; import jakarta.validation.constraints.NotNull; import java.time.LocalDate; +import java.time.LocalDateTime; import lombok.Builder; import lombok.NonNull; @@ -14,7 +15,7 @@ public record ClerkExamEventUpdateDTO( @NonNull @NotNull ExamLanguage language, @NonNull @NotNull ExamLevel level, @NonNull @NotNull LocalDate date, - @NonNull @NotNull LocalDate registrationCloses, + @NonNull @NotNull LocalDateTime registrationCloses, @NonNull @NotNull Boolean isHidden, @NonNull @NotNull Long maxParticipants ) diff --git a/backend/vkt/src/main/java/fi/oph/vkt/model/ExamEvent.java b/backend/vkt/src/main/java/fi/oph/vkt/model/ExamEvent.java index 752e1e7f0..ccecf5654 100644 --- a/backend/vkt/src/main/java/fi/oph/vkt/model/ExamEvent.java +++ b/backend/vkt/src/main/java/fi/oph/vkt/model/ExamEvent.java @@ -12,6 +12,7 @@ import jakarta.persistence.OneToMany; import jakarta.persistence.Table; import java.time.LocalDate; +import java.time.LocalDateTime; import java.util.ArrayList; import java.util.List; import lombok.Getter; @@ -39,8 +40,11 @@ public class ExamEvent extends BaseEntity { @Column(name = "date", nullable = false) private LocalDate date; + @Column(name = "registration_opens", nullable = false) + private LocalDateTime registrationOpens; + @Column(name = "registration_closes", nullable = false) - private LocalDate registrationCloses; + private LocalDateTime registrationCloses; @Column(name = "is_hidden", nullable = false) private boolean isHidden; diff --git a/backend/vkt/src/main/java/fi/oph/vkt/repository/ClerkExamEventProjection.java b/backend/vkt/src/main/java/fi/oph/vkt/repository/ClerkExamEventProjection.java index ba9e646c9..db97b4895 100644 --- a/backend/vkt/src/main/java/fi/oph/vkt/repository/ClerkExamEventProjection.java +++ b/backend/vkt/src/main/java/fi/oph/vkt/repository/ClerkExamEventProjection.java @@ -3,13 +3,15 @@ import fi.oph.vkt.model.type.ExamLanguage; import fi.oph.vkt.model.type.ExamLevel; import java.time.LocalDate; +import java.time.LocalDateTime; public record ClerkExamEventProjection( long id, ExamLanguage language, ExamLevel level, LocalDate date, - LocalDate registrationCloses, + LocalDateTime registrationCloses, + LocalDateTime registrationOpens, long participants, long maxParticipants, boolean isHidden, diff --git a/backend/vkt/src/main/java/fi/oph/vkt/repository/ExamEventRepository.java b/backend/vkt/src/main/java/fi/oph/vkt/repository/ExamEventRepository.java index f75f95405..fff23b441 100644 --- a/backend/vkt/src/main/java/fi/oph/vkt/repository/ExamEventRepository.java +++ b/backend/vkt/src/main/java/fi/oph/vkt/repository/ExamEventRepository.java @@ -11,11 +11,12 @@ public interface ExamEventRepository extends BaseRepository { @Query( "SELECT new fi.oph.vkt.repository.PublicExamEventProjection(e.id, e.language, e.date, e.registrationCloses," + - " COUNT(en), e.maxParticipants)" + + " e.registrationOpens, COUNT(en), e.maxParticipants)" + " FROM ExamEvent e" + " LEFT JOIN e.enrollments en ON en.status = 'COMPLETED' OR en.status = 'AWAITING_PAYMENT' OR en.status = 'AWAITING_APPROVAL' OR en.status = 'EXPECTING_PAYMENT_UNFINISHED_ENROLLMENT'" + " WHERE e.level = ?1" + - " AND e.registrationCloses >= CURRENT_DATE" + + " AND e.registrationCloses >= CURRENT_TIMESTAMP" + + " AND e.registrationOpens <= CURRENT_TIMESTAMP" + " AND e.isHidden = false" + " GROUP BY e.id" ) @@ -26,7 +27,8 @@ public interface ExamEventRepository extends BaseRepository { " FROM ExamEvent e" + " LEFT JOIN e.enrollments en ON en.status = 'QUEUED'" + " WHERE e.level = ?1" + - " AND e.registrationCloses >= CURRENT_DATE" + + " AND e.registrationCloses >= CURRENT_TIMESTAMP" + + " AND e.registrationOpens <= CURRENT_TIMESTAMP" + " AND e.isHidden = false" + " GROUP BY e.id" + " HAVING COUNT(en) > 0" @@ -44,7 +46,7 @@ public interface ExamEventRepository extends BaseRepository { @Query( "SELECT new fi.oph.vkt.repository.ClerkExamEventProjection(e.id, e.language, e.level, e.date," + - " e.registrationCloses, COUNT(en), e.maxParticipants, e.isHidden, COUNT(en.id) filter (where en.status = 'AWAITING_APPROVAL'))" + + " e.registrationCloses, e.registrationOpens, COUNT(en), e.maxParticipants, e.isHidden, COUNT(en.id) filter (where en.status = 'AWAITING_APPROVAL'))" + " FROM ExamEvent e" + " LEFT JOIN e.enrollments en ON en.status = 'COMPLETED' OR en.status = 'AWAITING_PAYMENT' OR en.status = 'AWAITING_APPROVAL' OR en.status = 'EXPECTING_PAYMENT_UNFINISHED_ENROLLMENT'" + " GROUP BY e.id" diff --git a/backend/vkt/src/main/java/fi/oph/vkt/repository/PublicExamEventProjection.java b/backend/vkt/src/main/java/fi/oph/vkt/repository/PublicExamEventProjection.java index c8a481253..2a92dfc7f 100644 --- a/backend/vkt/src/main/java/fi/oph/vkt/repository/PublicExamEventProjection.java +++ b/backend/vkt/src/main/java/fi/oph/vkt/repository/PublicExamEventProjection.java @@ -2,12 +2,14 @@ import fi.oph.vkt.model.type.ExamLanguage; import java.time.LocalDate; +import java.time.LocalDateTime; public record PublicExamEventProjection( long id, ExamLanguage language, LocalDate date, - LocalDate registrationCloses, + LocalDateTime registrationCloses, + LocalDateTime registrationOpens, long participants, long maxParticipants ) {} diff --git a/backend/vkt/src/main/java/fi/oph/vkt/service/ClerkExamEventService.java b/backend/vkt/src/main/java/fi/oph/vkt/service/ClerkExamEventService.java index 632f59cb3..87f2090f4 100644 --- a/backend/vkt/src/main/java/fi/oph/vkt/service/ClerkExamEventService.java +++ b/backend/vkt/src/main/java/fi/oph/vkt/service/ClerkExamEventService.java @@ -60,6 +60,7 @@ public List list() { .level(e.level()) .date(e.date()) .registrationCloses(e.registrationCloses()) + .registrationOpens(e.registrationOpens()) .participants(e.participants()) .maxParticipants(e.maxParticipants()) .isUnusedSeats(isUnusedSeats) diff --git a/backend/vkt/src/main/java/fi/oph/vkt/service/PublicEnrollmentService.java b/backend/vkt/src/main/java/fi/oph/vkt/service/PublicEnrollmentService.java index f9ec5816a..d1bca8b7f 100644 --- a/backend/vkt/src/main/java/fi/oph/vkt/service/PublicEnrollmentService.java +++ b/backend/vkt/src/main/java/fi/oph/vkt/service/PublicEnrollmentService.java @@ -76,7 +76,7 @@ public PublicEnrollmentInitialisationDTO initialiseEnrollment(final long examEve if (ExamEventUtil.isCongested(examEvent)) { throw new APIException(APIExceptionType.INITIALISE_ENROLLMENT_HAS_CONGESTION); } - if (examEvent.getRegistrationCloses().isBefore(LocalDate.now())) { + if (examEvent.getRegistrationCloses().isBefore(LocalDateTime.now())) { throw new APIException(APIExceptionType.INITIALISE_ENROLLMENT_REGISTRATION_CLOSED); } if (isPersonEnrolled(examEvent, person, enrollmentRepository)) { @@ -208,6 +208,7 @@ private PublicEnrollmentInitialisationDTO createEnrollmentInitialisationDTO( .language(examEvent.getLanguage()) .date(examEvent.getDate()) .registrationCloses(examEvent.getRegistrationCloses()) + .registrationOpens(examEvent.getRegistrationOpens()) .openings(openings) .hasCongestion(false) .build(); @@ -246,7 +247,7 @@ public PublicEnrollmentInitialisationDTO initialiseEnrollmentToQueue(final long if (openings > 0) { throw new APIException(APIExceptionType.INITIALISE_ENROLLMENT_TO_QUEUE_HAS_ROOM); } - if (examEvent.getRegistrationCloses().isBefore(LocalDate.now())) { + if (examEvent.getRegistrationCloses().isBefore(LocalDateTime.now())) { throw new APIException(APIExceptionType.INITIALISE_ENROLLMENT_REGISTRATION_CLOSED); } if (isPersonEnrolled(examEvent, person, enrollmentRepository)) { diff --git a/backend/vkt/src/main/java/fi/oph/vkt/service/PublicExamEventService.java b/backend/vkt/src/main/java/fi/oph/vkt/service/PublicExamEventService.java index 1710e3d0d..a81007076 100644 --- a/backend/vkt/src/main/java/fi/oph/vkt/service/PublicExamEventService.java +++ b/backend/vkt/src/main/java/fi/oph/vkt/service/PublicExamEventService.java @@ -9,6 +9,7 @@ import fi.oph.vkt.util.ExamEventUtil; import fi.oph.vkt.util.exception.NotFoundException; import java.time.LocalDate; +import java.time.LocalDateTime; import java.util.Comparator; import java.util.List; import java.util.Map; @@ -28,7 +29,7 @@ public class PublicExamEventService { public PublicExamEventDTO getExamEvent(final long examEventId) { final ExamEvent examEvent = examEventRepository.getReferenceById(examEventId); - if (examEvent.getRegistrationCloses().isBefore(LocalDate.now())) { + if (examEvent.getRegistrationCloses().isBefore(LocalDateTime.now())) { throw new NotFoundException(String.format("Exam event (%d) enrollment is closed", examEvent.getId())); } @@ -38,6 +39,7 @@ public PublicExamEventDTO getExamEvent(final long examEventId) { .language(examEvent.getLanguage()) .date(examEvent.getDate()) .registrationCloses(examEvent.getRegistrationCloses()) + .registrationOpens(examEvent.getRegistrationOpens()) .openings(ExamEventUtil.getOpenings(examEvent)) .hasCongestion(ExamEventUtil.isCongested(examEvent)) .build(); @@ -65,6 +67,7 @@ public List listExamEvents(final ExamLevel level) { .language(e.language()) .date(e.date()) .registrationCloses(e.registrationCloses()) + .registrationOpens(e.registrationOpens()) .openings(openings) .hasCongestion(hasCongestion) .build(); diff --git a/backend/vkt/src/main/java/fi/oph/vkt/util/ExamEventUtil.java b/backend/vkt/src/main/java/fi/oph/vkt/util/ExamEventUtil.java index bb1a4e33b..91b64787e 100644 --- a/backend/vkt/src/main/java/fi/oph/vkt/util/ExamEventUtil.java +++ b/backend/vkt/src/main/java/fi/oph/vkt/util/ExamEventUtil.java @@ -46,7 +46,7 @@ public static ClerkExamEventAuditDTO createExamEventAuditDTO(final ExamEvent exa .language(examEvent.getLanguage()) .level(examEvent.getLevel()) .date(DateUtil.formatOptionalDate(examEvent.getDate())) - .registrationCloses(DateUtil.formatOptionalDate(examEvent.getRegistrationCloses())) + .registrationCloses(DateUtil.formatOptionalDatetime(examEvent.getRegistrationCloses())) .isHidden(examEvent.isHidden()) .maxParticipants(examEvent.getMaxParticipants()) .build(); diff --git a/backend/vkt/src/main/resources/db/changelog/db.changelog-1.0.xml b/backend/vkt/src/main/resources/db/changelog/db.changelog-1.0.xml index e1ae54c78..7a1150ba1 100644 --- a/backend/vkt/src/main/resources/db/changelog/db.changelog-1.0.xml +++ b/backend/vkt/src/main/resources/db/changelog/db.changelog-1.0.xml @@ -780,4 +780,26 @@ + + + + + + ALTER TABLE exam_event DROP CONSTRAINT IF EXISTS ck_exam_event_registration_closes; + ALTER TABLE exam_event ALTER COLUMN registration_closes TYPE TIMESTAMP WITH TIME ZONE USING registration_closes + time '16:00:00'; + ALTER TABLE exam_event ADD COLUMN registration_opens TIMESTAMP WITH TIME ZONE; + UPDATE exam_event SET registration_opens = created_at; + ALTER TABLE exam_event ALTER COLUMN registration_opens SET NOT NULL; + + + + + + + + ALTER TABLE exam_event DROP CONSTRAINT CK_EXAM_EVENT_REGISTRATION_CLOSES; + ALTER TABLE exam_event ALTER COLUMN registration_closes TIMESTAMP WITH TIME ZONE; + ALTER TABLE exam_event ADD COLUMN registration_opens TIMESTAMP WITH TIME ZONE; + + diff --git a/backend/vkt/src/test/java/fi/oph/vkt/Factory.java b/backend/vkt/src/test/java/fi/oph/vkt/Factory.java index f440539bf..34acd5eff 100644 --- a/backend/vkt/src/test/java/fi/oph/vkt/Factory.java +++ b/backend/vkt/src/test/java/fi/oph/vkt/Factory.java @@ -26,7 +26,8 @@ public static ExamEvent examEvent(final ExamLanguage language) { examEvent.setLanguage(language); examEvent.setLevel(ExamLevel.EXCELLENT); examEvent.setDate(LocalDate.now().plusDays(8)); - examEvent.setRegistrationCloses(LocalDate.now()); + examEvent.setRegistrationCloses(LocalDateTime.now().plusDays(4)); + examEvent.setRegistrationOpens(LocalDateTime.now().minusDays(2)); examEvent.setHidden(false); examEvent.setMaxParticipants(10); diff --git a/backend/vkt/src/test/java/fi/oph/vkt/service/ClerkExamEventServiceTest.java b/backend/vkt/src/test/java/fi/oph/vkt/service/ClerkExamEventServiceTest.java index 0c2bf2406..2f8fc863f 100644 --- a/backend/vkt/src/test/java/fi/oph/vkt/service/ClerkExamEventServiceTest.java +++ b/backend/vkt/src/test/java/fi/oph/vkt/service/ClerkExamEventServiceTest.java @@ -35,6 +35,7 @@ import jakarta.annotation.Resource; import java.io.ByteArrayInputStream; import java.time.LocalDate; +import java.time.LocalDateTime; import java.util.HashMap; import java.util.List; import java.util.stream.IntStream; @@ -76,28 +77,28 @@ public void setup() { @Test public void testListExamEvents() { - final LocalDate now = LocalDate.now(); + final LocalDateTime now = LocalDateTime.now(); final ExamEvent pastEvent = Factory.examEvent(); - pastEvent.setDate(now.minusWeeks(3)); + pastEvent.setDate(now.toLocalDate().minusWeeks(3)); pastEvent.setRegistrationCloses(now.minusWeeks(4)); final ExamEvent eventToday = Factory.examEvent(ExamLanguage.FI); - eventToday.setDate(now); + eventToday.setDate(now.toLocalDate()); final ExamEvent eventWithRegistrationClosed = Factory.examEvent(); - eventWithRegistrationClosed.setDate(now.plusDays(3)); + eventWithRegistrationClosed.setDate(now.toLocalDate().plusDays(3)); eventWithRegistrationClosed.setRegistrationCloses(now.minusDays(1)); final ExamEvent hiddenEvent = Factory.examEvent(); hiddenEvent.setHidden(true); - hiddenEvent.setDate(now.plusWeeks(1)); + hiddenEvent.setDate(now.toLocalDate().plusWeeks(1)); final ExamEvent upcomingEventSv = Factory.examEvent(ExamLanguage.SV); final ExamEvent upcomingEventFi = Factory.examEvent(ExamLanguage.FI); final ExamEvent futureEvent = Factory.examEvent(ExamLanguage.FI); - futureEvent.setDate(now.plusWeeks(3)); + futureEvent.setDate(now.toLocalDate().plusWeeks(3)); futureEvent.setRegistrationCloses(now.plusWeeks(2)); futureEvent.setMaxParticipants(3); @@ -287,7 +288,7 @@ public void testCreateExamEvent() { .language(ExamLanguage.FI) .level(ExamLevel.EXCELLENT) .date(LocalDate.now().plusMonths(1)) - .registrationCloses(LocalDate.now().plusWeeks(1)) + .registrationCloses(LocalDateTime.now().plusWeeks(1)) .isHidden(true) .maxParticipants(2L) .build(); @@ -324,7 +325,11 @@ public void testCreateExamEventFailsOnDuplicate() { .date(LocalDate.now().plusMonths(1)); clerkExamEventService.createExamEvent( - duplicateDTOBuilder.registrationCloses(LocalDate.now().plusWeeks(1)).isHidden(true).maxParticipants(2L).build() + duplicateDTOBuilder + .registrationCloses(LocalDateTime.now().plusWeeks(1)) + .isHidden(true) + .maxParticipants(2L) + .build() ); reset(auditService); @@ -332,7 +337,7 @@ public void testCreateExamEventFailsOnDuplicate() { APIException.class, () -> clerkExamEventService.createExamEvent( - duplicateDTOBuilder.registrationCloses(LocalDate.now()).isHidden(false).maxParticipants(3L).build() + duplicateDTOBuilder.registrationCloses(LocalDateTime.now()).isHidden(false).maxParticipants(3L).build() ) ); assertEquals(APIExceptionType.EXAM_EVENT_DUPLICATE, ex.getExceptionType()); diff --git a/backend/vkt/src/test/java/fi/oph/vkt/service/PublicEnrollmentServiceTest.java b/backend/vkt/src/test/java/fi/oph/vkt/service/PublicEnrollmentServiceTest.java index 6839f067c..4406cf065 100644 --- a/backend/vkt/src/test/java/fi/oph/vkt/service/PublicEnrollmentServiceTest.java +++ b/backend/vkt/src/test/java/fi/oph/vkt/service/PublicEnrollmentServiceTest.java @@ -265,7 +265,7 @@ public void testInitialiseEnrollmentFailsWithCongestion() { @Test public void testInitialiseEnrollmentFailsWithRegistrationClosed() { final ExamEvent examEvent = Factory.examEvent(); - examEvent.setRegistrationCloses(LocalDate.now().minusDays(1)); + examEvent.setRegistrationCloses(LocalDateTime.now().minusDays(1)); entityManager.persist(examEvent); final Person person = createPerson(); diff --git a/backend/vkt/src/test/java/fi/oph/vkt/service/PublicExamEventServiceTest.java b/backend/vkt/src/test/java/fi/oph/vkt/service/PublicExamEventServiceTest.java index 43af606e8..dd992a5c9 100644 --- a/backend/vkt/src/test/java/fi/oph/vkt/service/PublicExamEventServiceTest.java +++ b/backend/vkt/src/test/java/fi/oph/vkt/service/PublicExamEventServiceTest.java @@ -50,40 +50,51 @@ public void setup() { @Test public void testListExcellentLevelExamEvents() { - final LocalDate now = LocalDate.now(); + final LocalDateTime now = LocalDateTime.now(); final ExamEvent pastEvent = Factory.examEvent(); - pastEvent.setDate(now.minusWeeks(3)); + pastEvent.setDate(now.toLocalDate().minusWeeks(3)); pastEvent.setRegistrationCloses(now.minusWeeks(4)); + pastEvent.setRegistrationOpens(now.minusWeeks(6)); final ExamEvent eventWithRegistrationClosed = Factory.examEvent(); - eventWithRegistrationClosed.setDate(now.plusDays(3)); + eventWithRegistrationClosed.setDate(now.toLocalDate().plusDays(3)); eventWithRegistrationClosed.setRegistrationCloses(now.minusDays(1)); + eventWithRegistrationClosed.setRegistrationOpens(now.minusDays(2)); final ExamEvent hiddenEvent = Factory.examEvent(); hiddenEvent.setHidden(true); - hiddenEvent.setDate(now.plusWeeks(1)); + hiddenEvent.setDate(now.toLocalDate().plusWeeks(1)); + hiddenEvent.setRegistrationOpens(now.plusDays(1)); final ExamEvent eventToday = Factory.examEvent(ExamLanguage.FI); - eventToday.setDate(now); + eventToday.setDate(now.toLocalDate()); eventToday.setMaxParticipants(6); final ExamEvent upcomingEventSv = Factory.examEvent(ExamLanguage.SV); final ExamEvent upcomingEventFi = Factory.examEvent(ExamLanguage.FI); final ExamEvent futureEvent1 = Factory.examEvent(ExamLanguage.SV); - futureEvent1.setDate(now.plusWeeks(4)); + futureEvent1.setDate(now.toLocalDate().plusWeeks(4)); futureEvent1.setRegistrationCloses(now.plusWeeks(3)); futureEvent1.setMaxParticipants(3); final ExamEvent futureEvent2 = Factory.examEvent(ExamLanguage.FI); - futureEvent2.setDate(now.plusWeeks(5)); + futureEvent2.setDate(now.toLocalDate().plusWeeks(5)); futureEvent2.setRegistrationCloses(now.plusWeeks(3)); + futureEvent2.setRegistrationOpens(now); futureEvent2.setMaxParticipants(4); + final ExamEvent futureEventNotOpen = Factory.examEvent(ExamLanguage.FI); + futureEventNotOpen.setDate(now.toLocalDate().plusDays(6)); + futureEventNotOpen.setRegistrationCloses(now.plusWeeks(3)); + futureEventNotOpen.setRegistrationOpens(now.plusDays(3)); + futureEventNotOpen.setMaxParticipants(4); + final ExamEvent futureEventWithoutRoom = Factory.examEvent(); - futureEventWithoutRoom.setDate(now.plusWeeks(6)); + futureEventWithoutRoom.setDate(now.toLocalDate().plusWeeks(6)); futureEventWithoutRoom.setMaxParticipants(0); + futureEventWithoutRoom.setRegistrationOpens(now); entityManager.persist(pastEvent); entityManager.persist(eventWithRegistrationClosed); @@ -94,6 +105,7 @@ public void testListExcellentLevelExamEvents() { entityManager.persist(futureEvent1); entityManager.persist(futureEvent2); entityManager.persist(futureEventWithoutRoom); + entityManager.persist(futureEventNotOpen); createReservations(futureEvent1, 2, LocalDateTime.now().plusMinutes(1)); createReservations(futureEvent1, 1, LocalDateTime.now()); @@ -192,10 +204,10 @@ private void testCongestion( final int howManyReservations, final boolean expectedCongestion ) { - final LocalDate now = LocalDate.now(); + final LocalDateTime now = LocalDateTime.now(); final ExamEvent event = Factory.examEvent(ExamLanguage.FI); - event.setDate(now.plusWeeks(5)); + event.setDate(now.toLocalDate().plusWeeks(5)); event.setRegistrationCloses(now.plusWeeks(3)); event.setMaxParticipants(2); @@ -246,10 +258,10 @@ private void assertExamEventDetails(final ExamEvent expected, final PublicExamEv @Test public void testExamEventHasNoOpeningsEvenIfOneInQueue() { - final LocalDate now = LocalDate.now(); + final LocalDateTime now = LocalDateTime.now(); final ExamEvent event = Factory.examEvent(ExamLanguage.FI); - event.setDate(now.plusWeeks(5)); + event.setDate(now.toLocalDate().plusWeeks(5)); event.setRegistrationCloses(now.plusWeeks(3)); event.setMaxParticipants(100); From 7ac6e9813d9d53efce73f72ae9164e3b315733ea Mon Sep 17 00:00:00 2001 From: Jarkko Pesonen <435495+jrkkp@users.noreply.github.com> Date: Wed, 28 Aug 2024 12:49:56 +0300 Subject: [PATCH 002/248] VKT(Backend & Frontend): support for exam event open and close times --- .../src/main/java/fi/oph/vkt/api/dto/PublicExamEventDTO.java | 1 + .../main/java/fi/oph/vkt/service/PublicExamEventService.java | 1 + frontend/packages/vkt/src/interfaces/publicExamEvent.ts | 5 ++++- frontend/packages/vkt/src/utils/serialization.ts | 1 + 4 files changed, 7 insertions(+), 1 deletion(-) diff --git a/backend/vkt/src/main/java/fi/oph/vkt/api/dto/PublicExamEventDTO.java b/backend/vkt/src/main/java/fi/oph/vkt/api/dto/PublicExamEventDTO.java index cb60e37f8..311a70a7b 100644 --- a/backend/vkt/src/main/java/fi/oph/vkt/api/dto/PublicExamEventDTO.java +++ b/backend/vkt/src/main/java/fi/oph/vkt/api/dto/PublicExamEventDTO.java @@ -14,5 +14,6 @@ public record PublicExamEventDTO( @NonNull LocalDateTime registrationCloses, @NonNull LocalDateTime registrationOpens, @NonNull Long openings, + @NonNull Boolean isOpen, @NonNull Boolean hasCongestion ) {} diff --git a/backend/vkt/src/main/java/fi/oph/vkt/service/PublicExamEventService.java b/backend/vkt/src/main/java/fi/oph/vkt/service/PublicExamEventService.java index a81007076..dcc4731f4 100644 --- a/backend/vkt/src/main/java/fi/oph/vkt/service/PublicExamEventService.java +++ b/backend/vkt/src/main/java/fi/oph/vkt/service/PublicExamEventService.java @@ -70,6 +70,7 @@ public List listExamEvents(final ExamLevel level) { .registrationOpens(e.registrationOpens()) .openings(openings) .hasCongestion(hasCongestion) + .isOpen(true) .build(); }) .sorted(Comparator.comparing(PublicExamEventDTO::date).thenComparing(PublicExamEventDTO::language)) diff --git a/frontend/packages/vkt/src/interfaces/publicExamEvent.ts b/frontend/packages/vkt/src/interfaces/publicExamEvent.ts index a7ad492f5..a9c008ef2 100644 --- a/frontend/packages/vkt/src/interfaces/publicExamEvent.ts +++ b/frontend/packages/vkt/src/interfaces/publicExamEvent.ts @@ -7,12 +7,15 @@ export interface PublicExamEvent extends WithId { language: Exclude; date: Dayjs; registrationCloses: Dayjs; + registrationOpens: Dayjs; openings: number; hasCongestion: boolean; + isOpen: boolean; } export interface PublicExamEventResponse - extends Omit { + extends Omit { date: string; registrationCloses: string; + registrationOpens: string; } diff --git a/frontend/packages/vkt/src/utils/serialization.ts b/frontend/packages/vkt/src/utils/serialization.ts index 326d53500..03c723a2a 100644 --- a/frontend/packages/vkt/src/utils/serialization.ts +++ b/frontend/packages/vkt/src/utils/serialization.ts @@ -39,6 +39,7 @@ export class SerializationUtils { ...publicExamEvent, date: dayjs(publicExamEvent.date), registrationCloses: dayjs(publicExamEvent.registrationCloses), + registrationOpens: dayjs(publicExamEvent.registrationOpens), }; } From 741cce6b7d4bf60a8dd8149fb5d8e36696c97236 Mon Sep 17 00:00:00 2001 From: Jarkko Pesonen <435495+jrkkp@users.noreply.github.com> Date: Mon, 2 Sep 2024 22:57:18 +0300 Subject: [PATCH 003/248] VKT(Backend & Frontend): support for exam event open and close times continues --- .../dto/clerk/ClerkExamEventCreateDTO.java | 7 +++ .../vkt/api/dto/clerk/ClerkExamEventDTO.java | 1 + .../clerk/ClerkExamEventDTOCommonFields.java | 1 + .../dto/clerk/ClerkExamEventUpdateDTO.java | 7 +++ .../vkt/service/ClerkExamEventService.java | 2 + .../vkt/service/PublicEnrollmentService.java | 1 + .../vkt/service/PublicExamEventService.java | 2 +- .../java/fi/oph/vkt/util/ExamEventUtil.java | 9 ++++ .../service/ClerkExamEventServiceTest.java | 6 ++- .../packages/shared/src/utils/date/date.ts | 4 ++ .../packages/vkt/public/i18n/fi-FI/clerk.json | 6 ++- .../create/ClerkExamRegistrationCloses.tsx | 21 +++++---- .../create/ClerkExamRegistrationOpens.tsx | 46 +++++++++++++++++++ .../overview/ClerkExamEventDetails.tsx | 2 +- .../overview/ClerkExamEventDetailsFields.tsx | 16 ++++++- .../vkt/src/interfaces/clerkExamEvent.ts | 9 +++- .../vkt/src/interfaces/clerkListExamEvent.ts | 7 ++- .../vkt/src/interfaces/publicExamEvent.ts | 5 +- .../src/pages/ClerkExamEventCreatePage.tsx | 4 ++ .../clerk_create_exam_event_page.spec.ts | 4 ++ .../page-objects/clerkCreateExamEventPage.ts | 14 ++++-- .../tests/msw/fixtures/publicExamEvents11.ts | 22 +++++++++ .../packages/vkt/src/utils/serialization.ts | 16 ++++++- 23 files changed, 189 insertions(+), 23 deletions(-) create mode 100644 frontend/packages/vkt/src/components/clerkExamEvent/create/ClerkExamRegistrationOpens.tsx diff --git a/backend/vkt/src/main/java/fi/oph/vkt/api/dto/clerk/ClerkExamEventCreateDTO.java b/backend/vkt/src/main/java/fi/oph/vkt/api/dto/clerk/ClerkExamEventCreateDTO.java index aba9e9e00..748d494db 100644 --- a/backend/vkt/src/main/java/fi/oph/vkt/api/dto/clerk/ClerkExamEventCreateDTO.java +++ b/backend/vkt/src/main/java/fi/oph/vkt/api/dto/clerk/ClerkExamEventCreateDTO.java @@ -1,5 +1,6 @@ package fi.oph.vkt.api.dto.clerk; +import com.fasterxml.jackson.annotation.JsonFormat; import fi.oph.vkt.model.type.ExamLanguage; import fi.oph.vkt.model.type.ExamLevel; import jakarta.validation.constraints.NotNull; @@ -13,7 +14,13 @@ public record ClerkExamEventCreateDTO( @NonNull @NotNull ExamLanguage language, @NonNull @NotNull ExamLevel level, @NonNull @NotNull LocalDate date, + + @JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss") @NonNull @NotNull LocalDateTime registrationCloses, + + @JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss") + @NonNull @NotNull LocalDateTime registrationOpens, + @NonNull @NotNull Boolean isHidden, @NonNull @NotNull Long maxParticipants ) diff --git a/backend/vkt/src/main/java/fi/oph/vkt/api/dto/clerk/ClerkExamEventDTO.java b/backend/vkt/src/main/java/fi/oph/vkt/api/dto/clerk/ClerkExamEventDTO.java index 285f6524b..21833ff10 100644 --- a/backend/vkt/src/main/java/fi/oph/vkt/api/dto/clerk/ClerkExamEventDTO.java +++ b/backend/vkt/src/main/java/fi/oph/vkt/api/dto/clerk/ClerkExamEventDTO.java @@ -17,6 +17,7 @@ public record ClerkExamEventDTO( @NonNull @NotNull ExamLevel level, @NonNull @NotNull LocalDate date, @NonNull @NotNull LocalDateTime registrationCloses, + @NonNull @NotNull LocalDateTime registrationOpens, @NonNull @NotNull Boolean isHidden, @NonNull @NotNull Long maxParticipants, @NonNull @NotNull List enrollments diff --git a/backend/vkt/src/main/java/fi/oph/vkt/api/dto/clerk/ClerkExamEventDTOCommonFields.java b/backend/vkt/src/main/java/fi/oph/vkt/api/dto/clerk/ClerkExamEventDTOCommonFields.java index 2aaec62d7..28bb3ddbc 100644 --- a/backend/vkt/src/main/java/fi/oph/vkt/api/dto/clerk/ClerkExamEventDTOCommonFields.java +++ b/backend/vkt/src/main/java/fi/oph/vkt/api/dto/clerk/ClerkExamEventDTOCommonFields.java @@ -10,6 +10,7 @@ public interface ClerkExamEventDTOCommonFields { ExamLevel level(); LocalDate date(); LocalDateTime registrationCloses(); + LocalDateTime registrationOpens(); Boolean isHidden(); Long maxParticipants(); } diff --git a/backend/vkt/src/main/java/fi/oph/vkt/api/dto/clerk/ClerkExamEventUpdateDTO.java b/backend/vkt/src/main/java/fi/oph/vkt/api/dto/clerk/ClerkExamEventUpdateDTO.java index a54a3ce4d..86001e04d 100644 --- a/backend/vkt/src/main/java/fi/oph/vkt/api/dto/clerk/ClerkExamEventUpdateDTO.java +++ b/backend/vkt/src/main/java/fi/oph/vkt/api/dto/clerk/ClerkExamEventUpdateDTO.java @@ -1,5 +1,6 @@ package fi.oph.vkt.api.dto.clerk; +import com.fasterxml.jackson.annotation.JsonFormat; import fi.oph.vkt.model.type.ExamLanguage; import fi.oph.vkt.model.type.ExamLevel; import jakarta.validation.constraints.NotNull; @@ -15,7 +16,13 @@ public record ClerkExamEventUpdateDTO( @NonNull @NotNull ExamLanguage language, @NonNull @NotNull ExamLevel level, @NonNull @NotNull LocalDate date, + + @JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss") @NonNull @NotNull LocalDateTime registrationCloses, + + @JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss") + @NonNull @NotNull LocalDateTime registrationOpens, + @NonNull @NotNull Boolean isHidden, @NonNull @NotNull Long maxParticipants ) diff --git a/backend/vkt/src/main/java/fi/oph/vkt/service/ClerkExamEventService.java b/backend/vkt/src/main/java/fi/oph/vkt/service/ClerkExamEventService.java index 87f2090f4..8d6f0b5e8 100644 --- a/backend/vkt/src/main/java/fi/oph/vkt/service/ClerkExamEventService.java +++ b/backend/vkt/src/main/java/fi/oph/vkt/service/ClerkExamEventService.java @@ -109,6 +109,7 @@ private ClerkExamEventDTO getExamEventWithoutAudit(final long examEventId) { .level(examEvent.getLevel()) .date(examEvent.getDate()) .registrationCloses(examEvent.getRegistrationCloses()) + .registrationOpens(examEvent.getRegistrationOpens()) .isHidden(examEvent.isHidden()) .maxParticipants(examEvent.getMaxParticipants()) .enrollments(enrollmentDTOs) @@ -164,6 +165,7 @@ private void copyDtoFieldsToExamEvent(final ClerkExamEventDTOCommonFields dto, f examEvent.setLevel(dto.level()); examEvent.setDate(dto.date()); examEvent.setRegistrationCloses(dto.registrationCloses()); + examEvent.setRegistrationOpens(dto.registrationOpens()); examEvent.setHidden(dto.isHidden()); examEvent.setMaxParticipants(dto.maxParticipants()); } diff --git a/backend/vkt/src/main/java/fi/oph/vkt/service/PublicEnrollmentService.java b/backend/vkt/src/main/java/fi/oph/vkt/service/PublicEnrollmentService.java index d1bca8b7f..7a67030e4 100644 --- a/backend/vkt/src/main/java/fi/oph/vkt/service/PublicEnrollmentService.java +++ b/backend/vkt/src/main/java/fi/oph/vkt/service/PublicEnrollmentService.java @@ -211,6 +211,7 @@ private PublicEnrollmentInitialisationDTO createEnrollmentInitialisationDTO( .registrationOpens(examEvent.getRegistrationOpens()) .openings(openings) .hasCongestion(false) + .isOpen(ExamEventUtil.isOpen(examEvent)) .build(); final PublicPersonDTO personDTO = PersonUtil.createPublicPersonDTO(person); diff --git a/backend/vkt/src/main/java/fi/oph/vkt/service/PublicExamEventService.java b/backend/vkt/src/main/java/fi/oph/vkt/service/PublicExamEventService.java index dcc4731f4..1a41c39f7 100644 --- a/backend/vkt/src/main/java/fi/oph/vkt/service/PublicExamEventService.java +++ b/backend/vkt/src/main/java/fi/oph/vkt/service/PublicExamEventService.java @@ -70,7 +70,7 @@ public List listExamEvents(final ExamLevel level) { .registrationOpens(e.registrationOpens()) .openings(openings) .hasCongestion(hasCongestion) - .isOpen(true) + .isOpen(ExamEventUtil.isOpen(e.registrationCloses(), e.registrationOpens())) .build(); }) .sorted(Comparator.comparing(PublicExamEventDTO::date).thenComparing(PublicExamEventDTO::language)) diff --git a/backend/vkt/src/main/java/fi/oph/vkt/util/ExamEventUtil.java b/backend/vkt/src/main/java/fi/oph/vkt/util/ExamEventUtil.java index 91b64787e..549cfdd26 100644 --- a/backend/vkt/src/main/java/fi/oph/vkt/util/ExamEventUtil.java +++ b/backend/vkt/src/main/java/fi/oph/vkt/util/ExamEventUtil.java @@ -5,6 +5,7 @@ import fi.oph.vkt.model.ExamEvent; import fi.oph.vkt.model.Reservation; import fi.oph.vkt.model.type.EnrollmentStatus; +import java.time.LocalDateTime; import java.util.List; public class ExamEventUtil { @@ -33,6 +34,14 @@ public static boolean isCongested(final ExamEvent examEvent) { return isCongested(openings, reservations); } + public static boolean isOpen(final ExamEvent examEvent) { + return ExamEventUtil.isOpen(examEvent.getRegistrationCloses(), examEvent.getRegistrationOpens()); + } + + public static boolean isOpen(final LocalDateTime closes, final LocalDateTime opens) { + return closes.isAfter(LocalDateTime.now()) && opens.isBefore(LocalDateTime.now()); + } + public static boolean isCongested(final long openings, final long reservations) { return openings > 0 && openings <= reservations; } diff --git a/backend/vkt/src/test/java/fi/oph/vkt/service/ClerkExamEventServiceTest.java b/backend/vkt/src/test/java/fi/oph/vkt/service/ClerkExamEventServiceTest.java index 2f8fc863f..03af95059 100644 --- a/backend/vkt/src/test/java/fi/oph/vkt/service/ClerkExamEventServiceTest.java +++ b/backend/vkt/src/test/java/fi/oph/vkt/service/ClerkExamEventServiceTest.java @@ -36,6 +36,7 @@ import java.io.ByteArrayInputStream; import java.time.LocalDate; import java.time.LocalDateTime; +import java.time.temporal.ChronoUnit; import java.util.HashMap; import java.util.List; import java.util.stream.IntStream; @@ -168,7 +169,10 @@ private void assertExamEventListDTODetails(final ExamEvent expected, final Clerk assertEquals(expected.getLanguage(), examEventListDTO.language()); assertEquals(expected.getLevel(), examEventListDTO.level()); assertEquals(expected.getDate(), examEventListDTO.date()); - assertEquals(expected.getRegistrationCloses(), examEventListDTO.registrationCloses()); + assertEquals( + expected.getRegistrationCloses().truncatedTo(ChronoUnit.MINUTES), + examEventListDTO.registrationCloses().truncatedTo(ChronoUnit.MINUTES) + ); assertEquals(expected.getMaxParticipants(), examEventListDTO.maxParticipants()); } diff --git a/frontend/packages/shared/src/utils/date/date.ts b/frontend/packages/shared/src/utils/date/date.ts index 75fd658e5..ffa05b770 100644 --- a/frontend/packages/shared/src/utils/date/date.ts +++ b/frontend/packages/shared/src/utils/date/date.ts @@ -74,6 +74,10 @@ export class DateUtils { return date?.format('YYYY-MM-DD'); } + static serializeDateTime(date?: Dayjs) { + return date?.format('YYYY-MM-DDTHH:mm:ss'); + } + static isValidDate(date?: Dayjs) { return date ? date.isValid() : false; } diff --git a/frontend/packages/vkt/public/i18n/fi-FI/clerk.json b/frontend/packages/vkt/public/i18n/fi-FI/clerk.json index 481c1cbbd..0fa2223b0 100644 --- a/frontend/packages/vkt/public/i18n/fi-FI/clerk.json +++ b/frontend/packages/vkt/public/i18n/fi-FI/clerk.json @@ -124,7 +124,8 @@ "fillings": "Paikkoja täytetty", "hidden": "Piilotettu", "language": "Kieli ja taso", - "registrationCloses": "Ilmoittautuminen sulkeutuu" + "registrationCloses": "Ilmoittautuminen sulkeutuu", + "registrationOpens": "Ilmoittautuminen avautuu" }, "more": "Lisätietoja", "title": "Tutkintotilaisuudet", @@ -146,7 +147,8 @@ "isHidden": "Piilotettu", "language": "Kieli", "level": "Taso", - "registrationCloses": "Ilmoittautuminen sulkeutuu" + "registrationCloses": "Ilmoittautuminen sulkeutuu", + "registrationOpens": "Ilmoittautuminen avautuu" }, "examEventListingHeader": { "CANCELED": "Peruutetut", diff --git a/frontend/packages/vkt/src/components/clerkExamEvent/create/ClerkExamRegistrationCloses.tsx b/frontend/packages/vkt/src/components/clerkExamEvent/create/ClerkExamRegistrationCloses.tsx index 484d7918c..f80f63c1c 100644 --- a/frontend/packages/vkt/src/components/clerkExamEvent/create/ClerkExamRegistrationCloses.tsx +++ b/frontend/packages/vkt/src/components/clerkExamEvent/create/ClerkExamRegistrationCloses.tsx @@ -20,7 +20,7 @@ export const ClerkExamRegistrationCloses = ({ const onRegistrationClosesChange = (value: Dayjs | null) => { const examFormDetails: DraftClerkExamEvent = { ...examForm, - registrationCloses: value ?? undefined, + registrationCloses: value?.hour(16).minute(0) ?? undefined, }; dispatch(updateClerkNewExamDate(examFormDetails)); }; @@ -28,16 +28,19 @@ export const ClerkExamRegistrationCloses = ({ return (

{t('header.registrationCloses')}

- +
+ + Klo 16:00 +
); }; diff --git a/frontend/packages/vkt/src/components/clerkExamEvent/create/ClerkExamRegistrationOpens.tsx b/frontend/packages/vkt/src/components/clerkExamEvent/create/ClerkExamRegistrationOpens.tsx new file mode 100644 index 000000000..76341e2e2 --- /dev/null +++ b/frontend/packages/vkt/src/components/clerkExamEvent/create/ClerkExamRegistrationOpens.tsx @@ -0,0 +1,46 @@ +import dayjs, { Dayjs } from 'dayjs'; +import { CustomDatePicker, H3 } from 'shared/components'; + +import { useClerkTranslation, useCommonTranslation } from 'configs/i18n'; +import { useAppDispatch } from 'configs/redux'; +import { DraftClerkExamEvent } from 'interfaces/clerkExamEvent'; +import { updateClerkNewExamDate } from 'redux/reducers/clerkNewExamDate'; + +export const ClerkExamRegistrationOpens = ({ + examForm, +}: { + examForm: DraftClerkExamEvent; +}) => { + const { t } = useClerkTranslation({ + keyPrefix: 'vkt.component.clerkExamEventListing', + }); + const translateCommon = useCommonTranslation(); + const dispatch = useAppDispatch(); + + const onRegistrationOpensChange = (value: Dayjs | null) => { + const examFormDetails: DraftClerkExamEvent = { + ...examForm, + registrationOpens: value?.hour(10).minute(0) ?? undefined, + }; + dispatch(updateClerkNewExamDate(examFormDetails)); + }; + + return ( +
+

{t('header.registrationOpens')}

+
+ + Klo 10:00 +
+
+ ); +}; diff --git a/frontend/packages/vkt/src/components/clerkExamEvent/overview/ClerkExamEventDetails.tsx b/frontend/packages/vkt/src/components/clerkExamEvent/overview/ClerkExamEventDetails.tsx index d2b281b81..5b0d214c5 100644 --- a/frontend/packages/vkt/src/components/clerkExamEvent/overview/ClerkExamEventDetails.tsx +++ b/frontend/packages/vkt/src/components/clerkExamEvent/overview/ClerkExamEventDetails.tsx @@ -203,7 +203,7 @@ export const ClerkExamEventDetails = () => { ( field: keyof Pick< ClerkExamEventBasicInformation, - 'date' | 'registrationCloses' + 'date' | 'registrationCloses' | 'registrationOpens' >, ) => (date: Dayjs | null) => { diff --git a/frontend/packages/vkt/src/components/clerkExamEvent/overview/ClerkExamEventDetailsFields.tsx b/frontend/packages/vkt/src/components/clerkExamEvent/overview/ClerkExamEventDetailsFields.tsx index aecd89ef9..d8e38daec 100644 --- a/frontend/packages/vkt/src/components/clerkExamEvent/overview/ClerkExamEventDetailsFields.tsx +++ b/frontend/packages/vkt/src/components/clerkExamEvent/overview/ClerkExamEventDetailsFields.tsx @@ -33,7 +33,7 @@ export const ClerkExamEventDetailsFields = ({ onDateChange: ( field: keyof Pick< ClerkExamEventBasicInformation, - 'date' | 'registrationCloses' + 'date' | 'registrationCloses' | 'registrationOpens' >, ) => (date: Dayjs | null) => void; onCheckBoxChange: ( @@ -111,6 +111,18 @@ export const ClerkExamEventDetailsFields = ({
+
+

{t('registrationOpens')}

+ +
+
+

{t('fillingsTotal')}

{ @@ -33,13 +35,18 @@ export interface DraftClerkExamEvent level?: ExamLevel; date?: Dayjs; registrationCloses?: Dayjs; + registrationOpens?: Dayjs; maxParticipants?: number; } export interface ClerkExamEventResponse - extends Omit { + extends Omit< + ClerkExamEvent, + 'date' | 'registrationCloses' | 'registrationOpens' | 'enrollments' + > { date: string; registrationCloses: string; + registrationOpens: string; enrollments: Array; } diff --git a/frontend/packages/vkt/src/interfaces/clerkListExamEvent.ts b/frontend/packages/vkt/src/interfaces/clerkListExamEvent.ts index 3c5a46c28..7a77af94d 100644 --- a/frontend/packages/vkt/src/interfaces/clerkListExamEvent.ts +++ b/frontend/packages/vkt/src/interfaces/clerkListExamEvent.ts @@ -8,6 +8,7 @@ export interface ClerkListExamEvent extends WithId { level: ExamLevel; date: Dayjs; registrationCloses: Dayjs; + registrationOpens: Dayjs; participants: number; maxParticipants: number; isUnusedSeats: boolean; @@ -16,7 +17,11 @@ export interface ClerkListExamEvent extends WithId { } export interface ClerkListExamEventResponse - extends Omit { + extends Omit< + ClerkListExamEvent, + 'date' | 'registrationCloses' | 'registrationOpens' + > { date: string; + registrationOpens: string; registrationCloses: string; } diff --git a/frontend/packages/vkt/src/interfaces/publicExamEvent.ts b/frontend/packages/vkt/src/interfaces/publicExamEvent.ts index a9c008ef2..5a558e1b0 100644 --- a/frontend/packages/vkt/src/interfaces/publicExamEvent.ts +++ b/frontend/packages/vkt/src/interfaces/publicExamEvent.ts @@ -14,7 +14,10 @@ export interface PublicExamEvent extends WithId { } export interface PublicExamEventResponse - extends Omit { + extends Omit< + PublicExamEvent, + 'date' | 'registrationCloses' | 'registrationOpens' + > { date: string; registrationCloses: string; registrationOpens: string; diff --git a/frontend/packages/vkt/src/pages/ClerkExamEventCreatePage.tsx b/frontend/packages/vkt/src/pages/ClerkExamEventCreatePage.tsx index a27247f84..fabb80d02 100644 --- a/frontend/packages/vkt/src/pages/ClerkExamEventCreatePage.tsx +++ b/frontend/packages/vkt/src/pages/ClerkExamEventCreatePage.tsx @@ -16,6 +16,7 @@ import { ClerkExamHideToggle } from 'components/clerkExamEvent/create/ClerkExamH import { ClerkExamLanguageLevel } from 'components/clerkExamEvent/create/ClerkExamLanguageLevel'; import { ClerkExamMaxParticipants } from 'components/clerkExamEvent/create/ClerkExamMaxParticipants'; import { ClerkExamRegistrationCloses } from 'components/clerkExamEvent/create/ClerkExamRegistrationCloses'; +import { ClerkExamRegistrationOpens } from 'components/clerkExamEvent/create/ClerkExamRegistrationOpens'; import { useClerkTranslation, useCommonTranslation } from 'configs/i18n'; import { useAppDispatch, useAppSelector } from 'configs/redux'; import { AppRoutes } from 'enums/app'; @@ -113,6 +114,9 @@ export const ClerkExamEventCreatePage: FC = () => {
+
+
+
diff --git a/frontend/packages/vkt/src/tests/cypress/integration/clerk_create_exam_event_page.spec.ts b/frontend/packages/vkt/src/tests/cypress/integration/clerk_create_exam_event_page.spec.ts index c648a3a87..17da4af6d 100644 --- a/frontend/packages/vkt/src/tests/cypress/integration/clerk_create_exam_event_page.spec.ts +++ b/frontend/packages/vkt/src/tests/cypress/integration/clerk_create_exam_event_page.spec.ts @@ -14,12 +14,16 @@ describe('ClerkCreateExamEventPage', () => { it('should enable save when valid form is inputted', () => { const examDate = dayjs().add(daysInFuture, 'day'); const closesDate = examDate.subtract(2, 'day'); + const opensDate = examDate.subtract(3, 'day'); onClerkExamEventCreatePage.inputLanguageAndLevel('Suomi, Erinomainen'); onClerkExamEventCreatePage.inputExamDate(examDate.format(dayFormat)); onClerkExamEventCreatePage.inputRegistrationClosesDate( closesDate.format(dayFormat), ); + onClerkExamEventCreatePage.inputRegistrationOpensDate( + opensDate.format(dayFormat), + ); onClerkExamEventCreatePage.saveButtonEnabledIs(false); onClerkExamEventCreatePage.inputMaxParticipants(20); onClerkExamEventCreatePage.clickIsHiddenToggle(); diff --git a/frontend/packages/vkt/src/tests/cypress/support/page-objects/clerkCreateExamEventPage.ts b/frontend/packages/vkt/src/tests/cypress/support/page-objects/clerkCreateExamEventPage.ts index ab6c5c93c..ecfbeef83 100644 --- a/frontend/packages/vkt/src/tests/cypress/support/page-objects/clerkCreateExamEventPage.ts +++ b/frontend/packages/vkt/src/tests/cypress/support/page-objects/clerkCreateExamEventPage.ts @@ -4,9 +4,13 @@ class ClerkExamEventCreatePage { cy.findByTestId('clerk-exam__event-information__lang-and-level'), dateInput: () => cy.findByTestId('clerk-exam__event-information__date').find('input'), - registrationInput: () => + registrationClosesInput: () => cy - .findByTestId('clerk-exam__event-information__registration') + .findByTestId('clerk-exam__event-information__registration-closes') + .find('input'), + registrationOpensInput: () => + cy + .findByTestId('clerk-exam__event-information__registration-opens') .find('input'), maxParticipantsInput: () => cy.findByTestId('clerk-exam__event-information__max-participants'), @@ -30,8 +34,12 @@ class ClerkExamEventCreatePage { this.elements.dateInput().should('be.visible').type(date); } + inputRegistrationOpensDate(date: string) { + this.elements.registrationOpensInput().should('be.visible').type(date); + } + inputRegistrationClosesDate(date: string) { - this.elements.registrationInput().should('be.visible').type(date); + this.elements.registrationClosesInput().should('be.visible').type(date); } inputMaxParticipants(max: number) { diff --git a/frontend/packages/vkt/src/tests/msw/fixtures/publicExamEvents11.ts b/frontend/packages/vkt/src/tests/msw/fixtures/publicExamEvents11.ts index bc76d199c..5940ce4f6 100644 --- a/frontend/packages/vkt/src/tests/msw/fixtures/publicExamEvents11.ts +++ b/frontend/packages/vkt/src/tests/msw/fixtures/publicExamEvents11.ts @@ -6,6 +6,8 @@ export const publicExamEvents11 = [ language: ExamLanguage.FI as Exclude, date: '2022-10-01', registrationCloses: '2022-09-27', + registrationOpens: '2022-09-23', + isOpen: true, openings: 1, hasCongestion: true, }, @@ -14,6 +16,8 @@ export const publicExamEvents11 = [ language: ExamLanguage.SV as Exclude, date: '2022-10-01', registrationCloses: '2022-09-27', + registrationOpens: '2022-09-23', + isOpen: true, openings: 2, hasCongestion: false, }, @@ -22,6 +26,8 @@ export const publicExamEvents11 = [ language: ExamLanguage.FI as Exclude, date: '2022-11-18', registrationCloses: '2022-11-06', + registrationOpens: '2022-11-03', + isOpen: true, openings: 5, hasCongestion: false, }, @@ -30,6 +36,8 @@ export const publicExamEvents11 = [ language: ExamLanguage.SV as Exclude, date: '2022-11-18', registrationCloses: '2022-11-06', + registrationOpens: '2022-11-03', + isOpen: true, openings: 7, hasCongestion: false, }, @@ -38,6 +46,8 @@ export const publicExamEvents11 = [ language: ExamLanguage.FI as Exclude, date: '2022-12-24', registrationCloses: '2022-12-10', + registrationOpens: '2022-12-03', + isOpen: true, openings: 0, hasCongestion: false, }, @@ -46,6 +56,8 @@ export const publicExamEvents11 = [ language: ExamLanguage.FI as Exclude, date: '2023-02-05', registrationCloses: '2023-01-22', + registrationOpens: '2023-01-13', + isOpen: true, openings: 7, hasCongestion: false, }, @@ -54,6 +66,8 @@ export const publicExamEvents11 = [ language: ExamLanguage.SV as Exclude, date: '2023-02-05', registrationCloses: '2023-01-22', + registrationOpens: '2023-01-13', + isOpen: true, openings: 8, hasCongestion: false, }, @@ -62,6 +76,8 @@ export const publicExamEvents11 = [ language: ExamLanguage.SV as Exclude, date: '2023-03-02', registrationCloses: '2023-03-01', + registrationOpens: '2023-02-28', + isOpen: true, openings: 9, hasCongestion: false, }, @@ -70,6 +86,8 @@ export const publicExamEvents11 = [ language: ExamLanguage.FI as Exclude, date: '2023-03-30', registrationCloses: '2023-03-16', + registrationOpens: '2023-03-16', + isOpen: true, openings: 9, hasCongestion: false, }, @@ -78,6 +96,8 @@ export const publicExamEvents11 = [ language: ExamLanguage.SV as Exclude, date: '2023-03-30', registrationCloses: '2023-03-16', + registrationOpens: '2023-03-12', + isOpen: true, openings: 10, hasCongestion: false, }, @@ -86,6 +106,8 @@ export const publicExamEvents11 = [ language: ExamLanguage.FI as Exclude, date: '2023-06-15', registrationCloses: '2023-06-01', + registrationOpens: '2023-05-22', + isOpen: true, openings: 9, hasCongestion: false, }, diff --git a/frontend/packages/vkt/src/utils/serialization.ts b/frontend/packages/vkt/src/utils/serialization.ts index 03c723a2a..80f661af4 100644 --- a/frontend/packages/vkt/src/utils/serialization.ts +++ b/frontend/packages/vkt/src/utils/serialization.ts @@ -72,6 +72,7 @@ export class SerializationUtils { ...listExamEvent, date: dayjs(listExamEvent.date), registrationCloses: dayjs(listExamEvent.registrationCloses), + registrationOpens: dayjs(listExamEvent.registrationOpens), }; } @@ -114,6 +115,7 @@ export class SerializationUtils { ...examEvent, date: dayjs(examEvent.date), registrationCloses: dayjs(examEvent.registrationCloses), + registrationOpens: dayjs(examEvent.registrationOpens), enrollments: examEvent.enrollments.map( SerializationUtils.deserializeClerkEnrollment, ), @@ -124,7 +126,12 @@ export class SerializationUtils { return { ...examEvent, date: DateUtils.serializeDate(examEvent.date), - registrationCloses: DateUtils.serializeDate(examEvent.registrationCloses), + registrationCloses: DateUtils.serializeDateTime( + examEvent.registrationCloses, + ), + registrationOpens: DateUtils.serializeDateTime( + examEvent.registrationOpens, + ), }; } @@ -132,7 +139,12 @@ export class SerializationUtils { return { ...examEvent, date: DateUtils.serializeDate(examEvent.date), - registrationCloses: DateUtils.serializeDate(examEvent.registrationCloses), + registrationCloses: DateUtils.serializeDateTime( + examEvent.registrationCloses, + ), + registrationOpens: DateUtils.serializeDateTime( + examEvent.registrationOpens, + ), enrollments: examEvent.enrollments.map( SerializationUtils.serializeClerkEnrollment, ), From 500c71c45d7ed2d76ca76261da084df441d1f14e Mon Sep 17 00:00:00 2001 From: Jarkko Pesonen <435495+jrkkp@users.noreply.github.com> Date: Tue, 3 Sep 2024 09:40:02 +0300 Subject: [PATCH 004/248] VKT(Backend) test fix --- .../oph/vkt/api/dto/clerk/ClerkExamEventCreateDTO.java | 9 ++------- .../oph/vkt/api/dto/clerk/ClerkExamEventUpdateDTO.java | 9 ++------- .../fi/oph/vkt/service/ClerkExamEventServiceTest.java | 3 +++ 3 files changed, 7 insertions(+), 14 deletions(-) diff --git a/backend/vkt/src/main/java/fi/oph/vkt/api/dto/clerk/ClerkExamEventCreateDTO.java b/backend/vkt/src/main/java/fi/oph/vkt/api/dto/clerk/ClerkExamEventCreateDTO.java index 748d494db..04f7843fc 100644 --- a/backend/vkt/src/main/java/fi/oph/vkt/api/dto/clerk/ClerkExamEventCreateDTO.java +++ b/backend/vkt/src/main/java/fi/oph/vkt/api/dto/clerk/ClerkExamEventCreateDTO.java @@ -14,13 +14,8 @@ public record ClerkExamEventCreateDTO( @NonNull @NotNull ExamLanguage language, @NonNull @NotNull ExamLevel level, @NonNull @NotNull LocalDate date, - - @JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss") - @NonNull @NotNull LocalDateTime registrationCloses, - - @JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss") - @NonNull @NotNull LocalDateTime registrationOpens, - + @JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss") @NonNull @NotNull LocalDateTime registrationCloses, + @JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss") @NonNull @NotNull LocalDateTime registrationOpens, @NonNull @NotNull Boolean isHidden, @NonNull @NotNull Long maxParticipants ) diff --git a/backend/vkt/src/main/java/fi/oph/vkt/api/dto/clerk/ClerkExamEventUpdateDTO.java b/backend/vkt/src/main/java/fi/oph/vkt/api/dto/clerk/ClerkExamEventUpdateDTO.java index 86001e04d..042aaf149 100644 --- a/backend/vkt/src/main/java/fi/oph/vkt/api/dto/clerk/ClerkExamEventUpdateDTO.java +++ b/backend/vkt/src/main/java/fi/oph/vkt/api/dto/clerk/ClerkExamEventUpdateDTO.java @@ -16,13 +16,8 @@ public record ClerkExamEventUpdateDTO( @NonNull @NotNull ExamLanguage language, @NonNull @NotNull ExamLevel level, @NonNull @NotNull LocalDate date, - - @JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss") - @NonNull @NotNull LocalDateTime registrationCloses, - - @JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss") - @NonNull @NotNull LocalDateTime registrationOpens, - + @JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss") @NonNull @NotNull LocalDateTime registrationCloses, + @JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss") @NonNull @NotNull LocalDateTime registrationOpens, @NonNull @NotNull Boolean isHidden, @NonNull @NotNull Long maxParticipants ) diff --git a/backend/vkt/src/test/java/fi/oph/vkt/service/ClerkExamEventServiceTest.java b/backend/vkt/src/test/java/fi/oph/vkt/service/ClerkExamEventServiceTest.java index 03af95059..2a4fb582d 100644 --- a/backend/vkt/src/test/java/fi/oph/vkt/service/ClerkExamEventServiceTest.java +++ b/backend/vkt/src/test/java/fi/oph/vkt/service/ClerkExamEventServiceTest.java @@ -293,6 +293,7 @@ public void testCreateExamEvent() { .level(ExamLevel.EXCELLENT) .date(LocalDate.now().plusMonths(1)) .registrationCloses(LocalDateTime.now().plusWeeks(1)) + .registrationOpens(LocalDateTime.now().plusDays(3)) .isHidden(true) .maxParticipants(2L) .build(); @@ -331,6 +332,7 @@ public void testCreateExamEventFailsOnDuplicate() { clerkExamEventService.createExamEvent( duplicateDTOBuilder .registrationCloses(LocalDateTime.now().plusWeeks(1)) + .registrationOpens(LocalDateTime.now().plusDays(3)) .isHidden(true) .maxParticipants(2L) .build() @@ -410,6 +412,7 @@ private static ClerkExamEventUpdateDTO.ClerkExamEventUpdateDTOBuilder createUpda .level(examEvent.getLevel()) .date(examEvent.getDate().plusDays(1)) .registrationCloses(examEvent.getRegistrationCloses().plusDays(1)) + .registrationOpens(examEvent.getRegistrationOpens().plusDays(1)) .isHidden(!examEvent.isHidden()) .maxParticipants(examEvent.getMaxParticipants() + 1); } From a8b40e8e3ed303ca82b008a68c81461ff63af0ca Mon Sep 17 00:00:00 2001 From: Jarkko Pesonen <435495+jrkkp@users.noreply.github.com> Date: Tue, 3 Sep 2024 09:53:47 +0300 Subject: [PATCH 005/248] VKT(Backend) test fix --- .../fi/oph/vkt/service/ClerkExamEventServiceTest.java | 4 ++++ .../fi/oph/vkt/service/PublicExamEventServiceTest.java | 10 +++++++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/backend/vkt/src/test/java/fi/oph/vkt/service/ClerkExamEventServiceTest.java b/backend/vkt/src/test/java/fi/oph/vkt/service/ClerkExamEventServiceTest.java index 2a4fb582d..60b22611b 100644 --- a/backend/vkt/src/test/java/fi/oph/vkt/service/ClerkExamEventServiceTest.java +++ b/backend/vkt/src/test/java/fi/oph/vkt/service/ClerkExamEventServiceTest.java @@ -173,6 +173,10 @@ private void assertExamEventListDTODetails(final ExamEvent expected, final Clerk expected.getRegistrationCloses().truncatedTo(ChronoUnit.MINUTES), examEventListDTO.registrationCloses().truncatedTo(ChronoUnit.MINUTES) ); + assertEquals( + expected.getRegistrationOpens().truncatedTo(ChronoUnit.MINUTES), + examEventListDTO.registrationOpens().truncatedTo(ChronoUnit.MINUTES) + ); assertEquals(expected.getMaxParticipants(), examEventListDTO.maxParticipants()); } diff --git a/backend/vkt/src/test/java/fi/oph/vkt/service/PublicExamEventServiceTest.java b/backend/vkt/src/test/java/fi/oph/vkt/service/PublicExamEventServiceTest.java index dd992a5c9..3a86caa15 100644 --- a/backend/vkt/src/test/java/fi/oph/vkt/service/PublicExamEventServiceTest.java +++ b/backend/vkt/src/test/java/fi/oph/vkt/service/PublicExamEventServiceTest.java @@ -18,6 +18,7 @@ import jakarta.annotation.Resource; import java.time.LocalDate; import java.time.LocalDateTime; +import java.time.temporal.ChronoUnit; import java.util.List; import java.util.stream.IntStream; import org.junit.jupiter.api.BeforeEach; @@ -253,7 +254,14 @@ private void assertExamEventDetails(final ExamEvent expected, final PublicExamEv assertEquals(expected.getId(), examEventDTO.id()); assertEquals(expected.getLanguage(), examEventDTO.language()); assertEquals(expected.getDate(), examEventDTO.date()); - assertEquals(expected.getRegistrationCloses(), examEventDTO.registrationCloses()); + assertEquals( + expected.getRegistrationCloses().truncatedTo(ChronoUnit.MINUTES), + examEventDTO.registrationCloses().truncatedTo(ChronoUnit.MINUTES) + ); + assertEquals( + expected.getRegistrationOpens().truncatedTo(ChronoUnit.MINUTES), + examEventDTO.registrationOpens().truncatedTo(ChronoUnit.MINUTES) + ); } @Test From 5f847fc4e5293c6896583de85bb7e9139aa05d9a Mon Sep 17 00:00:00 2001 From: Jarkko Pesonen <435495+jrkkp@users.noreply.github.com> Date: Tue, 3 Sep 2024 23:15:57 +0300 Subject: [PATCH 006/248] VKT(Backend & Frontend): registration opens front page listing refresh --- .../vkt/repository/ExamEventRepository.java | 2 - .../vkt/public/i18n/fi-FI/public.json | 7 +- .../publicExamEvent/PublicExamEventGrid.tsx | 12 +++ .../listing/PublicExamEventListingHeader.tsx | 2 +- .../listing/row/PublicExamEventCells.tsx | 83 ++++++++++++++----- .../vkt/src/redux/reducers/publicExamEvent.ts | 2 + .../vkt/src/redux/sagas/publicExamEvent.ts | 2 + 7 files changed, 86 insertions(+), 24 deletions(-) diff --git a/backend/vkt/src/main/java/fi/oph/vkt/repository/ExamEventRepository.java b/backend/vkt/src/main/java/fi/oph/vkt/repository/ExamEventRepository.java index fff23b441..4c2ef5653 100644 --- a/backend/vkt/src/main/java/fi/oph/vkt/repository/ExamEventRepository.java +++ b/backend/vkt/src/main/java/fi/oph/vkt/repository/ExamEventRepository.java @@ -16,7 +16,6 @@ public interface ExamEventRepository extends BaseRepository { " LEFT JOIN e.enrollments en ON en.status = 'COMPLETED' OR en.status = 'AWAITING_PAYMENT' OR en.status = 'AWAITING_APPROVAL' OR en.status = 'EXPECTING_PAYMENT_UNFINISHED_ENROLLMENT'" + " WHERE e.level = ?1" + " AND e.registrationCloses >= CURRENT_TIMESTAMP" + - " AND e.registrationOpens <= CURRENT_TIMESTAMP" + " AND e.isHidden = false" + " GROUP BY e.id" ) @@ -28,7 +27,6 @@ public interface ExamEventRepository extends BaseRepository { " LEFT JOIN e.enrollments en ON en.status = 'QUEUED'" + " WHERE e.level = ?1" + " AND e.registrationCloses >= CURRENT_TIMESTAMP" + - " AND e.registrationOpens <= CURRENT_TIMESTAMP" + " AND e.isHidden = false" + " GROUP BY e.id" + " HAVING COUNT(en) > 0" diff --git a/frontend/packages/vkt/public/i18n/fi-FI/public.json b/frontend/packages/vkt/public/i18n/fi-FI/public.json index 685789cde..7c0e5fc22 100644 --- a/frontend/packages/vkt/public/i18n/fi-FI/public.json +++ b/frontend/packages/vkt/public/i18n/fi-FI/public.json @@ -277,7 +277,8 @@ "examDate": "Tutkintopäivä", "language": "Kieli ja taso", "openings": "Paikkoja vapaana", - "registrationCloses": "Ilmoittautuminen sulkeutuu" + "registrationCloses": "Ilmoittautuminen sulkeutuu", + "registrationDates": "Ilmoittautumisaika" }, "openings": { "congestion": { @@ -290,9 +291,11 @@ } }, "row": { + "registrationTimeFormat": "L [klo.] HH:mm", "enroll": "Ilmoittaudu", "enrollLater": "Ilmoittaudu myöhemmin", - "enrollToQueue": "Ilmoittaudu jonoon" + "enrollToQueue": "Ilmoittaudu jonoon", + "registrationOpensAt": "Ilmoittautuminen alkaa {{registrationOpens}} " }, "title": "Tulevat tutkintotilaisuudet" }, diff --git a/frontend/packages/vkt/src/components/publicExamEvent/PublicExamEventGrid.tsx b/frontend/packages/vkt/src/components/publicExamEvent/PublicExamEventGrid.tsx index 3043c2524..f07bb7dc3 100644 --- a/frontend/packages/vkt/src/components/publicExamEvent/PublicExamEventGrid.tsx +++ b/frontend/packages/vkt/src/components/publicExamEvent/PublicExamEventGrid.tsx @@ -2,10 +2,12 @@ import OpenInNewIcon from '@mui/icons-material/OpenInNew'; import { Grid, Typography } from '@mui/material'; import { useEffect } from 'react'; import { H1, H2, HeaderSeparator, Text, WebLink } from 'shared/components'; +import { APIResponseStatus } from 'shared/enums'; import { PublicExamEventListing } from 'components/publicExamEvent/listing/PublicExamEventListing'; import { useCommonTranslation, usePublicTranslation } from 'configs/i18n'; import { useAppDispatch, useAppSelector } from 'configs/redux'; +import { useInterval } from 'hooks/useInterval'; import { resetPublicEnrollment } from 'redux/reducers/publicEnrollment'; import { loadPublicExamEvents } from 'redux/reducers/publicExamEvent'; import { publicExamEventsSelector } from 'redux/selectors/publicExamEvent'; @@ -34,6 +36,16 @@ export const PublicExamEventGrid = () => { dispatch(loadPublicExamEvents()); }, [dispatch]); + const listingRefresh = () => { + if (status === APIResponseStatus.Success) { + if (!document.hidden) { + dispatch(loadPublicExamEvents()); + } + } + }; + + useInterval(listingRefresh, 10000); // Every 10 seconds + return ( <> diff --git a/frontend/packages/vkt/src/components/publicExamEvent/listing/PublicExamEventListingHeader.tsx b/frontend/packages/vkt/src/components/publicExamEvent/listing/PublicExamEventListingHeader.tsx index c92c6042a..e56643bb9 100644 --- a/frontend/packages/vkt/src/components/publicExamEvent/listing/PublicExamEventListingHeader.tsx +++ b/frontend/packages/vkt/src/components/publicExamEvent/listing/PublicExamEventListingHeader.tsx @@ -17,7 +17,7 @@ export const PublicExamEventListingHeader = () => { {t('language')} {t('examDate')} - {t('registrationCloses')} + {t('registrationDates')} {t('openings')} {t('actions')} diff --git a/frontend/packages/vkt/src/components/publicExamEvent/listing/row/PublicExamEventCells.tsx b/frontend/packages/vkt/src/components/publicExamEvent/listing/row/PublicExamEventCells.tsx index a26520191..74e32a801 100644 --- a/frontend/packages/vkt/src/components/publicExamEvent/listing/row/PublicExamEventCells.tsx +++ b/frontend/packages/vkt/src/components/publicExamEvent/listing/row/PublicExamEventCells.tsx @@ -1,4 +1,5 @@ import { TableCell } from '@mui/material'; +import { Trans } from 'react-i18next'; import { useNavigate } from 'react-router-dom'; import { CustomButton, @@ -74,7 +75,7 @@ export const PublicExamEventPhoneCells = ({ }: { examEvent: PublicExamEvent; }) => { - const { language, date, registrationCloses } = examEvent; + const { language, date, registrationCloses, registrationOpens } = examEvent; // I18n const { t } = usePublicTranslation({ @@ -115,20 +116,41 @@ export const PublicExamEventPhoneCells = ({ {DateUtils.formatOptionalDate(date, 'l')}
- {t('header.registrationCloses')} - {DateUtils.formatOptionalDate(registrationCloses, 'l')} + {t('header.registrationDates')} + + {DateUtils.formatOptionalDateTime( + registrationOpens, + t('row.registrationTimeFormat'), + )} + - +
+ {DateUtils.formatOptionalDateTime(registrationCloses)} +
{t('header.openings')} {getOpeningsText(examEvent, t)}
- {renderEnrollmentButton( - examEvent, - examEvent === selectedExamEvent, - examEvent.hasCongestion || isInitialisationInProgress, - handleOnClick, - t, - translateCommon, + {!examEvent.isOpen ? ( + + ) : ( + renderEnrollmentButton( + examEvent, + examEvent === selectedExamEvent, + examEvent.hasCongestion || isInitialisationInProgress, + handleOnClick, + t, + translateCommon, + ) )}
@@ -140,7 +162,7 @@ export const PublicExamEventDesktopCells = ({ }: { examEvent: PublicExamEvent; }) => { - const { language, date, registrationCloses } = examEvent; + const { language, date, registrationCloses, registrationOpens } = examEvent; // I18n const { t } = usePublicTranslation({ @@ -178,17 +200,40 @@ export const PublicExamEventDesktopCells = ({ {DateUtils.formatOptionalDate(date, 'l')} - {DateUtils.formatOptionalDate(registrationCloses, 'l')} + + {DateUtils.formatOptionalDateTime( + registrationOpens, + t('row.registrationTimeFormat'), + )}{' '} + -
+ {DateUtils.formatOptionalDateTime( + registrationCloses, + t('row.registrationTimeFormat'), + )} +
{getOpeningsText(examEvent, t)} - {renderEnrollmentButton( - examEvent, - examEvent === selectedExamEvent, - examEvent.hasCongestion || isInitialisationInProgress, - handleOnClick, - t, - translateCommon, + {!examEvent.isOpen ? ( + + ) : ( + renderEnrollmentButton( + examEvent, + examEvent === selectedExamEvent, + examEvent.hasCongestion || isInitialisationInProgress, + handleOnClick, + t, + translateCommon, + ) )} diff --git a/frontend/packages/vkt/src/redux/reducers/publicExamEvent.ts b/frontend/packages/vkt/src/redux/reducers/publicExamEvent.ts index 0763e4f3f..653f7fa5f 100644 --- a/frontend/packages/vkt/src/redux/reducers/publicExamEvent.ts +++ b/frontend/packages/vkt/src/redux/reducers/publicExamEvent.ts @@ -23,6 +23,7 @@ const publicExamEventSlice = createSlice({ loadPublicExamEvents(state) { state.status = APIResponseStatus.InProgress; }, + refreshPublicExamEvents() {}, rejectPublicExamEvents(state) { state.status = APIResponseStatus.Error; }, @@ -48,6 +49,7 @@ const publicExamEventSlice = createSlice({ export const publicExamEventReducer = publicExamEventSlice.reducer; export const { loadPublicExamEvents, + refreshPublicExamEvents, rejectPublicExamEvents, resetPublicExamEventSelections, storePublicExamEvents, diff --git a/frontend/packages/vkt/src/redux/sagas/publicExamEvent.ts b/frontend/packages/vkt/src/redux/sagas/publicExamEvent.ts index 544e55c7a..a34f135bf 100644 --- a/frontend/packages/vkt/src/redux/sagas/publicExamEvent.ts +++ b/frontend/packages/vkt/src/redux/sagas/publicExamEvent.ts @@ -6,6 +6,7 @@ import { APIEndpoints } from 'enums/api'; import { PublicExamEventResponse } from 'interfaces/publicExamEvent'; import { loadPublicExamEvents, + refreshPublicExamEvents, rejectPublicExamEvents, storePublicExamEvents, } from 'redux/reducers/publicExamEvent'; @@ -28,4 +29,5 @@ function* loadPublicExamEventsSaga() { export function* watchPublicExamEvents() { yield takeLatest(loadPublicExamEvents.type, loadPublicExamEventsSaga); + yield takeLatest(refreshPublicExamEvents.type, loadPublicExamEventsSaga); } From 04574e9ca9dcfc9e8552354b7a3c3fe3b1772aae Mon Sep 17 00:00:00 2001 From: Jarkko Pesonen <435495+jrkkp@users.noreply.github.com> Date: Wed, 4 Sep 2024 12:26:02 +0300 Subject: [PATCH 007/248] VKT(Backend & Frontend): registration opens front page listing refresh --- .../oph/vkt/service/PublicExamEventServiceTest.java | 11 +++++++++-- .../publicExamEvent/PublicExamEventGrid.tsx | 7 +++++-- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/backend/vkt/src/test/java/fi/oph/vkt/service/PublicExamEventServiceTest.java b/backend/vkt/src/test/java/fi/oph/vkt/service/PublicExamEventServiceTest.java index 3a86caa15..a1e6f8390 100644 --- a/backend/vkt/src/test/java/fi/oph/vkt/service/PublicExamEventServiceTest.java +++ b/backend/vkt/src/test/java/fi/oph/vkt/service/PublicExamEventServiceTest.java @@ -16,7 +16,6 @@ import fi.oph.vkt.repository.ExamEventRepository; import fi.oph.vkt.repository.ReservationRepository; import jakarta.annotation.Resource; -import java.time.LocalDate; import java.time.LocalDateTime; import java.time.temporal.ChronoUnit; import java.util.List; @@ -117,10 +116,11 @@ public void testListExcellentLevelExamEvents() { createReservations(futureEvent2, 2, LocalDateTime.now().plusMinutes(1)); final List examEventDTOs = publicExamEventService.listExamEvents(ExamLevel.EXCELLENT); - assertEquals(6, examEventDTOs.size()); + assertEquals(7, examEventDTOs.size()); final List expectedExamEventsOrdered = List.of( eventToday, + futureEventNotOpen, upcomingEventFi, upcomingEventSv, futureEvent1, @@ -140,12 +140,19 @@ public void testListExcellentLevelExamEvents() { if (expected == futureEvent1) { assertEquals(3, dto.openings()); assertFalse(dto.hasCongestion(), "futureEvent1 should not have congestion"); + assertTrue(dto.isOpen()); } else if (expected == futureEvent2) { assertEquals(2, dto.openings()); assertTrue(dto.hasCongestion(), "futureEvent2 should have congestion"); + assertTrue(dto.isOpen()); + } else if (expected == futureEventNotOpen) { + assertEquals(expected.getMaxParticipants(), dto.openings()); + assertFalse(dto.hasCongestion()); + assertFalse(dto.isOpen(), "futureEventNotOpen should have isOpen = false"); } else { assertEquals(expected.getMaxParticipants(), dto.openings()); assertFalse(dto.hasCongestion()); + assertTrue(dto.isOpen()); } }); } diff --git a/frontend/packages/vkt/src/components/publicExamEvent/PublicExamEventGrid.tsx b/frontend/packages/vkt/src/components/publicExamEvent/PublicExamEventGrid.tsx index f07bb7dc3..880c24d3d 100644 --- a/frontend/packages/vkt/src/components/publicExamEvent/PublicExamEventGrid.tsx +++ b/frontend/packages/vkt/src/components/publicExamEvent/PublicExamEventGrid.tsx @@ -9,7 +9,10 @@ import { useCommonTranslation, usePublicTranslation } from 'configs/i18n'; import { useAppDispatch, useAppSelector } from 'configs/redux'; import { useInterval } from 'hooks/useInterval'; import { resetPublicEnrollment } from 'redux/reducers/publicEnrollment'; -import { loadPublicExamEvents } from 'redux/reducers/publicExamEvent'; +import { + loadPublicExamEvents, + refreshPublicExamEvents, +} from 'redux/reducers/publicExamEvent'; import { publicExamEventsSelector } from 'redux/selectors/publicExamEvent'; const BulletList = ({ points }: { points: Array }) => { @@ -39,7 +42,7 @@ export const PublicExamEventGrid = () => { const listingRefresh = () => { if (status === APIResponseStatus.Success) { if (!document.hidden) { - dispatch(loadPublicExamEvents()); + dispatch(refreshPublicExamEvents()); } } }; From 8e47a779b06b24f9465ff815c2afec6c1942429b Mon Sep 17 00:00:00 2001 From: Jarkko Pesonen <435495+jrkkp@users.noreply.github.com> Date: Wed, 4 Sep 2024 21:50:28 +0300 Subject: [PATCH 008/248] VKT(Backend & Frontend): some small tweaks and fixes --- .../packages/shared/src/utils/date/date.ts | 10 +++++ .../packages/vkt/public/i18n/fi-FI/clerk.json | 5 ++- .../vkt/public/i18n/fi-FI/public.json | 1 - .../create/ClerkExamRegistrationCloses.tsx | 6 +-- .../listing/ClerkExamEventListingHeader.tsx | 3 +- .../listing/ClerkExamEventListingRow.tsx | 5 ++- .../overview/ClerkExamEventDetailsFields.tsx | 39 +++++++++++++------ .../listing/row/PublicExamEventCells.tsx | 36 ++++++----------- frontend/packages/vkt/src/utils/dateTime.ts | 4 ++ 9 files changed, 65 insertions(+), 44 deletions(-) diff --git a/frontend/packages/shared/src/utils/date/date.ts b/frontend/packages/shared/src/utils/date/date.ts index ffa05b770..5a5ecb541 100644 --- a/frontend/packages/shared/src/utils/date/date.ts +++ b/frontend/packages/shared/src/utils/date/date.ts @@ -29,6 +29,16 @@ export class DateUtils { } } + static formatOptionalTime(date?: Dayjs, format = 'HH:mm') { + if (!date) { + return '-'; + } + + // Locale information is baked into the Dayjs instances when they are constructed. + // We need to override the instance's locale with the locale used by the app when formating the date. + return date.locale(dayjs.locale()).format(format); + } + static formatOptionalDate(date?: Dayjs, format = 'L') { if (!date) { return '-'; diff --git a/frontend/packages/vkt/public/i18n/fi-FI/clerk.json b/frontend/packages/vkt/public/i18n/fi-FI/clerk.json index 0fa2223b0..a5d505cd0 100644 --- a/frontend/packages/vkt/public/i18n/fi-FI/clerk.json +++ b/frontend/packages/vkt/public/i18n/fi-FI/clerk.json @@ -125,7 +125,8 @@ "hidden": "Piilotettu", "language": "Kieli ja taso", "registrationCloses": "Ilmoittautuminen sulkeutuu", - "registrationOpens": "Ilmoittautuminen avautuu" + "registrationOpens": "Ilmoittautuminen avautuu", + "registrationDates": "Ilmoittautumisaika" }, "more": "Lisätietoja", "title": "Tutkintotilaisuudet", @@ -148,6 +149,8 @@ "language": "Kieli", "level": "Taso", "registrationCloses": "Ilmoittautuminen sulkeutuu", + "registrationClosesTime": "klo 16.00", + "registrationOpensTime": "klo 10.00", "registrationOpens": "Ilmoittautuminen avautuu" }, "examEventListingHeader": { diff --git a/frontend/packages/vkt/public/i18n/fi-FI/public.json b/frontend/packages/vkt/public/i18n/fi-FI/public.json index 7c0e5fc22..a10a55ad1 100644 --- a/frontend/packages/vkt/public/i18n/fi-FI/public.json +++ b/frontend/packages/vkt/public/i18n/fi-FI/public.json @@ -291,7 +291,6 @@ } }, "row": { - "registrationTimeFormat": "L [klo.] HH:mm", "enroll": "Ilmoittaudu", "enrollLater": "Ilmoittaudu myöhemmin", "enrollToQueue": "Ilmoittaudu jonoon", diff --git a/frontend/packages/vkt/src/components/clerkExamEvent/create/ClerkExamRegistrationCloses.tsx b/frontend/packages/vkt/src/components/clerkExamEvent/create/ClerkExamRegistrationCloses.tsx index f80f63c1c..4b4ed7a48 100644 --- a/frontend/packages/vkt/src/components/clerkExamEvent/create/ClerkExamRegistrationCloses.tsx +++ b/frontend/packages/vkt/src/components/clerkExamEvent/create/ClerkExamRegistrationCloses.tsx @@ -12,7 +12,7 @@ export const ClerkExamRegistrationCloses = ({ examForm: DraftClerkExamEvent; }) => { const { t } = useClerkTranslation({ - keyPrefix: 'vkt.component.clerkExamEventListing', + keyPrefix: 'vkt.component.clerkExamEventOverview.examEventDetailsFields', }); const translateCommon = useCommonTranslation(); const dispatch = useAppDispatch(); @@ -30,7 +30,7 @@ export const ClerkExamRegistrationCloses = ({ className="rows gapped" data-testid="clerk-exam__event-information__registration-closes" > -

{t('header.registrationCloses')}

+

{t('registrationCloses')}

- Klo 16:00 + {t('registrationClosesTime')}
); diff --git a/frontend/packages/vkt/src/components/clerkExamEvent/listing/ClerkExamEventListingHeader.tsx b/frontend/packages/vkt/src/components/clerkExamEvent/listing/ClerkExamEventListingHeader.tsx index bb3c00bec..bef12ed2f 100644 --- a/frontend/packages/vkt/src/components/clerkExamEvent/listing/ClerkExamEventListingHeader.tsx +++ b/frontend/packages/vkt/src/components/clerkExamEvent/listing/ClerkExamEventListingHeader.tsx @@ -12,9 +12,10 @@ export const ClerkExamEventListingHeader = () => { {t('language')} {t('examDate')} - {t('registrationCloses')} + {t('registrationDates')} {t('fillings')} {t('hidden')} + ); diff --git a/frontend/packages/vkt/src/components/clerkExamEvent/listing/ClerkExamEventListingRow.tsx b/frontend/packages/vkt/src/components/clerkExamEvent/listing/ClerkExamEventListingRow.tsx index 798da6921..0a2235b5b 100644 --- a/frontend/packages/vkt/src/components/clerkExamEvent/listing/ClerkExamEventListingRow.tsx +++ b/frontend/packages/vkt/src/components/clerkExamEvent/listing/ClerkExamEventListingRow.tsx @@ -53,7 +53,10 @@ export const ClerkExamEventListingRow = ({ {DateTimeUtils.renderDate(examEvent.date)} - {DateTimeUtils.renderDate(examEvent.registrationCloses)} + + {DateTimeUtils.renderDateTime(examEvent.registrationOpens)} -
+ {DateTimeUtils.renderDateTime(examEvent.registrationCloses)} +
{`${examEvent.participants} / ${examEvent.maxParticipants}`} diff --git a/frontend/packages/vkt/src/components/clerkExamEvent/overview/ClerkExamEventDetailsFields.tsx b/frontend/packages/vkt/src/components/clerkExamEvent/overview/ClerkExamEventDetailsFields.tsx index d8e38daec..855b82a39 100644 --- a/frontend/packages/vkt/src/components/clerkExamEvent/overview/ClerkExamEventDetailsFields.tsx +++ b/frontend/packages/vkt/src/components/clerkExamEvent/overview/ClerkExamEventDetailsFields.tsx @@ -16,6 +16,7 @@ import { ClerkExamEvent, ClerkExamEventBasicInformation, } from 'interfaces/clerkExamEvent'; +import { DateTimeUtils } from 'utils/dateTime'; export const ClerkExamEventDetailsFields = ({ examEvent, @@ -116,24 +117,38 @@ export const ClerkExamEventDetailsFields = ({ data-testid="clerk-exam-event__basic-information__registrationOpens" >

{t('registrationOpens')}

- +
+ + onDateChange('registrationOpens')( + value?.hour(10).minute(0) ?? null, + ) + } + maxDate={examEvent.registrationCloses} + disabled={editDisabled} + /> + {DateTimeUtils.renderTime(examEvent.registrationOpens)} +

{t('registrationCloses')}

- +
+ + onDateChange('registrationCloses')( + value?.hour(16).minute(0) ?? null, + ) + } + maxDate={examEvent.date && examEvent.date.subtract(1, 'd')} + disabled={editDisabled} + /> + {DateTimeUtils.renderTime(examEvent.registrationCloses)} +
diff --git a/frontend/packages/vkt/src/components/publicExamEvent/listing/row/PublicExamEventCells.tsx b/frontend/packages/vkt/src/components/publicExamEvent/listing/row/PublicExamEventCells.tsx index 74e32a801..4de42c0b7 100644 --- a/frontend/packages/vkt/src/components/publicExamEvent/listing/row/PublicExamEventCells.tsx +++ b/frontend/packages/vkt/src/components/publicExamEvent/listing/row/PublicExamEventCells.tsx @@ -7,7 +7,6 @@ import { Text, } from 'shared/components'; import { APIResponseStatus, Color, Variant } from 'shared/enums'; -import { DateUtils } from 'shared/utils'; import { useCommonTranslation, usePublicTranslation } from 'configs/i18n'; import { useAppDispatch, useAppSelector } from 'configs/redux'; @@ -15,6 +14,7 @@ import { AppRoutes, ExamLevel } from 'enums/app'; import { PublicExamEvent } from 'interfaces/publicExamEvent'; import { storePublicExamEvent } from 'redux/reducers/publicEnrollment'; import { publicEnrollmentSelector } from 'redux/selectors/publicEnrollment'; +import { DateTimeUtils } from 'utils/dateTime'; import { ExamEventUtils } from 'utils/examEvent'; const getOpeningsText = ( @@ -113,18 +113,15 @@ export const PublicExamEventPhoneCells = ({
{t('header.examDate')} - {DateUtils.formatOptionalDate(date, 'l')} + {DateTimeUtils.renderDate(date)}
{t('header.registrationDates')} - {DateUtils.formatOptionalDateTime( - registrationOpens, - t('row.registrationTimeFormat'), - )} + {DateTimeUtils.renderDateTime(registrationOpens)} -
- {DateUtils.formatOptionalDateTime(registrationCloses)} + {DateTimeUtils.renderDateTime(registrationCloses)}
@@ -136,10 +133,8 @@ export const PublicExamEventPhoneCells = ({ t={t} i18nKey="row.registrationOpensAt" values={{ - registrationOpens: DateUtils.formatOptionalDateTime( - registrationOpens, - t('row.registrationTimeFormat'), - ), + registrationOpens: + DateTimeUtils.renderDateTime(registrationOpens), }} /> ) : ( @@ -197,19 +192,12 @@ export const PublicExamEventDesktopCells = ({ - {DateUtils.formatOptionalDate(date, 'l')} + {DateTimeUtils.renderDate(date)} - {DateUtils.formatOptionalDateTime( - registrationOpens, - t('row.registrationTimeFormat'), - )}{' '} - -
- {DateUtils.formatOptionalDateTime( - registrationCloses, - t('row.registrationTimeFormat'), - )} + {DateTimeUtils.renderDateTime(registrationOpens)} -
+ {DateTimeUtils.renderDateTime(registrationCloses)}
{getOpeningsText(examEvent, t)} @@ -219,10 +207,8 @@ export const PublicExamEventDesktopCells = ({ t={t} i18nKey="row.registrationOpensAt" values={{ - registrationOpens: DateUtils.formatOptionalDateTime( - registrationOpens, - t('row.registrationTimeFormat'), - ), + registrationOpens: + DateTimeUtils.renderDateTime(registrationOpens), }} /> ) : ( diff --git a/frontend/packages/vkt/src/utils/dateTime.ts b/frontend/packages/vkt/src/utils/dateTime.ts index f02832863..d25bb1791 100644 --- a/frontend/packages/vkt/src/utils/dateTime.ts +++ b/frontend/packages/vkt/src/utils/dateTime.ts @@ -13,6 +13,10 @@ export class DateTimeUtils { ); } + static renderTime(dateTime?: Dayjs) { + return DateUtils.formatOptionalTime(dateTime); + } + static renderDate(dateTime?: Dayjs) { const t = translateOutsideComponent(); From 976661882cb29364eca6f59be6d717263d2ef33a Mon Sep 17 00:00:00 2001 From: Jarkko Pesonen <435495+jrkkp@users.noreply.github.com> Date: Wed, 11 Sep 2024 16:43:22 +0300 Subject: [PATCH 009/248] VKT(Frontend & Backend): review fixes --- .../oph/vkt/api/dto/PublicExamEventDTO.java | 4 +-- .../vkt/service/PublicEnrollmentService.java | 6 ++--- .../vkt/service/PublicExamEventService.java | 9 ++++--- .../vkt/public/i18n/fi-FI/common.json | 5 +++- .../create/ClerkExamRegistrationCloses.tsx | 6 ++--- .../create/ClerkExamRegistrationOpens.tsx | 6 ++--- .../listing/ClerkExamEventListingRow.tsx | 2 +- .../overview/ClerkExamEventDetailsFields.tsx | 11 +++++--- .../PublicEnrollmentExamEventDetails.tsx | 6 ++--- .../listing/row/PublicExamEventCells.tsx | 10 +++---- frontend/packages/vkt/src/utils/dateTime.ts | 27 ++++++++++++++++++- 11 files changed, 62 insertions(+), 30 deletions(-) diff --git a/backend/vkt/src/main/java/fi/oph/vkt/api/dto/PublicExamEventDTO.java b/backend/vkt/src/main/java/fi/oph/vkt/api/dto/PublicExamEventDTO.java index 311a70a7b..b1e4bf88a 100644 --- a/backend/vkt/src/main/java/fi/oph/vkt/api/dto/PublicExamEventDTO.java +++ b/backend/vkt/src/main/java/fi/oph/vkt/api/dto/PublicExamEventDTO.java @@ -11,8 +11,8 @@ public record PublicExamEventDTO( @NonNull Long id, @NonNull ExamLanguage language, @NonNull LocalDate date, - @NonNull LocalDateTime registrationCloses, - @NonNull LocalDateTime registrationOpens, + @NonNull LocalDate registrationCloses, + @NonNull LocalDate registrationOpens, @NonNull Long openings, @NonNull Boolean isOpen, @NonNull Boolean hasCongestion diff --git a/backend/vkt/src/main/java/fi/oph/vkt/service/PublicEnrollmentService.java b/backend/vkt/src/main/java/fi/oph/vkt/service/PublicEnrollmentService.java index 45d5ce894..89950e1f4 100644 --- a/backend/vkt/src/main/java/fi/oph/vkt/service/PublicEnrollmentService.java +++ b/backend/vkt/src/main/java/fi/oph/vkt/service/PublicEnrollmentService.java @@ -211,8 +211,8 @@ private PublicEnrollmentInitialisationDTO createEnrollmentInitialisationDTO( .id(examEvent.getId()) .language(examEvent.getLanguage()) .date(examEvent.getDate()) - .registrationCloses(examEvent.getRegistrationCloses()) - .registrationOpens(examEvent.getRegistrationOpens()) + .registrationCloses(examEvent.getRegistrationCloses().toLocalDate()) + .registrationOpens(examEvent.getRegistrationOpens().toLocalDate()) .openings(openings) .hasCongestion(false) .isOpen(ExamEventUtil.isOpen(examEvent)) @@ -573,7 +573,7 @@ public Map getPresignedPostRequest( ) { final ExamEvent examEvent = examEventRepository.getReferenceById(examEventId); - if (person == null || examEvent.getRegistrationCloses().isBefore(LocalDate.now())) { + if (person == null || examEvent.getRegistrationCloses().isBefore(LocalDateTime.now())) { throw new NotFoundException("Uploading not allowed. Person is null or exam is closed"); } diff --git a/backend/vkt/src/main/java/fi/oph/vkt/service/PublicExamEventService.java b/backend/vkt/src/main/java/fi/oph/vkt/service/PublicExamEventService.java index 1a41c39f7..bd0723c4a 100644 --- a/backend/vkt/src/main/java/fi/oph/vkt/service/PublicExamEventService.java +++ b/backend/vkt/src/main/java/fi/oph/vkt/service/PublicExamEventService.java @@ -38,10 +38,11 @@ public PublicExamEventDTO getExamEvent(final long examEventId) { .id(examEvent.getId()) .language(examEvent.getLanguage()) .date(examEvent.getDate()) - .registrationCloses(examEvent.getRegistrationCloses()) - .registrationOpens(examEvent.getRegistrationOpens()) + .registrationCloses(examEvent.getRegistrationCloses().toLocalDate()) + .registrationOpens(examEvent.getRegistrationOpens().toLocalDate()) .openings(ExamEventUtil.getOpenings(examEvent)) .hasCongestion(ExamEventUtil.isCongested(examEvent)) + .isOpen(ExamEventUtil.isOpen(examEvent.getRegistrationCloses(), examEvent.getRegistrationOpens())) .build(); } @@ -66,8 +67,8 @@ public List listExamEvents(final ExamLevel level) { .id(e.id()) .language(e.language()) .date(e.date()) - .registrationCloses(e.registrationCloses()) - .registrationOpens(e.registrationOpens()) + .registrationCloses(e.registrationCloses().toLocalDate()) + .registrationOpens(e.registrationOpens().toLocalDate()) .openings(openings) .hasCongestion(hasCongestion) .isOpen(ExamEventUtil.isOpen(e.registrationCloses(), e.registrationOpens())) diff --git a/frontend/packages/vkt/public/i18n/fi-FI/common.json b/frontend/packages/vkt/public/i18n/fi-FI/common.json index 71db5ac5f..69348cbbd 100644 --- a/frontend/packages/vkt/public/i18n/fi-FI/common.json +++ b/frontend/packages/vkt/public/i18n/fi-FI/common.json @@ -46,7 +46,10 @@ }, "dates": { "dateTimeFormat": "l [klo] HH.mm", - "dateFormat": "l" + "timeFormat": "[klo] HH.mm", + "dateFormat": "l", + "registrationOpensAt": "klo 10:00", + "registrationClosesAt": "klo 16:00" }, "errors": { "api": { diff --git a/frontend/packages/vkt/src/components/clerkExamEvent/create/ClerkExamRegistrationCloses.tsx b/frontend/packages/vkt/src/components/clerkExamEvent/create/ClerkExamRegistrationCloses.tsx index 4b4ed7a48..8312fc4cc 100644 --- a/frontend/packages/vkt/src/components/clerkExamEvent/create/ClerkExamRegistrationCloses.tsx +++ b/frontend/packages/vkt/src/components/clerkExamEvent/create/ClerkExamRegistrationCloses.tsx @@ -1,5 +1,5 @@ import dayjs, { Dayjs } from 'dayjs'; -import { CustomDatePicker, H3 } from 'shared/components'; +import { CustomDatePicker, H3, Text } from 'shared/components'; import { useClerkTranslation, useCommonTranslation } from 'configs/i18n'; import { useAppDispatch } from 'configs/redux'; @@ -31,7 +31,7 @@ export const ClerkExamRegistrationCloses = ({ data-testid="clerk-exam__event-information__registration-closes" >

{t('registrationCloses')}

-
+
- {t('registrationClosesTime')} + {translateCommon('dates.registrationClosesAt')}
); diff --git a/frontend/packages/vkt/src/components/clerkExamEvent/create/ClerkExamRegistrationOpens.tsx b/frontend/packages/vkt/src/components/clerkExamEvent/create/ClerkExamRegistrationOpens.tsx index 76341e2e2..2cd58f085 100644 --- a/frontend/packages/vkt/src/components/clerkExamEvent/create/ClerkExamRegistrationOpens.tsx +++ b/frontend/packages/vkt/src/components/clerkExamEvent/create/ClerkExamRegistrationOpens.tsx @@ -1,5 +1,5 @@ import dayjs, { Dayjs } from 'dayjs'; -import { CustomDatePicker, H3 } from 'shared/components'; +import { CustomDatePicker, H3, Text } from 'shared/components'; import { useClerkTranslation, useCommonTranslation } from 'configs/i18n'; import { useAppDispatch } from 'configs/redux'; @@ -31,7 +31,7 @@ export const ClerkExamRegistrationOpens = ({ data-testid="clerk-exam__event-information__registration-opens" >

{t('header.registrationOpens')}

-
+
- Klo 10:00 + {translateCommon('dates.registrationOpensAt')}
); diff --git a/frontend/packages/vkt/src/components/clerkExamEvent/listing/ClerkExamEventListingRow.tsx b/frontend/packages/vkt/src/components/clerkExamEvent/listing/ClerkExamEventListingRow.tsx index 0a2235b5b..5c65d4177 100644 --- a/frontend/packages/vkt/src/components/clerkExamEvent/listing/ClerkExamEventListingRow.tsx +++ b/frontend/packages/vkt/src/components/clerkExamEvent/listing/ClerkExamEventListingRow.tsx @@ -54,7 +54,7 @@ export const ClerkExamEventListingRow = ({ - {DateTimeUtils.renderDateTime(examEvent.registrationOpens)} -
+ {DateTimeUtils.renderDateTime(examEvent.registrationOpens)} —
{DateTimeUtils.renderDateTime(examEvent.registrationCloses)}
diff --git a/frontend/packages/vkt/src/components/clerkExamEvent/overview/ClerkExamEventDetailsFields.tsx b/frontend/packages/vkt/src/components/clerkExamEvent/overview/ClerkExamEventDetailsFields.tsx index 855b82a39..e814ff977 100644 --- a/frontend/packages/vkt/src/components/clerkExamEvent/overview/ClerkExamEventDetailsFields.tsx +++ b/frontend/packages/vkt/src/components/clerkExamEvent/overview/ClerkExamEventDetailsFields.tsx @@ -6,6 +6,7 @@ import { CustomSwitch, CustomTextField, H3, + Text, } from 'shared/components'; import { TextFieldTypes, TextFieldVariant } from 'shared/enums'; import { ComboBoxOption } from 'shared/interfaces'; @@ -117,7 +118,7 @@ export const ClerkExamEventDetailsFields = ({ data-testid="clerk-exam-event__basic-information__registrationOpens" >

{t('registrationOpens')}

-
+
@@ -128,7 +129,7 @@ export const ClerkExamEventDetailsFields = ({ maxDate={examEvent.registrationCloses} disabled={editDisabled} /> - {DateTimeUtils.renderTime(examEvent.registrationOpens)} + {DateTimeUtils.renderTime(examEvent.registrationOpens)}

{t('registrationCloses')}

-
+
@@ -147,7 +148,9 @@ export const ClerkExamEventDetailsFields = ({ maxDate={examEvent.date && examEvent.date.subtract(1, 'd')} disabled={editDisabled} /> - {DateTimeUtils.renderTime(examEvent.registrationCloses)} + + {DateTimeUtils.renderTime(examEvent.registrationCloses)} +
diff --git a/frontend/packages/vkt/src/components/publicEnrollment/PublicEnrollmentExamEventDetails.tsx b/frontend/packages/vkt/src/components/publicEnrollment/PublicEnrollmentExamEventDetails.tsx index 77654bc78..a28e628a3 100644 --- a/frontend/packages/vkt/src/components/publicEnrollment/PublicEnrollmentExamEventDetails.tsx +++ b/frontend/packages/vkt/src/components/publicEnrollment/PublicEnrollmentExamEventDetails.tsx @@ -1,9 +1,9 @@ import { Text } from 'shared/components'; -import { DateUtils } from 'shared/utils'; import { useCommonTranslation, usePublicTranslation } from 'configs/i18n'; import { ExamLevel } from 'enums/app'; import { PublicExamEvent } from 'interfaces/publicExamEvent'; +import { DateTimeUtils } from 'utils/dateTime'; import { ExamEventUtils } from 'utils/examEvent'; export const PublicEnrollmentExamEventDetails = ({ @@ -42,12 +42,12 @@ export const PublicEnrollmentExamEventDetails = ({ {t('examDate')} {': '} - {DateUtils.formatOptionalDate(examEvent.date, 'l')} + {DateTimeUtils.renderDate(examEvent.date)} {t('registrationCloses')} {': '} - {DateUtils.formatOptionalDate(examEvent.registrationCloses, 'l')} + {DateTimeUtils.renderCloseDateTime(examEvent.registrationCloses)} {showOpenings && ( diff --git a/frontend/packages/vkt/src/components/publicExamEvent/listing/row/PublicExamEventCells.tsx b/frontend/packages/vkt/src/components/publicExamEvent/listing/row/PublicExamEventCells.tsx index 4de42c0b7..98841d082 100644 --- a/frontend/packages/vkt/src/components/publicExamEvent/listing/row/PublicExamEventCells.tsx +++ b/frontend/packages/vkt/src/components/publicExamEvent/listing/row/PublicExamEventCells.tsx @@ -118,10 +118,10 @@ export const PublicExamEventPhoneCells = ({
{t('header.registrationDates')} - {DateTimeUtils.renderDateTime(registrationOpens)} - - + {DateTimeUtils.renderOpenDateTime(registrationOpens)} + —
- {DateTimeUtils.renderDateTime(registrationCloses)} + {DateTimeUtils.renderCloseDateTime(registrationCloses)}
@@ -196,8 +196,8 @@ export const PublicExamEventDesktopCells = ({ - {DateTimeUtils.renderDateTime(registrationOpens)} -
- {DateTimeUtils.renderDateTime(registrationCloses)} + {DateTimeUtils.renderOpenDateTime(registrationOpens)} —
+ {DateTimeUtils.renderCloseDateTime(registrationCloses)}
{getOpeningsText(examEvent, t)} diff --git a/frontend/packages/vkt/src/utils/dateTime.ts b/frontend/packages/vkt/src/utils/dateTime.ts index d25bb1791..fee70c0ae 100644 --- a/frontend/packages/vkt/src/utils/dateTime.ts +++ b/frontend/packages/vkt/src/utils/dateTime.ts @@ -13,8 +13,33 @@ export class DateTimeUtils { ); } + static renderOpenDateTime(dateTime?: Dayjs) { + const t = translateOutsideComponent(); + + return ( + DateUtils.formatOptionalDate(dateTime, t('vkt.common.dates.dateFormat')) + + ' ' + + t('vkt.common.dates.registrationOpensAt') + ); + } + + static renderCloseDateTime(dateTime?: Dayjs) { + const t = translateOutsideComponent(); + + return ( + DateUtils.formatOptionalDate(dateTime, t('vkt.common.dates.dateFormat')) + + ' ' + + t('vkt.common.dates.registrationClosesAt') + ); + } + static renderTime(dateTime?: Dayjs) { - return DateUtils.formatOptionalTime(dateTime); + const t = translateOutsideComponent(); + + return DateUtils.formatOptionalTime( + dateTime, + t('vkt.common.dates.timeFormat'), + ); } static renderDate(dateTime?: Dayjs) { From 0b5a5ea808ca5017739f6871da925196c1a99052 Mon Sep 17 00:00:00 2001 From: Jarkko Pesonen <435495+jrkkp@users.noreply.github.com> Date: Wed, 11 Sep 2024 16:50:32 +0300 Subject: [PATCH 010/248] VKT(Frontend): Cypress test fix --- .../vkt/src/tests/cypress/integration/public_enrollment.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/packages/vkt/src/tests/cypress/integration/public_enrollment.spec.ts b/frontend/packages/vkt/src/tests/cypress/integration/public_enrollment.spec.ts index 7d4f4c207..d4dd3ae56 100644 --- a/frontend/packages/vkt/src/tests/cypress/integration/public_enrollment.spec.ts +++ b/frontend/packages/vkt/src/tests/cypress/integration/public_enrollment.spec.ts @@ -95,7 +95,7 @@ describe('Public enrollment', () => { ); onPublicEnrollmentPage.clickNext(); onPublicEnrollmentPage.expectEnrollmentDetails( - 'Tutkinto: Ruotsi, erinomainen taitoTutkintopäivä: 22.3.2022Ilmoittautuminen sulkeutuu: 15.3.2022Paikkoja vapaana: 6', + 'Tutkinto: Ruotsi, erinomainen taitoTutkintopäivä: 22.3.2022Ilmoittautuminen sulkeutuu: 15.3.2022 klo 16:00Paikkoja vapaana: 6', ); onPublicEnrollmentPage.expectEnrollmentPersonDetails( 'Sukunimi:TestiläEtunimet:Tessa', From c75d6d89374e530c9a465498177aee6ba70ec405 Mon Sep 17 00:00:00 2001 From: Jarkko Pesonen <435495+jrkkp@users.noreply.github.com> Date: Wed, 11 Sep 2024 17:04:23 +0300 Subject: [PATCH 011/248] VKT(Backend): unit test fixes --- .../oph/vkt/service/PublicEnrollmentServiceTest.java | 2 +- .../oph/vkt/service/PublicExamEventServiceTest.java | 11 ++--------- 2 files changed, 3 insertions(+), 10 deletions(-) diff --git a/backend/vkt/src/test/java/fi/oph/vkt/service/PublicEnrollmentServiceTest.java b/backend/vkt/src/test/java/fi/oph/vkt/service/PublicEnrollmentServiceTest.java index 6f7da7bba..279f8025a 100644 --- a/backend/vkt/src/test/java/fi/oph/vkt/service/PublicEnrollmentServiceTest.java +++ b/backend/vkt/src/test/java/fi/oph/vkt/service/PublicEnrollmentServiceTest.java @@ -320,7 +320,7 @@ private void assertInitialisedEnrollmentDTO( assertEquals(examEvent.getId(), examEventDTO.id()); assertEquals(examEvent.getLanguage(), examEventDTO.language()); assertEquals(examEvent.getDate(), examEventDTO.date()); - assertEquals(examEvent.getRegistrationCloses(), examEventDTO.registrationCloses()); + assertEquals(examEvent.getRegistrationCloses().toLocalDate(), examEventDTO.registrationCloses()); assertEquals(openings, examEventDTO.openings()); assertFalse(examEventDTO.hasCongestion()); diff --git a/backend/vkt/src/test/java/fi/oph/vkt/service/PublicExamEventServiceTest.java b/backend/vkt/src/test/java/fi/oph/vkt/service/PublicExamEventServiceTest.java index a1e6f8390..bad537d21 100644 --- a/backend/vkt/src/test/java/fi/oph/vkt/service/PublicExamEventServiceTest.java +++ b/backend/vkt/src/test/java/fi/oph/vkt/service/PublicExamEventServiceTest.java @@ -17,7 +17,6 @@ import fi.oph.vkt.repository.ReservationRepository; import jakarta.annotation.Resource; import java.time.LocalDateTime; -import java.time.temporal.ChronoUnit; import java.util.List; import java.util.stream.IntStream; import org.junit.jupiter.api.BeforeEach; @@ -261,14 +260,8 @@ private void assertExamEventDetails(final ExamEvent expected, final PublicExamEv assertEquals(expected.getId(), examEventDTO.id()); assertEquals(expected.getLanguage(), examEventDTO.language()); assertEquals(expected.getDate(), examEventDTO.date()); - assertEquals( - expected.getRegistrationCloses().truncatedTo(ChronoUnit.MINUTES), - examEventDTO.registrationCloses().truncatedTo(ChronoUnit.MINUTES) - ); - assertEquals( - expected.getRegistrationOpens().truncatedTo(ChronoUnit.MINUTES), - examEventDTO.registrationOpens().truncatedTo(ChronoUnit.MINUTES) - ); + assertEquals(expected.getRegistrationCloses().toLocalDate(), examEventDTO.registrationCloses()); + assertEquals(expected.getRegistrationOpens().toLocalDate(), examEventDTO.registrationOpens()); } @Test From be8dff4a9f80809ba3a4d0773baec3e386fd477d Mon Sep 17 00:00:00 2001 From: Jarkko Pesonen <435495+jrkkp@users.noreply.github.com> Date: Thu, 12 Sep 2024 10:51:01 +0300 Subject: [PATCH 012/248] VKT(Frontend): time format localisation fix --- frontend/packages/vkt/public/i18n/fi-FI/common.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/packages/vkt/public/i18n/fi-FI/common.json b/frontend/packages/vkt/public/i18n/fi-FI/common.json index 69348cbbd..7e4e3a4f6 100644 --- a/frontend/packages/vkt/public/i18n/fi-FI/common.json +++ b/frontend/packages/vkt/public/i18n/fi-FI/common.json @@ -48,8 +48,8 @@ "dateTimeFormat": "l [klo] HH.mm", "timeFormat": "[klo] HH.mm", "dateFormat": "l", - "registrationOpensAt": "klo 10:00", - "registrationClosesAt": "klo 16:00" + "registrationOpensAt": "klo 10.00", + "registrationClosesAt": "klo 16.00" }, "errors": { "api": { From ab2a48711c67c3310582bcfc3524094ac82f916d Mon Sep 17 00:00:00 2001 From: Jarkko Pesonen <435495+jrkkp@users.noreply.github.com> Date: Thu, 12 Sep 2024 10:58:07 +0300 Subject: [PATCH 013/248] VKT(Frontend): oops, cypress fix --- .../vkt/src/tests/cypress/integration/public_enrollment.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/packages/vkt/src/tests/cypress/integration/public_enrollment.spec.ts b/frontend/packages/vkt/src/tests/cypress/integration/public_enrollment.spec.ts index d4dd3ae56..96609e45d 100644 --- a/frontend/packages/vkt/src/tests/cypress/integration/public_enrollment.spec.ts +++ b/frontend/packages/vkt/src/tests/cypress/integration/public_enrollment.spec.ts @@ -95,7 +95,7 @@ describe('Public enrollment', () => { ); onPublicEnrollmentPage.clickNext(); onPublicEnrollmentPage.expectEnrollmentDetails( - 'Tutkinto: Ruotsi, erinomainen taitoTutkintopäivä: 22.3.2022Ilmoittautuminen sulkeutuu: 15.3.2022 klo 16:00Paikkoja vapaana: 6', + 'Tutkinto: Ruotsi, erinomainen taitoTutkintopäivä: 22.3.2022Ilmoittautuminen sulkeutuu: 15.3.2022 klo 16.00Paikkoja vapaana: 6', ); onPublicEnrollmentPage.expectEnrollmentPersonDetails( 'Sukunimi:TestiläEtunimet:Tessa', From 0fb5eed846204ef3d72b5336f461ae9bed47d400 Mon Sep 17 00:00:00 2001 From: Jarkko Pesonen <435495+jrkkp@users.noreply.github.com> Date: Tue, 17 Sep 2024 15:35:09 +0300 Subject: [PATCH 014/248] VKT(Backend): good and satisfactory level enrollment core functionality --- .../java/fi/oph/vkt/api/PublicController.java | 55 +++++++++--- .../oph/vkt/model/EnrollmentAppointment.java | 87 +++++++++++++++++++ .../main/java/fi/oph/vkt/model/Payment.java | 8 +- .../fi/oph/vkt/model/type/EnrollmentType.java | 1 + .../EnrollmentAppointmentRepository.java | 17 ++++ .../fi/oph/vkt/service/PublicAuthService.java | 4 +- .../PublicEnrollmentAppointmentService.java | 63 ++++++++++++++ .../java/fi/oph/vkt/util/UIRouteUtil.java | 4 + .../db/changelog/db.changelog-1.0.xml | 28 ++++++ 9 files changed, 253 insertions(+), 14 deletions(-) create mode 100644 backend/vkt/src/main/java/fi/oph/vkt/model/EnrollmentAppointment.java create mode 100644 backend/vkt/src/main/java/fi/oph/vkt/repository/EnrollmentAppointmentRepository.java create mode 100644 backend/vkt/src/main/java/fi/oph/vkt/service/PublicEnrollmentAppointmentService.java 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..f46656219 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 @@ -9,6 +9,7 @@ import fi.oph.vkt.api.dto.PublicPersonDTO; import fi.oph.vkt.api.dto.PublicReservationDTO; import fi.oph.vkt.model.Enrollment; +import fi.oph.vkt.model.EnrollmentAppointment; import fi.oph.vkt.model.FeatureFlag; import fi.oph.vkt.model.Person; import fi.oph.vkt.model.type.AppLocale; @@ -18,6 +19,7 @@ import fi.oph.vkt.service.FeatureFlagService; import fi.oph.vkt.service.PaymentService; import fi.oph.vkt.service.PublicAuthService; +import fi.oph.vkt.service.PublicEnrollmentAppointmentService; import fi.oph.vkt.service.PublicEnrollmentService; import fi.oph.vkt.service.PublicExamEventService; import fi.oph.vkt.service.PublicPersonService; @@ -66,6 +68,9 @@ public class PublicController { @Resource private PublicEnrollmentService publicEnrollmentService; + @Resource + private PublicEnrollmentAppointmentService publicEnrollmentAppointmentService; + @Resource private PublicExamEventService publicExamEventService; @@ -180,6 +185,30 @@ public PublicReservationDTO renewReservation(@PathVariable final long reservatio return publicReservationService.renewReservation(reservationId, person); } + @GetMapping(path = "/appointment/{enrollmentAppointmentId:\\d+}/redirect/{authHash:[a-z0-9\\-]+}") + public void createSessionAndRedirectToEnrollmentAppointment( + final HttpServletResponse httpResponse, + @PathVariable final long enrollmentAppointmentId, + @PathVariable final String authHash, + final HttpSession session + ) throws IOException { + try { + final EnrollmentAppointment enrollmentAppointment = publicEnrollmentAppointmentService.getEnrollmentAppointmentByHash( + enrollmentAppointmentId, + authHash + ); + SessionUtil.setPersonId(session, enrollmentAppointment.getId()); + + httpResponse.sendRedirect(uiRouteUtil.getEnrollmentAppointmentUrl(enrollmentAppointment.getId())); + } catch (final APIException e) { + LOG.warn("Encountered known error, redirecting to front page. Error:", e); + httpResponse.sendRedirect(uiRouteUtil.getPublicFrontPageUrlWithError(e.getExceptionType())); + } catch (final Exception e) { + LOG.error("Encountered unknown error, redirecting to front page. Error:", e); + httpResponse.sendRedirect(uiRouteUtil.getPublicFrontPageUrlWithGenericError()); + } + } + @GetMapping(path = "/examEvent/{examEventId:\\d+}/redirect/{paymentLinkHash:[a-z0-9\\-]+}") public void createSessionAndRedirectToPreview( final HttpServletResponse httpResponse, @@ -211,16 +240,16 @@ public void deleteReservation(@PathVariable final long reservationId, final Http publicReservationService.deleteReservation(reservationId, person); } - @GetMapping(path = "/auth/login/{examEventId:\\d+}/{type:\\w+}") + @GetMapping(path = "/auth/login/{targetId:\\d+}/{type:\\w+}") public void casLoginRedirect( final HttpServletResponse httpResponse, - @PathVariable final long examEventId, + @PathVariable final long targetId, @PathVariable final String type, @RequestParam final Optional locale, final HttpSession session ) throws IOException { final String casLoginUrl = publicAuthService.createCasLoginUrl( - examEventId, + targetId, EnrollmentType.fromString(type), locale.isPresent() ? AppLocale.fromString(locale.get()) : AppLocale.FI ); @@ -232,26 +261,32 @@ public void casLoginRedirect( httpResponse.sendRedirect(casLoginUrl); } - @GetMapping(path = "/auth/validate/{examEventId:\\d+}/{type:\\w+}") + @GetMapping(path = "/auth/validate/{targetId:\\d+}/{type:\\w+}") public void validateTicket( @RequestParam final String ticket, - @PathVariable final long examEventId, + @PathVariable final long targetId, @PathVariable final String type, final HttpSession session, final HttpServletResponse httpResponse ) throws IOException { try { final EnrollmentType enrollmentType = EnrollmentType.fromString(type); - final Person person = publicAuthService.createPersonFromTicket(ticket, examEventId, enrollmentType); + final Person person = publicAuthService.createPersonFromTicket(ticket, targetId, enrollmentType); SessionUtil.setPersonId(session, person.getId()); if (enrollmentType.equals(EnrollmentType.QUEUE)) { - publicEnrollmentService.initialiseEnrollmentToQueue(examEventId, person); - } else { - publicEnrollmentService.initialiseEnrollment(examEventId, person); + publicEnrollmentService.initialiseEnrollmentToQueue(targetId, person); + } else if (enrollmentType.equals(EnrollmentType.RESERVATION)) { + publicEnrollmentService.initialiseEnrollment(targetId, person); + } else if (enrollmentType.equals(EnrollmentType.APPOINTMENT)) { + publicEnrollmentAppointmentService.savePersonInfo(targetId, person); } - httpResponse.sendRedirect(uiRouteUtil.getEnrollmentContactDetailsUrl(examEventId)); + if (enrollmentType.equals(EnrollmentType.APPOINTMENT)) { + httpResponse.sendRedirect(uiRouteUtil.getEnrollmentAppointmentUrl(targetId)); + } else { + httpResponse.sendRedirect(uiRouteUtil.getEnrollmentContactDetailsUrl(targetId)); + } } catch (final APIException e) { LOG.warn("Encountered known error, redirecting to front page. Error:", e); httpResponse.sendRedirect(uiRouteUtil.getPublicFrontPageUrlWithError(e.getExceptionType())); diff --git a/backend/vkt/src/main/java/fi/oph/vkt/model/EnrollmentAppointment.java b/backend/vkt/src/main/java/fi/oph/vkt/model/EnrollmentAppointment.java new file mode 100644 index 000000000..24d177545 --- /dev/null +++ b/backend/vkt/src/main/java/fi/oph/vkt/model/EnrollmentAppointment.java @@ -0,0 +1,87 @@ +package fi.oph.vkt.model; + +import fi.oph.vkt.model.type.EnrollmentStatus; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToMany; +import jakarta.persistence.OneToOne; +import jakarta.persistence.Table; +import jakarta.validation.constraints.Size; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +@Entity +@Table(name = "enrollment") +public class EnrollmentAppointment extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "enrollment_appointment_id", nullable = false) + private long id; + + @Column(name = "skill_oral") + private boolean oralSkill; + + @Column(name = "skill_textual") + private boolean textualSkill; + + @Column(name = "skill_understanding") + private boolean understandingSkill; + + @Column(name = "partial_exam_speaking") + private boolean speakingPartialExam; + + @Column(name = "partial_exam_speech_comprehension") + private boolean speechComprehensionPartialExam; + + @Column(name = "partial_exam_writing") + private boolean writingPartialExam; + + @Column(name = "partial_exam_reading_comprehension") + private boolean readingComprehensionPartialExam; + + @Column(name = "digital_certificate_consent") + private boolean digitalCertificateConsent; + + @Column(name = "email") + private String email; + + @Column(name = "phone_number") + private String phoneNumber; + + @Column(name = "street") + private String street; + + @Column(name = "postal_code") + private String postalCode; + + @Column(name = "town") + private String town; + + @Column(name = "country") + private String country; + + @Size(max = 255) + @Column(name = "auth_hash", unique = true) + private String authHash; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "person_id", referencedColumnName = "person_id") + private Person person; + + @OneToMany(mappedBy = "enrollmentAppointment") + private List payments = new ArrayList<>(); +} diff --git a/backend/vkt/src/main/java/fi/oph/vkt/model/Payment.java b/backend/vkt/src/main/java/fi/oph/vkt/model/Payment.java index a1ea43d1c..d10709091 100644 --- a/backend/vkt/src/main/java/fi/oph/vkt/model/Payment.java +++ b/backend/vkt/src/main/java/fi/oph/vkt/model/Payment.java @@ -27,10 +27,14 @@ public class Payment extends BaseEntity { @Column(name = "payment_id", nullable = false) private long id; - @ManyToOne(fetch = FetchType.LAZY, optional = false) - @JoinColumn(name = "enrollment_id", referencedColumnName = "enrollment_id", nullable = false) + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "enrollment_id", referencedColumnName = "enrollment_id") private Enrollment enrollment; + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "enrollment_appointment_id", referencedColumnName = "enrollment_appointment_id") + private EnrollmentAppointment enrollmentAppointment; + @Column(name = "amount", nullable = false) private int amount; diff --git a/backend/vkt/src/main/java/fi/oph/vkt/model/type/EnrollmentType.java b/backend/vkt/src/main/java/fi/oph/vkt/model/type/EnrollmentType.java index 5336881c7..a313245ff 100644 --- a/backend/vkt/src/main/java/fi/oph/vkt/model/type/EnrollmentType.java +++ b/backend/vkt/src/main/java/fi/oph/vkt/model/type/EnrollmentType.java @@ -2,6 +2,7 @@ public enum EnrollmentType { RESERVATION("reservation"), + APPOINTMENT("appointment"), QUEUE("queue"); private final String text; 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 new file mode 100644 index 000000000..41e47c33b --- /dev/null +++ b/backend/vkt/src/main/java/fi/oph/vkt/repository/EnrollmentAppointmentRepository.java @@ -0,0 +1,17 @@ +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.Person; +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); +} diff --git a/backend/vkt/src/main/java/fi/oph/vkt/service/PublicAuthService.java b/backend/vkt/src/main/java/fi/oph/vkt/service/PublicAuthService.java index 524da8011..1071f6a34 100644 --- a/backend/vkt/src/main/java/fi/oph/vkt/service/PublicAuthService.java +++ b/backend/vkt/src/main/java/fi/oph/vkt/service/PublicAuthService.java @@ -42,10 +42,10 @@ public class PublicAuthService { private final CasTicketRepository casTicketRepository; private final CasSessionMappingStorage sessionMappingStorage; - public String createCasLoginUrl(final long examEventId, final EnrollmentType type, final AppLocale appLocale) { + public String createCasLoginUrl(final long targetId, final EnrollmentType type, final AppLocale appLocale) { final String casLoginUrl = environment.getRequiredProperty("app.cas-oppija.login-url"); final String casServiceUrl = URLEncoder.encode( - String.format(environment.getRequiredProperty("app.cas-oppija.service-url"), examEventId, type), + String.format(environment.getRequiredProperty("app.cas-oppija.service-url"), targetId, type), StandardCharsets.UTF_8 ); return casLoginUrl + "?service=" + casServiceUrl + "&locale=" + appLocale.name().toLowerCase(); 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 new file mode 100644 index 000000000..b22e4156e --- /dev/null +++ b/backend/vkt/src/main/java/fi/oph/vkt/service/PublicEnrollmentAppointmentService.java @@ -0,0 +1,63 @@ +package fi.oph.vkt.service; + +import fi.oph.vkt.api.dto.FreeEnrollmentAttachmentDTO; +import fi.oph.vkt.api.dto.FreeEnrollmentDetails; +import fi.oph.vkt.api.dto.FreeEnrollmentDetailsDTO; +import fi.oph.vkt.api.dto.PublicEducationDTO; +import fi.oph.vkt.api.dto.PublicEnrollmentCreateDTO; +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.PublicFreeEnrollmentBasisDTO; +import fi.oph.vkt.api.dto.PublicPersonDTO; +import fi.oph.vkt.api.dto.PublicReservationDTO; +import fi.oph.vkt.model.Enrollment; +import fi.oph.vkt.model.EnrollmentAppointment; +import fi.oph.vkt.model.ExamEvent; +import fi.oph.vkt.model.FeatureFlag; +import fi.oph.vkt.model.FreeEnrollment; +import fi.oph.vkt.model.Person; +import fi.oph.vkt.model.Reservation; +import fi.oph.vkt.model.UploadedFileAttachment; +import fi.oph.vkt.model.type.EnrollmentStatus; +import fi.oph.vkt.model.type.FreeEnrollmentSource; +import fi.oph.vkt.model.type.FreeEnrollmentType; +import fi.oph.vkt.repository.EnrollmentAppointmentRepository; +import fi.oph.vkt.repository.EnrollmentRepository; +import fi.oph.vkt.repository.ExamEventRepository; +import fi.oph.vkt.repository.FreeEnrollmentRepository; +import fi.oph.vkt.repository.ReservationRepository; +import fi.oph.vkt.repository.UploadedFileAttachmentRepository; +import fi.oph.vkt.service.aws.S3Service; +import fi.oph.vkt.service.koski.KoskiService; +import fi.oph.vkt.util.EnrollmentUtil; +import fi.oph.vkt.util.ExamEventUtil; +import fi.oph.vkt.util.PersonUtil; +import fi.oph.vkt.util.exception.APIException; +import fi.oph.vkt.util.exception.APIExceptionType; +import fi.oph.vkt.util.exception.NotFoundException; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; +import lombok.RequiredArgsConstructor; +import org.apache.commons.io.FilenameUtils; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +public class PublicEnrollmentAppointmentService extends AbstractEnrollmentService { + + private final EnrollmentAppointmentRepository enrollmentAppointmentRepository; + + public EnrollmentAppointment getEnrollmentAppointmentByHash( + final long enrollmentAppointmentId, + final String authHash + ) { + return enrollmentAppointmentRepository.findByIdAndAuthHash(enrollmentAppointmentId, authHash).orElseThrow(); + } + + public void savePersonInfo(long targetId, Person person) {} +} diff --git a/backend/vkt/src/main/java/fi/oph/vkt/util/UIRouteUtil.java b/backend/vkt/src/main/java/fi/oph/vkt/util/UIRouteUtil.java index 027d520a9..18ba970b3 100644 --- a/backend/vkt/src/main/java/fi/oph/vkt/util/UIRouteUtil.java +++ b/backend/vkt/src/main/java/fi/oph/vkt/util/UIRouteUtil.java @@ -30,4 +30,8 @@ public String getPublicFrontPageUrlWithError(final APIExceptionType exceptionTyp private String getPublicBaseUrl() { return environment.getRequiredProperty("app.base-url.public"); } + + public String getEnrollmentAppointmentUrl(final long enrollmentAppointmentId) { + return String.format("%s/ota-yhteytta/%s/tunnistaudu", getPublicBaseUrl(), enrollmentAppointmentId); + } } diff --git a/backend/vkt/src/main/resources/db/changelog/db.changelog-1.0.xml b/backend/vkt/src/main/resources/db/changelog/db.changelog-1.0.xml index 694e56614..7301f26a7 100644 --- a/backend/vkt/src/main/resources/db/changelog/db.changelog-1.0.xml +++ b/backend/vkt/src/main/resources/db/changelog/db.changelog-1.0.xml @@ -859,4 +859,32 @@ ALTER TABLE exam_event ADD COLUMN registration_opens TIMESTAMP WITH TIME ZONE; + + + + + + + + + + + + + + + + + + + + + + + + + + + + From 5b119c90b23b9c1ba37461e35927fb46ceddc0e2 Mon Sep 17 00:00:00 2001 From: Jarkko Pesonen <435495+jrkkp@users.noreply.github.com> Date: Fri, 20 Sep 2024 09:53:22 +0300 Subject: [PATCH 015/248] VKT(Backend): good and satisfactory level enrollment payment functionality --- .../java/fi/oph/vkt/api/PublicController.java | 16 +++-- .../fi/oph/vkt/service/PaymentService.java | 69 +++++++++++++++++++ .../java/fi/oph/vkt/util/EnrollmentUtil.java | 25 +++++++ 3 files changed, 103 insertions(+), 7 deletions(-) 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 f46656219..df437d1c8 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 @@ -321,20 +321,22 @@ public void logout(final HttpSession session, final HttpServletResponse httpResp httpResponse.sendRedirect(publicAuthService.createCasLogoutUrl()); } - @GetMapping(path = "/payment/create/{enrollmentId:\\d+}/redirect") + @GetMapping(path = "/payment/create/{targetId:\\d+}/{type:\\w}/redirect") public void createPaymentAndRedirect( - @PathVariable final Long enrollmentId, + @PathVariable final Long targetId, + @PathVariable final String type, @RequestParam final Optional locale, final HttpSession session, final HttpServletResponse httpResponse ) throws IOException { try { + final EnrollmentType enrollmentType = EnrollmentType.fromString(type); final Person person = publicPersonService.getPerson(SessionUtil.getPersonId(session)); - final String redirectUrl = paymentService.createPaymentForEnrollment( - enrollmentId, - person, - locale.isPresent() ? AppLocale.fromString(locale.get()) : AppLocale.FI - ); + final AppLocale localeOrDefault = locale.isPresent() ? AppLocale.fromString(locale.get()) : AppLocale.FI; + + final String redirectUrl = enrollmentType.equals(EnrollmentType.APPOINTMENT) + ? paymentService.createPaymentForEnrollmentAppointment(targetId, person, localeOrDefault) + : paymentService.createPaymentForEnrollment(targetId, person, localeOrDefault); httpResponse.sendRedirect(redirectUrl); } catch (final APIException e) { diff --git a/backend/vkt/src/main/java/fi/oph/vkt/service/PaymentService.java b/backend/vkt/src/main/java/fi/oph/vkt/service/PaymentService.java index cd7b03ced..ecf64357c 100644 --- a/backend/vkt/src/main/java/fi/oph/vkt/service/PaymentService.java +++ b/backend/vkt/src/main/java/fi/oph/vkt/service/PaymentService.java @@ -2,6 +2,7 @@ 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.Payment; import fi.oph.vkt.model.Person; @@ -14,6 +15,7 @@ import fi.oph.vkt.payment.paytrail.Item; import fi.oph.vkt.payment.paytrail.PaytrailConfig; import fi.oph.vkt.payment.paytrail.PaytrailResponseDTO; +import fi.oph.vkt.repository.EnrollmentAppointmentRepository; import fi.oph.vkt.repository.EnrollmentRepository; import fi.oph.vkt.repository.PaymentRepository; import fi.oph.vkt.util.EnrollmentUtil; @@ -24,6 +26,7 @@ import java.util.ArrayList; import java.util.List; import java.util.Map; +import java.util.Optional; import lombok.RequiredArgsConstructor; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -40,6 +43,7 @@ public class PaymentService { private final PaymentProvider paymentProvider; private final PaymentRepository paymentRepository; private final EnrollmentRepository enrollmentRepository; + private final EnrollmentAppointmentRepository enrollmentAppointmentRepository; private final Environment environment; private final PublicEnrollmentEmailService publicEnrollmentEmailService; @@ -53,6 +57,24 @@ private Item getItem(final EnrollmentSkill enrollmentSkill, final int unitPrice) .build(); } + private List getItems(final EnrollmentAppointment enrollmentAppointment) { + final List itemList = new ArrayList<>(); + + if (enrollmentAppointment.isTextualSkill()) { + itemList.add(getItem(EnrollmentSkill.TEXTUAL, EnrollmentUtil.getTextualSkillFee(enrollmentAppointment))); + } + if (enrollmentAppointment.isOralSkill()) { + itemList.add(getItem(EnrollmentSkill.ORAL, EnrollmentUtil.getOralSkillFee(enrollmentAppointment))); + } + if (enrollmentAppointment.isUnderstandingSkill()) { + itemList.add( + getItem(EnrollmentSkill.UNDERSTANDING, EnrollmentUtil.getUnderstandingSkillFee(enrollmentAppointment)) + ); + } + + return itemList; + } + private List getItems(final Enrollment enrollment, final FreeEnrollmentDetails freeEnrollmentDetails) { final List itemList = new ArrayList<>(); @@ -164,6 +186,53 @@ private String getFinalizePaymentRedirectUrl(final Long paymentId, final String return String.format("%s/ilmoittaudu/%d/maksu/%s", baseUrl, examEvent.getId(), state); } + @Transactional + public String createPaymentForEnrollmentAppointment( + final Long enrollmentId, + final Person person, + final AppLocale appLocale + ) { + final EnrollmentAppointment enrollmentAppointment = enrollmentAppointmentRepository + .findById(enrollmentId) + .orElseThrow(() -> new NotFoundException("Enrollment not found")); + + if (enrollmentAppointment.getPerson() == null || enrollmentAppointment.getPerson().getId() != person.getId()) { + throw new APIException(APIExceptionType.PAYMENT_PERSON_SESSION_MISMATCH); + } + + final List itemList = getItems(enrollmentAppointment); + final Customer customer = Customer + .builder() + .email(getCustomerField(enrollmentAppointment.getEmail(), Customer.EMAIL_MAX_LENGTH)) + .phone(getCustomerField(enrollmentAppointment.getPhoneNumber(), Customer.PHONE_MAX_LENGTH)) + .firstName(getCustomerField(person.getFirstName(), Customer.FIRST_NAME_MAX_LENGTH)) + .lastName(getCustomerField(person.getLastName(), Customer.LAST_NAME_MAX_LENGTH)) + .build(); + + final int amount = EnrollmentUtil.getTotalFee(enrollmentAppointment); + + final Payment payment = new Payment(); + payment.setEnrollmentAppointment(enrollmentAppointment); + payment.setAmount(amount); + paymentRepository.saveAndFlush(payment); + + final PaytrailResponseDTO response = paymentProvider.createPayment( + itemList, + payment.getId(), + customer, + amount, + appLocale + ); + + payment.setTransactionId(response.getTransactionId()); + payment.setReference(response.getReference()); + payment.setPaymentUrl(response.getHref()); + payment.setPaymentStatus(PaymentStatus.NEW); + paymentRepository.saveAndFlush(payment); + + return payment.getPaymentUrl(); + } + @Transactional public String createPaymentForEnrollment(final Long enrollmentId, final Person person, final AppLocale appLocale) { final Enrollment enrollment = enrollmentRepository diff --git a/backend/vkt/src/main/java/fi/oph/vkt/util/EnrollmentUtil.java b/backend/vkt/src/main/java/fi/oph/vkt/util/EnrollmentUtil.java index 779705bfa..4b1e507b6 100644 --- a/backend/vkt/src/main/java/fi/oph/vkt/util/EnrollmentUtil.java +++ b/backend/vkt/src/main/java/fi/oph/vkt/util/EnrollmentUtil.java @@ -2,6 +2,7 @@ import fi.oph.vkt.api.dto.FreeEnrollmentDetails; import fi.oph.vkt.model.Enrollment; +import fi.oph.vkt.model.EnrollmentAppointment; import fi.oph.vkt.model.Person; import fi.oph.vkt.model.type.ExamLevel; import java.util.regex.Matcher; @@ -12,6 +13,14 @@ public class EnrollmentUtil { private static final int SKILL_FEE = 25700; public static final Integer FREE_ENROLLMENT_LIMIT = 3; + public static int getTotalFee(final EnrollmentAppointment enrollmentAppointment) { + return ( + getTextualSkillFee(enrollmentAppointment) + + getOralSkillFee(enrollmentAppointment) + + getUnderstandingSkillFee(enrollmentAppointment) + ); + } + public static int getTotalFee(final Enrollment enrollment, final FreeEnrollmentDetails freeEnrollmentDetails) { return ( getTextualSkillFee(enrollment, freeEnrollmentDetails) + @@ -20,6 +29,10 @@ public static int getTotalFee(final Enrollment enrollment, final FreeEnrollmentD ); } + public static int getTextualSkillFee(final EnrollmentAppointment enrollmentAppointment) { + return enrollmentAppointment.isTextualSkill() ? SKILL_FEE : 0; + } + public static int getTextualSkillFee(final Enrollment enrollment, final FreeEnrollmentDetails freeEnrollmentDetails) { if (!enrollment.isTextualSkill()) { return 0; @@ -30,6 +43,10 @@ public static int getTextualSkillFee(final Enrollment enrollment, final FreeEnro : SKILL_FEE; } + public static int getOralSkillFee(final EnrollmentAppointment enrollmentAppointment) { + return enrollmentAppointment.isOralSkill() ? SKILL_FEE : 0; + } + public static int getOralSkillFee(final Enrollment enrollment, final FreeEnrollmentDetails freeEnrollmentDetails) { if (!enrollment.isOralSkill()) { return 0; @@ -52,6 +69,14 @@ public static boolean validateAttachmentId(final String attachmentId, final Pers return parsedExamEventId.equals(examEventId) && matcher.group(2).equals(person.getUuid().toString()); } + public static int getUnderstandingSkillFee(final EnrollmentAppointment enrollmentAppointment) { + if (enrollmentAppointment.isTextualSkill() && enrollmentAppointment.isOralSkill()) { + return 0; + } + + return SKILL_FEE; + } + public static int getUnderstandingSkillFee(final Enrollment enrollment) { if (enrollment.isTextualSkill() && enrollment.isOralSkill()) { return 0; From c5bee412acb9b49b0667b641c13a2576b67ed5a5 Mon Sep 17 00:00:00 2001 From: Jarkko Pesonen <435495+jrkkp@users.noreply.github.com> Date: Mon, 23 Sep 2024 15:23:10 +0300 Subject: [PATCH 016/248] VKT(Frontend): enrollment appointment grid --- ...PublicEnrollmentAppointmentDesktopGrid.tsx | 144 ++++++++++++++++++ .../PublicEnrollmentAppointmentGrid.tsx | 54 +++++++ frontend/packages/vkt/src/enums/app.ts | 2 + .../pages/PublicEnrollmentAppointmentPage.tsx | 23 +++ .../packages/vkt/src/routers/AppRouter.tsx | 11 ++ 5 files changed, 234 insertions(+) create mode 100644 frontend/packages/vkt/src/components/publicEnrollmentAppointment/PublicEnrollmentAppointmentDesktopGrid.tsx create mode 100644 frontend/packages/vkt/src/components/publicEnrollmentAppointment/PublicEnrollmentAppointmentGrid.tsx create mode 100644 frontend/packages/vkt/src/pages/PublicEnrollmentAppointmentPage.tsx diff --git a/frontend/packages/vkt/src/components/publicEnrollmentAppointment/PublicEnrollmentAppointmentDesktopGrid.tsx b/frontend/packages/vkt/src/components/publicEnrollmentAppointment/PublicEnrollmentAppointmentDesktopGrid.tsx new file mode 100644 index 000000000..1399711ad --- /dev/null +++ b/frontend/packages/vkt/src/components/publicEnrollmentAppointment/PublicEnrollmentAppointmentDesktopGrid.tsx @@ -0,0 +1,144 @@ +import { Grid, Paper } from '@mui/material'; +import { LoadingProgressIndicator } from 'shared/components'; +import { APIResponseStatus } from 'shared/enums'; + +import { PublicEnrollmentControlButtons } from 'components/publicEnrollment/PublicEnrollmentControlButtons'; +import { PublicEnrollmentExamEventDetails } from 'components/publicEnrollment/PublicEnrollmentExamEventDetails'; +import { PublicEnrollmentPaymentSum } from 'components/publicEnrollment/PublicEnrollmentPaymentSum'; +import { PublicEnrollmentStepContents } from 'components/publicEnrollment/PublicEnrollmentStepContents'; +import { PublicEnrollmentStepHeading } from 'components/publicEnrollment/PublicEnrollmentStepHeading'; +import { PublicEnrollmentStepper } from 'components/publicEnrollment/PublicEnrollmentStepper'; +import { PublicEnrollmentTimer } from 'components/publicEnrollment/PublicEnrollmentTimer'; +import { useCommonTranslation } from 'configs/i18n'; +import { useAppSelector } from 'configs/redux'; +import { PublicEnrollmentFormStep } from 'enums/publicEnrollment'; +import { PublicExamEvent } from 'interfaces/publicExamEvent'; +import { publicEnrollmentSelector } from 'redux/selectors/publicEnrollment'; +import { ExamEventUtils } from 'utils/examEvent'; + +export const PublicEnrollmentAppointmentDesktopGrid = ({ + activeStep, + isStepValid, + isShiftedFromQueue, + isExamEventDetailsAvailable, + isPaymentSumAvailable, + isPreviewStepActive, + isPreviewPassed, + isEnrollmentToQueue, + showValidation, + setIsStepValid, + setShowValidation, + examEvent, +}: { + activeStep: PublicEnrollmentFormStep; + isStepValid: boolean; + isExamEventDetailsAvailable: boolean; + isPaymentSumAvailable: boolean; + isPreviewStepActive: boolean; + isShiftedFromQueue: boolean; + isPreviewPassed: boolean; + isEnrollmentToQueue: boolean; + showValidation: boolean; + setIsStepValid: (isValid: boolean) => void; + setShowValidation: (showValidation: boolean) => void; + examEvent: PublicExamEvent; +}) => { + const translateCommon = useCommonTranslation(); + + const { + enrollmentSubmitStatus, + renewReservationStatus, + cancelStatus, + enrollment, + reservation, + freeEnrollmentDetails, + } = useAppSelector(publicEnrollmentSelector); + + const isRenewOrCancelLoading = [ + renewReservationStatus, + cancelStatus, + ].includes(APIResponseStatus.InProgress); + + const isEnrollmentSubmitLoading = + enrollmentSubmitStatus === APIResponseStatus.InProgress; + + const includePaymentStep = + ExamEventUtils.hasOpenings(examEvent) && !enrollment.isFree; + + return ( + <> + + + +
+ + {reservation && !isPreviewPassed && ( + + )} + + {isExamEventDetailsAvailable && ( + + )} + + {isPaymentSumAvailable && ( + + )} + {activeStep > PublicEnrollmentFormStep.Authenticate && + !isPreviewPassed && ( + + )} +
+
+
+
+ + ); +}; diff --git a/frontend/packages/vkt/src/components/publicEnrollmentAppointment/PublicEnrollmentAppointmentGrid.tsx b/frontend/packages/vkt/src/components/publicEnrollmentAppointment/PublicEnrollmentAppointmentGrid.tsx new file mode 100644 index 000000000..5979d6c93 --- /dev/null +++ b/frontend/packages/vkt/src/components/publicEnrollmentAppointment/PublicEnrollmentAppointmentGrid.tsx @@ -0,0 +1,54 @@ +import { Grid } from '@mui/material'; +import { useEffect, useState } from 'react'; +import { useNavigate, useParams } from 'react-router'; +import { LoadingProgressIndicator } from 'shared/components'; +import { APIResponseStatus } from 'shared/enums'; +import { useWindowProperties } from 'shared/hooks'; + +import { SessionExpiredModal } from 'components/layouts/SessionExpiredModal'; +import { PublicEnrollmentDesktopGrid } from 'components/publicEnrollment/PublicEnrollmentDesktopGrid'; +import { PublicEnrollmentPhoneGrid } from 'components/publicEnrollment/PublicEnrollmentPhoneGrid'; +import { useCommonTranslation } from 'configs/i18n'; +import { useAppDispatch, useAppSelector } from 'configs/redux'; +import { AppRoutes, EnrollmentStatus } from 'enums/app'; +import { PublicEnrollmentFormStep } from 'enums/publicEnrollment'; +import { useAuthentication } from 'hooks/useAuthentication'; +import { useNavigationProtection } from 'hooks/useNavigationProtection'; +import { + loadEnrollmentInitialisation, + loadPublicExamEvent, + resetPublicEnrollment, + setPublicEnrollmentExamEventIdIfNotSet, +} from 'redux/reducers/publicEnrollment'; +import { publicEnrollmentSelector } from 'redux/selectors/publicEnrollment'; +import { ExamEventUtils } from 'utils/examEvent'; + +export const PublicEnrollmentAppointmentGrid = ({ + activeStep, +}: { + activeStep: PublicEnrollmentFormStep; +}) => { + return ( + + + + ); +}; diff --git a/frontend/packages/vkt/src/enums/app.ts b/frontend/packages/vkt/src/enums/app.ts index c48f75c63..144369578 100644 --- a/frontend/packages/vkt/src/enums/app.ts +++ b/frontend/packages/vkt/src/enums/app.ts @@ -15,6 +15,8 @@ export enum AppRoutes { PublicEnrollmentPaymentSuccess = '/vkt/ilmoittaudu/:examEventId/maksu/valmis', PublicEnrollmentDoneQueued = '/vkt/ilmoittaudu/:examEventId/jono-valmis', PublicEnrollmentDone = '/vkt/ilmoittaudu/:examEventId/valmis', + PublicEnrollmentAppointment = '/vkt/markkinapaikka', + PublicAuthAppointment = '/vkt/markkinapaikka/:enrollmentId/tunnistaudu', ClerkHomePage = '/vkt/virkailija', ClerkExamEventCreatePage = '/vkt/virkailija/tutkintotilaisuus/luo', ClerkExamEventOverviewPage = '/vkt/virkailija/tutkintotilaisuus/:examEventId', diff --git a/frontend/packages/vkt/src/pages/PublicEnrollmentAppointmentPage.tsx b/frontend/packages/vkt/src/pages/PublicEnrollmentAppointmentPage.tsx new file mode 100644 index 000000000..558f75b3d --- /dev/null +++ b/frontend/packages/vkt/src/pages/PublicEnrollmentAppointmentPage.tsx @@ -0,0 +1,23 @@ +import { Box, Grid } from '@mui/material'; + +import { PublicEnrollmentAppointmentGrid } from 'components/publicEnrollmentAppointment/PublicEnrollmentAppointmentGrid'; +import { PublicEnrollmentFormStep } from 'enums/publicEnrollment'; + +export const PublicEnrollmentAppointmentPage = ({ + activeStep, +}: { + activeStep: PublicEnrollmentFormStep; +}) => { + return ( + + + + + + ); +}; diff --git a/frontend/packages/vkt/src/routers/AppRouter.tsx b/frontend/packages/vkt/src/routers/AppRouter.tsx index 9c9fb3e7a..e941a8cd2 100644 --- a/frontend/packages/vkt/src/routers/AppRouter.tsx +++ b/frontend/packages/vkt/src/routers/AppRouter.tsx @@ -178,6 +178,17 @@ export const AppRouter: FC = () => { } /> + + + + + } + /> Date: Tue, 24 Sep 2024 09:52:12 +0300 Subject: [PATCH 017/248] VKT(Frontend): enrollment appointment auth & payment grid --- ...PublicEnrollmentAppointmentDesktopGrid.tsx | 35 ++---- ...ublicEnrollmentAppointmentStepContents.tsx | 23 ++++ ...PublicEnrollmentAppointmentStepHeading.tsx | 40 ++++++ .../PublicEnrollmentAppointmentStepper.tsx | 114 ++++++++++++++++++ .../steps/Authenticate.tsx | 63 ++++++++++ .../vkt/src/enums/publicEnrollment.ts | 8 ++ 6 files changed, 258 insertions(+), 25 deletions(-) create mode 100644 frontend/packages/vkt/src/components/publicEnrollmentAppointment/PublicEnrollmentAppointmentStepContents.tsx create mode 100644 frontend/packages/vkt/src/components/publicEnrollmentAppointment/PublicEnrollmentAppointmentStepHeading.tsx create mode 100644 frontend/packages/vkt/src/components/publicEnrollmentAppointment/PublicEnrollmentAppointmentStepper.tsx create mode 100644 frontend/packages/vkt/src/components/publicEnrollmentAppointment/steps/Authenticate.tsx diff --git a/frontend/packages/vkt/src/components/publicEnrollmentAppointment/PublicEnrollmentAppointmentDesktopGrid.tsx b/frontend/packages/vkt/src/components/publicEnrollmentAppointment/PublicEnrollmentAppointmentDesktopGrid.tsx index 1399711ad..ebaa7b310 100644 --- a/frontend/packages/vkt/src/components/publicEnrollmentAppointment/PublicEnrollmentAppointmentDesktopGrid.tsx +++ b/frontend/packages/vkt/src/components/publicEnrollmentAppointment/PublicEnrollmentAppointmentDesktopGrid.tsx @@ -2,13 +2,11 @@ import { Grid, Paper } from '@mui/material'; import { LoadingProgressIndicator } from 'shared/components'; import { APIResponseStatus } from 'shared/enums'; -import { PublicEnrollmentControlButtons } from 'components/publicEnrollment/PublicEnrollmentControlButtons'; -import { PublicEnrollmentExamEventDetails } from 'components/publicEnrollment/PublicEnrollmentExamEventDetails'; -import { PublicEnrollmentPaymentSum } from 'components/publicEnrollment/PublicEnrollmentPaymentSum'; -import { PublicEnrollmentStepContents } from 'components/publicEnrollment/PublicEnrollmentStepContents'; -import { PublicEnrollmentStepHeading } from 'components/publicEnrollment/PublicEnrollmentStepHeading'; -import { PublicEnrollmentStepper } from 'components/publicEnrollment/PublicEnrollmentStepper'; -import { PublicEnrollmentTimer } from 'components/publicEnrollment/PublicEnrollmentTimer'; +import { PublicEnrollmentAppointmentControlButtons } from 'components/publicEnrollment/PublicEnrollmentAppointmentControlButtons'; +import { PublicEnrollmentAppointmentPaymentSum } from 'components/publicEnrollment/PublicEnrollmentAppointmentPaymentSum'; +import { PublicEnrollmentAppointmentStepContents } from 'components/publicEnrollment/PublicEnrollmentAppointmentStepContents'; +import { PublicEnrollmentAppointmentStepHeading } from 'components/publicEnrollmentAppointment/PublicEnrollmentAppointmentStepHeading'; +import { PublicEnrollmentAppointmentStepper } from 'components/publicEnrollmentAppointment/PublicEnrollmentAppointmentStepper'; import { useCommonTranslation } from 'configs/i18n'; import { useAppSelector } from 'configs/redux'; import { PublicEnrollmentFormStep } from 'enums/publicEnrollment'; @@ -81,28 +79,15 @@ export const PublicEnrollmentAppointmentDesktopGrid = ({ : 'public-enrollment__grid__form-container' } > - - {reservation && !isPreviewPassed && ( - - )} - - {isExamEventDetailsAvailable && ( - - )} - {isPaymentSumAvailable && ( - )} {activeStep > PublicEnrollmentFormStep.Authenticate && !isPreviewPassed && ( - { + const navigate = useNavigate(); + + switch (activeStep) { + case PublicEnrollmentAppointmentFormStep.Authenticate: + return ; + } +}; diff --git a/frontend/packages/vkt/src/components/publicEnrollmentAppointment/PublicEnrollmentAppointmentStepHeading.tsx b/frontend/packages/vkt/src/components/publicEnrollmentAppointment/PublicEnrollmentAppointmentStepHeading.tsx new file mode 100644 index 000000000..d2cfc4130 --- /dev/null +++ b/frontend/packages/vkt/src/components/publicEnrollmentAppointment/PublicEnrollmentAppointmentStepHeading.tsx @@ -0,0 +1,40 @@ +import { useEffect } from 'react'; +import { H1, HeaderSeparator } from 'shared/components'; +import { useFocus, useWindowProperties } from 'shared/hooks'; + +import { usePublicTranslation } from 'configs/i18n'; +import { PublicEnrollmentFormStep } from 'enums/publicEnrollment'; + +export const PublicEnrollmentAppointmentStepHeading = ({ + activeStep, + isEnrollmentToQueue, +}: { + activeStep: PublicEnrollmentFormStep; + isEnrollmentToQueue: boolean; +}) => { + const { t } = usePublicTranslation({ + keyPrefix: 'vkt.component.publicEnrollment.stepHeading', + }); + const [ref, setFocus] = useFocus(); + const { isPhone } = useWindowProperties(); + + useEffect(() => { + if (!isPhone) { + setFocus(); + } + }, [setFocus, isPhone]); + + const headingText = + activeStep === PublicEnrollmentFormStep.Authenticate + ? isEnrollmentToQueue + ? t(`toQueue.${PublicEnrollmentFormStep[activeStep]}`) + : t(`toExam.${PublicEnrollmentFormStep[activeStep]}`) + : t(`common.${PublicEnrollmentFormStep[activeStep]}`); + + return ( +
+

{headingText}

+ +
+ ); +}; diff --git a/frontend/packages/vkt/src/components/publicEnrollmentAppointment/PublicEnrollmentAppointmentStepper.tsx b/frontend/packages/vkt/src/components/publicEnrollmentAppointment/PublicEnrollmentAppointmentStepper.tsx new file mode 100644 index 000000000..7cc156c60 --- /dev/null +++ b/frontend/packages/vkt/src/components/publicEnrollmentAppointment/PublicEnrollmentAppointmentStepper.tsx @@ -0,0 +1,114 @@ +import { Step, StepLabel, Stepper } from '@mui/material'; +import { CircularStepper } from 'shared/components'; +import { Color } from 'shared/enums'; +import { useWindowProperties } from 'shared/hooks'; + +import { usePublicTranslation } from 'configs/i18n'; +import { PublicEnrollmentFormStep } from 'enums/publicEnrollment'; +import { PublicEnrollmentUtils } from 'utils/publicEnrollment'; + +export const PublicEnrollmentStepper = ({ + activeStep, + includePaymentStep, +}: { + activeStep: PublicEnrollmentFormStep; + includePaymentStep: boolean; +}) => { + const { isPhone } = useWindowProperties(); + + const { t } = usePublicTranslation({ + keyPrefix: 'vkt.component.publicEnrollment.stepper', + }); + + const steps = PublicEnrollmentUtils.getEnrollmentSteps(includePaymentStep); + + const doneStepNumber = steps.length; + + const getDescription = (step: PublicEnrollmentFormStep) => { + return t(`step.${PublicEnrollmentFormStep[step]}`); + }; + + const getStepAriaLabel = (stepNumber: number, stepIndex: number) => { + const part = t('phaseNumber', { + current: stepIndex + 1, + total: steps.length, + }); + const statusText = isStepCompleted(stepNumber) ? t('completed') : ''; + const partStatus = statusText ? `${part}, ${statusText}` : part; + + return `${t('phase')} ${partStatus}: ${getDescription(stepNumber)}`; + }; + + const getDesktopActiveStep = () => { + // "Hack" for not having Mui-Active for Payment step + if (activeStep === PublicEnrollmentFormStep.Payment) { + return activeStep; + } else if ( + activeStep === PublicEnrollmentFormStep.Done || + activeStep === PublicEnrollmentFormStep.DoneQueued + ) { + return doneStepNumber - 1; + } + + return activeStep - 1; + }; + + const hasError = (step: PublicEnrollmentFormStep) => { + return step === PublicEnrollmentFormStep.Payment && step === activeStep; + }; + + const isStepCompleted = (step: PublicEnrollmentFormStep) => { + return step < activeStep; + }; + + const stepValue = Math.min(activeStep, doneStepNumber); + + const mobileStepValue = stepValue * (100 / doneStepNumber); + const mobilePhaseText = `${stepValue}/${doneStepNumber}`; + const mobileAriaLabel = `${t('phase')} ${mobilePhaseText}: ${t( + `step.${PublicEnrollmentFormStep[activeStep]}`, + )}`; + + return isPhone ? ( + + ) : ( + + {steps.map((step, index) => ( + + {/* eslint-disable jsx-a11y/aria-role */} + + {/* eslint-enable */} + {getDescription(step)} + + + ))} + + ); +}; diff --git a/frontend/packages/vkt/src/components/publicEnrollmentAppointment/steps/Authenticate.tsx b/frontend/packages/vkt/src/components/publicEnrollmentAppointment/steps/Authenticate.tsx new file mode 100644 index 000000000..76c667f91 --- /dev/null +++ b/frontend/packages/vkt/src/components/publicEnrollmentAppointment/steps/Authenticate.tsx @@ -0,0 +1,63 @@ +import { useState } from 'react'; +import { CustomButton, LoadingProgressIndicator } from 'shared/components'; +import { Color, Variant } from 'shared/enums'; + +import { useCommonTranslation, usePublicTranslation } from 'configs/i18n'; +import { useAppDispatch } from 'configs/redux'; +import { PublicExamEvent } from 'interfaces/publicExamEvent'; +import { cancelPublicEnrollment } from 'redux/reducers/publicEnrollment'; +import { ExamEventUtils } from 'utils/examEvent'; +import { RouteUtils } from 'utils/routes'; + +export const Authenticate = ({ examEvent }: { examEvent: PublicExamEvent }) => { + const [isAuthRedirecting, setIsAuthRedirecting] = useState(false); + const { t } = usePublicTranslation({ + keyPrefix: 'vkt.component.publicEnrollment.steps.authenticate', + }); + const translateCommon = useCommonTranslation(); + + const dispatch = useAppDispatch(); + + const onAuthenticate = () => { + setIsAuthRedirecting(true); + + const type = ExamEventUtils.hasOpenings(examEvent) + ? 'reservation' + : 'queue'; + + window.location.href = RouteUtils.getAuthLoginApiRoute(examEvent.id, type); + }; + + const onCancel = () => { + dispatch(cancelPublicEnrollment()); + }; + + return ( +
+ + + {t('auth')} + + + + {translateCommon('cancel')} + +
+ ); +}; diff --git a/frontend/packages/vkt/src/enums/publicEnrollment.ts b/frontend/packages/vkt/src/enums/publicEnrollment.ts index a180893ba..7fd4a4bd5 100644 --- a/frontend/packages/vkt/src/enums/publicEnrollment.ts +++ b/frontend/packages/vkt/src/enums/publicEnrollment.ts @@ -9,3 +9,11 @@ export enum PublicEnrollmentFormStep { DoneQueued, Done, } + +export enum PublicEnrollmentAppointmentFormStep { + Authenticate = 1, + Preview, + Payment, + PaymentSuccess, + Done, +} From dead010104d3ff170b03e90637e13193b2b19d04 Mon Sep 17 00:00:00 2001 From: Jarkko Pesonen <435495+jrkkp@users.noreply.github.com> Date: Wed, 25 Sep 2024 16:22:40 +0300 Subject: [PATCH 018/248] VKT(Frontend): enrollment appointment continues --- ...licEnrollmentAppointmentControlButtons.tsx | 181 ++++++++++++++++++ ...PublicEnrollmentAppointmentDesktopGrid.tsx | 107 ++--------- .../PublicEnrollmentAppointmentGrid.tsx | 37 +--- .../PublicEnrollmentAppointmentPaymentSum.tsx | 28 +++ ...ublicEnrollmentAppointmentStepContents.tsx | 18 +- ...PublicEnrollmentAppointmentStepHeading.tsx | 6 +- .../PublicEnrollmentAppointmentStepper.tsx | 37 ++-- .../steps/Authenticate.tsx | 8 +- .../steps/FillContactDetails.tsx | 164 ++++++++++++++++ .../steps/Preview.tsx | 165 ++++++++++++++++ frontend/packages/vkt/src/enums/app.ts | 2 + .../vkt/src/enums/publicEnrollment.ts | 1 + .../vkt/src/interfaces/publicEnrollment.ts | 3 + .../reducers/publicEnrollmentAppointment.ts | 84 ++++++++ .../packages/vkt/src/redux/store/index.ts | 2 + .../packages/vkt/src/routers/AppRouter.tsx | 33 +++- .../vkt/src/utils/publicEnrollment.ts | 19 +- 17 files changed, 718 insertions(+), 177 deletions(-) create mode 100644 frontend/packages/vkt/src/components/publicEnrollmentAppointment/PublicEnrollmentAppointmentControlButtons.tsx create mode 100644 frontend/packages/vkt/src/components/publicEnrollmentAppointment/PublicEnrollmentAppointmentPaymentSum.tsx create mode 100644 frontend/packages/vkt/src/components/publicEnrollmentAppointment/steps/FillContactDetails.tsx create mode 100644 frontend/packages/vkt/src/components/publicEnrollmentAppointment/steps/Preview.tsx create mode 100644 frontend/packages/vkt/src/redux/reducers/publicEnrollmentAppointment.ts diff --git a/frontend/packages/vkt/src/components/publicEnrollmentAppointment/PublicEnrollmentAppointmentControlButtons.tsx b/frontend/packages/vkt/src/components/publicEnrollmentAppointment/PublicEnrollmentAppointmentControlButtons.tsx new file mode 100644 index 000000000..e2ceeadf3 --- /dev/null +++ b/frontend/packages/vkt/src/components/publicEnrollmentAppointment/PublicEnrollmentAppointmentControlButtons.tsx @@ -0,0 +1,181 @@ +import { + ArrowBackOutlined as ArrowBackIcon, + ArrowForwardOutlined as ArrowForwardIcon, +} from '@mui/icons-material'; +import { useEffect, useState } from 'react'; +import { useNavigate } from 'react-router'; +import { CustomButton, LoadingProgressIndicator } from 'shared/components'; +import { APIResponseStatus, Color, Severity, Variant } from 'shared/enums'; +import { useDialog } from 'shared/hooks'; + +import { useCommonTranslation, usePublicTranslation } from 'configs/i18n'; +import { useAppDispatch } from 'configs/redux'; +import { PublicEnrollmentFormStep } from 'enums/publicEnrollment'; +import { RouteUtils } from 'utils/routes'; + +export const PublicEnrollmentAppointmentControlButtons = ({ + activeStep, +}: { + activeStep: PublicEnrollmentFormStep; +}) => { + const { t } = usePublicTranslation({ + keyPrefix: 'vkt.component.publicEnrollment.controlButtons', + }); + const translateCommon = useCommonTranslation(); + const [isPaymentLoading, setIsPaymentLoading] = useState(false); + + // FIXME + const submitStatus = APIResponseStatus.NotStarted; + const dispatch = useAppDispatch(); + const navigate = useNavigate(); + + const { showDialog } = useDialog(); + + const submitButtonText = () => { + return t('pay'); + }; + + const handleCancelBtnClick = () => { + cancelPublicEnrollment(); + + showDialog({ + title: t('cancelDialog.title'), + severity: Severity.Info, + description: t('cancelDialog.description'), + actions: [ + { + title: translateCommon('back'), + variant: Variant.Outlined, + }, + { + title: translateCommon('yes'), + variant: Variant.Contained, + action: () => { + dispatch(confirmAction); + }, + }, + ], + }); + }; + + useEffect(() => { + if (submitStatus === APIResponseStatus.Success) { + // Safari needs time to re-render loading indicator + setTimeout(() => { + window.location.href = RouteUtils.getPaymentCreateApiRoute( + enrollment.id, + ); + }, 200); + dispatch(setLoadingPayment()); + } + }, [submitStatus, dispatch]); + + const handleBackBtnClick = () => { + const nextStep: PublicEnrollmentFormStep = activeStep - 1; + navigate(RouteUtils.stepToRoute(nextStep, examEventId)); + }; + + const handleNextBtnClick = () => { + if (isStepValid) { + setShowValidation(false); + const nextStep: PublicEnrollmentFormStep = activeStep + 1; + navigate(RouteUtils.stepToRoute(nextStep, examEventId)); + } else { + setShowValidation(true); + } + }; + + const handleSubmitBtnClick = () => { + if (isStepValid) { + setIsPaymentLoading(true); + setShowValidation(false); + dispatch( + loadPublicEnrollmentUpdate({ + enrollment, + examEventId, + }), + ); + } else { + setShowValidation(true); + } + }; + + const CancelButton = () => ( + <> + + {translateCommon('cancel')} + + + ); + + const BackButton = () => ( + } + disabled={ + activeStep == PublicEnrollmentFormStep.FillContactDetails || + isPaymentLoading + } + > + {translateCommon('back')} + + ); + + const NextButton = () => ( + } + disabled={isPaymentLoading} + > + {translateCommon('next')} + + ); + + const SubmitButton = () => ( + + + {submitButtonText()} + + + ); + + const renderBack = true; + + const renderNext = [ + PublicEnrollmentFormStep.FillContactDetails, + PublicEnrollmentFormStep.EducationDetails, + PublicEnrollmentFormStep.SelectExam, + ].includes(activeStep); + + const renderSubmit = activeStep === PublicEnrollmentFormStep.Preview; + + return ( +
+ {CancelButton()} + {renderBack && BackButton()} + {renderNext && NextButton()} + {renderSubmit && SubmitButton()} +
+ ); +}; diff --git a/frontend/packages/vkt/src/components/publicEnrollmentAppointment/PublicEnrollmentAppointmentDesktopGrid.tsx b/frontend/packages/vkt/src/components/publicEnrollmentAppointment/PublicEnrollmentAppointmentDesktopGrid.tsx index ebaa7b310..ace544b77 100644 --- a/frontend/packages/vkt/src/components/publicEnrollmentAppointment/PublicEnrollmentAppointmentDesktopGrid.tsx +++ b/frontend/packages/vkt/src/components/publicEnrollmentAppointment/PublicEnrollmentAppointmentDesktopGrid.tsx @@ -1,125 +1,44 @@ import { Grid, Paper } from '@mui/material'; import { LoadingProgressIndicator } from 'shared/components'; -import { APIResponseStatus } from 'shared/enums'; -import { PublicEnrollmentAppointmentControlButtons } from 'components/publicEnrollment/PublicEnrollmentAppointmentControlButtons'; -import { PublicEnrollmentAppointmentPaymentSum } from 'components/publicEnrollment/PublicEnrollmentAppointmentPaymentSum'; -import { PublicEnrollmentAppointmentStepContents } from 'components/publicEnrollment/PublicEnrollmentAppointmentStepContents'; +import { PublicEnrollmentAppointmentControlButtons } from 'components/publicEnrollmentAppointment/PublicEnrollmentAppointmentControlButtons'; +import { PublicEnrollmentAppointmentPaymentSum } from 'components/publicEnrollmentAppointment/PublicEnrollmentAppointmentPaymentSum'; +import { PublicEnrollmentAppointmentStepContents } from 'components/publicEnrollmentAppointment/PublicEnrollmentAppointmentStepContents'; import { PublicEnrollmentAppointmentStepHeading } from 'components/publicEnrollmentAppointment/PublicEnrollmentAppointmentStepHeading'; import { PublicEnrollmentAppointmentStepper } from 'components/publicEnrollmentAppointment/PublicEnrollmentAppointmentStepper'; import { useCommonTranslation } from 'configs/i18n'; -import { useAppSelector } from 'configs/redux'; import { PublicEnrollmentFormStep } from 'enums/publicEnrollment'; -import { PublicExamEvent } from 'interfaces/publicExamEvent'; -import { publicEnrollmentSelector } from 'redux/selectors/publicEnrollment'; -import { ExamEventUtils } from 'utils/examEvent'; export const PublicEnrollmentAppointmentDesktopGrid = ({ activeStep, - isStepValid, - isShiftedFromQueue, - isExamEventDetailsAvailable, - isPaymentSumAvailable, - isPreviewStepActive, - isPreviewPassed, - isEnrollmentToQueue, - showValidation, - setIsStepValid, - setShowValidation, - examEvent, }: { activeStep: PublicEnrollmentFormStep; - isStepValid: boolean; - isExamEventDetailsAvailable: boolean; - isPaymentSumAvailable: boolean; - isPreviewStepActive: boolean; - isShiftedFromQueue: boolean; - isPreviewPassed: boolean; - isEnrollmentToQueue: boolean; - showValidation: boolean; - setIsStepValid: (isValid: boolean) => void; - setShowValidation: (showValidation: boolean) => void; - examEvent: PublicExamEvent; }) => { const translateCommon = useCommonTranslation(); - const { - enrollmentSubmitStatus, - renewReservationStatus, - cancelStatus, - enrollment, - reservation, - freeEnrollmentDetails, - } = useAppSelector(publicEnrollmentSelector); - - const isRenewOrCancelLoading = [ - renewReservationStatus, - cancelStatus, - ].includes(APIResponseStatus.InProgress); - - const isEnrollmentSubmitLoading = - enrollmentSubmitStatus === APIResponseStatus.InProgress; - - const includePaymentStep = - ExamEventUtils.hasOpenings(examEvent) && !enrollment.isFree; - return ( <> -
- - +
+ + - {isPaymentSumAvailable && ( - PublicEnrollmentFormStep.Authenticate && ( + + )} + {activeStep > PublicEnrollmentFormStep.Authenticate && ( + )} - {activeStep > PublicEnrollmentFormStep.Authenticate && - !isPreviewPassed && ( - - )}
diff --git a/frontend/packages/vkt/src/components/publicEnrollmentAppointment/PublicEnrollmentAppointmentGrid.tsx b/frontend/packages/vkt/src/components/publicEnrollmentAppointment/PublicEnrollmentAppointmentGrid.tsx index 5979d6c93..6189d763f 100644 --- a/frontend/packages/vkt/src/components/publicEnrollmentAppointment/PublicEnrollmentAppointmentGrid.tsx +++ b/frontend/packages/vkt/src/components/publicEnrollmentAppointment/PublicEnrollmentAppointmentGrid.tsx @@ -1,27 +1,7 @@ import { Grid } from '@mui/material'; -import { useEffect, useState } from 'react'; -import { useNavigate, useParams } from 'react-router'; -import { LoadingProgressIndicator } from 'shared/components'; -import { APIResponseStatus } from 'shared/enums'; -import { useWindowProperties } from 'shared/hooks'; -import { SessionExpiredModal } from 'components/layouts/SessionExpiredModal'; -import { PublicEnrollmentDesktopGrid } from 'components/publicEnrollment/PublicEnrollmentDesktopGrid'; -import { PublicEnrollmentPhoneGrid } from 'components/publicEnrollment/PublicEnrollmentPhoneGrid'; -import { useCommonTranslation } from 'configs/i18n'; -import { useAppDispatch, useAppSelector } from 'configs/redux'; -import { AppRoutes, EnrollmentStatus } from 'enums/app'; +import { PublicEnrollmentAppointmentDesktopGrid } from 'components/publicEnrollmentAppointment/PublicEnrollmentAppointmentDesktopGrid'; import { PublicEnrollmentFormStep } from 'enums/publicEnrollment'; -import { useAuthentication } from 'hooks/useAuthentication'; -import { useNavigationProtection } from 'hooks/useNavigationProtection'; -import { - loadEnrollmentInitialisation, - loadPublicExamEvent, - resetPublicEnrollment, - setPublicEnrollmentExamEventIdIfNotSet, -} from 'redux/reducers/publicEnrollment'; -import { publicEnrollmentSelector } from 'redux/selectors/publicEnrollment'; -import { ExamEventUtils } from 'utils/examEvent'; export const PublicEnrollmentAppointmentGrid = ({ activeStep, @@ -35,20 +15,7 @@ export const PublicEnrollmentAppointmentGrid = ({ direction="column" className="public-enrollment" > - + ); }; diff --git a/frontend/packages/vkt/src/components/publicEnrollmentAppointment/PublicEnrollmentAppointmentPaymentSum.tsx b/frontend/packages/vkt/src/components/publicEnrollmentAppointment/PublicEnrollmentAppointmentPaymentSum.tsx new file mode 100644 index 000000000..2bbac1219 --- /dev/null +++ b/frontend/packages/vkt/src/components/publicEnrollmentAppointment/PublicEnrollmentAppointmentPaymentSum.tsx @@ -0,0 +1,28 @@ +import { H1 } from 'shared/components'; + +import { usePublicTranslation } from 'configs/i18n'; +import { PublicEnrollmentUtils } from 'utils/publicEnrollment'; + +export const PublicEnrollmentAppointmentPaymentSum = () => { + const { t } = usePublicTranslation({ + keyPrefix: 'vkt.component.publicEnrollment.paymentSum', + }); + + const sum = PublicEnrollmentUtils.calculateAppointmentPaymentSum(); + + const content = + sum === 0 + ? `${t('title')}: ${t('free')}` + : `${t('title')}: ${sum.toFixed(2).replace('.', ',')} €`; + + return ( +
+

+ {content} +

+
+ ); +}; diff --git a/frontend/packages/vkt/src/components/publicEnrollmentAppointment/PublicEnrollmentAppointmentStepContents.tsx b/frontend/packages/vkt/src/components/publicEnrollmentAppointment/PublicEnrollmentAppointmentStepContents.tsx index 53452932c..ab43c5f13 100644 --- a/frontend/packages/vkt/src/components/publicEnrollmentAppointment/PublicEnrollmentAppointmentStepContents.tsx +++ b/frontend/packages/vkt/src/components/publicEnrollmentAppointment/PublicEnrollmentAppointmentStepContents.tsx @@ -1,23 +1,19 @@ -import { useEffect } from 'react'; -import { useNavigate } from 'react-router'; - import { Authenticate } from 'components/publicEnrollmentAppointment/steps/Authenticate'; -import { AppRoutes } from 'enums/app'; +import { FillContactDetails } from 'components/publicEnrollmentAppointment/steps/FillContactDetails'; +import { Preview } from 'components/publicEnrollmentAppointment/steps/Preview'; import { PublicEnrollmentAppointmentFormStep } from 'enums/publicEnrollment'; -import { PublicEnrollment } from 'interfaces/publicEnrollment'; -import { PublicExamEvent } from 'interfaces/publicExamEvent'; export const PublicEnrollmentAppointmentStepContents = ({ activeStep, - enrollment, }: { activeStep: PublicEnrollmentAppointmentFormStep; - enrollment: PublicEnrollment; }) => { - const navigate = useNavigate(); - switch (activeStep) { case PublicEnrollmentAppointmentFormStep.Authenticate: - return ; + return ; + case PublicEnrollmentAppointmentFormStep.FillContactDetails: + return ; + case PublicEnrollmentAppointmentFormStep.Preview: + return ; } }; diff --git a/frontend/packages/vkt/src/components/publicEnrollmentAppointment/PublicEnrollmentAppointmentStepHeading.tsx b/frontend/packages/vkt/src/components/publicEnrollmentAppointment/PublicEnrollmentAppointmentStepHeading.tsx index d2cfc4130..72d49ce9b 100644 --- a/frontend/packages/vkt/src/components/publicEnrollmentAppointment/PublicEnrollmentAppointmentStepHeading.tsx +++ b/frontend/packages/vkt/src/components/publicEnrollmentAppointment/PublicEnrollmentAppointmentStepHeading.tsx @@ -7,10 +7,8 @@ import { PublicEnrollmentFormStep } from 'enums/publicEnrollment'; export const PublicEnrollmentAppointmentStepHeading = ({ activeStep, - isEnrollmentToQueue, }: { activeStep: PublicEnrollmentFormStep; - isEnrollmentToQueue: boolean; }) => { const { t } = usePublicTranslation({ keyPrefix: 'vkt.component.publicEnrollment.stepHeading', @@ -26,9 +24,7 @@ export const PublicEnrollmentAppointmentStepHeading = ({ const headingText = activeStep === PublicEnrollmentFormStep.Authenticate - ? isEnrollmentToQueue - ? t(`toQueue.${PublicEnrollmentFormStep[activeStep]}`) - : t(`toExam.${PublicEnrollmentFormStep[activeStep]}`) + ? t(`toExam.${PublicEnrollmentFormStep[activeStep]}`) : t(`common.${PublicEnrollmentFormStep[activeStep]}`); return ( diff --git a/frontend/packages/vkt/src/components/publicEnrollmentAppointment/PublicEnrollmentAppointmentStepper.tsx b/frontend/packages/vkt/src/components/publicEnrollmentAppointment/PublicEnrollmentAppointmentStepper.tsx index 7cc156c60..c1e550dc3 100644 --- a/frontend/packages/vkt/src/components/publicEnrollmentAppointment/PublicEnrollmentAppointmentStepper.tsx +++ b/frontend/packages/vkt/src/components/publicEnrollmentAppointment/PublicEnrollmentAppointmentStepper.tsx @@ -4,15 +4,13 @@ import { Color } from 'shared/enums'; import { useWindowProperties } from 'shared/hooks'; import { usePublicTranslation } from 'configs/i18n'; -import { PublicEnrollmentFormStep } from 'enums/publicEnrollment'; +import { PublicEnrollmentAppointmentFormStep } from 'enums/publicEnrollment'; import { PublicEnrollmentUtils } from 'utils/publicEnrollment'; -export const PublicEnrollmentStepper = ({ +export const PublicEnrollmentAppointmentStepper = ({ activeStep, - includePaymentStep, }: { - activeStep: PublicEnrollmentFormStep; - includePaymentStep: boolean; + activeStep: PublicEnrollmentAppointmentFormStep; }) => { const { isPhone } = useWindowProperties(); @@ -20,12 +18,12 @@ export const PublicEnrollmentStepper = ({ keyPrefix: 'vkt.component.publicEnrollment.stepper', }); - const steps = PublicEnrollmentUtils.getEnrollmentSteps(includePaymentStep); + const steps = PublicEnrollmentUtils.getEnrollmentAppointmentSteps(); const doneStepNumber = steps.length; - const getDescription = (step: PublicEnrollmentFormStep) => { - return t(`step.${PublicEnrollmentFormStep[step]}`); + const getDescription = (step: PublicEnrollmentAppointmentFormStep) => { + return t(`step.${PublicEnrollmentAppointmentFormStep[step]}`); }; const getStepAriaLabel = (stepNumber: number, stepIndex: number) => { @@ -40,24 +38,17 @@ export const PublicEnrollmentStepper = ({ }; const getDesktopActiveStep = () => { - // "Hack" for not having Mui-Active for Payment step - if (activeStep === PublicEnrollmentFormStep.Payment) { - return activeStep; - } else if ( - activeStep === PublicEnrollmentFormStep.Done || - activeStep === PublicEnrollmentFormStep.DoneQueued - ) { - return doneStepNumber - 1; - } - return activeStep - 1; }; - const hasError = (step: PublicEnrollmentFormStep) => { - return step === PublicEnrollmentFormStep.Payment && step === activeStep; + const hasError = (step: PublicEnrollmentAppointmentFormStep) => { + return ( + step === PublicEnrollmentAppointmentFormStep.Payment && + step === activeStep + ); }; - const isStepCompleted = (step: PublicEnrollmentFormStep) => { + const isStepCompleted = (step: PublicEnrollmentAppointmentFormStep) => { return step < activeStep; }; @@ -66,7 +57,7 @@ export const PublicEnrollmentStepper = ({ const mobileStepValue = stepValue * (100 / doneStepNumber); const mobilePhaseText = `${stepValue}/${doneStepNumber}`; const mobileAriaLabel = `${t('phase')} ${mobilePhaseText}: ${t( - `step.${PublicEnrollmentFormStep[activeStep]}`, + `step.${PublicEnrollmentAppointmentFormStep[activeStep]}`, )}`; return isPhone ? ( @@ -75,7 +66,7 @@ export const PublicEnrollmentStepper = ({ ariaLabel={mobileAriaLabel} phaseText={mobilePhaseText} color={ - activeStep === PublicEnrollmentFormStep.Payment + activeStep === PublicEnrollmentAppointmentFormStep.Payment ? Color.Error : Color.Secondary } diff --git a/frontend/packages/vkt/src/components/publicEnrollmentAppointment/steps/Authenticate.tsx b/frontend/packages/vkt/src/components/publicEnrollmentAppointment/steps/Authenticate.tsx index 76c667f91..10ecca4fb 100644 --- a/frontend/packages/vkt/src/components/publicEnrollmentAppointment/steps/Authenticate.tsx +++ b/frontend/packages/vkt/src/components/publicEnrollmentAppointment/steps/Authenticate.tsx @@ -4,12 +4,10 @@ import { Color, Variant } from 'shared/enums'; import { useCommonTranslation, usePublicTranslation } from 'configs/i18n'; import { useAppDispatch } from 'configs/redux'; -import { PublicExamEvent } from 'interfaces/publicExamEvent'; import { cancelPublicEnrollment } from 'redux/reducers/publicEnrollment'; -import { ExamEventUtils } from 'utils/examEvent'; import { RouteUtils } from 'utils/routes'; -export const Authenticate = ({ examEvent }: { examEvent: PublicExamEvent }) => { +export const Authenticate = () => { const [isAuthRedirecting, setIsAuthRedirecting] = useState(false); const { t } = usePublicTranslation({ keyPrefix: 'vkt.component.publicEnrollment.steps.authenticate', @@ -21,9 +19,7 @@ export const Authenticate = ({ examEvent }: { examEvent: PublicExamEvent }) => { const onAuthenticate = () => { setIsAuthRedirecting(true); - const type = ExamEventUtils.hasOpenings(examEvent) - ? 'reservation' - : 'queue'; + const type = 'appointment'; window.location.href = RouteUtils.getAuthLoginApiRoute(examEvent.id, type); }; diff --git a/frontend/packages/vkt/src/components/publicEnrollmentAppointment/steps/FillContactDetails.tsx b/frontend/packages/vkt/src/components/publicEnrollmentAppointment/steps/FillContactDetails.tsx new file mode 100644 index 000000000..d85872dc9 --- /dev/null +++ b/frontend/packages/vkt/src/components/publicEnrollmentAppointment/steps/FillContactDetails.tsx @@ -0,0 +1,164 @@ +import { Divider } from '@mui/material'; +import { ChangeEvent, useEffect, useState } from 'react'; +import { H2, LabeledTextField, Text } from 'shared/components'; +import { InputAutoComplete, TextFieldTypes } from 'shared/enums'; +import { useWindowProperties } from 'shared/hooks'; +import { TextField } from 'shared/interfaces'; +import { FieldErrors } from 'shared/utils'; + +import { CertificateShipping } from 'components/publicEnrollment/steps/CertificateShipping'; +import { PersonDetails } from 'components/publicEnrollment/steps/PersonDetails'; +import { useCommonTranslation, usePublicTranslation } from 'configs/i18n'; +import { useAppDispatch } from 'configs/redux'; +import { + PublicEnrollment, + PublicEnrollmentContactDetails, +} from 'interfaces/publicEnrollment'; +import { updatePublicEnrollment } from 'redux/reducers/publicEnrollment'; + +const fields: Array> = [ + { + name: 'email', + required: true, + type: TextFieldTypes.Email, + maxLength: 255, + }, + { + name: 'emailConfirmation', + required: true, + type: TextFieldTypes.Email, + maxLength: 255, + }, + { + name: 'phoneNumber', + required: true, + type: TextFieldTypes.PhoneNumber, + maxLength: 255, + }, +]; + +const emailsMatch = ( + t: (key: string) => string, + errors: FieldErrors, + values: PublicEnrollmentContactDetails, + dirtyFields?: Array, +) => { + if ( + values.email !== values.emailConfirmation && + (!dirtyFields || dirtyFields.includes('emailConfirmation')) + ) { + return { + ...errors, + ['emailConfirmation']: + errors['emailConfirmation'] ?? t('mismatchingEmailsError'), + }; + } + + return errors; +}; + +export const FillContactDetails = ( +{ + isLoading, +}: { + isLoading: boolean; +} +) => { + const { t } = usePublicTranslation({ + keyPrefix: 'vkt.component.publicEnrollment.steps.fillContactDetails', + }); + const translateCommon = useCommonTranslation(); + + const [dirtyFields, setDirtyFields] = useState< + Array + >([]); + + const dispatch = useAppDispatch(); + const errors = []; + + const handleChange = + (fieldName: keyof PublicEnrollmentContactDetails) => + (event: ChangeEvent) => { + dispatch( + updatePublicEnrollment({ + [fieldName]: event.target.value, + }), + ); + }; + + const handleBlur = + (fieldName: keyof PublicEnrollmentContactDetails) => () => { + if (!dirtyFields.includes(fieldName)) { + setDirtyFields([...dirtyFields, fieldName]); + } + if (fieldName === 'phoneNumber') { + dispatch( + updatePublicEnrollment({ + phoneNumber: enrollment.phoneNumber.replace(/\s/g, ''), + }), + ); + } + }; + + const showCustomTextFieldError = ( + fieldName: keyof PublicEnrollmentContactDetails, + ) => { + return false; + }; + + const getCustomTextFieldAttributes = ( + fieldName: keyof PublicEnrollmentContactDetails, + ) => ({ + id: `public-enrollment__contact-details__${fieldName}-field`, + label: t(`${fieldName}.label`), + onBlur: handleBlur(fieldName), + onChange: handleChange(fieldName), + error: showCustomTextFieldError(fieldName), + helperText: errors[fieldName], + required: true, + disabled: isLoading, + }); + + const { isPhone } = useWindowProperties(); + + return ( +
+ +
+

{t('title')}

+ {translateCommon('requiredFieldsInfo')} +
+ + { + e.preventDefault(); + + return false; + }} + /> +
+
+ + {!isPhone && } + +
+ ); +}; diff --git a/frontend/packages/vkt/src/components/publicEnrollmentAppointment/steps/Preview.tsx b/frontend/packages/vkt/src/components/publicEnrollmentAppointment/steps/Preview.tsx new file mode 100644 index 000000000..b0b55470a --- /dev/null +++ b/frontend/packages/vkt/src/components/publicEnrollmentAppointment/steps/Preview.tsx @@ -0,0 +1,165 @@ +import OpenInNewIcon from '@mui/icons-material/OpenInNew'; +import { + Checkbox, + Divider, + FormControlLabel, + FormHelperText, +} from '@mui/material'; +import { useEffect } from 'react'; +import { Trans } from 'react-i18next'; +import { H2, Text, WebLink } from 'shared/components'; +import { APIResponseStatus, Color } from 'shared/enums'; + +import { ExamEventDetails } from 'components/publicEnrollment/steps/ExamEventDetails'; +import { PersonDetails } from 'components/publicEnrollment/steps/PersonDetails'; +import { useCommonTranslation, usePublicTranslation } from 'configs/i18n'; +import { useAppDispatch, useAppSelector } from 'configs/redux'; +import { PublicEnrollmentAppointment } from 'interfaces/publicEnrollment'; +import { updatePublicEnrollment } from 'redux/reducers/publicEnrollment'; +import { publicEnrollmentSelector } from 'redux/selectors/publicEnrollment'; +import { EnrollmentUtils } from 'utils/enrollment'; + +const ContactDetails = ({ enrollment }: { enrollment: PublicEnrollmentAppointment }) => { + const { t } = usePublicTranslation({ + keyPrefix: 'vkt.component.publicEnrollment.steps.preview.contactDetails', + }); + const translateCommon = useCommonTranslation(); + + return ( +
+

{t('title')}

+
+
+ + {t('email')} + {':'} + + {enrollment.email} +
+
+ + {t('phoneNumber')} + {':'} + + + {enrollment.phoneNumber} + +
+
+ + {translateCommon('enrollment.certificateShipping.addressTitle')} + {':'} + + + {enrollment.street} + {', '} + {enrollment.postalCode} + {', '} + {enrollment.town} + {', '} + {enrollment.country} + +
+
+
+ ); +}; + +const PrivacyStatementCheckboxLabel = ({ + enrollment, +}: { + enrollment: PublicEnrollmentAppointment; +}) => { + const { t } = usePublicTranslation({ + keyPrefix: 'vkt.component.publicEnrollment.steps.preview.privacyStatement', + }); + const translateCommon = useCommonTranslation(); + + return ( + + } + /> + + ); +}; + +export const Preview = ({ + enrollment, + isLoading, + setIsStepValid, + showValidation, +}: { + enrollment: PublicEnrollmentAppointment; + isLoading: boolean; + setIsStepValid: (isValid: boolean) => void; + showValidation: boolean; +}) => { + const translateCommon = useCommonTranslation(); + + const { paymentLoadingStatus } = useAppSelector(publicEnrollmentSelector); + + useEffect(() => { + setIsStepValid(enrollment.privacyStatementConfirmation); + }, [setIsStepValid, enrollment]); + + const dispatch = useAppDispatch(); + + const handleCheckboxClick = () => { + dispatch( + updatePublicEnrollment({ + privacyStatementConfirmation: !enrollment.privacyStatementConfirmation, + }), + ); + }; + + const hasPrivacyStatementError = + showValidation && !enrollment.privacyStatementConfirmation; + + return ( +
+ + + + + +
+

{translateCommon('acceptTerms')}

+
+ + } + label={} + className={`public-enrollment__grid__preview__privacy-statement-checkbox-label ${ + hasPrivacyStatementError && 'checkbox-error' + }`} + /> + {hasPrivacyStatementError && ( + + {translateCommon('errors.customTextField.required')} + + )} +
+
+
+ ); +}; diff --git a/frontend/packages/vkt/src/enums/app.ts b/frontend/packages/vkt/src/enums/app.ts index 144369578..4ed26359a 100644 --- a/frontend/packages/vkt/src/enums/app.ts +++ b/frontend/packages/vkt/src/enums/app.ts @@ -17,6 +17,8 @@ export enum AppRoutes { PublicEnrollmentDone = '/vkt/ilmoittaudu/:examEventId/valmis', PublicEnrollmentAppointment = '/vkt/markkinapaikka', PublicAuthAppointment = '/vkt/markkinapaikka/:enrollmentId/tunnistaudu', + PublicEnrollmentAppointmentContactDetails = '/vkt/markkinapaikka/:enrollmentId/tiedot', + PublicEnrollmentAppointmentPreview = '/vkt/markkinapaikka/:enrollmentId/esikatsele', ClerkHomePage = '/vkt/virkailija', ClerkExamEventCreatePage = '/vkt/virkailija/tutkintotilaisuus/luo', ClerkExamEventOverviewPage = '/vkt/virkailija/tutkintotilaisuus/:examEventId', diff --git a/frontend/packages/vkt/src/enums/publicEnrollment.ts b/frontend/packages/vkt/src/enums/publicEnrollment.ts index 7fd4a4bd5..e32e5e25a 100644 --- a/frontend/packages/vkt/src/enums/publicEnrollment.ts +++ b/frontend/packages/vkt/src/enums/publicEnrollment.ts @@ -12,6 +12,7 @@ export enum PublicEnrollmentFormStep { export enum PublicEnrollmentAppointmentFormStep { Authenticate = 1, + FillContactDetails, Preview, Payment, PaymentSuccess, diff --git a/frontend/packages/vkt/src/interfaces/publicEnrollment.ts b/frontend/packages/vkt/src/interfaces/publicEnrollment.ts index 72c5093d4..1f8900477 100644 --- a/frontend/packages/vkt/src/interfaces/publicEnrollment.ts +++ b/frontend/packages/vkt/src/interfaces/publicEnrollment.ts @@ -57,6 +57,9 @@ export interface PublicEnrollment isQueued?: boolean; } +export interface PublicEnrollmentAppointment extends PublicEnrollment { +} + export interface PublicEnrollmentResponse extends Omit< PublicEnrollment, diff --git a/frontend/packages/vkt/src/redux/reducers/publicEnrollmentAppointment.ts b/frontend/packages/vkt/src/redux/reducers/publicEnrollmentAppointment.ts new file mode 100644 index 000000000..61cc3c8b3 --- /dev/null +++ b/frontend/packages/vkt/src/redux/reducers/publicEnrollmentAppointment.ts @@ -0,0 +1,84 @@ +import { createSlice, current, PayloadAction } from '@reduxjs/toolkit'; +import { APIResponseStatus } from 'shared/enums'; + +import { + Attachment, + PublicFreeEnrollmentDetails, +} from 'interfaces/publicEducation'; +import { + PublicEnrollment, + PublicReservation, +} from 'interfaces/publicEnrollment'; +import { PublicExamEvent } from 'interfaces/publicExamEvent'; +import { PublicPerson } from 'interfaces/publicPerson'; +import { EnrollmentUtils } from 'utils/enrollment'; + +export interface PublicEnrollmentAppointmentState { + loadEnrollmentStatus: APIResponseStatus; + enrollmentSubmitStatus: APIResponseStatus; + paymentLoadingStatus: APIResponseStatus; + cancelStatus: APIResponseStatus; + enrollment: PublicEnrollment; + person?: PublicPerson; +} + +export const initialState: PublicEnrollmentState = { + loadEnrollmentStatus: APIResponseStatus.NotStarted, + enrollmentSubmitStatus: APIResponseStatus.NotStarted, + paymentLoadingStatus: APIResponseStatus.NotStarted, + cancelStatus: APIResponseStatus.NotStarted, + enrollment: { + email: '', + emailConfirmation: '', + phoneNumber: '', + isFree: false, + oralSkill: false, + textualSkill: false, + understandingSkill: false, + speakingPartialExam: false, + speechComprehensionPartialExam: false, + writingPartialExam: false, + readingComprehensionPartialExam: false, + digitalCertificateConsent: false, + street: '', + postalCode: '', + town: '', + country: '', + id: undefined, + hasPreviousEnrollment: undefined, + previousEnrollment: '', + privacyStatementConfirmation: false, + status: undefined, + examEventId: undefined, + hasPaymentLink: undefined, + isQueued: undefined, + }, + person: undefined, +}; + +const publicEnrollmentAppointmentSlice = createSlice({ + name: 'publicEnrollmentAppointment', + initialState, + reducers: { + loadPublicEnrollmentAppointment(state, _action: PayloadAction) { + state.loadEnrollmentStatus = APIResponseStatus.InProgress; + }, + rejectPublicEnrollmentAppointment(state) { + state.loadEnrollmentStatus = APIResponseStatus.Error; + }, + storePublicEnrollmentAppointment(state, action: PayloadAction) { + state.loadEnrollmentStatus = APIResponseStatus.Success; + }, + setLoadingPayment(state) { + state.paymentLoadingStatus = APIResponseStatus.InProgress; + }, + }, +}); + +export const publicEnrollmentAppointmentReducer = publicEnrollmentAppointmentSlice.reducer; +export const { + loadPublicEnrollmentAppointment, + rejectPublicEnrollmentAppointment, + storePublicEnrollmentAppointment, + setLoadingPayment, +} = publicEnrollmentAppointmentSlice.actions; diff --git a/frontend/packages/vkt/src/redux/store/index.ts b/frontend/packages/vkt/src/redux/store/index.ts index 0ff0ae0b6..c994904ee 100644 --- a/frontend/packages/vkt/src/redux/store/index.ts +++ b/frontend/packages/vkt/src/redux/store/index.ts @@ -13,6 +13,7 @@ import { clerkUserReducer } from 'redux/reducers/clerkUser'; import { featureFlagsReducer } from 'redux/reducers/featureFlags'; import { publicEducationReducer } from 'redux/reducers/publicEducation'; import { publicEnrollmentReducer } from 'redux/reducers/publicEnrollment'; +import { publicEnrollmentAppointmentReducer } from 'redux/reducers/publicEnrollmentAppointment'; import { publicExamEventReducer } from 'redux/reducers/publicExamEvent'; import { publicFileUploadReducer } from 'redux/reducers/publicFileUpload'; import { publicUserReducer } from 'redux/reducers/publicUser'; @@ -38,6 +39,7 @@ const reducer = combineReducers({ featureFlags: featureFlagsReducer, publicFileUpload: publicFileUploadReducer, publicEducation: publicEducationReducer, + publicEnrollmentAppointment: publicEnrollmentAppointmentReducer, }); const persistedReducer = persistReducer(persistConfig, reducer); diff --git a/frontend/packages/vkt/src/routers/AppRouter.tsx b/frontend/packages/vkt/src/routers/AppRouter.tsx index e941a8cd2..518ba6e45 100644 --- a/frontend/packages/vkt/src/routers/AppRouter.tsx +++ b/frontend/packages/vkt/src/routers/AppRouter.tsx @@ -20,7 +20,10 @@ import { Header } from 'components/layouts/Header'; import { useCommonTranslation } from 'configs/i18n'; import { useAppDispatch, useAppSelector } from 'configs/redux'; import { AppRoutes } from 'enums/app'; -import { PublicEnrollmentFormStep } from 'enums/publicEnrollment'; +import { + PublicEnrollmentAppointmentFormStep, + PublicEnrollmentFormStep, +} from 'enums/publicEnrollment'; import { useAPIErrorToast } from 'hooks/useAPIErrorToast'; import { AccessibilityStatementPage } from 'pages/AccessibilityStatementPage'; import { ClerkEnrollmentOverviewPage } from 'pages/ClerkEnrollmentOverviewPage'; @@ -29,6 +32,7 @@ import { ClerkExamEventOverviewPage } from 'pages/ClerkExamEventOverviewPage'; import { ClerkHomePage } from 'pages/ClerkHomePage'; import { LogoutSuccess } from 'pages/LogoutSuccess'; import { NotFoundPage } from 'pages/NotFoundPage'; +import { PublicEnrollmentAppointmentPage } from 'pages/PublicEnrollmentAppointmentPage'; import { PublicEnrollmentPage } from 'pages/PublicEnrollmentPage'; import { PublicHomePage } from 'pages/PublicHomePage'; import { loadFeatureFlags } from 'redux/reducers/featureFlags'; @@ -184,11 +188,36 @@ export const AppRouter: FC = () => { element={ + + } + /> + + } /> + + + + } + /> + Date: Sun, 29 Sep 2024 23:22:47 +0300 Subject: [PATCH 019/248] VKT(Frontend): enrollment appointment continues --- .../PublicEnrollmentAppointmentDesktopGrid.tsx | 4 ++++ .../PublicEnrollmentAppointmentGrid.tsx | 12 +++++++++--- .../PublicEnrollmentAppointmentStepContents.tsx | 5 ++++- .../steps/FillContactDetails.tsx | 12 ++---------- .../publicEnrollmentAppointment/steps/Preview.tsx | 6 +++++- .../packages/vkt/src/interfaces/publicEnrollment.ts | 3 +-- .../src/pages/PublicEnrollmentAppointmentPage.tsx | 4 ++-- .../redux/reducers/publicEnrollmentAppointment.ts | 8 ++++++-- .../redux/selectors/publicEnrollmentAppointment.ts | 6 ++++++ frontend/packages/vkt/src/routers/AppRouter.tsx | 4 +--- 10 files changed, 40 insertions(+), 24 deletions(-) create mode 100644 frontend/packages/vkt/src/redux/selectors/publicEnrollmentAppointment.ts diff --git a/frontend/packages/vkt/src/components/publicEnrollmentAppointment/PublicEnrollmentAppointmentDesktopGrid.tsx b/frontend/packages/vkt/src/components/publicEnrollmentAppointment/PublicEnrollmentAppointmentDesktopGrid.tsx index ace544b77..166d47558 100644 --- a/frontend/packages/vkt/src/components/publicEnrollmentAppointment/PublicEnrollmentAppointmentDesktopGrid.tsx +++ b/frontend/packages/vkt/src/components/publicEnrollmentAppointment/PublicEnrollmentAppointmentDesktopGrid.tsx @@ -8,11 +8,14 @@ import { PublicEnrollmentAppointmentStepHeading } from 'components/publicEnrollm import { PublicEnrollmentAppointmentStepper } from 'components/publicEnrollmentAppointment/PublicEnrollmentAppointmentStepper'; import { useCommonTranslation } from 'configs/i18n'; import { PublicEnrollmentFormStep } from 'enums/publicEnrollment'; +import { PublicEnrollmentAppointment } from 'interfaces/publicEnrollment'; export const PublicEnrollmentAppointmentDesktopGrid = ({ activeStep, + enrollment, }: { activeStep: PublicEnrollmentFormStep; + enrollment: PublicEnrollmentAppointment; }) => { const translateCommon = useCommonTranslation(); @@ -30,6 +33,7 @@ export const PublicEnrollmentAppointmentDesktopGrid = ({ {activeStep > PublicEnrollmentFormStep.Authenticate && ( diff --git a/frontend/packages/vkt/src/components/publicEnrollmentAppointment/PublicEnrollmentAppointmentGrid.tsx b/frontend/packages/vkt/src/components/publicEnrollmentAppointment/PublicEnrollmentAppointmentGrid.tsx index 6189d763f..a01237a76 100644 --- a/frontend/packages/vkt/src/components/publicEnrollmentAppointment/PublicEnrollmentAppointmentGrid.tsx +++ b/frontend/packages/vkt/src/components/publicEnrollmentAppointment/PublicEnrollmentAppointmentGrid.tsx @@ -1,13 +1,16 @@ import { Grid } from '@mui/material'; import { PublicEnrollmentAppointmentDesktopGrid } from 'components/publicEnrollmentAppointment/PublicEnrollmentAppointmentDesktopGrid'; -import { PublicEnrollmentFormStep } from 'enums/publicEnrollment'; +import { PublicEnrollmentAppointmentFormStep } from 'enums/publicEnrollment'; +import { publicEnrollmentAppointmentSelector } from 'redux/selectors/publicEnrollmentAppointment'; export const PublicEnrollmentAppointmentGrid = ({ activeStep, }: { - activeStep: PublicEnrollmentFormStep; + activeStep: PublicEnrollmentAppointmentFormStep; }) => { + const { enrollment } = useAppSelector(publicEnrollmentAppointmentSelector); + return ( - + ); }; diff --git a/frontend/packages/vkt/src/components/publicEnrollmentAppointment/PublicEnrollmentAppointmentStepContents.tsx b/frontend/packages/vkt/src/components/publicEnrollmentAppointment/PublicEnrollmentAppointmentStepContents.tsx index ab43c5f13..1589a0447 100644 --- a/frontend/packages/vkt/src/components/publicEnrollmentAppointment/PublicEnrollmentAppointmentStepContents.tsx +++ b/frontend/packages/vkt/src/components/publicEnrollmentAppointment/PublicEnrollmentAppointmentStepContents.tsx @@ -2,11 +2,14 @@ import { Authenticate } from 'components/publicEnrollmentAppointment/steps/Authe import { FillContactDetails } from 'components/publicEnrollmentAppointment/steps/FillContactDetails'; import { Preview } from 'components/publicEnrollmentAppointment/steps/Preview'; import { PublicEnrollmentAppointmentFormStep } from 'enums/publicEnrollment'; +import { PublicEnrollmentAppointment } from 'interfaces/publicEnrollment'; export const PublicEnrollmentAppointmentStepContents = ({ activeStep, + enrollment, }: { activeStep: PublicEnrollmentAppointmentFormStep; + enrollment: PublicEnrollmentAppointment; }) => { switch (activeStep) { case PublicEnrollmentAppointmentFormStep.Authenticate: @@ -14,6 +17,6 @@ export const PublicEnrollmentAppointmentStepContents = ({ case PublicEnrollmentAppointmentFormStep.FillContactDetails: return ; case PublicEnrollmentAppointmentFormStep.Preview: - return ; + return ; } }; diff --git a/frontend/packages/vkt/src/components/publicEnrollmentAppointment/steps/FillContactDetails.tsx b/frontend/packages/vkt/src/components/publicEnrollmentAppointment/steps/FillContactDetails.tsx index d85872dc9..9b81b5237 100644 --- a/frontend/packages/vkt/src/components/publicEnrollmentAppointment/steps/FillContactDetails.tsx +++ b/frontend/packages/vkt/src/components/publicEnrollmentAppointment/steps/FillContactDetails.tsx @@ -57,13 +57,7 @@ const emailsMatch = ( return errors; }; -export const FillContactDetails = ( -{ - isLoading, -}: { - isLoading: boolean; -} -) => { +export const FillContactDetails = ({ isLoading }: { isLoading: boolean }) => { const { t } = usePublicTranslation({ keyPrefix: 'vkt.component.publicEnrollment.steps.fillContactDetails', }); @@ -156,9 +150,7 @@ export const FillContactDetails = ( autoComplete={InputAutoComplete.PhoneNumber} /> {!isPhone && } - +
); }; diff --git a/frontend/packages/vkt/src/components/publicEnrollmentAppointment/steps/Preview.tsx b/frontend/packages/vkt/src/components/publicEnrollmentAppointment/steps/Preview.tsx index b0b55470a..ba3f4a739 100644 --- a/frontend/packages/vkt/src/components/publicEnrollmentAppointment/steps/Preview.tsx +++ b/frontend/packages/vkt/src/components/publicEnrollmentAppointment/steps/Preview.tsx @@ -19,7 +19,11 @@ import { updatePublicEnrollment } from 'redux/reducers/publicEnrollment'; import { publicEnrollmentSelector } from 'redux/selectors/publicEnrollment'; import { EnrollmentUtils } from 'utils/enrollment'; -const ContactDetails = ({ enrollment }: { enrollment: PublicEnrollmentAppointment }) => { +const ContactDetails = ({ + enrollment, +}: { + enrollment: PublicEnrollmentAppointment; +}) => { const { t } = usePublicTranslation({ keyPrefix: 'vkt.component.publicEnrollment.steps.preview.contactDetails', }); diff --git a/frontend/packages/vkt/src/interfaces/publicEnrollment.ts b/frontend/packages/vkt/src/interfaces/publicEnrollment.ts index 1f8900477..96f4f4b19 100644 --- a/frontend/packages/vkt/src/interfaces/publicEnrollment.ts +++ b/frontend/packages/vkt/src/interfaces/publicEnrollment.ts @@ -57,8 +57,7 @@ export interface PublicEnrollment isQueued?: boolean; } -export interface PublicEnrollmentAppointment extends PublicEnrollment { -} +export interface PublicEnrollmentAppointment extends PublicEnrollment {} export interface PublicEnrollmentResponse extends Omit< diff --git a/frontend/packages/vkt/src/pages/PublicEnrollmentAppointmentPage.tsx b/frontend/packages/vkt/src/pages/PublicEnrollmentAppointmentPage.tsx index 558f75b3d..b12b63c9b 100644 --- a/frontend/packages/vkt/src/pages/PublicEnrollmentAppointmentPage.tsx +++ b/frontend/packages/vkt/src/pages/PublicEnrollmentAppointmentPage.tsx @@ -1,12 +1,12 @@ import { Box, Grid } from '@mui/material'; import { PublicEnrollmentAppointmentGrid } from 'components/publicEnrollmentAppointment/PublicEnrollmentAppointmentGrid'; -import { PublicEnrollmentFormStep } from 'enums/publicEnrollment'; +import { PublicEnrollmentAppointmentFormStep } from 'enums/publicEnrollment'; export const PublicEnrollmentAppointmentPage = ({ activeStep, }: { - activeStep: PublicEnrollmentFormStep; + activeStep: PublicEnrollmentAppointmentFormStep; }) => { return ( diff --git a/frontend/packages/vkt/src/redux/reducers/publicEnrollmentAppointment.ts b/frontend/packages/vkt/src/redux/reducers/publicEnrollmentAppointment.ts index 61cc3c8b3..bb865f271 100644 --- a/frontend/packages/vkt/src/redux/reducers/publicEnrollmentAppointment.ts +++ b/frontend/packages/vkt/src/redux/reducers/publicEnrollmentAppointment.ts @@ -66,7 +66,10 @@ const publicEnrollmentAppointmentSlice = createSlice({ rejectPublicEnrollmentAppointment(state) { state.loadEnrollmentStatus = APIResponseStatus.Error; }, - storePublicEnrollmentAppointment(state, action: PayloadAction) { + storePublicEnrollmentAppointment( + state, + action: PayloadAction, + ) { state.loadEnrollmentStatus = APIResponseStatus.Success; }, setLoadingPayment(state) { @@ -75,7 +78,8 @@ const publicEnrollmentAppointmentSlice = createSlice({ }, }); -export const publicEnrollmentAppointmentReducer = publicEnrollmentAppointmentSlice.reducer; +export const publicEnrollmentAppointmentReducer = + publicEnrollmentAppointmentSlice.reducer; export const { loadPublicEnrollmentAppointment, rejectPublicEnrollmentAppointment, diff --git a/frontend/packages/vkt/src/redux/selectors/publicEnrollmentAppointment.ts b/frontend/packages/vkt/src/redux/selectors/publicEnrollmentAppointment.ts new file mode 100644 index 000000000..7320f7f35 --- /dev/null +++ b/frontend/packages/vkt/src/redux/selectors/publicEnrollmentAppointment.ts @@ -0,0 +1,6 @@ +import { RootState } from 'configs/redux'; +import { PublicEnrollmentAppointmentState } from 'redux/reducers/publicEnrollmentAppointment'; + +export const publicEnrollmentAppointmentSelector = ( + state: RootState, +): PublicEnrollmentAppointmentState => state.publicEnrollmentAppointment; diff --git a/frontend/packages/vkt/src/routers/AppRouter.tsx b/frontend/packages/vkt/src/routers/AppRouter.tsx index 518ba6e45..a005d7da1 100644 --- a/frontend/packages/vkt/src/routers/AppRouter.tsx +++ b/frontend/packages/vkt/src/routers/AppRouter.tsx @@ -210,9 +210,7 @@ export const AppRouter: FC = () => { element={ } From 4bb8098036ceac543e7240a612c713ea014011eb Mon Sep 17 00:00:00 2001 From: Jarkko Pesonen <435495+jrkkp@users.noreply.github.com> Date: Mon, 30 Sep 2024 22:30:19 +0300 Subject: [PATCH 020/248] VKT(Frontend): enrollment appointment continues --- ...licEnrollmentAppointmentControlButtons.tsx | 25 ++---------- ...PublicEnrollmentAppointmentDesktopGrid.tsx | 1 + .../PublicEnrollmentAppointmentGrid.tsx | 1 + ...ublicEnrollmentAppointmentStepContents.tsx | 2 +- .../steps/FillContactDetails.tsx | 40 +++++++------------ .../steps/Preview.tsx | 13 +----- .../reducers/publicEnrollmentAppointment.ts | 20 ++-------- 7 files changed, 26 insertions(+), 76 deletions(-) diff --git a/frontend/packages/vkt/src/components/publicEnrollmentAppointment/PublicEnrollmentAppointmentControlButtons.tsx b/frontend/packages/vkt/src/components/publicEnrollmentAppointment/PublicEnrollmentAppointmentControlButtons.tsx index e2ceeadf3..385ff0568 100644 --- a/frontend/packages/vkt/src/components/publicEnrollmentAppointment/PublicEnrollmentAppointmentControlButtons.tsx +++ b/frontend/packages/vkt/src/components/publicEnrollmentAppointment/PublicEnrollmentAppointmentControlButtons.tsx @@ -15,8 +15,10 @@ import { RouteUtils } from 'utils/routes'; export const PublicEnrollmentAppointmentControlButtons = ({ activeStep, + enrollment, }: { activeStep: PublicEnrollmentFormStep; + enrollment: PublicEnrollmentAppointment; }) => { const { t } = usePublicTranslation({ keyPrefix: 'vkt.component.publicEnrollment.controlButtons', @@ -36,26 +38,7 @@ export const PublicEnrollmentAppointmentControlButtons = ({ }; const handleCancelBtnClick = () => { - cancelPublicEnrollment(); - - showDialog({ - title: t('cancelDialog.title'), - severity: Severity.Info, - description: t('cancelDialog.description'), - actions: [ - { - title: translateCommon('back'), - variant: Variant.Outlined, - }, - { - title: translateCommon('yes'), - variant: Variant.Contained, - action: () => { - dispatch(confirmAction); - }, - }, - ], - }); + // FIXME }; useEffect(() => { @@ -68,7 +51,7 @@ export const PublicEnrollmentAppointmentControlButtons = ({ }, 200); dispatch(setLoadingPayment()); } - }, [submitStatus, dispatch]); + }, [submitStatus, enrollment.id, dispatch]); const handleBackBtnClick = () => { const nextStep: PublicEnrollmentFormStep = activeStep - 1; diff --git a/frontend/packages/vkt/src/components/publicEnrollmentAppointment/PublicEnrollmentAppointmentDesktopGrid.tsx b/frontend/packages/vkt/src/components/publicEnrollmentAppointment/PublicEnrollmentAppointmentDesktopGrid.tsx index 166d47558..e4d2315a3 100644 --- a/frontend/packages/vkt/src/components/publicEnrollmentAppointment/PublicEnrollmentAppointmentDesktopGrid.tsx +++ b/frontend/packages/vkt/src/components/publicEnrollmentAppointment/PublicEnrollmentAppointmentDesktopGrid.tsx @@ -41,6 +41,7 @@ export const PublicEnrollmentAppointmentDesktopGrid = ({ {activeStep > PublicEnrollmentFormStep.Authenticate && ( )}
diff --git a/frontend/packages/vkt/src/components/publicEnrollmentAppointment/PublicEnrollmentAppointmentGrid.tsx b/frontend/packages/vkt/src/components/publicEnrollmentAppointment/PublicEnrollmentAppointmentGrid.tsx index a01237a76..9a341e285 100644 --- a/frontend/packages/vkt/src/components/publicEnrollmentAppointment/PublicEnrollmentAppointmentGrid.tsx +++ b/frontend/packages/vkt/src/components/publicEnrollmentAppointment/PublicEnrollmentAppointmentGrid.tsx @@ -1,6 +1,7 @@ import { Grid } from '@mui/material'; import { PublicEnrollmentAppointmentDesktopGrid } from 'components/publicEnrollmentAppointment/PublicEnrollmentAppointmentDesktopGrid'; +import { useAppSelector } from 'configs/redux'; import { PublicEnrollmentAppointmentFormStep } from 'enums/publicEnrollment'; import { publicEnrollmentAppointmentSelector } from 'redux/selectors/publicEnrollmentAppointment'; diff --git a/frontend/packages/vkt/src/components/publicEnrollmentAppointment/PublicEnrollmentAppointmentStepContents.tsx b/frontend/packages/vkt/src/components/publicEnrollmentAppointment/PublicEnrollmentAppointmentStepContents.tsx index 1589a0447..ede5e7c10 100644 --- a/frontend/packages/vkt/src/components/publicEnrollmentAppointment/PublicEnrollmentAppointmentStepContents.tsx +++ b/frontend/packages/vkt/src/components/publicEnrollmentAppointment/PublicEnrollmentAppointmentStepContents.tsx @@ -15,7 +15,7 @@ export const PublicEnrollmentAppointmentStepContents = ({ case PublicEnrollmentAppointmentFormStep.Authenticate: return ; case PublicEnrollmentAppointmentFormStep.FillContactDetails: - return ; + return ; case PublicEnrollmentAppointmentFormStep.Preview: return ; } diff --git a/frontend/packages/vkt/src/components/publicEnrollmentAppointment/steps/FillContactDetails.tsx b/frontend/packages/vkt/src/components/publicEnrollmentAppointment/steps/FillContactDetails.tsx index 9b81b5237..78a065b83 100644 --- a/frontend/packages/vkt/src/components/publicEnrollmentAppointment/steps/FillContactDetails.tsx +++ b/frontend/packages/vkt/src/components/publicEnrollmentAppointment/steps/FillContactDetails.tsx @@ -12,6 +12,7 @@ import { useCommonTranslation, usePublicTranslation } from 'configs/i18n'; import { useAppDispatch } from 'configs/redux'; import { PublicEnrollment, + PublicEnrollmentAppointment, PublicEnrollmentContactDetails, } from 'interfaces/publicEnrollment'; import { updatePublicEnrollment } from 'redux/reducers/publicEnrollment'; @@ -57,7 +58,13 @@ const emailsMatch = ( return errors; }; -export const FillContactDetails = ({ isLoading }: { isLoading: boolean }) => { +export const FillContactDetails = ({ + isLoading, + enrollment, +}: { + isLoading: boolean; + enrollment: PublicEnrollmentAppointment; +}) => { const { t } = usePublicTranslation({ keyPrefix: 'vkt.component.publicEnrollment.steps.fillContactDetails', }); @@ -118,30 +125,6 @@ export const FillContactDetails = ({ isLoading }: { isLoading: boolean }) => { return (
-
-

{t('title')}

- {translateCommon('requiredFieldsInfo')} -
- - { - e.preventDefault(); - - return false; - }} - /> -
-
{ autoComplete={InputAutoComplete.PhoneNumber} /> {!isPhone && } - + console.log(isValid)} + showValidation={false} + />
); }; diff --git a/frontend/packages/vkt/src/components/publicEnrollmentAppointment/steps/Preview.tsx b/frontend/packages/vkt/src/components/publicEnrollmentAppointment/steps/Preview.tsx index ba3f4a739..d3a5a880c 100644 --- a/frontend/packages/vkt/src/components/publicEnrollmentAppointment/steps/Preview.tsx +++ b/frontend/packages/vkt/src/components/publicEnrollmentAppointment/steps/Preview.tsx @@ -5,7 +5,6 @@ import { FormControlLabel, FormHelperText, } from '@mui/material'; -import { useEffect } from 'react'; import { Trans } from 'react-i18next'; import { H2, Text, WebLink } from 'shared/components'; import { APIResponseStatus, Color } from 'shared/enums'; @@ -17,7 +16,6 @@ import { useAppDispatch, useAppSelector } from 'configs/redux'; import { PublicEnrollmentAppointment } from 'interfaces/publicEnrollment'; import { updatePublicEnrollment } from 'redux/reducers/publicEnrollment'; import { publicEnrollmentSelector } from 'redux/selectors/publicEnrollment'; -import { EnrollmentUtils } from 'utils/enrollment'; const ContactDetails = ({ enrollment, @@ -98,22 +96,14 @@ const PrivacyStatementCheckboxLabel = ({ export const Preview = ({ enrollment, isLoading, - setIsStepValid, - showValidation, }: { enrollment: PublicEnrollmentAppointment; isLoading: boolean; - setIsStepValid: (isValid: boolean) => void; - showValidation: boolean; }) => { const translateCommon = useCommonTranslation(); const { paymentLoadingStatus } = useAppSelector(publicEnrollmentSelector); - useEffect(() => { - setIsStepValid(enrollment.privacyStatementConfirmation); - }, [setIsStepValid, enrollment]); - const dispatch = useAppDispatch(); const handleCheckboxClick = () => { @@ -124,8 +114,7 @@ export const Preview = ({ ); }; - const hasPrivacyStatementError = - showValidation && !enrollment.privacyStatementConfirmation; + const hasPrivacyStatementError = !enrollment.privacyStatementConfirmation; return (
diff --git a/frontend/packages/vkt/src/redux/reducers/publicEnrollmentAppointment.ts b/frontend/packages/vkt/src/redux/reducers/publicEnrollmentAppointment.ts index bb865f271..dc88e9aba 100644 --- a/frontend/packages/vkt/src/redux/reducers/publicEnrollmentAppointment.ts +++ b/frontend/packages/vkt/src/redux/reducers/publicEnrollmentAppointment.ts @@ -1,24 +1,15 @@ -import { createSlice, current, PayloadAction } from '@reduxjs/toolkit'; +import { createSlice, PayloadAction } from '@reduxjs/toolkit'; import { APIResponseStatus } from 'shared/enums'; -import { - Attachment, - PublicFreeEnrollmentDetails, -} from 'interfaces/publicEducation'; -import { - PublicEnrollment, - PublicReservation, -} from 'interfaces/publicEnrollment'; -import { PublicExamEvent } from 'interfaces/publicExamEvent'; +import { PublicEnrollmentAppointment } from 'interfaces/publicEnrollment'; import { PublicPerson } from 'interfaces/publicPerson'; -import { EnrollmentUtils } from 'utils/enrollment'; export interface PublicEnrollmentAppointmentState { loadEnrollmentStatus: APIResponseStatus; enrollmentSubmitStatus: APIResponseStatus; paymentLoadingStatus: APIResponseStatus; cancelStatus: APIResponseStatus; - enrollment: PublicEnrollment; + enrollment: PublicEnrollmentAppointment; person?: PublicPerson; } @@ -66,10 +57,7 @@ const publicEnrollmentAppointmentSlice = createSlice({ rejectPublicEnrollmentAppointment(state) { state.loadEnrollmentStatus = APIResponseStatus.Error; }, - storePublicEnrollmentAppointment( - state, - action: PayloadAction, - ) { + storePublicEnrollmentAppointment(state) { state.loadEnrollmentStatus = APIResponseStatus.Success; }, setLoadingPayment(state) { From b36b02fd8781e412ba5b38f10008ba27b890c526 Mon Sep 17 00:00:00 2001 From: Jarkko Pesonen <435495+jrkkp@users.noreply.github.com> Date: Tue, 1 Oct 2024 17:17:14 +0300 Subject: [PATCH 021/248] VKT(Frontend & Backend): enrollment appointment continues --- backend/vkt/db/4_init.sql | 11 +++++ .../java/fi/oph/vkt/api/PublicController.java | 11 +++++ .../dto/PublicEnrollmentAppointmentDTO.java | 28 +++++++++++ .../oph/vkt/model/EnrollmentAppointment.java | 6 ++- .../vkt/service/PublicEnrollmentService.java | 45 ++++++++++++++++- .../java/fi/oph/vkt/util/UIRouteUtil.java | 2 +- .../db/changelog/db.changelog-1.0.xml | 33 ++++++++++++- ...licEnrollmentAppointmentControlButtons.tsx | 30 ++++++------ ...PublicEnrollmentAppointmentDesktopGrid.tsx | 13 +++++ .../PublicEnrollmentAppointmentGrid.tsx | 34 ++++++++++++- ...ublicEnrollmentAppointmentStepContents.tsx | 15 +++++- .../steps/Authenticate.tsx | 8 ++- .../steps/FillContactDetails.tsx | 10 +++- .../steps/PersonDetails.tsx | 47 ++++++++++++++++++ frontend/packages/vkt/src/enums/api.ts | 3 +- .../vkt/src/interfaces/publicEnrollment.ts | 17 ++++++- .../reducers/publicEnrollmentAppointment.ts | 7 ++- .../packages/vkt/src/redux/sagas/index.ts | 2 + .../sagas/publicEnrollmentAppointment.ts | 46 +++++++++++++++++ frontend/packages/vkt/src/utils/routes.ts | 49 +++++++++++++++++-- .../packages/vkt/src/utils/serialization.ts | 11 +++++ 21 files changed, 392 insertions(+), 36 deletions(-) create mode 100644 backend/vkt/src/main/java/fi/oph/vkt/api/dto/PublicEnrollmentAppointmentDTO.java create mode 100644 frontend/packages/vkt/src/components/publicEnrollmentAppointment/steps/PersonDetails.tsx create mode 100644 frontend/packages/vkt/src/redux/sagas/publicEnrollmentAppointment.ts diff --git a/backend/vkt/db/4_init.sql b/backend/vkt/db/4_init.sql index a0315efbe..b9f3d1c3d 100644 --- a/backend/vkt/db/4_init.sql +++ b/backend/vkt/db/4_init.sql @@ -375,3 +375,14 @@ SELECT exam_event_id, (SELECT max(person_id) FROM person), 'CANCELED', true, 'foo@bar.invalid', '0404040404', null, null, null, null FROM exam_event; + +-- Insert enrollment appointment +INSERT INTO enrollment_appointment(person_id, + skill_oral, skill_textual, skill_understanding, + partial_exam_speaking, partial_exam_speech_comprehension, partial_exam_writing, partial_exam_reading_comprehension, + status, digital_certificate_consent, email, phone_number, street, postal_code, town, country) +VALUES (SELECT max(person_id) FROM person), + true, true, true, + true, true, true, true, + 'COMPLETED', true, + 'foo@bar.invalid', '0404040404', null, null, null, null; 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 df437d1c8..9e1b30210 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 @@ -2,6 +2,7 @@ import com.fasterxml.jackson.core.JsonProcessingException; import fi.oph.vkt.api.dto.PublicEducationDTO; +import fi.oph.vkt.api.dto.PublicEnrollmentAppointmentDTO; import fi.oph.vkt.api.dto.PublicEnrollmentCreateDTO; import fi.oph.vkt.api.dto.PublicEnrollmentDTO; import fi.oph.vkt.api.dto.PublicEnrollmentInitialisationDTO; @@ -149,6 +150,16 @@ public PublicExamEventDTO getExamEventInfo(@PathVariable final long examEventId) return publicExamEventService.getExamEvent(examEventId); } + @GetMapping(path = "/enrollment/appointment/{enrollmentAppointmentId:\\d+}") + public PublicEnrollmentAppointmentDTO getEnrollmentAppointment( + @PathVariable final long enrollmentAppointmentId, + final HttpSession session + ) { + final Person person = publicAuthService.getPersonFromSession(session); + + return publicEnrollmentService.getEnrollmentAppointment(enrollmentAppointmentId, person); + } + @GetMapping(path = "/education") public List getEducation(final HttpSession session) throws JsonProcessingException { final Person person = publicAuthService.getPersonFromSession(session); diff --git a/backend/vkt/src/main/java/fi/oph/vkt/api/dto/PublicEnrollmentAppointmentDTO.java b/backend/vkt/src/main/java/fi/oph/vkt/api/dto/PublicEnrollmentAppointmentDTO.java new file mode 100644 index 000000000..1b35540fe --- /dev/null +++ b/backend/vkt/src/main/java/fi/oph/vkt/api/dto/PublicEnrollmentAppointmentDTO.java @@ -0,0 +1,28 @@ +package fi.oph.vkt.api.dto; + +import fi.oph.vkt.model.type.EnrollmentStatus; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.Builder; +import lombok.NonNull; + +@Builder +public record PublicEnrollmentAppointmentDTO( + @NonNull @NotNull Long id, + @NonNull @NotNull Boolean oralSkill, + @NonNull @NotNull Boolean textualSkill, + @NonNull @NotNull Boolean understandingSkill, + @NonNull @NotNull Boolean speakingPartialExam, + @NonNull @NotNull Boolean speechComprehensionPartialExam, + @NonNull @NotNull Boolean writingPartialExam, + @NonNull @NotNull Boolean readingComprehensionPartialExam, + @NonNull @NotNull EnrollmentStatus status, + String previousEnrollment, + @NonNull @NotNull Boolean digitalCertificateConsent, + @NonNull @NotBlank String email, + @NonNull @NotBlank String phoneNumber, + String street, + String postalCode, + String town, + String country +) {} diff --git a/backend/vkt/src/main/java/fi/oph/vkt/model/EnrollmentAppointment.java b/backend/vkt/src/main/java/fi/oph/vkt/model/EnrollmentAppointment.java index 24d177545..f0dfbd48e 100644 --- a/backend/vkt/src/main/java/fi/oph/vkt/model/EnrollmentAppointment.java +++ b/backend/vkt/src/main/java/fi/oph/vkt/model/EnrollmentAppointment.java @@ -24,7 +24,7 @@ @Getter @Setter @Entity -@Table(name = "enrollment") +@Table(name = "enrollment_appointment") public class EnrollmentAppointment extends BaseEntity { @Id @@ -53,6 +53,10 @@ public class EnrollmentAppointment extends BaseEntity { @Column(name = "partial_exam_reading_comprehension") private boolean readingComprehensionPartialExam; + @Column(name = "status", nullable = false) + @Enumerated(value = EnumType.STRING) + private EnrollmentStatus status; + @Column(name = "digital_certificate_consent") private boolean digitalCertificateConsent; diff --git a/backend/vkt/src/main/java/fi/oph/vkt/service/PublicEnrollmentService.java b/backend/vkt/src/main/java/fi/oph/vkt/service/PublicEnrollmentService.java index 89950e1f4..84f049355 100644 --- a/backend/vkt/src/main/java/fi/oph/vkt/service/PublicEnrollmentService.java +++ b/backend/vkt/src/main/java/fi/oph/vkt/service/PublicEnrollmentService.java @@ -4,6 +4,7 @@ import fi.oph.vkt.api.dto.FreeEnrollmentDetails; import fi.oph.vkt.api.dto.FreeEnrollmentDetailsDTO; import fi.oph.vkt.api.dto.PublicEducationDTO; +import fi.oph.vkt.api.dto.PublicEnrollmentAppointmentDTO; import fi.oph.vkt.api.dto.PublicEnrollmentCreateDTO; import fi.oph.vkt.api.dto.PublicEnrollmentDTO; import fi.oph.vkt.api.dto.PublicEnrollmentInitialisationDTO; @@ -12,6 +13,7 @@ import fi.oph.vkt.api.dto.PublicPersonDTO; import fi.oph.vkt.api.dto.PublicReservationDTO; import fi.oph.vkt.model.Enrollment; +import fi.oph.vkt.model.EnrollmentAppointment; import fi.oph.vkt.model.ExamEvent; import fi.oph.vkt.model.FeatureFlag; import fi.oph.vkt.model.FreeEnrollment; @@ -21,6 +23,7 @@ import fi.oph.vkt.model.type.EnrollmentStatus; import fi.oph.vkt.model.type.FreeEnrollmentSource; import fi.oph.vkt.model.type.FreeEnrollmentType; +import fi.oph.vkt.repository.EnrollmentAppointmentRepository; import fi.oph.vkt.repository.EnrollmentRepository; import fi.oph.vkt.repository.ExamEventRepository; import fi.oph.vkt.repository.FreeEnrollmentRepository; @@ -34,7 +37,6 @@ import fi.oph.vkt.util.exception.APIException; import fi.oph.vkt.util.exception.APIExceptionType; import fi.oph.vkt.util.exception.NotFoundException; -import java.time.LocalDate; import java.time.LocalDateTime; import java.util.List; import java.util.Map; @@ -50,6 +52,7 @@ public class PublicEnrollmentService extends AbstractEnrollmentService { private final EnrollmentRepository enrollmentRepository; + private final EnrollmentAppointmentRepository enrollmentAppointmentRepository; private final ExamEventRepository examEventRepository; private final PublicEnrollmentEmailService publicEnrollmentEmailService; private final PublicReservationService publicReservationService; @@ -583,4 +586,44 @@ public Map getPresignedPostRequest( return s3Service.getPresignedPostRequest(key, extension); } + + private PublicEnrollmentAppointmentDTO createEnrollmentAppointmentDTO( + final EnrollmentAppointment enrollmentAppointment + ) { + return PublicEnrollmentAppointmentDTO + .builder() + .id(enrollmentAppointment.getId()) + .oralSkill(enrollmentAppointment.isOralSkill()) + .textualSkill(enrollmentAppointment.isTextualSkill()) + .understandingSkill(enrollmentAppointment.isUnderstandingSkill()) + .speakingPartialExam(enrollmentAppointment.isSpeakingPartialExam()) + .speechComprehensionPartialExam(enrollmentAppointment.isSpeechComprehensionPartialExam()) + .writingPartialExam(enrollmentAppointment.isWritingPartialExam()) + .readingComprehensionPartialExam(enrollmentAppointment.isReadingComprehensionPartialExam()) + .digitalCertificateConsent(enrollmentAppointment.isDigitalCertificateConsent()) + .email(enrollmentAppointment.getEmail()) + .phoneNumber(enrollmentAppointment.getPhoneNumber()) + .street(enrollmentAppointment.getStreet()) + .postalCode(enrollmentAppointment.getPostalCode()) + .town(enrollmentAppointment.getTown()) + .country(enrollmentAppointment.getCountry()) + .status(enrollmentAppointment.getStatus()) + .build(); + } + + @Transactional(readOnly = true) + public PublicEnrollmentAppointmentDTO getEnrollmentAppointment( + final long enrollmentAppointmentId, + final Person person + ) { + final EnrollmentAppointment enrollmentAppointment = enrollmentAppointmentRepository.getReferenceById( + enrollmentAppointmentId + ); + + if (person.getId() != enrollmentAppointment.getPerson().getId()) { + throw new APIException(APIExceptionType.RESERVATION_PERSON_SESSION_MISMATCH); + } + + return createEnrollmentAppointmentDTO(enrollmentAppointment); + } } diff --git a/backend/vkt/src/main/java/fi/oph/vkt/util/UIRouteUtil.java b/backend/vkt/src/main/java/fi/oph/vkt/util/UIRouteUtil.java index 18ba970b3..7a57de784 100644 --- a/backend/vkt/src/main/java/fi/oph/vkt/util/UIRouteUtil.java +++ b/backend/vkt/src/main/java/fi/oph/vkt/util/UIRouteUtil.java @@ -32,6 +32,6 @@ private String getPublicBaseUrl() { } public String getEnrollmentAppointmentUrl(final long enrollmentAppointmentId) { - return String.format("%s/ota-yhteytta/%s/tunnistaudu", getPublicBaseUrl(), enrollmentAppointmentId); + return String.format("%s/markkinapaikka/%s/tiedot", getPublicBaseUrl(), enrollmentAppointmentId); } } diff --git a/backend/vkt/src/main/resources/db/changelog/db.changelog-1.0.xml b/backend/vkt/src/main/resources/db/changelog/db.changelog-1.0.xml index 7301f26a7..642c4637f 100644 --- a/backend/vkt/src/main/resources/db/changelog/db.changelog-1.0.xml +++ b/backend/vkt/src/main/resources/db/changelog/db.changelog-1.0.xml @@ -864,6 +864,9 @@ + + + @@ -874,9 +877,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + diff --git a/frontend/packages/vkt/src/components/publicEnrollmentAppointment/PublicEnrollmentAppointmentControlButtons.tsx b/frontend/packages/vkt/src/components/publicEnrollmentAppointment/PublicEnrollmentAppointmentControlButtons.tsx index 385ff0568..6db645c55 100644 --- a/frontend/packages/vkt/src/components/publicEnrollmentAppointment/PublicEnrollmentAppointmentControlButtons.tsx +++ b/frontend/packages/vkt/src/components/publicEnrollmentAppointment/PublicEnrollmentAppointmentControlButtons.tsx @@ -10,15 +10,19 @@ import { useDialog } from 'shared/hooks'; import { useCommonTranslation, usePublicTranslation } from 'configs/i18n'; import { useAppDispatch } from 'configs/redux'; -import { PublicEnrollmentFormStep } from 'enums/publicEnrollment'; +import { PublicEnrollmentAppointmentFormStep } from 'enums/publicEnrollment'; import { RouteUtils } from 'utils/routes'; export const PublicEnrollmentAppointmentControlButtons = ({ activeStep, enrollment, + isStepValid, + setShowValidation, }: { - activeStep: PublicEnrollmentFormStep; + activeStep: PublicEnrollmentAppointmentFormStep; enrollment: PublicEnrollmentAppointment; + isStepValid: boolean; + setShowValidation: (showValidation: boolean) => void; }) => { const { t } = usePublicTranslation({ keyPrefix: 'vkt.component.publicEnrollment.controlButtons', @@ -54,15 +58,15 @@ export const PublicEnrollmentAppointmentControlButtons = ({ }, [submitStatus, enrollment.id, dispatch]); const handleBackBtnClick = () => { - const nextStep: PublicEnrollmentFormStep = activeStep - 1; - navigate(RouteUtils.stepToRoute(nextStep, examEventId)); + const nextStep: PublicEnrollmentAppointmentFormStep = activeStep - 1; + navigate(RouteUtils.appointmentStepToRoute(nextStep, enrollment.id)); }; const handleNextBtnClick = () => { if (isStepValid) { setShowValidation(false); - const nextStep: PublicEnrollmentFormStep = activeStep + 1; - navigate(RouteUtils.stepToRoute(nextStep, examEventId)); + const nextStep: PublicEnrollmentAppointmentFormStep = activeStep + 1; + navigate(RouteUtils.appointmentStepToRoute(nextStep, enrollment.id)); } else { setShowValidation(true); } @@ -105,7 +109,7 @@ export const PublicEnrollmentAppointmentControlButtons = ({ data-testid="public-enrollment__controlButtons__back" startIcon={} disabled={ - activeStep == PublicEnrollmentFormStep.FillContactDetails || + activeStep == PublicEnrollmentAppointmentFormStep.FillContactDetails || isPaymentLoading } > @@ -129,7 +133,7 @@ export const PublicEnrollmentAppointmentControlButtons = ({ const SubmitButton = () => ( diff --git a/frontend/packages/vkt/src/components/publicEnrollmentAppointment/PublicEnrollmentAppointmentDesktopGrid.tsx b/frontend/packages/vkt/src/components/publicEnrollmentAppointment/PublicEnrollmentAppointmentDesktopGrid.tsx index e4d2315a3..cc1ef643b 100644 --- a/frontend/packages/vkt/src/components/publicEnrollmentAppointment/PublicEnrollmentAppointmentDesktopGrid.tsx +++ b/frontend/packages/vkt/src/components/publicEnrollmentAppointment/PublicEnrollmentAppointmentDesktopGrid.tsx @@ -13,9 +13,17 @@ import { PublicEnrollmentAppointment } from 'interfaces/publicEnrollment'; export const PublicEnrollmentAppointmentDesktopGrid = ({ activeStep, enrollment, + isStepValid, + showValidation, + setIsStepValid, + setShowValidation, }: { activeStep: PublicEnrollmentFormStep; + isStepValid: boolean; enrollment: PublicEnrollmentAppointment; + showValidation: boolean; + setIsStepValid: (isValid: boolean) => void; + setShowValidation: (showValidation: boolean) => void; }) => { const translateCommon = useCommonTranslation(); @@ -34,6 +42,8 @@ export const PublicEnrollmentAppointmentDesktopGrid = ({ {activeStep > PublicEnrollmentFormStep.Authenticate && ( @@ -42,6 +52,9 @@ export const PublicEnrollmentAppointmentDesktopGrid = ({ )}
diff --git a/frontend/packages/vkt/src/components/publicEnrollmentAppointment/PublicEnrollmentAppointmentGrid.tsx b/frontend/packages/vkt/src/components/publicEnrollmentAppointment/PublicEnrollmentAppointmentGrid.tsx index 9a341e285..f640994f4 100644 --- a/frontend/packages/vkt/src/components/publicEnrollmentAppointment/PublicEnrollmentAppointmentGrid.tsx +++ b/frontend/packages/vkt/src/components/publicEnrollmentAppointment/PublicEnrollmentAppointmentGrid.tsx @@ -1,16 +1,42 @@ import { Grid } from '@mui/material'; +import { useParams } from 'react-router'; import { PublicEnrollmentAppointmentDesktopGrid } from 'components/publicEnrollmentAppointment/PublicEnrollmentAppointmentDesktopGrid'; -import { useAppSelector } from 'configs/redux'; +import { useAppDispatch, useAppSelector } from 'configs/redux'; +import { useEffect, useState } from 'react'; +import { APIResponseStatus } from 'shared/enums'; import { PublicEnrollmentAppointmentFormStep } from 'enums/publicEnrollment'; import { publicEnrollmentAppointmentSelector } from 'redux/selectors/publicEnrollmentAppointment'; +import { loadPublicEnrollmentAppointment } from 'redux/reducers/publicEnrollmentAppointment'; export const PublicEnrollmentAppointmentGrid = ({ activeStep, }: { activeStep: PublicEnrollmentAppointmentFormStep; }) => { - const { enrollment } = useAppSelector(publicEnrollmentAppointmentSelector); + const params = useParams(); + const dispatch = useAppDispatch(); + const { enrollment, loadEnrollmentStatus } = useAppSelector(publicEnrollmentAppointmentSelector); + const [isStepValid, setIsStepValid] = useState(false); + const [showValidation, setShowValidation] = useState(false); + + const isAuthenticatePassed = + activeStep > PublicEnrollmentAppointmentFormStep.Authenticate; + + useEffect(() => { + console.log( + isAuthenticatePassed, + loadEnrollmentStatus === APIResponseStatus.NotStarted, + params.enrollmentId + ); + if ( + isAuthenticatePassed && + loadEnrollmentStatus === APIResponseStatus.NotStarted && + params.enrollmentId + ) { + dispatch(loadPublicEnrollmentAppointment(+params.enrollmentId)); + } + }, [dispatch, loadEnrollmentStatus, isAuthenticatePassed, params.enrollmentId]); return ( ); diff --git a/frontend/packages/vkt/src/components/publicEnrollmentAppointment/PublicEnrollmentAppointmentStepContents.tsx b/frontend/packages/vkt/src/components/publicEnrollmentAppointment/PublicEnrollmentAppointmentStepContents.tsx index ede5e7c10..9953ef9a6 100644 --- a/frontend/packages/vkt/src/components/publicEnrollmentAppointment/PublicEnrollmentAppointmentStepContents.tsx +++ b/frontend/packages/vkt/src/components/publicEnrollmentAppointment/PublicEnrollmentAppointmentStepContents.tsx @@ -7,15 +7,26 @@ import { PublicEnrollmentAppointment } from 'interfaces/publicEnrollment'; export const PublicEnrollmentAppointmentStepContents = ({ activeStep, enrollment, + setIsStepValid, + showValidation, }: { activeStep: PublicEnrollmentAppointmentFormStep; enrollment: PublicEnrollmentAppointment; + setIsStepValid: (isValid: boolean) => void; + showValidation: boolean; }) => { switch (activeStep) { case PublicEnrollmentAppointmentFormStep.Authenticate: - return ; + return ; case PublicEnrollmentAppointmentFormStep.FillContactDetails: - return ; + return ( + + ); case PublicEnrollmentAppointmentFormStep.Preview: return ; } diff --git a/frontend/packages/vkt/src/components/publicEnrollmentAppointment/steps/Authenticate.tsx b/frontend/packages/vkt/src/components/publicEnrollmentAppointment/steps/Authenticate.tsx index 10ecca4fb..13ae02bfa 100644 --- a/frontend/packages/vkt/src/components/publicEnrollmentAppointment/steps/Authenticate.tsx +++ b/frontend/packages/vkt/src/components/publicEnrollmentAppointment/steps/Authenticate.tsx @@ -7,7 +7,11 @@ import { useAppDispatch } from 'configs/redux'; import { cancelPublicEnrollment } from 'redux/reducers/publicEnrollment'; import { RouteUtils } from 'utils/routes'; -export const Authenticate = () => { +export const Authenticate = ({ + enrollment, +} : { + enrollment: PublicEnrollmentAppointment; +}) => { const [isAuthRedirecting, setIsAuthRedirecting] = useState(false); const { t } = usePublicTranslation({ keyPrefix: 'vkt.component.publicEnrollment.steps.authenticate', @@ -21,7 +25,7 @@ export const Authenticate = () => { const type = 'appointment'; - window.location.href = RouteUtils.getAuthLoginApiRoute(examEvent.id, type); + window.location.href = RouteUtils.getAuthLoginApiRoute(enrollment.id, type); }; const onCancel = () => { diff --git a/frontend/packages/vkt/src/components/publicEnrollmentAppointment/steps/FillContactDetails.tsx b/frontend/packages/vkt/src/components/publicEnrollmentAppointment/steps/FillContactDetails.tsx index 78a065b83..149d09179 100644 --- a/frontend/packages/vkt/src/components/publicEnrollmentAppointment/steps/FillContactDetails.tsx +++ b/frontend/packages/vkt/src/components/publicEnrollmentAppointment/steps/FillContactDetails.tsx @@ -7,7 +7,7 @@ import { TextField } from 'shared/interfaces'; import { FieldErrors } from 'shared/utils'; import { CertificateShipping } from 'components/publicEnrollment/steps/CertificateShipping'; -import { PersonDetails } from 'components/publicEnrollment/steps/PersonDetails'; +import { PersonDetails } from 'components/publicEnrollmentAppointment/steps/PersonDetails'; import { useCommonTranslation, usePublicTranslation } from 'configs/i18n'; import { useAppDispatch } from 'configs/redux'; import { @@ -61,9 +61,13 @@ const emailsMatch = ( export const FillContactDetails = ({ isLoading, enrollment, + setIsStepValid, + showValidation, }: { isLoading: boolean; enrollment: PublicEnrollmentAppointment; + setIsStepValid: (isValid: boolean) => void; + showValidation: boolean; }) => { const { t } = usePublicTranslation({ keyPrefix: 'vkt.component.publicEnrollment.steps.fillContactDetails', @@ -77,6 +81,10 @@ export const FillContactDetails = ({ const dispatch = useAppDispatch(); const errors = []; + useEffect(() => { + setIsStepValid(true); + }); + const handleChange = (fieldName: keyof PublicEnrollmentContactDetails) => (event: ChangeEvent) => { diff --git a/frontend/packages/vkt/src/components/publicEnrollmentAppointment/steps/PersonDetails.tsx b/frontend/packages/vkt/src/components/publicEnrollmentAppointment/steps/PersonDetails.tsx new file mode 100644 index 000000000..69a662d6e --- /dev/null +++ b/frontend/packages/vkt/src/components/publicEnrollmentAppointment/steps/PersonDetails.tsx @@ -0,0 +1,47 @@ +import { H2, Text } from 'shared/components'; + +import { usePublicTranslation } from 'configs/i18n'; +import { useAppSelector } from 'configs/redux'; +import { PublicPerson } from 'interfaces/publicPerson'; +import { publicEnrollmentAppointmentSelector } from 'redux/selectors/publicEnrollmentAppointment'; + +export const PersonDetails = ({ + isPreviewStep, +}: { + isPreviewStep: boolean; +}) => { + const { t } = usePublicTranslation({ + keyPrefix: 'vkt.component.publicEnrollment.steps.personDetails', + }); + + const { person } = useAppSelector(publicEnrollmentAppointmentSelector); + + if (!person) { + return null; + } + + const displayField = (field: keyof PublicPerson) => ( +
+ + {t(field)} + {':'} + + {person[field]} +
+ ); + + return ( +
+

{t('title')}

+
+ {displayField('lastName')} + {displayField('firstName')} +
+
+ ); +}; diff --git a/frontend/packages/vkt/src/enums/api.ts b/frontend/packages/vkt/src/enums/api.ts index 4d90b4fba..59878f6d6 100644 --- a/frontend/packages/vkt/src/enums/api.ts +++ b/frontend/packages/vkt/src/enums/api.ts @@ -1,7 +1,8 @@ export enum APIEndpoints { - PublicAuthLogin = '/vkt/api/v1/auth/login/:examEventId/:type?locale=:locale', + PublicAuthLogin = '/vkt/api/v1/auth/login/:targetId/:type?locale=:locale', PublicAuthLogout = '/vkt/api/v1/auth/logout', PublicExamEvent = '/vkt/api/v1/examEvent', + PublicEnrollmentAppointment = '/vkt/api/v1/enrollment/appointment', PublicEnrollment = '/vkt/api/v1/enrollment', PublicReservation = '/vkt/api/v1/reservation', PublicEducation = '/vkt/api/v1/education', diff --git a/frontend/packages/vkt/src/interfaces/publicEnrollment.ts b/frontend/packages/vkt/src/interfaces/publicEnrollment.ts index 96f4f4b19..8f599ba15 100644 --- a/frontend/packages/vkt/src/interfaces/publicEnrollment.ts +++ b/frontend/packages/vkt/src/interfaces/publicEnrollment.ts @@ -57,8 +57,6 @@ export interface PublicEnrollment isQueued?: boolean; } -export interface PublicEnrollmentAppointment extends PublicEnrollment {} - export interface PublicEnrollmentResponse extends Omit< PublicEnrollment, @@ -71,3 +69,18 @@ export interface PublicEnrollmentResponse WithId { status: EnrollmentStatus; } + +export interface PublicEnrollmentAppointment extends PublicEnrollment {} + +export interface PublicEnrollmentAppointmentResponse + extends Omit< + PublicEnrollmentAppointment, + | 'emailConfirmation' + | 'id' + | 'hasPreviousEnrollment' + | 'privacyStatementConfirmation' + | 'status' + >, + WithId { + status: EnrollmentStatus; +} diff --git a/frontend/packages/vkt/src/redux/reducers/publicEnrollmentAppointment.ts b/frontend/packages/vkt/src/redux/reducers/publicEnrollmentAppointment.ts index dc88e9aba..b6ee68f41 100644 --- a/frontend/packages/vkt/src/redux/reducers/publicEnrollmentAppointment.ts +++ b/frontend/packages/vkt/src/redux/reducers/publicEnrollmentAppointment.ts @@ -35,7 +35,7 @@ export const initialState: PublicEnrollmentState = { postalCode: '', town: '', country: '', - id: undefined, + id: 1, // FIXME hasPreviousEnrollment: undefined, previousEnrollment: '', privacyStatementConfirmation: false, @@ -44,7 +44,10 @@ export const initialState: PublicEnrollmentState = { hasPaymentLink: undefined, isQueued: undefined, }, - person: undefined, + person: { // FIXME + firstName: 'foo', + lastName: 'bar', + }, }; const publicEnrollmentAppointmentSlice = createSlice({ diff --git a/frontend/packages/vkt/src/redux/sagas/index.ts b/frontend/packages/vkt/src/redux/sagas/index.ts index cfaf07a1d..69b3a6c80 100644 --- a/frontend/packages/vkt/src/redux/sagas/index.ts +++ b/frontend/packages/vkt/src/redux/sagas/index.ts @@ -8,6 +8,7 @@ import { watchClerkUser } from 'redux/sagas/clerkUser'; import { watchFeatureFlags } from 'redux/sagas/featureFlags'; import { watchPublicEducation } from 'redux/sagas/publicEducation'; import { watchPublicEnrollments } from 'redux/sagas/publicEnrollment'; +import { watchPublicEnrollmentAppointments } from 'redux/sagas/publicEnrollmentAppointment'; import { watchPublicExamEvents } from 'redux/sagas/publicExamEvent'; import { watchFileUpload } from 'redux/sagas/publicFileUpload'; import { watchPublicUser } from 'redux/sagas/publicUser'; @@ -25,5 +26,6 @@ export default function* rootSaga() { watchFeatureFlags(), watchFileUpload(), watchPublicEducation(), + watchPublicEnrollmentAppointments(), ]); } diff --git a/frontend/packages/vkt/src/redux/sagas/publicEnrollmentAppointment.ts b/frontend/packages/vkt/src/redux/sagas/publicEnrollmentAppointment.ts new file mode 100644 index 000000000..29026eb95 --- /dev/null +++ b/frontend/packages/vkt/src/redux/sagas/publicEnrollmentAppointment.ts @@ -0,0 +1,46 @@ +import { PayloadAction } from '@reduxjs/toolkit'; +import { AxiosError, AxiosResponse } from 'axios'; +import { call, put, takeLatest } from 'redux-saga/effects'; + +import axiosInstance from 'configs/axios'; +import { APIEndpoints } from 'enums/api'; +import { + PublicEnrollment, + PublicReservationDetailsResponse, + PublicReservationResponse, +} from 'interfaces/publicEnrollment'; +import { PublicEnrollmentAppointmentResponse } from 'interfaces/publicEnrollment'; +import { setAPIError } from 'redux/reducers/APIError'; +import { + loadPublicEnrollmentAppointment, + storePublicEnrollmentAppointment, + rejectPublicEnrollmentAppointment, +} from 'redux/reducers/publicEnrollmentAppointment'; +import { NotifierUtils } from 'utils/notifier'; +import { SerializationUtils } from 'utils/serialization'; + +function* loadPublicEnrollmentAppointmentSaga(action: PayloadAction) { + try { + const enrollmentId = action.payload; + const loadUrl = `${APIEndpoints.PublicEnrollmentAppointment}/${enrollmentId}`; + + const response: AxiosResponse = yield call( + axiosInstance.get, + loadUrl, + ); + + const enrollmentAppointment = SerializationUtils.deserializePublicEnrollmentAppointment( + response.data, + ); + + yield put(storePublicEnrollmentAppointment(enrollmentAppointment)); + } catch (error) { + const errorMessage = NotifierUtils.getAPIErrorMessage(error as AxiosError); + yield put(setAPIError(errorMessage)); + yield put(rejectPublicEnrollmentAppointment()); + } +} + +export function* watchPublicEnrollmentAppointments() { + yield takeLatest(loadPublicEnrollmentAppointment, loadPublicEnrollmentAppointmentSaga); +} diff --git a/frontend/packages/vkt/src/utils/routes.ts b/frontend/packages/vkt/src/utils/routes.ts index dc3fefe26..3a1ccc2f6 100644 --- a/frontend/packages/vkt/src/utils/routes.ts +++ b/frontend/packages/vkt/src/utils/routes.ts @@ -3,13 +3,13 @@ import { AppLanguage } from 'shared/enums'; import { getCurrentLang } from 'configs/i18n'; import { APIEndpoints } from 'enums/api'; import { AppRoutes } from 'enums/app'; -import { PublicEnrollmentFormStep } from 'enums/publicEnrollment'; +import { PublicEnrollmentFormStep, PublicEnrollmentAppointmentFormStep } from 'enums/publicEnrollment'; export class RouteUtils { - static getAuthLoginApiRoute(examEventId: number, type: string) { + static getAuthLoginApiRoute(targetId: number, type: string) { return APIEndpoints.PublicAuthLogin.replace( - ':examEventId', - `${examEventId}`, + ':targetId', + `${targetId}`, ) .replace(':type', type) .replace(':locale', RouteUtils.getApiRouteLocale()); @@ -89,4 +89,45 @@ export class RouteUtils { static replaceExamEventId(route: string, examEventId: number) { return route.replace(':examEventId', examEventId.toString()); } + + static appointmentStepToRoute(step: PublicEnrollmentAppointmentFormStep, enrollmentId: number) { + switch (step) { + case PublicEnrollmentAppointmentFormStep.Authenticate: + return RouteUtils.replaceEnrollmentId(AppRoutes.PublicAuthAppointment, enrollmentId); + + case PublicEnrollmentAppointmentFormStep.FillContactDetails: + return RouteUtils.replaceEnrollmentId( + AppRoutes.PublicEnrollmentAppointmentContactDetails, + enrollmentId, + ); + + case PublicEnrollmentAppointmentFormStep.Preview: + return RouteUtils.replaceEnrollmentId( + AppRoutes.PublicEnrollmentAppointmentPreview, + enrollmentId, + ); + + case PublicEnrollmentAppointmentFormStep.Payment: + return RouteUtils.replaceEnrollmentId( + AppRoutes.PublicEnrollmentPaymentFail, + enrollmentId, + ); + + case PublicEnrollmentAppointmentFormStep.PaymentSuccess: + return RouteUtils.replaceEnrollmentId( + AppRoutes.PublicEnrollmentPaymentSuccess, + enrollmentId, + ); + + case PublicEnrollmentAppointmentFormStep.Done: + return RouteUtils.replaceEnrollmentId( + AppRoutes.PublicEnrollmentDone, + enrollmentId, + ); + } + } + static replaceEnrollmentId(route: string, enrollmentId: number) { + return route.replace(':enrollmentId', enrollmentId.toString()); + } + } diff --git a/frontend/packages/vkt/src/utils/serialization.ts b/frontend/packages/vkt/src/utils/serialization.ts index 80f661af4..6218a8689 100644 --- a/frontend/packages/vkt/src/utils/serialization.ts +++ b/frontend/packages/vkt/src/utils/serialization.ts @@ -43,6 +43,17 @@ export class SerializationUtils { }; } + static deserializePublicEnrollmentAppointment( + enrollment: PublicEnrollmentResponse, + ): PublicEnrollment { + return { + ...enrollment, + emailConfirmation: '', + hasPreviousEnrollment: !!enrollment.previousEnrollment, + privacyStatementConfirmation: false, + }; + } + static deserializePublicEnrollment( enrollment: PublicEnrollmentResponse, ): PublicEnrollment { From 1845ba1fbacc19d4d3befc3135ba0c2df0d4a8c6 Mon Sep 17 00:00:00 2001 From: Jarkko Pesonen <435495+jrkkp@users.noreply.github.com> Date: Wed, 2 Oct 2024 22:32:06 +0300 Subject: [PATCH 022/248] VKT(Frontend & Backend): enrollment appointment continues --- .../java/fi/oph/vkt/api/PublicController.java | 19 +- .../dto/PublicEnrollmentAppointmentDTO.java | 3 +- .../PublicEnrollmentAppointmentUpdateDTO.java | 19 ++ .../fi/oph/vkt/service/PaymentService.java | 42 ++++- .../vkt/service/PublicEnrollmentService.java | 36 ++++ .../vkt/public/i18n/fi-FI/public.json | 1 + ...licEnrollmentAppointmentControlButtons.tsx | 28 ++- ...PublicEnrollmentAppointmentDesktopGrid.tsx | 22 ++- .../PublicEnrollmentAppointmentGrid.tsx | 24 +-- ...ublicEnrollmentAppointmentStepContents.tsx | 8 +- .../PublicEnrollmentAppointmentStepper.tsx | 4 +- .../steps/Authenticate.tsx | 11 +- .../steps/CertificateShipping.tsx | 176 ++++++++++++++++++ .../steps/FillContactDetails.tsx | 6 +- .../steps/PaymentFail.tsx | 105 +++++++++++ .../steps/PaymentSuccess.tsx | 44 +++++ .../steps/PersonDetails.tsx | 10 +- .../steps/Preview.tsx | 2 +- frontend/packages/vkt/src/enums/api.ts | 2 +- frontend/packages/vkt/src/enums/app.ts | 2 + .../vkt/src/enums/publicEnrollment.ts | 3 +- .../reducers/publicEnrollmentAppointment.ts | 32 +++- .../sagas/publicEnrollmentAppointment.ts | 54 ++++-- .../packages/vkt/src/routers/AppRouter.tsx | 22 +++ .../vkt/src/utils/publicEnrollment.ts | 4 +- frontend/packages/vkt/src/utils/routes.ts | 28 ++- .../packages/vkt/src/utils/serialization.ts | 6 +- 27 files changed, 615 insertions(+), 98 deletions(-) create mode 100644 backend/vkt/src/main/java/fi/oph/vkt/api/dto/PublicEnrollmentAppointmentUpdateDTO.java create mode 100644 frontend/packages/vkt/src/components/publicEnrollmentAppointment/steps/CertificateShipping.tsx create mode 100644 frontend/packages/vkt/src/components/publicEnrollmentAppointment/steps/PaymentFail.tsx create mode 100644 frontend/packages/vkt/src/components/publicEnrollmentAppointment/steps/PaymentSuccess.tsx 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 9e1b30210..a0b26df80 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 @@ -3,6 +3,7 @@ import com.fasterxml.jackson.core.JsonProcessingException; import fi.oph.vkt.api.dto.PublicEducationDTO; import fi.oph.vkt.api.dto.PublicEnrollmentAppointmentDTO; +import fi.oph.vkt.api.dto.PublicEnrollmentAppointmentUpdateDTO; import fi.oph.vkt.api.dto.PublicEnrollmentCreateDTO; import fi.oph.vkt.api.dto.PublicEnrollmentDTO; import fi.oph.vkt.api.dto.PublicEnrollmentInitialisationDTO; @@ -29,6 +30,7 @@ import fi.oph.vkt.util.SessionUtil; import fi.oph.vkt.util.UIRouteUtil; import fi.oph.vkt.util.exception.APIException; +import fi.oph.vkt.util.exception.APIExceptionType; import fi.oph.vkt.util.exception.NotFoundException; import jakarta.annotation.Resource; import jakarta.servlet.http.HttpServletResponse; @@ -160,6 +162,21 @@ public PublicEnrollmentAppointmentDTO getEnrollmentAppointment( return publicEnrollmentService.getEnrollmentAppointment(enrollmentAppointmentId, person); } + @PostMapping(path = "/enrollment/appointment/{enrollmentAppointmentId:\\d+}") + @ResponseStatus(HttpStatus.CREATED) + public PublicEnrollmentAppointmentDTO saveEnrollmentAppointment( + @RequestBody @Valid final PublicEnrollmentAppointmentUpdateDTO dto, + @PathVariable final long enrollmentAppointmentId, + final HttpSession session) { + final Person person = publicAuthService.getPersonFromSession(session); + + if (enrollmentAppointmentId != dto.id()) { + throw new APIException(APIExceptionType.RESERVATION_PERSON_SESSION_MISMATCH); + } + + return publicEnrollmentService.saveEnrollmentAppointment(dto, person); + } + @GetMapping(path = "/education") public List getEducation(final HttpSession session) throws JsonProcessingException { final Person person = publicAuthService.getPersonFromSession(session); @@ -332,7 +349,7 @@ public void logout(final HttpSession session, final HttpServletResponse httpResp httpResponse.sendRedirect(publicAuthService.createCasLogoutUrl()); } - @GetMapping(path = "/payment/create/{targetId:\\d+}/{type:\\w}/redirect") + @GetMapping(path = "/payment/create/{targetId:\\d+}/{type:\\w+}/redirect") public void createPaymentAndRedirect( @PathVariable final Long targetId, @PathVariable final String type, diff --git a/backend/vkt/src/main/java/fi/oph/vkt/api/dto/PublicEnrollmentAppointmentDTO.java b/backend/vkt/src/main/java/fi/oph/vkt/api/dto/PublicEnrollmentAppointmentDTO.java index 1b35540fe..9ed890bf2 100644 --- a/backend/vkt/src/main/java/fi/oph/vkt/api/dto/PublicEnrollmentAppointmentDTO.java +++ b/backend/vkt/src/main/java/fi/oph/vkt/api/dto/PublicEnrollmentAppointmentDTO.java @@ -24,5 +24,6 @@ public record PublicEnrollmentAppointmentDTO( String street, String postalCode, String town, - String country + String country, + @NonNull @NotNull PublicPersonDTO person ) {} diff --git a/backend/vkt/src/main/java/fi/oph/vkt/api/dto/PublicEnrollmentAppointmentUpdateDTO.java b/backend/vkt/src/main/java/fi/oph/vkt/api/dto/PublicEnrollmentAppointmentUpdateDTO.java new file mode 100644 index 000000000..b11e76fe6 --- /dev/null +++ b/backend/vkt/src/main/java/fi/oph/vkt/api/dto/PublicEnrollmentAppointmentUpdateDTO.java @@ -0,0 +1,19 @@ +package fi.oph.vkt.api.dto; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.Builder; +import lombok.NonNull; + +@Builder +public record PublicEnrollmentAppointmentUpdateDTO( + @NotNull long id, + String previousEnrollment, + @NonNull @NotNull Boolean digitalCertificateConsent, + @NonNull @NotBlank String phoneNumber, + String street, + String postalCode, + String town, + String country +) { +} diff --git a/backend/vkt/src/main/java/fi/oph/vkt/service/PaymentService.java b/backend/vkt/src/main/java/fi/oph/vkt/service/PaymentService.java index ecf64357c..5229a3030 100644 --- a/backend/vkt/src/main/java/fi/oph/vkt/service/PaymentService.java +++ b/backend/vkt/src/main/java/fi/oph/vkt/service/PaymentService.java @@ -100,6 +100,16 @@ private EnrollmentStatus getPaymentSuccessEnrollmentNextStatus(final Enrollment return enrollment.enrollmentNeedsApproval() ? EnrollmentStatus.AWAITING_APPROVAL : EnrollmentStatus.COMPLETED; } + private void setEnrollmentStatus(final EnrollmentAppointment enrollmentAppointment, final PaymentStatus paymentStatus) { + switch (paymentStatus) { + case NEW, PENDING, DELAYED -> {} + case OK -> enrollmentAppointment.setStatus(EnrollmentStatus.COMPLETED); + case FAIL -> { + enrollmentAppointment.setStatus(EnrollmentStatus.CANCELED_UNFINISHED_ENROLLMENT); + } + } + } + private void setEnrollmentStatus(final Enrollment enrollment, final PaymentStatus paymentStatus) { switch (paymentStatus) { case NEW -> { @@ -153,14 +163,27 @@ public Payment finalizePayment(final Long paymentId, final Map p throw new APIException(APIExceptionType.PAYMENT_REFERENCE_MISMATCH); } - final Enrollment enrollment = payment.getEnrollment(); - setEnrollmentStatus(enrollment, newStatus); + if (payment.getEnrollment() != null) { + final Enrollment enrollment = payment.getEnrollment(); + setEnrollmentStatus(enrollment, newStatus); - payment.setPaymentStatus(newStatus); - paymentRepository.saveAndFlush(payment); + payment.setPaymentStatus(newStatus); + paymentRepository.saveAndFlush(payment); + + if (newStatus == PaymentStatus.OK) { + publicEnrollmentEmailService.sendEnrollmentConfirmationEmail(enrollment); + } + } else { + final EnrollmentAppointment enrollmentAppointment = payment.getEnrollmentAppointment(); + setEnrollmentStatus(enrollmentAppointment, newStatus); - if (newStatus == PaymentStatus.OK) { - publicEnrollmentEmailService.sendEnrollmentConfirmationEmail(enrollment); + payment.setPaymentStatus(newStatus); + paymentRepository.saveAndFlush(payment); + + // FIXME + if (newStatus == PaymentStatus.OK) { + //publicEnrollmentEmailService.sendEnrollmentConfirmationEmail(enrollmentAppointment); + } } return payment; @@ -181,9 +204,10 @@ private String getFinalizePaymentRedirectUrl(final Long paymentId, final String final Payment payment = paymentRepository .findById(paymentId) .orElseThrow(() -> new NotFoundException("Payment not found")); - final ExamEvent examEvent = payment.getEnrollment().getExamEvent(); - - return String.format("%s/ilmoittaudu/%d/maksu/%s", baseUrl, examEvent.getId(), state); + + return payment.getEnrollment() != null + ? String.format("%s/ilmoittaudu/%d/maksu/%s", baseUrl, payment.getEnrollment().getExamEvent().getId(), state) + : String.format("%s/markkinapaikka/%d/maksu/%s", baseUrl, payment.getEnrollmentAppointment().getId(), state); } @Transactional diff --git a/backend/vkt/src/main/java/fi/oph/vkt/service/PublicEnrollmentService.java b/backend/vkt/src/main/java/fi/oph/vkt/service/PublicEnrollmentService.java index 84f049355..815e2e8f1 100644 --- a/backend/vkt/src/main/java/fi/oph/vkt/service/PublicEnrollmentService.java +++ b/backend/vkt/src/main/java/fi/oph/vkt/service/PublicEnrollmentService.java @@ -5,6 +5,7 @@ import fi.oph.vkt.api.dto.FreeEnrollmentDetailsDTO; import fi.oph.vkt.api.dto.PublicEducationDTO; import fi.oph.vkt.api.dto.PublicEnrollmentAppointmentDTO; +import fi.oph.vkt.api.dto.PublicEnrollmentAppointmentUpdateDTO; import fi.oph.vkt.api.dto.PublicEnrollmentCreateDTO; import fi.oph.vkt.api.dto.PublicEnrollmentDTO; import fi.oph.vkt.api.dto.PublicEnrollmentInitialisationDTO; @@ -474,6 +475,13 @@ private void clearAddress(final Enrollment enrollment) { enrollment.setCountry(null); } + private void clearAddress(final EnrollmentAppointment enrollmentAppointment) { + enrollmentAppointment.setStreet(null); + enrollmentAppointment.setPostalCode(null); + enrollmentAppointment.setTown(null); + enrollmentAppointment.setCountry(null); + } + @Transactional public PublicEnrollmentDTO createEnrollmentToQueue( final PublicEnrollmentCreateDTO dto, @@ -590,6 +598,8 @@ public Map getPresignedPostRequest( private PublicEnrollmentAppointmentDTO createEnrollmentAppointmentDTO( final EnrollmentAppointment enrollmentAppointment ) { + final PublicPersonDTO personDTO = PersonUtil.createPublicPersonDTO(enrollmentAppointment.getPerson()); + return PublicEnrollmentAppointmentDTO .builder() .id(enrollmentAppointment.getId()) @@ -608,6 +618,7 @@ private PublicEnrollmentAppointmentDTO createEnrollmentAppointmentDTO( .town(enrollmentAppointment.getTown()) .country(enrollmentAppointment.getCountry()) .status(enrollmentAppointment.getStatus()) + .person(personDTO) .build(); } @@ -626,4 +637,29 @@ public PublicEnrollmentAppointmentDTO getEnrollmentAppointment( return createEnrollmentAppointmentDTO(enrollmentAppointment); } + + @Transactional + public PublicEnrollmentAppointmentDTO saveEnrollmentAppointment(final PublicEnrollmentAppointmentUpdateDTO dto, final Person person) { + final EnrollmentAppointment enrollmentAppointment = enrollmentAppointmentRepository.getReferenceById( + dto.id() + ); + + if (person.getId() != enrollmentAppointment.getPerson().getId()) { + throw new APIException(APIExceptionType.RESERVATION_PERSON_SESSION_MISMATCH); + } + + enrollmentAppointment.setPerson(person); + enrollmentAppointment.setStreet(dto.street()); + enrollmentAppointment.setPostalCode(dto.postalCode()); + enrollmentAppointment.setTown(dto.town()); + enrollmentAppointment.setCountry(dto.country()); + + if (dto.digitalCertificateConsent()) { + clearAddress(enrollmentAppointment); + } + + enrollmentAppointmentRepository.saveAndFlush(enrollmentAppointment); + + return createEnrollmentAppointmentDTO(enrollmentAppointment); + } } diff --git a/frontend/packages/vkt/public/i18n/fi-FI/public.json b/frontend/packages/vkt/public/i18n/fi-FI/public.json index 4dc263c1a..a730e108a 100644 --- a/frontend/packages/vkt/public/i18n/fi-FI/public.json +++ b/frontend/packages/vkt/public/i18n/fi-FI/public.json @@ -88,6 +88,7 @@ "EducationDetails": "Koulutustiedot", "FillContactDetails": "Täytä yhteystietosi", "Payment": "Maksu", + "PaymentFail": "Maksu", "PaymentSuccess": "Valmis", "Preview": "Esikatsele", "SelectExam": "Valitse tutkinto" diff --git a/frontend/packages/vkt/src/components/publicEnrollmentAppointment/PublicEnrollmentAppointmentControlButtons.tsx b/frontend/packages/vkt/src/components/publicEnrollmentAppointment/PublicEnrollmentAppointmentControlButtons.tsx index 6db645c55..bfa2b4834 100644 --- a/frontend/packages/vkt/src/components/publicEnrollmentAppointment/PublicEnrollmentAppointmentControlButtons.tsx +++ b/frontend/packages/vkt/src/components/publicEnrollmentAppointment/PublicEnrollmentAppointmentControlButtons.tsx @@ -11,6 +11,10 @@ import { useDialog } from 'shared/hooks'; import { useCommonTranslation, usePublicTranslation } from 'configs/i18n'; import { useAppDispatch } from 'configs/redux'; import { PublicEnrollmentAppointmentFormStep } from 'enums/publicEnrollment'; +import { + loadPublicEnrollmentSave, + setLoadingPayment, +} from 'redux/reducers/publicEnrollmentAppointment'; import { RouteUtils } from 'utils/routes'; export const PublicEnrollmentAppointmentControlButtons = ({ @@ -18,11 +22,13 @@ export const PublicEnrollmentAppointmentControlButtons = ({ enrollment, isStepValid, setShowValidation, + submitStatus, }: { activeStep: PublicEnrollmentAppointmentFormStep; enrollment: PublicEnrollmentAppointment; isStepValid: boolean; setShowValidation: (showValidation: boolean) => void; + submitStatus: APIResponseStatus; }) => { const { t } = usePublicTranslation({ keyPrefix: 'vkt.component.publicEnrollment.controlButtons', @@ -30,17 +36,11 @@ export const PublicEnrollmentAppointmentControlButtons = ({ const translateCommon = useCommonTranslation(); const [isPaymentLoading, setIsPaymentLoading] = useState(false); - // FIXME - const submitStatus = APIResponseStatus.NotStarted; const dispatch = useAppDispatch(); const navigate = useNavigate(); const { showDialog } = useDialog(); - const submitButtonText = () => { - return t('pay'); - }; - const handleCancelBtnClick = () => { // FIXME }; @@ -51,6 +51,7 @@ export const PublicEnrollmentAppointmentControlButtons = ({ setTimeout(() => { window.location.href = RouteUtils.getPaymentCreateApiRoute( enrollment.id, + 'appointment', ); }, 200); dispatch(setLoadingPayment()); @@ -76,12 +77,7 @@ export const PublicEnrollmentAppointmentControlButtons = ({ if (isStepValid) { setIsPaymentLoading(true); setShowValidation(false); - dispatch( - loadPublicEnrollmentUpdate({ - enrollment, - examEventId, - }), - ); + dispatch(loadPublicEnrollmentSave(enrollment)); } else { setShowValidation(true); } @@ -142,14 +138,16 @@ export const PublicEnrollmentAppointmentControlButtons = ({ data-testid="public-enrollment__controlButtons__submit" disabled={isPaymentLoading} > - {submitButtonText()} + {t('pay')} ); const renderBack = true; - const renderNext = activeStep === PublicEnrollmentAppointmentFormStep.FillContactDetails; - const renderSubmit = activeStep === PublicEnrollmentAppointmentFormStep.Preview; + const renderNext = + activeStep === PublicEnrollmentAppointmentFormStep.FillContactDetails; + const renderSubmit = + activeStep === PublicEnrollmentAppointmentFormStep.Preview; return (
diff --git a/frontend/packages/vkt/src/components/publicEnrollmentAppointment/PublicEnrollmentAppointmentDesktopGrid.tsx b/frontend/packages/vkt/src/components/publicEnrollmentAppointment/PublicEnrollmentAppointmentDesktopGrid.tsx index cc1ef643b..2069edc66 100644 --- a/frontend/packages/vkt/src/components/publicEnrollmentAppointment/PublicEnrollmentAppointmentDesktopGrid.tsx +++ b/frontend/packages/vkt/src/components/publicEnrollmentAppointment/PublicEnrollmentAppointmentDesktopGrid.tsx @@ -7,8 +7,10 @@ import { PublicEnrollmentAppointmentStepContents } from 'components/publicEnroll import { PublicEnrollmentAppointmentStepHeading } from 'components/publicEnrollmentAppointment/PublicEnrollmentAppointmentStepHeading'; import { PublicEnrollmentAppointmentStepper } from 'components/publicEnrollmentAppointment/PublicEnrollmentAppointmentStepper'; import { useCommonTranslation } from 'configs/i18n'; -import { PublicEnrollmentFormStep } from 'enums/publicEnrollment'; +import { useAppSelector } from 'configs/redux'; +import { PublicEnrollmentAppointmentFormStep } from 'enums/publicEnrollment'; import { PublicEnrollmentAppointment } from 'interfaces/publicEnrollment'; +import { publicEnrollmentAppointmentSelector } from 'redux/selectors/publicEnrollmentAppointment'; export const PublicEnrollmentAppointmentDesktopGrid = ({ activeStep, @@ -27,6 +29,16 @@ export const PublicEnrollmentAppointmentDesktopGrid = ({ }) => { const translateCommon = useCommonTranslation(); + const { enrollmentSubmitStatus } = useAppSelector( + publicEnrollmentAppointmentSelector, + ); + + const showPaymentSum = + activeStep === PublicEnrollmentAppointmentFormStep.Preview; + const showControlButtons = + activeStep > PublicEnrollmentAppointmentFormStep.Authenticate && + activeStep <= PublicEnrollmentAppointmentFormStep.Preview; + return ( <> @@ -45,16 +57,14 @@ export const PublicEnrollmentAppointmentDesktopGrid = ({ showValidation={showValidation} setIsStepValid={setIsStepValid} /> - {activeStep > PublicEnrollmentFormStep.Authenticate && ( - - )} - {activeStep > PublicEnrollmentFormStep.Authenticate && ( + {showPaymentSum && } + {showControlButtons && ( )}
diff --git a/frontend/packages/vkt/src/components/publicEnrollmentAppointment/PublicEnrollmentAppointmentGrid.tsx b/frontend/packages/vkt/src/components/publicEnrollmentAppointment/PublicEnrollmentAppointmentGrid.tsx index f640994f4..e4a97daea 100644 --- a/frontend/packages/vkt/src/components/publicEnrollmentAppointment/PublicEnrollmentAppointmentGrid.tsx +++ b/frontend/packages/vkt/src/components/publicEnrollmentAppointment/PublicEnrollmentAppointmentGrid.tsx @@ -1,13 +1,13 @@ import { Grid } from '@mui/material'; - +import { useEffect, useState } from 'react'; import { useParams } from 'react-router'; +import { APIResponseStatus } from 'shared/enums'; + import { PublicEnrollmentAppointmentDesktopGrid } from 'components/publicEnrollmentAppointment/PublicEnrollmentAppointmentDesktopGrid'; import { useAppDispatch, useAppSelector } from 'configs/redux'; -import { useEffect, useState } from 'react'; -import { APIResponseStatus } from 'shared/enums'; import { PublicEnrollmentAppointmentFormStep } from 'enums/publicEnrollment'; -import { publicEnrollmentAppointmentSelector } from 'redux/selectors/publicEnrollmentAppointment'; import { loadPublicEnrollmentAppointment } from 'redux/reducers/publicEnrollmentAppointment'; +import { publicEnrollmentAppointmentSelector } from 'redux/selectors/publicEnrollmentAppointment'; export const PublicEnrollmentAppointmentGrid = ({ activeStep, @@ -16,7 +16,9 @@ export const PublicEnrollmentAppointmentGrid = ({ }) => { const params = useParams(); const dispatch = useAppDispatch(); - const { enrollment, loadEnrollmentStatus } = useAppSelector(publicEnrollmentAppointmentSelector); + const { enrollment, loadEnrollmentStatus } = useAppSelector( + publicEnrollmentAppointmentSelector, + ); const [isStepValid, setIsStepValid] = useState(false); const [showValidation, setShowValidation] = useState(false); @@ -24,11 +26,6 @@ export const PublicEnrollmentAppointmentGrid = ({ activeStep > PublicEnrollmentAppointmentFormStep.Authenticate; useEffect(() => { - console.log( - isAuthenticatePassed, - loadEnrollmentStatus === APIResponseStatus.NotStarted, - params.enrollmentId - ); if ( isAuthenticatePassed && loadEnrollmentStatus === APIResponseStatus.NotStarted && @@ -36,7 +33,12 @@ export const PublicEnrollmentAppointmentGrid = ({ ) { dispatch(loadPublicEnrollmentAppointment(+params.enrollmentId)); } - }, [dispatch, loadEnrollmentStatus, isAuthenticatePassed, params.enrollmentId]); + }, [ + dispatch, + loadEnrollmentStatus, + isAuthenticatePassed, + params.enrollmentId, + ]); return ( { switch (activeStep) { case PublicEnrollmentAppointmentFormStep.Authenticate: - return ; + return ; case PublicEnrollmentAppointmentFormStep.FillContactDetails: return ( ; + case PublicEnrollmentAppointmentFormStep.PaymentFail: + return ; + case PublicEnrollmentAppointmentFormStep.PaymentSuccess: + return ; } }; diff --git a/frontend/packages/vkt/src/components/publicEnrollmentAppointment/PublicEnrollmentAppointmentStepper.tsx b/frontend/packages/vkt/src/components/publicEnrollmentAppointment/PublicEnrollmentAppointmentStepper.tsx index c1e550dc3..3efeb0962 100644 --- a/frontend/packages/vkt/src/components/publicEnrollmentAppointment/PublicEnrollmentAppointmentStepper.tsx +++ b/frontend/packages/vkt/src/components/publicEnrollmentAppointment/PublicEnrollmentAppointmentStepper.tsx @@ -43,7 +43,7 @@ export const PublicEnrollmentAppointmentStepper = ({ const hasError = (step: PublicEnrollmentAppointmentFormStep) => { return ( - step === PublicEnrollmentAppointmentFormStep.Payment && + step === PublicEnrollmentAppointmentFormStep.PaymentFail && step === activeStep ); }; @@ -66,7 +66,7 @@ export const PublicEnrollmentAppointmentStepper = ({ ariaLabel={mobileAriaLabel} phaseText={mobilePhaseText} color={ - activeStep === PublicEnrollmentAppointmentFormStep.Payment + activeStep === PublicEnrollmentAppointmentFormStep.PaymentFail ? Color.Error : Color.Secondary } diff --git a/frontend/packages/vkt/src/components/publicEnrollmentAppointment/steps/Authenticate.tsx b/frontend/packages/vkt/src/components/publicEnrollmentAppointment/steps/Authenticate.tsx index 13ae02bfa..dbbe67579 100644 --- a/frontend/packages/vkt/src/components/publicEnrollmentAppointment/steps/Authenticate.tsx +++ b/frontend/packages/vkt/src/components/publicEnrollmentAppointment/steps/Authenticate.tsx @@ -1,4 +1,5 @@ import { useState } from 'react'; +import { useParams } from 'react-router'; import { CustomButton, LoadingProgressIndicator } from 'shared/components'; import { Color, Variant } from 'shared/enums'; @@ -7,16 +8,14 @@ import { useAppDispatch } from 'configs/redux'; import { cancelPublicEnrollment } from 'redux/reducers/publicEnrollment'; import { RouteUtils } from 'utils/routes'; -export const Authenticate = ({ - enrollment, -} : { - enrollment: PublicEnrollmentAppointment; -}) => { +export const Authenticate = () => { + const params = useParams(); const [isAuthRedirecting, setIsAuthRedirecting] = useState(false); const { t } = usePublicTranslation({ keyPrefix: 'vkt.component.publicEnrollment.steps.authenticate', }); const translateCommon = useCommonTranslation(); + const enrollmentId = +params.enrollmentId; const dispatch = useAppDispatch(); @@ -25,7 +24,7 @@ export const Authenticate = ({ const type = 'appointment'; - window.location.href = RouteUtils.getAuthLoginApiRoute(enrollment.id, type); + window.location.href = RouteUtils.getAuthLoginApiRoute(enrollmentId, type); }; const onCancel = () => { diff --git a/frontend/packages/vkt/src/components/publicEnrollmentAppointment/steps/CertificateShipping.tsx b/frontend/packages/vkt/src/components/publicEnrollmentAppointment/steps/CertificateShipping.tsx new file mode 100644 index 000000000..c536561e8 --- /dev/null +++ b/frontend/packages/vkt/src/components/publicEnrollmentAppointment/steps/CertificateShipping.tsx @@ -0,0 +1,176 @@ +import { Checkbox, Collapse, FormControlLabel } from '@mui/material'; +import { ChangeEvent, useEffect, useState } from 'react'; +import { H2, LabeledTextField, Text } from 'shared/components'; +import { Color, InputAutoComplete, TextFieldTypes } from 'shared/enums'; +import { TextField } from 'shared/interfaces'; +import { getErrors, hasErrors } from 'shared/utils'; + +import { useCommonTranslation, usePublicTranslation } from 'configs/i18n'; +import { useAppDispatch } from 'configs/redux'; +import { CertificateShippingTextFields } from 'interfaces/common/enrollment'; +import { PublicEnrollmentAppointment } from 'interfaces/publicEnrollment'; +import { updatePublicEnrollment } from 'redux/reducers/publicEnrollmentAppointment'; + +const fields: Array> = [ + { + name: 'street', + required: true, + type: TextFieldTypes.Text, + maxLength: 255, + }, + { + name: 'postalCode', + required: true, + type: TextFieldTypes.Text, + maxLength: 255, + }, + { name: 'town', required: true, type: TextFieldTypes.Text, maxLength: 255 }, + { + name: 'country', + required: true, + type: TextFieldTypes.Text, + maxLength: 255, + }, +]; + +export const CertificateShipping = ({ + enrollment, + editingDisabled, + setValid, + showValidation, +}: { + enrollment: PublicEnrollmentAppointment; + editingDisabled: boolean; + setValid: (isValid: boolean) => void; + showValidation: boolean; +}) => { + const { t } = usePublicTranslation({ + keyPrefix: 'vkt.component.publicEnrollment.steps.addressDetails', + }); + const translateCommon = useCommonTranslation(); + const digitalConsentEnabled = false; + + const [dirtyFields, setDirtyFields] = useState< + Array + >([]); + + const dispatch = useAppDispatch(); + + useEffect(() => { + if (enrollment.digitalCertificateConsent) { + setValid(true); + + return; + } + + setValid( + !hasErrors({ + fields, + values: enrollment, + t: translateCommon, + }), + ); + }, [setValid, enrollment, translateCommon]); + + const dirty = showValidation ? undefined : dirtyFields; + const errors = getErrors({ + fields, + values: enrollment, + t: translateCommon, + dirtyFields: dirty, + }); + + const handleChange = + (fieldName: keyof CertificateShippingTextFields) => + (event: ChangeEvent) => { + dispatch( + updatePublicEnrollment({ + [fieldName]: event.target.value, + }), + ); + }; + + const handleBlur = (fieldName: keyof CertificateShippingTextFields) => () => { + if (!dirtyFields.includes(fieldName)) { + setDirtyFields([...dirtyFields, fieldName]); + } + }; + + const showCustomTextFieldError = ( + fieldName: keyof CertificateShippingTextFields, + ) => { + return !!errors[fieldName]; + }; + + const getCustomTextFieldAttributes = ( + fieldName: keyof CertificateShippingTextFields, + ) => ({ + id: `public-enrollment__certificate-shipping__${fieldName}-field`, + type: TextFieldTypes.Text, + label: `${translateCommon(`enrollment.textFields.${fieldName}`)} *`, + onBlur: handleBlur(fieldName), + onChange: handleChange(fieldName), + error: showCustomTextFieldError(fieldName), + helperText: errors[fieldName], + required: true, + disabled: editingDisabled, + }); + + const handleCheckboxClick = () => { + dispatch( + updatePublicEnrollment({ + digitalCertificateConsent: !enrollment.digitalCertificateConsent, + }), + ); + }; + + return ( +
+

{t('title')}

+ {digitalConsentEnabled && ( + + } + label={translateCommon('enrollment.certificateShipping.consent')} + /> + )} + + + {translateCommon('enrollment.certificateShipping.description')} + +
+ + + + +
+
+
+ ); +}; diff --git a/frontend/packages/vkt/src/components/publicEnrollmentAppointment/steps/FillContactDetails.tsx b/frontend/packages/vkt/src/components/publicEnrollmentAppointment/steps/FillContactDetails.tsx index 149d09179..7d8908127 100644 --- a/frontend/packages/vkt/src/components/publicEnrollmentAppointment/steps/FillContactDetails.tsx +++ b/frontend/packages/vkt/src/components/publicEnrollmentAppointment/steps/FillContactDetails.tsx @@ -6,7 +6,7 @@ import { useWindowProperties } from 'shared/hooks'; import { TextField } from 'shared/interfaces'; import { FieldErrors } from 'shared/utils'; -import { CertificateShipping } from 'components/publicEnrollment/steps/CertificateShipping'; +import { CertificateShipping } from 'components/publicEnrollmentAppointment/steps/CertificateShipping'; import { PersonDetails } from 'components/publicEnrollmentAppointment/steps/PersonDetails'; import { useCommonTranslation, usePublicTranslation } from 'configs/i18n'; import { useAppDispatch } from 'configs/redux'; @@ -136,7 +136,7 @@ export const FillContactDetails = ({ @@ -144,7 +144,7 @@ export const FillContactDetails = ({ console.log(isValid)} + setValid={setIsStepValid} showValidation={false} />
diff --git a/frontend/packages/vkt/src/components/publicEnrollmentAppointment/steps/PaymentFail.tsx b/frontend/packages/vkt/src/components/publicEnrollmentAppointment/steps/PaymentFail.tsx new file mode 100644 index 000000000..5646783a4 --- /dev/null +++ b/frontend/packages/vkt/src/components/publicEnrollmentAppointment/steps/PaymentFail.tsx @@ -0,0 +1,105 @@ +import { useEffect, useState } from 'react'; +import { + CustomButton, + LoadingProgressIndicator, + Text, +} from 'shared/components'; +import { APIResponseStatus, Color, Severity, Variant } from 'shared/enums'; +import { useDialog, useToast } from 'shared/hooks'; + +import { useCommonTranslation, usePublicTranslation } from 'configs/i18n'; +import { useAppDispatch, useAppSelector } from 'configs/redux'; +import { PublicEnrollment } from 'interfaces/publicEnrollment'; +import { cancelPublicEnrollment } from 'redux/reducers/publicEnrollment'; +import { publicEnrollmentSelector } from 'redux/selectors/publicEnrollment'; +import { RouteUtils } from 'utils/routes'; + +export const PaymentFail = ({ + enrollment, +}: { + enrollment: PublicEnrollment; +}) => { + const { t } = usePublicTranslation({ + keyPrefix: 'vkt.component.publicEnrollment', + }); + const translateCommon = useCommonTranslation(); + const dispatch = useAppDispatch(); + + const { showToast } = useToast(); + const { showDialog } = useDialog(); + const [isPaymentLoading, setIsPaymentLoading] = useState(false); + const { cancelStatus } = useAppSelector(publicEnrollmentSelector); + const isCancelLoading = cancelStatus === APIResponseStatus.InProgress; + const isLoading = isPaymentLoading || isCancelLoading; + + const handleTryAgainBtnClick = () => { + setIsPaymentLoading(true); + + // Safari needs time to re-render loading indicator + setTimeout(() => { + window.location.href = RouteUtils.getPaymentCreateApiRoute(enrollment.id); + }, 200); + }; + + const handleCancelBtnClick = () => { + showDialog({ + title: t('controlButtons.cancelDialog.title'), + severity: Severity.Info, + description: t('controlButtons.cancelDialog.description'), + actions: [ + { + title: translateCommon('back'), + variant: Variant.Outlined, + }, + { + title: translateCommon('yes'), + variant: Variant.Contained, + action: () => { + dispatch(cancelPublicEnrollment()); + }, + }, + ], + }); + }; + + useEffect(() => { + showToast({ + severity: Severity.Error, + description: t('steps.paymentFail.toast'), + }); + }, [t, showToast]); + + return ( +
+ {t('steps.paymentFail.description')} +
+ + + {t('steps.paymentFail.cancel')} + + + + + {t('steps.paymentFail.tryAgain')} + + +
+
+ ); +}; diff --git a/frontend/packages/vkt/src/components/publicEnrollmentAppointment/steps/PaymentSuccess.tsx b/frontend/packages/vkt/src/components/publicEnrollmentAppointment/steps/PaymentSuccess.tsx new file mode 100644 index 000000000..fa7d7015b --- /dev/null +++ b/frontend/packages/vkt/src/components/publicEnrollmentAppointment/steps/PaymentSuccess.tsx @@ -0,0 +1,44 @@ +import { useNavigate } from 'react-router'; +import { CustomButton, Text } from 'shared/components'; + +import { useCommonTranslation, usePublicTranslation } from 'configs/i18n'; +import { useAppDispatch } from 'configs/redux'; +import { AppRoutes } from 'enums/app'; +import { PublicEnrollment } from 'interfaces/publicEnrollment'; +import { resetPublicEnrollment } from 'redux/reducers/publicEnrollment'; +import { resetPublicExamEventSelections } from 'redux/reducers/publicExamEvent'; + +export const PaymentSuccess = ({ + enrollment, +}: { + enrollment: PublicEnrollment; +}) => { + const { t } = usePublicTranslation({ + keyPrefix: 'vkt.component.publicEnrollment.steps.paymentSuccess', + }); + const translateCommon = useCommonTranslation(); + + const dispatch = useAppDispatch(); + const navigate = useNavigate(); + + const resetAndRedirect = () => { + dispatch(resetPublicExamEventSelections()); + dispatch(resetPublicEnrollment()); + navigate(AppRoutes.PublicHomePage); + }; + + return ( +
+ {t('description1')} + {`${t('description2')}: ${enrollment.email}`} + + {translateCommon('backToHomePage')} + +
+ ); +}; diff --git a/frontend/packages/vkt/src/components/publicEnrollmentAppointment/steps/PersonDetails.tsx b/frontend/packages/vkt/src/components/publicEnrollmentAppointment/steps/PersonDetails.tsx index 69a662d6e..36163a3a1 100644 --- a/frontend/packages/vkt/src/components/publicEnrollmentAppointment/steps/PersonDetails.tsx +++ b/frontend/packages/vkt/src/components/publicEnrollmentAppointment/steps/PersonDetails.tsx @@ -14,9 +14,9 @@ export const PersonDetails = ({ keyPrefix: 'vkt.component.publicEnrollment.steps.personDetails', }); - const { person } = useAppSelector(publicEnrollmentAppointmentSelector); + const { enrollment } = useAppSelector(publicEnrollmentAppointmentSelector); - if (!person) { + if (!enrollment.person) { return null; } @@ -26,7 +26,7 @@ export const PersonDetails = ({ {t(field)} {':'} - {person[field]} + {enrollment.person[field]} ); @@ -34,9 +34,7 @@ export const PersonDetails = ({

{t('title')}

{displayField('lastName')} diff --git a/frontend/packages/vkt/src/components/publicEnrollmentAppointment/steps/Preview.tsx b/frontend/packages/vkt/src/components/publicEnrollmentAppointment/steps/Preview.tsx index d3a5a880c..4ca4a3c2e 100644 --- a/frontend/packages/vkt/src/components/publicEnrollmentAppointment/steps/Preview.tsx +++ b/frontend/packages/vkt/src/components/publicEnrollmentAppointment/steps/Preview.tsx @@ -14,7 +14,7 @@ import { PersonDetails } from 'components/publicEnrollment/steps/PersonDetails'; import { useCommonTranslation, usePublicTranslation } from 'configs/i18n'; import { useAppDispatch, useAppSelector } from 'configs/redux'; import { PublicEnrollmentAppointment } from 'interfaces/publicEnrollment'; -import { updatePublicEnrollment } from 'redux/reducers/publicEnrollment'; +import { updatePublicEnrollment } from 'redux/reducers/publicEnrollmentAppointment'; import { publicEnrollmentSelector } from 'redux/selectors/publicEnrollment'; const ContactDetails = ({ diff --git a/frontend/packages/vkt/src/enums/api.ts b/frontend/packages/vkt/src/enums/api.ts index 59878f6d6..94ceda0d8 100644 --- a/frontend/packages/vkt/src/enums/api.ts +++ b/frontend/packages/vkt/src/enums/api.ts @@ -6,7 +6,7 @@ export enum APIEndpoints { PublicEnrollment = '/vkt/api/v1/enrollment', PublicReservation = '/vkt/api/v1/reservation', PublicEducation = '/vkt/api/v1/education', - PaymentCreate = '/vkt/api/v1/payment/create/:enrollmentId/redirect?locale=:locale', + PaymentCreate = '/vkt/api/v1/payment/create/:enrollmentId/:type/redirect?locale=:locale', ClerkExamEvent = '/vkt/api/v1/clerk/examEvent', ClerkUser = '/vkt/api/v1/clerk/user', PublicUser = '/vkt/api/v1/auth/info', diff --git a/frontend/packages/vkt/src/enums/app.ts b/frontend/packages/vkt/src/enums/app.ts index 4ed26359a..62bfc5788 100644 --- a/frontend/packages/vkt/src/enums/app.ts +++ b/frontend/packages/vkt/src/enums/app.ts @@ -19,6 +19,8 @@ export enum AppRoutes { PublicAuthAppointment = '/vkt/markkinapaikka/:enrollmentId/tunnistaudu', PublicEnrollmentAppointmentContactDetails = '/vkt/markkinapaikka/:enrollmentId/tiedot', PublicEnrollmentAppointmentPreview = '/vkt/markkinapaikka/:enrollmentId/esikatsele', + PublicEnrollmentAppointmentPaymentFail = '/vkt/markkinapaikka/:enrollmentId/maksu/peruutettu', + PublicEnrollmentAppointmentPaymentSuccess = '/vkt/markkinapaikka/:enrollmentId/maksu/valmis', ClerkHomePage = '/vkt/virkailija', ClerkExamEventCreatePage = '/vkt/virkailija/tutkintotilaisuus/luo', ClerkExamEventOverviewPage = '/vkt/virkailija/tutkintotilaisuus/:examEventId', diff --git a/frontend/packages/vkt/src/enums/publicEnrollment.ts b/frontend/packages/vkt/src/enums/publicEnrollment.ts index e32e5e25a..30be2b485 100644 --- a/frontend/packages/vkt/src/enums/publicEnrollment.ts +++ b/frontend/packages/vkt/src/enums/publicEnrollment.ts @@ -14,7 +14,6 @@ export enum PublicEnrollmentAppointmentFormStep { Authenticate = 1, FillContactDetails, Preview, - Payment, + PaymentFail, PaymentSuccess, - Done, } diff --git a/frontend/packages/vkt/src/redux/reducers/publicEnrollmentAppointment.ts b/frontend/packages/vkt/src/redux/reducers/publicEnrollmentAppointment.ts index b6ee68f41..d08b4de85 100644 --- a/frontend/packages/vkt/src/redux/reducers/publicEnrollmentAppointment.ts +++ b/frontend/packages/vkt/src/redux/reducers/publicEnrollmentAppointment.ts @@ -35,7 +35,7 @@ export const initialState: PublicEnrollmentState = { postalCode: '', town: '', country: '', - id: 1, // FIXME + id: undefined, hasPreviousEnrollment: undefined, previousEnrollment: '', privacyStatementConfirmation: false, @@ -43,10 +43,7 @@ export const initialState: PublicEnrollmentState = { examEventId: undefined, hasPaymentLink: undefined, isQueued: undefined, - }, - person: { // FIXME - firstName: 'foo', - lastName: 'bar', + person: undefined, }, }; @@ -60,12 +57,32 @@ const publicEnrollmentAppointmentSlice = createSlice({ rejectPublicEnrollmentAppointment(state) { state.loadEnrollmentStatus = APIResponseStatus.Error; }, - storePublicEnrollmentAppointment(state) { + storePublicEnrollmentAppointmentSave( + state, + action: PayloadAction, + ) { + state.enrollmentSubmitStatus = APIResponseStatus.Success; + state.enrollment = action.payload; + }, + storePublicEnrollmentAppointment( + state, + action: PayloadAction, + ) { state.loadEnrollmentStatus = APIResponseStatus.Success; + state.enrollment = action.payload; + }, + updatePublicEnrollment( + state, + action: PayloadAction>, + ) { + state.enrollment = { ...state.enrollment, ...action.payload }; }, setLoadingPayment(state) { state.paymentLoadingStatus = APIResponseStatus.InProgress; }, + loadPublicEnrollmentSave(state, _action: PayloadAction) { + state.enrollmentSubmitStatus = APIResponseStatus.InProgress; + }, }, }); @@ -74,6 +91,9 @@ export const publicEnrollmentAppointmentReducer = export const { loadPublicEnrollmentAppointment, rejectPublicEnrollmentAppointment, + storePublicEnrollmentAppointmentSave, storePublicEnrollmentAppointment, + updatePublicEnrollment, + loadPublicEnrollmentSave, setLoadingPayment, } = publicEnrollmentAppointmentSlice.actions; diff --git a/frontend/packages/vkt/src/redux/sagas/publicEnrollmentAppointment.ts b/frontend/packages/vkt/src/redux/sagas/publicEnrollmentAppointment.ts index 29026eb95..107038994 100644 --- a/frontend/packages/vkt/src/redux/sagas/publicEnrollmentAppointment.ts +++ b/frontend/packages/vkt/src/redux/sagas/publicEnrollmentAppointment.ts @@ -5,16 +5,16 @@ import { call, put, takeLatest } from 'redux-saga/effects'; import axiosInstance from 'configs/axios'; import { APIEndpoints } from 'enums/api'; import { - PublicEnrollment, - PublicReservationDetailsResponse, - PublicReservationResponse, + PublicEnrollmentAppointment, + PublicEnrollmentAppointmentResponse, } from 'interfaces/publicEnrollment'; -import { PublicEnrollmentAppointmentResponse } from 'interfaces/publicEnrollment'; import { setAPIError } from 'redux/reducers/APIError'; import { loadPublicEnrollmentAppointment, - storePublicEnrollmentAppointment, + loadPublicEnrollmentSave, rejectPublicEnrollmentAppointment, + storePublicEnrollmentAppointment, + storePublicEnrollmentAppointmentSave, } from 'redux/reducers/publicEnrollmentAppointment'; import { NotifierUtils } from 'utils/notifier'; import { SerializationUtils } from 'utils/serialization'; @@ -24,14 +24,11 @@ function* loadPublicEnrollmentAppointmentSaga(action: PayloadAction) { const enrollmentId = action.payload; const loadUrl = `${APIEndpoints.PublicEnrollmentAppointment}/${enrollmentId}`; - const response: AxiosResponse = yield call( - axiosInstance.get, - loadUrl, - ); + const response: AxiosResponse = + yield call(axiosInstance.get, loadUrl); - const enrollmentAppointment = SerializationUtils.deserializePublicEnrollmentAppointment( - response.data, - ); + const enrollmentAppointment = + SerializationUtils.deserializePublicEnrollmentAppointment(response.data); yield put(storePublicEnrollmentAppointment(enrollmentAppointment)); } catch (error) { @@ -41,6 +38,37 @@ function* loadPublicEnrollmentAppointmentSaga(action: PayloadAction) { } } +function* loadPublicEnrollmentSaveSaga( + action: PayloadAction, +) { + const enrollment = action.payload; + + try { + const body = { + id: enrollment.id, + phoneNumber: enrollment.phoneNumber, + digitalCertificateConsent: enrollment.digitalCertificateConsent, + street: enrollment.street, + town: enrollment.town, + postalCode: enrollment.postalCode, + country: enrollment.country, + }; + + const saveUrl = `${APIEndpoints.PublicEnrollmentAppointment}/${enrollment.id}`; + const response: AxiosResponse = + yield call(axiosInstance.post, saveUrl, body); + yield put(storePublicEnrollmentAppointmentSave(response.data)); + } catch (error) { + const errorMessage = NotifierUtils.getAPIErrorMessage(error as AxiosError); + yield put(setAPIError(errorMessage)); + yield put(rejectPublicEnrollmentSave()); + } +} + export function* watchPublicEnrollmentAppointments() { - yield takeLatest(loadPublicEnrollmentAppointment, loadPublicEnrollmentAppointmentSaga); + yield takeLatest(loadPublicEnrollmentSave, loadPublicEnrollmentSaveSaga); + yield takeLatest( + loadPublicEnrollmentAppointment, + loadPublicEnrollmentAppointmentSaga, + ); } diff --git a/frontend/packages/vkt/src/routers/AppRouter.tsx b/frontend/packages/vkt/src/routers/AppRouter.tsx index a005d7da1..6f2975753 100644 --- a/frontend/packages/vkt/src/routers/AppRouter.tsx +++ b/frontend/packages/vkt/src/routers/AppRouter.tsx @@ -215,6 +215,28 @@ export const AppRouter: FC = () => { } /> + + + + } + /> + + + + } + /> Date: Thu, 3 Oct 2024 22:46:13 +0300 Subject: [PATCH 023/248] VKT(Frontend & Backend): enrollment appointment continues --- .../java/fi/oph/vkt/api/PublicController.java | 7 ++- .../PublicEnrollmentAppointmentUpdateDTO.java | 3 +- .../java/fi/oph/vkt/model/type/ExamLevel.java | 1 + .../fi/oph/vkt/service/PaymentService.java | 44 +++++++++++---- .../vkt/service/PublicEnrollmentService.java | 9 +-- .../oph/vkt/service/PaymentServiceTest.java | 46 ++++++++++++++-- .../service/PublicEnrollmentServiceTest.java | 6 ++ ...licEnrollmentAppointmentControlButtons.tsx | 5 +- .../steps/FillContactDetails.tsx | 55 +------------------ .../steps/PersonDetails.tsx | 6 +- .../vkt/src/interfaces/publicEnrollment.ts | 4 +- .../reducers/publicEnrollmentAppointment.ts | 13 ++++- .../sagas/publicEnrollmentAppointment.ts | 8 ++- frontend/packages/vkt/src/utils/routes.ts | 10 +--- 14 files changed, 118 insertions(+), 99 deletions(-) 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 a0b26df80..0b2136649 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 @@ -165,9 +165,10 @@ public PublicEnrollmentAppointmentDTO getEnrollmentAppointment( @PostMapping(path = "/enrollment/appointment/{enrollmentAppointmentId:\\d+}") @ResponseStatus(HttpStatus.CREATED) public PublicEnrollmentAppointmentDTO saveEnrollmentAppointment( - @RequestBody @Valid final PublicEnrollmentAppointmentUpdateDTO dto, - @PathVariable final long enrollmentAppointmentId, - final HttpSession session) { + @RequestBody @Valid final PublicEnrollmentAppointmentUpdateDTO dto, + @PathVariable final long enrollmentAppointmentId, + final HttpSession session + ) { final Person person = publicAuthService.getPersonFromSession(session); if (enrollmentAppointmentId != dto.id()) { diff --git a/backend/vkt/src/main/java/fi/oph/vkt/api/dto/PublicEnrollmentAppointmentUpdateDTO.java b/backend/vkt/src/main/java/fi/oph/vkt/api/dto/PublicEnrollmentAppointmentUpdateDTO.java index b11e76fe6..7c0ea7364 100644 --- a/backend/vkt/src/main/java/fi/oph/vkt/api/dto/PublicEnrollmentAppointmentUpdateDTO.java +++ b/backend/vkt/src/main/java/fi/oph/vkt/api/dto/PublicEnrollmentAppointmentUpdateDTO.java @@ -15,5 +15,4 @@ public record PublicEnrollmentAppointmentUpdateDTO( String postalCode, String town, String country -) { -} +) {} diff --git a/backend/vkt/src/main/java/fi/oph/vkt/model/type/ExamLevel.java b/backend/vkt/src/main/java/fi/oph/vkt/model/type/ExamLevel.java index 929d5ee90..a73c25fa8 100644 --- a/backend/vkt/src/main/java/fi/oph/vkt/model/type/ExamLevel.java +++ b/backend/vkt/src/main/java/fi/oph/vkt/model/type/ExamLevel.java @@ -2,4 +2,5 @@ public enum ExamLevel { EXCELLENT, + GOOD, } diff --git a/backend/vkt/src/main/java/fi/oph/vkt/service/PaymentService.java b/backend/vkt/src/main/java/fi/oph/vkt/service/PaymentService.java index 5229a3030..fe4e7c06d 100644 --- a/backend/vkt/src/main/java/fi/oph/vkt/service/PaymentService.java +++ b/backend/vkt/src/main/java/fi/oph/vkt/service/PaymentService.java @@ -9,6 +9,7 @@ import fi.oph.vkt.model.type.AppLocale; import fi.oph.vkt.model.type.EnrollmentSkill; import fi.oph.vkt.model.type.EnrollmentStatus; +import fi.oph.vkt.model.type.ExamLevel; import fi.oph.vkt.model.type.PaymentStatus; import fi.oph.vkt.payment.PaymentProvider; import fi.oph.vkt.payment.paytrail.Customer; @@ -47,13 +48,13 @@ public class PaymentService { private final Environment environment; private final PublicEnrollmentEmailService publicEnrollmentEmailService; - private Item getItem(final EnrollmentSkill enrollmentSkill, final int unitPrice) { + private Item getItem(final EnrollmentSkill enrollmentSkill, final int unitPrice, final ExamLevel examLevel) { return Item .builder() .units(1) .unitPrice(unitPrice) .vatPercentage(PaytrailConfig.VAT) - .productCode(enrollmentSkill.toString()) + .productCode(examLevel.toString() + "-" + enrollmentSkill.toString()) .build(); } @@ -61,14 +62,22 @@ private List getItems(final EnrollmentAppointment enrollmentAppointment) { final List itemList = new ArrayList<>(); if (enrollmentAppointment.isTextualSkill()) { - itemList.add(getItem(EnrollmentSkill.TEXTUAL, EnrollmentUtil.getTextualSkillFee(enrollmentAppointment))); + itemList.add( + getItem(EnrollmentSkill.TEXTUAL, EnrollmentUtil.getTextualSkillFee(enrollmentAppointment), ExamLevel.GOOD) + ); } if (enrollmentAppointment.isOralSkill()) { - itemList.add(getItem(EnrollmentSkill.ORAL, EnrollmentUtil.getOralSkillFee(enrollmentAppointment))); + itemList.add( + getItem(EnrollmentSkill.ORAL, EnrollmentUtil.getOralSkillFee(enrollmentAppointment), ExamLevel.GOOD) + ); } if (enrollmentAppointment.isUnderstandingSkill()) { itemList.add( - getItem(EnrollmentSkill.UNDERSTANDING, EnrollmentUtil.getUnderstandingSkillFee(enrollmentAppointment)) + getItem( + EnrollmentSkill.UNDERSTANDING, + EnrollmentUtil.getUnderstandingSkillFee(enrollmentAppointment), + ExamLevel.GOOD + ) ); } @@ -80,14 +89,26 @@ private List getItems(final Enrollment enrollment, final FreeEnrollmentDet if (enrollment.isTextualSkill()) { itemList.add( - getItem(EnrollmentSkill.TEXTUAL, EnrollmentUtil.getTextualSkillFee(enrollment, freeEnrollmentDetails)) + getItem( + EnrollmentSkill.TEXTUAL, + EnrollmentUtil.getTextualSkillFee(enrollment, freeEnrollmentDetails), + ExamLevel.EXCELLENT + ) ); } if (enrollment.isOralSkill()) { - itemList.add(getItem(EnrollmentSkill.ORAL, EnrollmentUtil.getOralSkillFee(enrollment, freeEnrollmentDetails))); + itemList.add( + getItem( + EnrollmentSkill.ORAL, + EnrollmentUtil.getOralSkillFee(enrollment, freeEnrollmentDetails), + ExamLevel.EXCELLENT + ) + ); } if (enrollment.isUnderstandingSkill()) { - itemList.add(getItem(EnrollmentSkill.UNDERSTANDING, EnrollmentUtil.getUnderstandingSkillFee(enrollment))); + itemList.add( + getItem(EnrollmentSkill.UNDERSTANDING, EnrollmentUtil.getUnderstandingSkillFee(enrollment), ExamLevel.EXCELLENT) + ); } return itemList; @@ -100,7 +121,10 @@ private EnrollmentStatus getPaymentSuccessEnrollmentNextStatus(final Enrollment return enrollment.enrollmentNeedsApproval() ? EnrollmentStatus.AWAITING_APPROVAL : EnrollmentStatus.COMPLETED; } - private void setEnrollmentStatus(final EnrollmentAppointment enrollmentAppointment, final PaymentStatus paymentStatus) { + private void setEnrollmentStatus( + final EnrollmentAppointment enrollmentAppointment, + final PaymentStatus paymentStatus + ) { switch (paymentStatus) { case NEW, PENDING, DELAYED -> {} case OK -> enrollmentAppointment.setStatus(EnrollmentStatus.COMPLETED); @@ -204,7 +228,7 @@ private String getFinalizePaymentRedirectUrl(final Long paymentId, final String final Payment payment = paymentRepository .findById(paymentId) .orElseThrow(() -> new NotFoundException("Payment not found")); - + return payment.getEnrollment() != null ? String.format("%s/ilmoittaudu/%d/maksu/%s", baseUrl, payment.getEnrollment().getExamEvent().getId(), state) : String.format("%s/markkinapaikka/%d/maksu/%s", baseUrl, payment.getEnrollmentAppointment().getId(), state); diff --git a/backend/vkt/src/main/java/fi/oph/vkt/service/PublicEnrollmentService.java b/backend/vkt/src/main/java/fi/oph/vkt/service/PublicEnrollmentService.java index 815e2e8f1..f324aeba3 100644 --- a/backend/vkt/src/main/java/fi/oph/vkt/service/PublicEnrollmentService.java +++ b/backend/vkt/src/main/java/fi/oph/vkt/service/PublicEnrollmentService.java @@ -639,10 +639,11 @@ public PublicEnrollmentAppointmentDTO getEnrollmentAppointment( } @Transactional - public PublicEnrollmentAppointmentDTO saveEnrollmentAppointment(final PublicEnrollmentAppointmentUpdateDTO dto, final Person person) { - final EnrollmentAppointment enrollmentAppointment = enrollmentAppointmentRepository.getReferenceById( - dto.id() - ); + public PublicEnrollmentAppointmentDTO saveEnrollmentAppointment( + final PublicEnrollmentAppointmentUpdateDTO dto, + final Person person + ) { + final EnrollmentAppointment enrollmentAppointment = enrollmentAppointmentRepository.getReferenceById(dto.id()); if (person.getId() != enrollmentAppointment.getPerson().getId()) { throw new APIException(APIExceptionType.RESERVATION_PERSON_SESSION_MISMATCH); diff --git a/backend/vkt/src/test/java/fi/oph/vkt/service/PaymentServiceTest.java b/backend/vkt/src/test/java/fi/oph/vkt/service/PaymentServiceTest.java index 95284767a..faa742915 100644 --- a/backend/vkt/src/test/java/fi/oph/vkt/service/PaymentServiceTest.java +++ b/backend/vkt/src/test/java/fi/oph/vkt/service/PaymentServiceTest.java @@ -23,11 +23,13 @@ import fi.oph.vkt.model.type.AppLocale; import fi.oph.vkt.model.type.EnrollmentSkill; import fi.oph.vkt.model.type.EnrollmentStatus; +import fi.oph.vkt.model.type.ExamLevel; import fi.oph.vkt.model.type.PaymentStatus; import fi.oph.vkt.payment.paytrail.Customer; import fi.oph.vkt.payment.paytrail.Item; import fi.oph.vkt.payment.paytrail.PaytrailPaymentProvider; import fi.oph.vkt.payment.paytrail.PaytrailResponseDTO; +import fi.oph.vkt.repository.EnrollmentAppointmentRepository; import fi.oph.vkt.repository.EnrollmentRepository; import fi.oph.vkt.repository.PaymentRepository; import fi.oph.vkt.util.exception.APIException; @@ -56,6 +58,9 @@ public class PaymentServiceTest { @Resource private EnrollmentRepository enrollmentRepository; + @Resource + private EnrollmentAppointmentRepository enrollmentAppointmentRepository; + @Resource private TestEntityManager entityManager; @@ -94,19 +99,32 @@ public void testCreatePaymentWithAllSkills() { paymentProvider, paymentRepository, enrollmentRepository, + enrollmentAppointmentRepository, environment, publicEnrollmentEmailService ); final String redirectUrl = paymentService.createPaymentForEnrollment(enrollment.getId(), person, AppLocale.FI); final List items = List.of( - Item.builder().units(1).unitPrice(25700).vatPercentage(0).productCode(EnrollmentSkill.TEXTUAL.toString()).build(), - Item.builder().units(1).unitPrice(25700).vatPercentage(0).productCode(EnrollmentSkill.ORAL.toString()).build(), + Item + .builder() + .units(1) + .unitPrice(25700) + .vatPercentage(0) + .productCode(ExamLevel.EXCELLENT + "-" + EnrollmentSkill.TEXTUAL) + .build(), + Item + .builder() + .units(1) + .unitPrice(25700) + .vatPercentage(0) + .productCode(ExamLevel.EXCELLENT + "-" + EnrollmentSkill.ORAL) + .build(), Item .builder() .units(1) .unitPrice(0) .vatPercentage(0) - .productCode(EnrollmentSkill.UNDERSTANDING.toString()) + .productCode(ExamLevel.EXCELLENT + "-" + EnrollmentSkill.UNDERSTANDING) .build() ); final Customer customer = Customer @@ -160,19 +178,26 @@ public void testCreatePaymentWithoutOralSkill() { paymentProvider, paymentRepository, enrollmentRepository, + enrollmentAppointmentRepository, environment, publicEnrollmentEmailService ); final String redirectUrl = paymentService.createPaymentForEnrollment(enrollment.getId(), person, AppLocale.FI); final List items = List.of( - Item.builder().units(1).unitPrice(25700).vatPercentage(0).productCode(EnrollmentSkill.TEXTUAL.toString()).build(), + Item + .builder() + .units(1) + .unitPrice(25700) + .vatPercentage(0) + .productCode(ExamLevel.EXCELLENT + "-" + EnrollmentSkill.TEXTUAL) + .build(), Item .builder() .units(1) .unitPrice(0) .vatPercentage(0) - .productCode(EnrollmentSkill.UNDERSTANDING.toString()) + .productCode(ExamLevel.EXCELLENT + "-" + EnrollmentSkill.UNDERSTANDING) .build() ); @@ -203,6 +228,7 @@ public void testCreatePaymentPassesProperCustomerDataToPaymentProvider() { paymentProvider, paymentRepository, enrollmentRepository, + enrollmentAppointmentRepository, environment, publicEnrollmentEmailService ); @@ -237,6 +263,7 @@ public void testCreatePaymentWrongPerson() { paymentProvider, paymentRepository, enrollmentRepository, + enrollmentAppointmentRepository, environment, publicEnrollmentEmailService ); @@ -259,6 +286,7 @@ public void testCreatePaymentEnrollmentNotFound() { paymentProvider, paymentRepository, enrollmentRepository, + enrollmentAppointmentRepository, environment, publicEnrollmentEmailService ); @@ -287,6 +315,7 @@ public void testFinalizePaymentOnSuccess() throws IOException, InterruptedExcept paymentProvider, paymentRepository, enrollmentRepository, + enrollmentAppointmentRepository, environment, publicEnrollmentEmailService ); @@ -315,6 +344,7 @@ public void testFinalizePaymentOnFailure() throws IOException, InterruptedExcept paymentProvider, paymentRepository, enrollmentRepository, + enrollmentAppointmentRepository, environment, publicEnrollmentEmailService ); @@ -340,6 +370,7 @@ public void testFinalizePaymentValidationFailed() { paymentProvider, paymentRepository, enrollmentRepository, + enrollmentAppointmentRepository, environment, publicEnrollmentEmailService ); @@ -368,6 +399,7 @@ public void testFinalizePaymentOnFailureAlreadyPaid() { paymentProvider, paymentRepository, enrollmentRepository, + enrollmentAppointmentRepository, environment, publicEnrollmentEmailService ); @@ -396,6 +428,7 @@ public void testFinalizePaymentOnSuccessAlreadyPaid() throws IOException, Interr paymentProvider, paymentRepository, enrollmentRepository, + enrollmentAppointmentRepository, environment, publicEnrollmentEmailService ); @@ -420,6 +453,7 @@ public void testFinalizePaymentAmountMustMatch() { paymentProvider, paymentRepository, enrollmentRepository, + enrollmentAppointmentRepository, environment, publicEnrollmentEmailService ); @@ -449,6 +483,7 @@ public void testFinalizePaymentReferenceIdMustMatch() { paymentProvider, paymentRepository, enrollmentRepository, + enrollmentAppointmentRepository, environment, publicEnrollmentEmailService ); @@ -471,6 +506,7 @@ public void testFinalizePaymentPaymentNotFound() { paymentProvider, paymentRepository, enrollmentRepository, + enrollmentAppointmentRepository, environment, publicEnrollmentEmailService ); diff --git a/backend/vkt/src/test/java/fi/oph/vkt/service/PublicEnrollmentServiceTest.java b/backend/vkt/src/test/java/fi/oph/vkt/service/PublicEnrollmentServiceTest.java index 279f8025a..77062ba9c 100644 --- a/backend/vkt/src/test/java/fi/oph/vkt/service/PublicEnrollmentServiceTest.java +++ b/backend/vkt/src/test/java/fi/oph/vkt/service/PublicEnrollmentServiceTest.java @@ -20,11 +20,13 @@ import fi.oph.vkt.api.dto.PublicExamEventDTO; import fi.oph.vkt.api.dto.PublicPersonDTO; import fi.oph.vkt.model.Enrollment; +import fi.oph.vkt.model.EnrollmentAppointment; import fi.oph.vkt.model.ExamEvent; import fi.oph.vkt.model.FeatureFlag; import fi.oph.vkt.model.Person; import fi.oph.vkt.model.Reservation; import fi.oph.vkt.model.type.EnrollmentStatus; +import fi.oph.vkt.repository.EnrollmentAppointmentRepository; import fi.oph.vkt.repository.EnrollmentRepository; import fi.oph.vkt.repository.ExamEventRepository; import fi.oph.vkt.repository.FreeEnrollmentRepository; @@ -60,6 +62,9 @@ public class PublicEnrollmentServiceTest { @Resource private EnrollmentRepository enrollmentRepository; + @Resource + private EnrollmentAppointmentRepository enrollmentAppointmentRepository; + @Resource private ExamEventRepository examEventRepository; @@ -103,6 +108,7 @@ public void setup() throws IOException, InterruptedException { publicEnrollmentService = new PublicEnrollmentService( enrollmentRepository, + enrollmentAppointmentRepository, examEventRepository, publicEnrollmentEmailServiceMock, publicReservationService, diff --git a/frontend/packages/vkt/src/components/publicEnrollmentAppointment/PublicEnrollmentAppointmentControlButtons.tsx b/frontend/packages/vkt/src/components/publicEnrollmentAppointment/PublicEnrollmentAppointmentControlButtons.tsx index bfa2b4834..872c67d87 100644 --- a/frontend/packages/vkt/src/components/publicEnrollmentAppointment/PublicEnrollmentAppointmentControlButtons.tsx +++ b/frontend/packages/vkt/src/components/publicEnrollmentAppointment/PublicEnrollmentAppointmentControlButtons.tsx @@ -5,8 +5,7 @@ import { import { useEffect, useState } from 'react'; import { useNavigate } from 'react-router'; import { CustomButton, LoadingProgressIndicator } from 'shared/components'; -import { APIResponseStatus, Color, Severity, Variant } from 'shared/enums'; -import { useDialog } from 'shared/hooks'; +import { APIResponseStatus, Color, Variant } from 'shared/enums'; import { useCommonTranslation, usePublicTranslation } from 'configs/i18n'; import { useAppDispatch } from 'configs/redux'; @@ -39,8 +38,6 @@ export const PublicEnrollmentAppointmentControlButtons = ({ const dispatch = useAppDispatch(); const navigate = useNavigate(); - const { showDialog } = useDialog(); - const handleCancelBtnClick = () => { // FIXME }; diff --git a/frontend/packages/vkt/src/components/publicEnrollmentAppointment/steps/FillContactDetails.tsx b/frontend/packages/vkt/src/components/publicEnrollmentAppointment/steps/FillContactDetails.tsx index 7d8908127..5869d9f69 100644 --- a/frontend/packages/vkt/src/components/publicEnrollmentAppointment/steps/FillContactDetails.tsx +++ b/frontend/packages/vkt/src/components/publicEnrollmentAppointment/steps/FillContactDetails.tsx @@ -1,78 +1,31 @@ import { Divider } from '@mui/material'; import { ChangeEvent, useEffect, useState } from 'react'; -import { H2, LabeledTextField, Text } from 'shared/components'; +import { LabeledTextField } from 'shared/components'; import { InputAutoComplete, TextFieldTypes } from 'shared/enums'; import { useWindowProperties } from 'shared/hooks'; -import { TextField } from 'shared/interfaces'; -import { FieldErrors } from 'shared/utils'; import { CertificateShipping } from 'components/publicEnrollmentAppointment/steps/CertificateShipping'; import { PersonDetails } from 'components/publicEnrollmentAppointment/steps/PersonDetails'; -import { useCommonTranslation, usePublicTranslation } from 'configs/i18n'; +import { usePublicTranslation } from 'configs/i18n'; import { useAppDispatch } from 'configs/redux'; import { - PublicEnrollment, PublicEnrollmentAppointment, PublicEnrollmentContactDetails, } from 'interfaces/publicEnrollment'; import { updatePublicEnrollment } from 'redux/reducers/publicEnrollment'; -const fields: Array> = [ - { - name: 'email', - required: true, - type: TextFieldTypes.Email, - maxLength: 255, - }, - { - name: 'emailConfirmation', - required: true, - type: TextFieldTypes.Email, - maxLength: 255, - }, - { - name: 'phoneNumber', - required: true, - type: TextFieldTypes.PhoneNumber, - maxLength: 255, - }, -]; - -const emailsMatch = ( - t: (key: string) => string, - errors: FieldErrors, - values: PublicEnrollmentContactDetails, - dirtyFields?: Array, -) => { - if ( - values.email !== values.emailConfirmation && - (!dirtyFields || dirtyFields.includes('emailConfirmation')) - ) { - return { - ...errors, - ['emailConfirmation']: - errors['emailConfirmation'] ?? t('mismatchingEmailsError'), - }; - } - - return errors; -}; - export const FillContactDetails = ({ isLoading, enrollment, setIsStepValid, - showValidation, }: { isLoading: boolean; enrollment: PublicEnrollmentAppointment; setIsStepValid: (isValid: boolean) => void; - showValidation: boolean; }) => { const { t } = usePublicTranslation({ keyPrefix: 'vkt.component.publicEnrollment.steps.fillContactDetails', }); - const translateCommon = useCommonTranslation(); const [dirtyFields, setDirtyFields] = useState< Array @@ -109,9 +62,7 @@ export const FillContactDetails = ({ } }; - const showCustomTextFieldError = ( - fieldName: keyof PublicEnrollmentContactDetails, - ) => { + const showCustomTextFieldError = () => { return false; }; diff --git a/frontend/packages/vkt/src/components/publicEnrollmentAppointment/steps/PersonDetails.tsx b/frontend/packages/vkt/src/components/publicEnrollmentAppointment/steps/PersonDetails.tsx index 36163a3a1..38b78291d 100644 --- a/frontend/packages/vkt/src/components/publicEnrollmentAppointment/steps/PersonDetails.tsx +++ b/frontend/packages/vkt/src/components/publicEnrollmentAppointment/steps/PersonDetails.tsx @@ -5,11 +5,7 @@ import { useAppSelector } from 'configs/redux'; import { PublicPerson } from 'interfaces/publicPerson'; import { publicEnrollmentAppointmentSelector } from 'redux/selectors/publicEnrollmentAppointment'; -export const PersonDetails = ({ - isPreviewStep, -}: { - isPreviewStep: boolean; -}) => { +export const PersonDetails = () => { const { t } = usePublicTranslation({ keyPrefix: 'vkt.component.publicEnrollment.steps.personDetails', }); diff --git a/frontend/packages/vkt/src/interfaces/publicEnrollment.ts b/frontend/packages/vkt/src/interfaces/publicEnrollment.ts index 8f599ba15..3e8ffae0a 100644 --- a/frontend/packages/vkt/src/interfaces/publicEnrollment.ts +++ b/frontend/packages/vkt/src/interfaces/publicEnrollment.ts @@ -70,7 +70,9 @@ export interface PublicEnrollmentResponse status: EnrollmentStatus; } -export interface PublicEnrollmentAppointment extends PublicEnrollment {} +export interface PublicEnrollmentAppointment extends PublicEnrollment { + person: PublicPerson; +} export interface PublicEnrollmentAppointmentResponse extends Omit< diff --git a/frontend/packages/vkt/src/redux/reducers/publicEnrollmentAppointment.ts b/frontend/packages/vkt/src/redux/reducers/publicEnrollmentAppointment.ts index d08b4de85..69c00a839 100644 --- a/frontend/packages/vkt/src/redux/reducers/publicEnrollmentAppointment.ts +++ b/frontend/packages/vkt/src/redux/reducers/publicEnrollmentAppointment.ts @@ -13,7 +13,7 @@ export interface PublicEnrollmentAppointmentState { person?: PublicPerson; } -export const initialState: PublicEnrollmentState = { +export const initialState: PublicEnrollmentAppointmentState = { loadEnrollmentStatus: APIResponseStatus.NotStarted, enrollmentSubmitStatus: APIResponseStatus.NotStarted, paymentLoadingStatus: APIResponseStatus.NotStarted, @@ -43,7 +43,11 @@ export const initialState: PublicEnrollmentState = { examEventId: undefined, hasPaymentLink: undefined, isQueued: undefined, - person: undefined, + person: { + id: -1, + firstName: '', + lastName: '', + }, }, }; @@ -80,7 +84,10 @@ const publicEnrollmentAppointmentSlice = createSlice({ setLoadingPayment(state) { state.paymentLoadingStatus = APIResponseStatus.InProgress; }, - loadPublicEnrollmentSave(state, _action: PayloadAction) { + loadPublicEnrollmentSave( + state, + _action: PayloadAction, + ) { state.enrollmentSubmitStatus = APIResponseStatus.InProgress; }, }, diff --git a/frontend/packages/vkt/src/redux/sagas/publicEnrollmentAppointment.ts b/frontend/packages/vkt/src/redux/sagas/publicEnrollmentAppointment.ts index 107038994..832e5bced 100644 --- a/frontend/packages/vkt/src/redux/sagas/publicEnrollmentAppointment.ts +++ b/frontend/packages/vkt/src/redux/sagas/publicEnrollmentAppointment.ts @@ -57,11 +57,15 @@ function* loadPublicEnrollmentSaveSaga( const saveUrl = `${APIEndpoints.PublicEnrollmentAppointment}/${enrollment.id}`; const response: AxiosResponse = yield call(axiosInstance.post, saveUrl, body); - yield put(storePublicEnrollmentAppointmentSave(response.data)); + + const enrollmentAppointment = + SerializationUtils.deserializePublicEnrollmentAppointment(response.data); + + yield put(storePublicEnrollmentAppointmentSave(enrollmentAppointment)); } catch (error) { const errorMessage = NotifierUtils.getAPIErrorMessage(error as AxiosError); yield put(setAPIError(errorMessage)); - yield put(rejectPublicEnrollmentSave()); + yield put(rejectPublicEnrollmentAppointment()); } } diff --git a/frontend/packages/vkt/src/utils/routes.ts b/frontend/packages/vkt/src/utils/routes.ts index 083585203..c12230684 100644 --- a/frontend/packages/vkt/src/utils/routes.ts +++ b/frontend/packages/vkt/src/utils/routes.ts @@ -16,7 +16,7 @@ export class RouteUtils { } // FIXME add type definition - static getPaymentCreateApiRoute(enrollmentId?: number, type: string) { + static getPaymentCreateApiRoute(enrollmentId: number, type: string) { return APIEndpoints.PaymentCreate.replace( ':enrollmentId', `${enrollmentId}`, @@ -116,7 +116,7 @@ export class RouteUtils { enrollmentId, ); - case PublicEnrollmentAppointmentFormStep.Payment: + case PublicEnrollmentAppointmentFormStep.PaymentFail: return RouteUtils.replaceEnrollmentId( AppRoutes.PublicEnrollmentPaymentFail, enrollmentId, @@ -127,12 +127,6 @@ export class RouteUtils { AppRoutes.PublicEnrollmentPaymentSuccess, enrollmentId, ); - - case PublicEnrollmentAppointmentFormStep.Done: - return RouteUtils.replaceEnrollmentId( - AppRoutes.PublicEnrollmentDone, - enrollmentId, - ); } } static replaceEnrollmentId(route: string, enrollmentId: number) { From b01f9c32adee9450c95ea863290162e34d3b85c0 Mon Sep 17 00:00:00 2001 From: Jarkko Pesonen <435495+jrkkp@users.noreply.github.com> Date: Mon, 7 Oct 2024 09:55:32 +0300 Subject: [PATCH 024/248] VKT(Frontend): enrollment appointment continues --- .../PublicEnrollmentControlButtons.tsx | 2 ++ .../publicEnrollment/steps/PaymentFail.tsx | 5 ++- ...licEnrollmentAppointmentControlButtons.tsx | 3 +- ...PublicEnrollmentAppointmentDesktopGrid.tsx | 2 +- ...ublicEnrollmentAppointmentStepContents.tsx | 3 +- ...PublicEnrollmentAppointmentStepHeading.tsx | 10 +++--- .../steps/Authenticate.tsx | 8 +++-- .../steps/FillContactDetails.tsx | 6 ++-- .../steps/PaymentFail.tsx | 36 +++++++++++-------- .../reducers/publicEnrollmentAppointment.ts | 2 +- .../clerk_enrollment_details.spec.ts | 9 ----- frontend/packages/vkt/src/utils/routes.ts | 8 +++-- 12 files changed, 51 insertions(+), 43 deletions(-) diff --git a/frontend/packages/vkt/src/components/publicEnrollment/PublicEnrollmentControlButtons.tsx b/frontend/packages/vkt/src/components/publicEnrollment/PublicEnrollmentControlButtons.tsx index 77bb8e495..ff0577278 100644 --- a/frontend/packages/vkt/src/components/publicEnrollment/PublicEnrollmentControlButtons.tsx +++ b/frontend/packages/vkt/src/components/publicEnrollment/PublicEnrollmentControlButtons.tsx @@ -114,6 +114,7 @@ export const PublicEnrollmentControlButtons = ({ // Safari needs time to re-render loading indicator setTimeout(() => { window.location.href = RouteUtils.getPaymentCreateApiRoute( + 'reservation', enrollment.id, ); }, 200); @@ -158,6 +159,7 @@ export const PublicEnrollmentControlButtons = ({ // Safari needs time to re-render loading indicator setTimeout(() => { window.location.href = RouteUtils.getPaymentCreateApiRoute( + 'reservation', enrollment.id, ); }, 200); diff --git a/frontend/packages/vkt/src/components/publicEnrollment/steps/PaymentFail.tsx b/frontend/packages/vkt/src/components/publicEnrollment/steps/PaymentFail.tsx index 5646783a4..6713811a6 100644 --- a/frontend/packages/vkt/src/components/publicEnrollment/steps/PaymentFail.tsx +++ b/frontend/packages/vkt/src/components/publicEnrollment/steps/PaymentFail.tsx @@ -37,7 +37,10 @@ export const PaymentFail = ({ // Safari needs time to re-render loading indicator setTimeout(() => { - window.location.href = RouteUtils.getPaymentCreateApiRoute(enrollment.id); + window.location.href = RouteUtils.getPaymentCreateApiRoute( + 'reservation', + enrollment.id, + ); }, 200); }; diff --git a/frontend/packages/vkt/src/components/publicEnrollmentAppointment/PublicEnrollmentAppointmentControlButtons.tsx b/frontend/packages/vkt/src/components/publicEnrollmentAppointment/PublicEnrollmentAppointmentControlButtons.tsx index 872c67d87..235628267 100644 --- a/frontend/packages/vkt/src/components/publicEnrollmentAppointment/PublicEnrollmentAppointmentControlButtons.tsx +++ b/frontend/packages/vkt/src/components/publicEnrollmentAppointment/PublicEnrollmentAppointmentControlButtons.tsx @@ -10,6 +10,7 @@ import { APIResponseStatus, Color, Variant } from 'shared/enums'; import { useCommonTranslation, usePublicTranslation } from 'configs/i18n'; import { useAppDispatch } from 'configs/redux'; import { PublicEnrollmentAppointmentFormStep } from 'enums/publicEnrollment'; +import { PublicEnrollmentAppointment } from 'interfaces/publicEnrollment'; import { loadPublicEnrollmentSave, setLoadingPayment, @@ -47,8 +48,8 @@ export const PublicEnrollmentAppointmentControlButtons = ({ // Safari needs time to re-render loading indicator setTimeout(() => { window.location.href = RouteUtils.getPaymentCreateApiRoute( - enrollment.id, 'appointment', + enrollment.id, ); }, 200); dispatch(setLoadingPayment()); diff --git a/frontend/packages/vkt/src/components/publicEnrollmentAppointment/PublicEnrollmentAppointmentDesktopGrid.tsx b/frontend/packages/vkt/src/components/publicEnrollmentAppointment/PublicEnrollmentAppointmentDesktopGrid.tsx index 2069edc66..34d6e415c 100644 --- a/frontend/packages/vkt/src/components/publicEnrollmentAppointment/PublicEnrollmentAppointmentDesktopGrid.tsx +++ b/frontend/packages/vkt/src/components/publicEnrollmentAppointment/PublicEnrollmentAppointmentDesktopGrid.tsx @@ -20,7 +20,7 @@ export const PublicEnrollmentAppointmentDesktopGrid = ({ setIsStepValid, setShowValidation, }: { - activeStep: PublicEnrollmentFormStep; + activeStep: PublicEnrollmentAppointmentFormStep; isStepValid: boolean; enrollment: PublicEnrollmentAppointment; showValidation: boolean; diff --git a/frontend/packages/vkt/src/components/publicEnrollmentAppointment/PublicEnrollmentAppointmentStepContents.tsx b/frontend/packages/vkt/src/components/publicEnrollmentAppointment/PublicEnrollmentAppointmentStepContents.tsx index b3fe6894f..1a15b8d58 100644 --- a/frontend/packages/vkt/src/components/publicEnrollmentAppointment/PublicEnrollmentAppointmentStepContents.tsx +++ b/frontend/packages/vkt/src/components/publicEnrollmentAppointment/PublicEnrollmentAppointmentStepContents.tsx @@ -26,13 +26,12 @@ export const PublicEnrollmentAppointmentStepContents = ({ enrollment={enrollment} isLoading={false} setIsStepValid={setIsStepValid} - showValidation={showValidation} /> ); case PublicEnrollmentAppointmentFormStep.Preview: return ; case PublicEnrollmentAppointmentFormStep.PaymentFail: - return ; + return ; case PublicEnrollmentAppointmentFormStep.PaymentSuccess: return ; } diff --git a/frontend/packages/vkt/src/components/publicEnrollmentAppointment/PublicEnrollmentAppointmentStepHeading.tsx b/frontend/packages/vkt/src/components/publicEnrollmentAppointment/PublicEnrollmentAppointmentStepHeading.tsx index 72d49ce9b..310887561 100644 --- a/frontend/packages/vkt/src/components/publicEnrollmentAppointment/PublicEnrollmentAppointmentStepHeading.tsx +++ b/frontend/packages/vkt/src/components/publicEnrollmentAppointment/PublicEnrollmentAppointmentStepHeading.tsx @@ -3,12 +3,12 @@ import { H1, HeaderSeparator } from 'shared/components'; import { useFocus, useWindowProperties } from 'shared/hooks'; import { usePublicTranslation } from 'configs/i18n'; -import { PublicEnrollmentFormStep } from 'enums/publicEnrollment'; +import { PublicEnrollmentAppointmentFormStep } from 'enums/publicEnrollment'; export const PublicEnrollmentAppointmentStepHeading = ({ activeStep, }: { - activeStep: PublicEnrollmentFormStep; + activeStep: PublicEnrollmentAppointmentFormStep; }) => { const { t } = usePublicTranslation({ keyPrefix: 'vkt.component.publicEnrollment.stepHeading', @@ -23,9 +23,9 @@ export const PublicEnrollmentAppointmentStepHeading = ({ }, [setFocus, isPhone]); const headingText = - activeStep === PublicEnrollmentFormStep.Authenticate - ? t(`toExam.${PublicEnrollmentFormStep[activeStep]}`) - : t(`common.${PublicEnrollmentFormStep[activeStep]}`); + activeStep === PublicEnrollmentAppointmentFormStep.Authenticate + ? t(`toExam.${PublicEnrollmentAppointmentFormStep[activeStep]}`) + : t(`common.${PublicEnrollmentAppointmentFormStep[activeStep]}`); return (
diff --git a/frontend/packages/vkt/src/components/publicEnrollmentAppointment/steps/Authenticate.tsx b/frontend/packages/vkt/src/components/publicEnrollmentAppointment/steps/Authenticate.tsx index dbbe67579..6683a4af8 100644 --- a/frontend/packages/vkt/src/components/publicEnrollmentAppointment/steps/Authenticate.tsx +++ b/frontend/packages/vkt/src/components/publicEnrollmentAppointment/steps/Authenticate.tsx @@ -15,10 +15,14 @@ export const Authenticate = () => { keyPrefix: 'vkt.component.publicEnrollment.steps.authenticate', }); const translateCommon = useCommonTranslation(); - const enrollmentId = +params.enrollmentId; - const dispatch = useAppDispatch(); + if (!params.enrollmentId) { + return <>; + } + + const enrollmentId = +params.enrollmentId; + const onAuthenticate = () => { setIsAuthRedirecting(true); diff --git a/frontend/packages/vkt/src/components/publicEnrollmentAppointment/steps/FillContactDetails.tsx b/frontend/packages/vkt/src/components/publicEnrollmentAppointment/steps/FillContactDetails.tsx index 5869d9f69..0134c0d6b 100644 --- a/frontend/packages/vkt/src/components/publicEnrollmentAppointment/steps/FillContactDetails.tsx +++ b/frontend/packages/vkt/src/components/publicEnrollmentAppointment/steps/FillContactDetails.tsx @@ -32,7 +32,6 @@ export const FillContactDetails = ({ >([]); const dispatch = useAppDispatch(); - const errors = []; useEffect(() => { setIsStepValid(true); @@ -73,8 +72,7 @@ export const FillContactDetails = ({ label: t(`${fieldName}.label`), onBlur: handleBlur(fieldName), onChange: handleChange(fieldName), - error: showCustomTextFieldError(fieldName), - helperText: errors[fieldName], + error: showCustomTextFieldError(), required: true, disabled: isLoading, }); @@ -83,7 +81,7 @@ export const FillContactDetails = ({ return (
- + { +export const PaymentFail = () => { const { t } = usePublicTranslation({ keyPrefix: 'vkt.component.publicEnrollment', }); @@ -29,18 +25,10 @@ export const PaymentFail = ({ const { showDialog } = useDialog(); const [isPaymentLoading, setIsPaymentLoading] = useState(false); const { cancelStatus } = useAppSelector(publicEnrollmentSelector); + const params = useParams(); const isCancelLoading = cancelStatus === APIResponseStatus.InProgress; const isLoading = isPaymentLoading || isCancelLoading; - const handleTryAgainBtnClick = () => { - setIsPaymentLoading(true); - - // Safari needs time to re-render loading indicator - setTimeout(() => { - window.location.href = RouteUtils.getPaymentCreateApiRoute(enrollment.id); - }, 200); - }; - const handleCancelBtnClick = () => { showDialog({ title: t('controlButtons.cancelDialog.title'), @@ -69,6 +57,24 @@ export const PaymentFail = ({ }); }, [t, showToast]); + if (!params.enrollmentId) { + return <>; + } + + const enrollmentId = +params.enrollmentId; + + const handleTryAgainBtnClick = () => { + setIsPaymentLoading(true); + + // Safari needs time to re-render loading indicator + setTimeout(() => { + window.location.href = RouteUtils.getPaymentCreateApiRoute( + 'appointment', + enrollmentId, + ); + }, 200); + }; + return (
{t('steps.paymentFail.description')} diff --git a/frontend/packages/vkt/src/redux/reducers/publicEnrollmentAppointment.ts b/frontend/packages/vkt/src/redux/reducers/publicEnrollmentAppointment.ts index 69c00a839..384fcfa47 100644 --- a/frontend/packages/vkt/src/redux/reducers/publicEnrollmentAppointment.ts +++ b/frontend/packages/vkt/src/redux/reducers/publicEnrollmentAppointment.ts @@ -13,7 +13,7 @@ export interface PublicEnrollmentAppointmentState { person?: PublicPerson; } -export const initialState: PublicEnrollmentAppointmentState = { +const initialState: PublicEnrollmentAppointmentState = { loadEnrollmentStatus: APIResponseStatus.NotStarted, enrollmentSubmitStatus: APIResponseStatus.NotStarted, paymentLoadingStatus: APIResponseStatus.NotStarted, diff --git a/frontend/packages/vkt/src/tests/cypress/integration/enrollmentDetails/clerk_enrollment_details.spec.ts b/frontend/packages/vkt/src/tests/cypress/integration/enrollmentDetails/clerk_enrollment_details.spec.ts index ee651dc3e..60d50b75d 100644 --- a/frontend/packages/vkt/src/tests/cypress/integration/enrollmentDetails/clerk_enrollment_details.spec.ts +++ b/frontend/packages/vkt/src/tests/cypress/integration/enrollmentDetails/clerk_enrollment_details.spec.ts @@ -9,15 +9,6 @@ describe('ClerkEnrollmentOverview:ClerkEnrollmentDetails', () => { const nameFields = ['firstName', 'lastName']; const contactDetailsFields = ['email', 'phoneNumber']; const addressFields = ['street', 'postalCode', 'town', 'country']; - const partialsExamsAndSkillsFields = [ - 'oralSkill', - 'textualSkill', - 'understandingSkill', - 'speakingPartialExam', - 'speechComprehensionPartialExam', - 'writingPartialExam', - 'readingComprehensionPartialExam', - ]; beforeEach(() => { cy.openClerkExamEventPage(clerkExamEvent.id); diff --git a/frontend/packages/vkt/src/utils/routes.ts b/frontend/packages/vkt/src/utils/routes.ts index c12230684..7314e85a6 100644 --- a/frontend/packages/vkt/src/utils/routes.ts +++ b/frontend/packages/vkt/src/utils/routes.ts @@ -16,7 +16,7 @@ export class RouteUtils { } // FIXME add type definition - static getPaymentCreateApiRoute(enrollmentId: number, type: string) { + static getPaymentCreateApiRoute(type: string, enrollmentId?: number) { return APIEndpoints.PaymentCreate.replace( ':enrollmentId', `${enrollmentId}`, @@ -95,8 +95,12 @@ export class RouteUtils { static appointmentStepToRoute( step: PublicEnrollmentAppointmentFormStep, - enrollmentId: number, + enrollmentId?: number, ) { + if (!enrollmentId) { + return ''; + } + switch (step) { case PublicEnrollmentAppointmentFormStep.Authenticate: return RouteUtils.replaceEnrollmentId( From 71c7129218f58ff4d984596721c7b45590e7ae0a Mon Sep 17 00:00:00 2001 From: Pyry Koivisto Date: Thu, 19 Sep 2024 16:02:32 +0300 Subject: [PATCH 025/248] VKT(Backend): Feature flag for good and satisfactory level support --- backend/vkt/src/main/java/fi/oph/vkt/model/FeatureFlag.java | 3 ++- backend/vkt/src/main/resources/application.yaml | 1 + .../main/resources/oph-configuration/vkt.properties.template | 1 + 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/backend/vkt/src/main/java/fi/oph/vkt/model/FeatureFlag.java b/backend/vkt/src/main/java/fi/oph/vkt/model/FeatureFlag.java index 4a95dbaed..0f58cd21c 100644 --- a/backend/vkt/src/main/java/fi/oph/vkt/model/FeatureFlag.java +++ b/backend/vkt/src/main/java/fi/oph/vkt/model/FeatureFlag.java @@ -1,7 +1,8 @@ package fi.oph.vkt.model; public enum FeatureFlag { - FREE_ENROLLMENT_FOR_HIGHEST_LEVEL_ALLOWED("freeEnrollmentAllowed"); + FREE_ENROLLMENT_FOR_HIGHEST_LEVEL_ALLOWED("freeEnrollmentAllowed"), + GOOD_AND_SATISFACTORY_LEVEL("goodAndSatisfactoryLevel"); private final String propertyKey; diff --git a/backend/vkt/src/main/resources/application.yaml b/backend/vkt/src/main/resources/application.yaml index 20c4b3b40..77b466e79 100644 --- a/backend/vkt/src/main/resources/application.yaml +++ b/backend/vkt/src/main/resources/application.yaml @@ -96,5 +96,6 @@ app: account: ${payment.paytrail.account:null} featureFlags: freeEnrollmentAllowed: ${feature-flags.free-enrollment-allowed:false} + goodAndSatisfactoryLevel: ${feature-flags.good-and-satisfactory-level:false} aws: s3-bucket: ${aws.s3-bucket:test} diff --git a/backend/vkt/src/main/resources/oph-configuration/vkt.properties.template b/backend/vkt/src/main/resources/oph-configuration/vkt.properties.template index 1e845099c..f513d1c73 100644 --- a/backend/vkt/src/main/resources/oph-configuration/vkt.properties.template +++ b/backend/vkt/src/main/resources/oph-configuration/vkt.properties.template @@ -33,5 +33,6 @@ koski.user={{vkt_koski_user}} koski.password={{vkt_koski_password}} feature-flags.free-enrollment-allowed={{vkt_feature_free_enrollment_allowed}} +feature-flags.good-and-satisfactory-level={{vkt_feature_good_and_satisfactory_level}} aws.s3-bucket={{vkt_aws_s3_bucket}} From ee95d47b92ac1822a4d93267ccdb5b78ccebe39b Mon Sep 17 00:00:00 2001 From: Pyry Koivisto Date: Mon, 23 Sep 2024 15:00:00 +0300 Subject: [PATCH 026/248] Remove obsolete comment from package.json --- frontend/package.json | 3 --- 1 file changed, 3 deletions(-) diff --git a/frontend/package.json b/frontend/package.json index 7bc4471b3..ff9c4f6f1 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -109,8 +109,5 @@ "webpack": "^5.94.0", "webpack-cli": "^5.1.4", "webpack-dev-server": "^4.15.1" - }, - "@comments": { - "react-router-dom": "Do not update past 6.13.0 until useBlocker is fixed, see https://github.com/remix-run/react-router/issues/11155" } } From 9a735990c82a47370c9f0ec795a5890bfe489f5c Mon Sep 17 00:00:00 2001 From: Pyry Koivisto Date: Tue, 24 Sep 2024 19:58:39 +0300 Subject: [PATCH 027/248] VKT(Frontend): Introduce feature flag for good and satisfactory level --- frontend/packages/vkt/src/interfaces/featureFlags.ts | 1 + frontend/packages/vkt/src/redux/reducers/featureFlags.ts | 2 +- frontend/packages/vkt/src/redux/selectors/featureFlags.ts | 4 +++- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/frontend/packages/vkt/src/interfaces/featureFlags.ts b/frontend/packages/vkt/src/interfaces/featureFlags.ts index b56e45771..39a1a2334 100644 --- a/frontend/packages/vkt/src/interfaces/featureFlags.ts +++ b/frontend/packages/vkt/src/interfaces/featureFlags.ts @@ -1,5 +1,6 @@ export interface FeatureFlags { freeEnrollmentAllowed: boolean; + goodAndSatisfactoryLevel: boolean; } export interface FeatureFlagsResponse extends Partial {} diff --git a/frontend/packages/vkt/src/redux/reducers/featureFlags.ts b/frontend/packages/vkt/src/redux/reducers/featureFlags.ts index fd64cb5b2..a5ee4fae9 100644 --- a/frontend/packages/vkt/src/redux/reducers/featureFlags.ts +++ b/frontend/packages/vkt/src/redux/reducers/featureFlags.ts @@ -3,7 +3,7 @@ import { APIResponseStatus } from 'shared/enums'; import { FeatureFlags } from 'interfaces/featureFlags'; -interface FeatureFlagsState extends Partial { +export interface FeatureFlagsState extends Partial { status: APIResponseStatus; } diff --git a/frontend/packages/vkt/src/redux/selectors/featureFlags.ts b/frontend/packages/vkt/src/redux/selectors/featureFlags.ts index ab1750508..2fb3e93de 100644 --- a/frontend/packages/vkt/src/redux/selectors/featureFlags.ts +++ b/frontend/packages/vkt/src/redux/selectors/featureFlags.ts @@ -1,3 +1,5 @@ import { RootState } from 'configs/redux'; +import { FeatureFlagsState } from 'redux/reducers/featureFlags'; -export const featureFlagsSelector = (state: RootState) => state.featureFlags; +export const featureFlagsSelector = (state: RootState): FeatureFlagsState => + state.featureFlags; From bfe8576e2428a639de87ff1d11656a3c9873d88b Mon Sep 17 00:00:00 2001 From: Pyry Koivisto Date: Tue, 24 Sep 2024 20:02:06 +0300 Subject: [PATCH 028/248] VKT(Frontend): Refactor: move pages around a bit --- .../packages/vkt/src/pages/PublicHomePage.tsx | 88 ++++++++++++++++++- .../PublicEnrollmentPage.tsx | 0 .../PublicExcellentLevelLandingPage.tsx | 19 ++++ ...licGoodAndSatisfactoryLevelLandingPage.tsx | 27 ++++++ .../packages/vkt/src/routers/AppRouter.tsx | 22 ++++- 5 files changed, 152 insertions(+), 4 deletions(-) rename frontend/packages/vkt/src/pages/{ => excellentLevel}/PublicEnrollmentPage.tsx (100%) create mode 100644 frontend/packages/vkt/src/pages/excellentLevel/PublicExcellentLevelLandingPage.tsx create mode 100644 frontend/packages/vkt/src/pages/goodAndSatisfactoryLevel/PublicGoodAndSatisfactoryLevelLandingPage.tsx diff --git a/frontend/packages/vkt/src/pages/PublicHomePage.tsx b/frontend/packages/vkt/src/pages/PublicHomePage.tsx index 046134309..076d94da5 100644 --- a/frontend/packages/vkt/src/pages/PublicHomePage.tsx +++ b/frontend/packages/vkt/src/pages/PublicHomePage.tsx @@ -1,9 +1,75 @@ -import { Box, Grid } from '@mui/material'; +import { Box, Grid, Paper, Typography } from '@mui/material'; import { FC } from 'react'; +import { Link } from 'react-router-dom'; +import { H1, H2, H3, HeaderSeparator, Text, WebLink } from 'shared/components'; -import { PublicExamEventGrid } from 'components/publicExamEvent/PublicExamEventGrid'; +import { usePublicTranslation } from 'configs/i18n'; +import { AppRoutes } from 'enums/app'; + +const ExcellentLevelCard = () => { + return ( + +
+

Erinomaisen taidon tutkinnot

+ Tekstiä. + + Toinen kappale. Hieman pituuttakin tälle paragraaaaaafffille. + + + Katso lisätietoja OPH:n verkkosivuilla:{' '} + + + + Siirry{' '} + + erinomaisen taidon tutkintojen sivulle. + + +
+
+ ); +}; + +const GoodAndSatisfactoryLevelCard = () => { + return ( + +
+

Hyvän ja tyydyttävän taidon tutkinnot

+ + Yhdellä kokeella voit osoittaa tuloksesta riippuen hyvää tai + tyydyttävää taitoa. + + + Toinen kappale tekstiä. Käytetään h3-tyyliä, mutta ilman vastaavaa + DOM-elementtiä. + + + Katso lisätietoja OPH:n verkkosivuilla:{' '} + + + + Siirry{' '} + + hyvän ja tyydyttävän taidon tutkintojen sivulle. + + +
+
+ ); +}; export const PublicHomePage: FC = () => { + const { t } = usePublicTranslation({ + keyPrefix: 'vkt.component.publicHomePage', + }); + return ( { direction="column" className="public-homepage__grid-container" > - + +

{t('title')}

+ +
+ + {t('description.part1')} +
+ {t('description.part2')} +
+

{t('selectExamination.heading')}

+ {t('selectExamination.description')} +
+ + +
+
+
); diff --git a/frontend/packages/vkt/src/pages/PublicEnrollmentPage.tsx b/frontend/packages/vkt/src/pages/excellentLevel/PublicEnrollmentPage.tsx similarity index 100% rename from frontend/packages/vkt/src/pages/PublicEnrollmentPage.tsx rename to frontend/packages/vkt/src/pages/excellentLevel/PublicEnrollmentPage.tsx diff --git a/frontend/packages/vkt/src/pages/excellentLevel/PublicExcellentLevelLandingPage.tsx b/frontend/packages/vkt/src/pages/excellentLevel/PublicExcellentLevelLandingPage.tsx new file mode 100644 index 000000000..9f7532058 --- /dev/null +++ b/frontend/packages/vkt/src/pages/excellentLevel/PublicExcellentLevelLandingPage.tsx @@ -0,0 +1,19 @@ +import { Box, Grid } from '@mui/material'; +import { FC } from 'react'; + +import { PublicExamEventGrid } from 'components/publicExamEvent/PublicExamEventGrid'; + +export const PublicExcellentLevelLandingPage: FC = () => { + return ( + + + + + + ); +}; diff --git a/frontend/packages/vkt/src/pages/goodAndSatisfactoryLevel/PublicGoodAndSatisfactoryLevelLandingPage.tsx b/frontend/packages/vkt/src/pages/goodAndSatisfactoryLevel/PublicGoodAndSatisfactoryLevelLandingPage.tsx new file mode 100644 index 000000000..d711dd2a6 --- /dev/null +++ b/frontend/packages/vkt/src/pages/goodAndSatisfactoryLevel/PublicGoodAndSatisfactoryLevelLandingPage.tsx @@ -0,0 +1,27 @@ +import { Box, Grid } from '@mui/material'; +import { FC } from 'react'; +import { H1, HeaderSeparator } from 'shared/components'; + +import { usePublicTranslation } from 'configs/i18n'; + +export const PublicGoodAndSatisfactoryLevelLandingPage: FC = () => { + const { t } = usePublicTranslation({ + keyPrefix: 'vkt.component.goodAndSatisfactoryLevel', + }); + + return ( + + + +

{t('title')}

+ +
+
+
+ ); +}; diff --git a/frontend/packages/vkt/src/routers/AppRouter.tsx b/frontend/packages/vkt/src/routers/AppRouter.tsx index 9c9fb3e7a..86598b3cb 100644 --- a/frontend/packages/vkt/src/routers/AppRouter.tsx +++ b/frontend/packages/vkt/src/routers/AppRouter.tsx @@ -27,9 +27,11 @@ import { ClerkEnrollmentOverviewPage } from 'pages/ClerkEnrollmentOverviewPage'; import { ClerkExamEventCreatePage } from 'pages/ClerkExamEventCreatePage'; import { ClerkExamEventOverviewPage } from 'pages/ClerkExamEventOverviewPage'; import { ClerkHomePage } from 'pages/ClerkHomePage'; +import { PublicEnrollmentPage } from 'pages/excellentLevel/PublicEnrollmentPage'; +import { PublicExcellentLevelLandingPage } from 'pages/excellentLevel/PublicExcellentLevelLandingPage'; +import { PublicGoodAndSatisfactoryLevelLandingPage } from 'pages/goodAndSatisfactoryLevel/PublicGoodAndSatisfactoryLevelLandingPage'; import { LogoutSuccess } from 'pages/LogoutSuccess'; import { NotFoundPage } from 'pages/NotFoundPage'; -import { PublicEnrollmentPage } from 'pages/PublicEnrollmentPage'; import { PublicHomePage } from 'pages/PublicHomePage'; import { loadFeatureFlags } from 'redux/reducers/featureFlags'; import { featureFlagsSelector } from 'redux/selectors/featureFlags'; @@ -75,17 +77,35 @@ export const AppRouter: FC = () => {
); + // TODO Consider serving different page as front page when feature flag for good and satisfactory levels is enabled? const FrontPage = ( ); + // TODO Enable / disable routes for good and satisfactory level based on feature flag? const router = createBrowserRouter( createRoutesFromElements( + + + + } + /> + + + + } + /> Date: Tue, 24 Sep 2024 20:03:54 +0300 Subject: [PATCH 029/248] VKT(Frontend): Include NavigationLinks in Header --- .../vkt/public/i18n/fi-FI/common.json | 10 ++- .../vkt/public/i18n/sv-SE/common.json | 8 ++- .../vkt/public/i18n/sv-SE/public.json | 3 + .../vkt/src/components/layouts/Header.tsx | 63 ++++++++++++++++++- frontend/packages/vkt/src/enums/app.ts | 6 ++ 5 files changed, 85 insertions(+), 5 deletions(-) diff --git a/frontend/packages/vkt/public/i18n/fi-FI/common.json b/frontend/packages/vkt/public/i18n/fi-FI/common.json index e88025b21..5dbd7e469 100644 --- a/frontend/packages/vkt/public/i18n/fi-FI/common.json +++ b/frontend/packages/vkt/public/i18n/fi-FI/common.json @@ -100,7 +100,13 @@ "header": { "accessibility": { "continueToMain": "Jatka sisältöön", - "langSelectorAriaLabel": "Kieli / Språk" + "langSelectorAriaLabel": "Kieli / Språk", + "mainNavigation": "Päänavigaatio" + }, + "publicNavigationLinks": { + "excellentLevel": "Erinomaisen taidon tutkinnot", + "frontPage": "Etusivu", + "goodAndSatisfactoryLevel": "Hyvän ja tyydyttävän taidon tutkinnot" }, "sessionState": { "logOut": "Kirjaudu ulos" @@ -148,8 +154,10 @@ "clerkHomepage": "Virkailija", "contactDetails": "Ilmoittautuminen - täytä yhteystietosi", "educationDetails": "Ilmoittautuminen - koulutustiedot", + "excellentLevelLanding": "Erinomaisen taidon tutkinnot", "done": "Ilmoittautuminen - valmis", "frontPage": "Etusivu", + "goodAndSatisfactoryLevelLanding": "Hyvän ja tyydyttävän taidon tutkinnot", "logoutSuccess": "Uloskirjautuminen onnnistui", "notFound": "Etsimääsi sivua ei löytynyt", "paymentFail": "Ilmoittautuminen - maksua ei suoritettu", diff --git a/frontend/packages/vkt/public/i18n/sv-SE/common.json b/frontend/packages/vkt/public/i18n/sv-SE/common.json index 73db44729..c836a2c34 100644 --- a/frontend/packages/vkt/public/i18n/sv-SE/common.json +++ b/frontend/packages/vkt/public/i18n/sv-SE/common.json @@ -97,7 +97,13 @@ "header": { "accessibility": { "continueToMain": "Fortsätt till innehållet", - "langSelectorAriaLabel": "Kieli / Språk" + "langSelectorAriaLabel": "Kieli / Språk", + "mainNavigation": "Huvudnavigering" + }, + "publicNavigationLinks": { + "excellentLevel": "Examina som gäller utmärkta språkkunskaper", + "frontPage": "Framsida", + "goodAndSatisfactoryLevel": "Examina som gäller goda eller nöjaktiga språkkunskaper" }, "sessionState": { "logOut": "Logga ut" diff --git a/frontend/packages/vkt/public/i18n/sv-SE/public.json b/frontend/packages/vkt/public/i18n/sv-SE/public.json index 3b365c016..1396ccdef 100644 --- a/frontend/packages/vkt/public/i18n/sv-SE/public.json +++ b/frontend/packages/vkt/public/i18n/sv-SE/public.json @@ -321,6 +321,9 @@ "logoutSuccessPage": { "heading": "Utloggning lyckades", "info": "Du har loggat ut. Stäng alla fönster i webbläsaren." + }, + "goodAndSatisfactoryLevel": { + "title": "Examina som gäller goda eller nöjaktiga språkkunskaper" } } } diff --git a/frontend/packages/vkt/src/components/layouts/Header.tsx b/frontend/packages/vkt/src/components/layouts/Header.tsx index d90596d2b..520db4e88 100644 --- a/frontend/packages/vkt/src/components/layouts/Header.tsx +++ b/frontend/packages/vkt/src/components/layouts/Header.tsx @@ -1,8 +1,9 @@ import { AppBar, Toolbar } from '@mui/material'; -import { Link } from 'react-router-dom'; +import { Link, matchPath, useLocation } from 'react-router-dom'; import { CookieBanner, LangSelector, + NavigationLinks, OPHClerkLogo, OPHLogoViewer, SkipLink, @@ -21,12 +22,61 @@ import { getSupportedLangs, useCommonTranslation, } from 'configs/i18n'; -import { useAppDispatch } from 'configs/redux'; -import { AppRoutes } from 'enums/app'; +import { useAppDispatch, useAppSelector } from 'configs/redux'; +import { AppRoutes, PublicNavigationLink } from 'enums/app'; import { useAuthentication } from 'hooks/useAuthentication'; import { useInterval } from 'hooks/useInterval'; import { loadClerkUser } from 'redux/reducers/clerkUser'; import { loadPublicUser } from 'redux/reducers/publicUser'; +import { featureFlagsSelector } from 'redux/selectors/featureFlags'; + +const isPathActive = (currentPath: string, route: AppRoutes) => + !!matchPath({ path: route, end: false }, currentPath); + +const PublicNavigationLinks = () => { + const translateCommon = useCommonTranslation(); + const { pathname } = useLocation(); + const { goodAndSatisfactoryLevel } = useAppSelector(featureFlagsSelector); + const excellentLevelLink = { + active: isPathActive(pathname, AppRoutes.PublicExcellentLevelLanding), + label: translateCommon( + `header.publicNavigationLinks.${PublicNavigationLink.ExcellentLevel}`, + ), + href: AppRoutes.PublicExcellentLevelLanding, + }; + + const navigationLinks = goodAndSatisfactoryLevel + ? [ + { + active: isPathActive(pathname, AppRoutes.PublicHomePage), + label: translateCommon( + `header.publicNavigationLinks.${PublicNavigationLink.FrontPage}`, + ), + href: AppRoutes.PublicHomePage, + }, + excellentLevelLink, + { + active: isPathActive( + pathname, + AppRoutes.PublicGoodAndSatisfactoryLevelLanding, + ), + label: translateCommon( + `header.publicNavigationLinks.${PublicNavigationLink.GoodAndSatisfactoryLevel}`, + ), + href: AppRoutes.PublicGoodAndSatisfactoryLevelLanding, + }, + ] + : [excellentLevelLink]; + + return ( + + ); +}; export const Header = (): JSX.Element => { const dispatch = useAppDispatch(); @@ -40,9 +90,13 @@ export const Header = (): JSX.Element => { const { isAuthenticated, isClerkUI, clerkUser, publicUser } = useAuthentication(); + const goodAndSatisfactoryLevelSupported = + useAppSelector(featureFlagsSelector).goodAndSatisfactoryLevel; const logoRedirectURL = isAuthenticated ? AppRoutes.ClerkHomePage : AppRoutes.PublicHomePage; + const activeUrl = window.location.href; + const isPublicUrl = !activeUrl.includes(AppRoutes.ClerkHomePage); const { isPhone } = useWindowProperties(); const isClerkAuthenticationValid = @@ -110,6 +164,9 @@ export const Header = (): JSX.Element => {
{isAuthenticated && } + {isPublicUrl && goodAndSatisfactoryLevelSupported && ( + + )}
{isAuthenticated && } diff --git a/frontend/packages/vkt/src/enums/app.ts b/frontend/packages/vkt/src/enums/app.ts index c48f75c63..e994a3ee1 100644 --- a/frontend/packages/vkt/src/enums/app.ts +++ b/frontend/packages/vkt/src/enums/app.ts @@ -67,3 +67,9 @@ export enum PaymentStatus { PENDING = 'PENDING', DELAYED = 'DELAYED', } + +export enum PublicNavigationLink { + FrontPage = 'frontPage', + ExcellentLevel = 'excellentLevel', + GoodAndSatisfactoryLevel = 'goodAndSatisfactoryLevel', +} From 54b90bb639b365fb5b099f0869533e0d3b474371 Mon Sep 17 00:00:00 2001 From: Pyry Koivisto Date: Tue, 24 Sep 2024 20:05:46 +0300 Subject: [PATCH 030/248] VKT(Frontend): Text and styling changes [deploy] --- .../packages/vkt/public/i18n/fi-FI/public.json | 14 ++++++++++++++ .../vkt/src/styles/pages/_public-homepage.scss | 15 +++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/frontend/packages/vkt/public/i18n/fi-FI/public.json b/frontend/packages/vkt/public/i18n/fi-FI/public.json index b4d8124e1..b5f06db4e 100644 --- a/frontend/packages/vkt/public/i18n/fi-FI/public.json +++ b/frontend/packages/vkt/public/i18n/fi-FI/public.json @@ -322,6 +322,20 @@ "logoutSuccessPage": { "heading": "Uloskirjautuminen onnnistui", "info": "Olet kirjautunut ulos. Suljethan vielä kaikki selainikkunat." + }, + "goodAndSatisfactoryLevel": { + "title": "Hyvän ja tyydyttävän taidon tutkinnot" + }, + "publicHomePage": { + "description": { + "part1": "Valtionhallinnon kielitutkinnot (VKT) on tarkoitettu julkishallinnon henkilöstön toisen kotimaisen kielen hallinnan osoittamiseen.", + "part2": "Tutkinnoilla osoitetaan suomen tai ruotsin kielen suullinen, kirjallinen ja ymmärtämisen taito." + }, + "selectExamination": { + "description": "Valtionhallinnon kielitutkintoja on kahta eri tyyppiä: toisella voit osoittaa erinomaisen taidon hallintaa ja toisella joko hyvän tai tyydyttävän taidon hallintaa.", + "heading": "Valitse tutkintosi" + }, + "title": "Valtionhallinnon kielitutkinnot (VKT)" } } } diff --git a/frontend/packages/vkt/src/styles/pages/_public-homepage.scss b/frontend/packages/vkt/src/styles/pages/_public-homepage.scss index dbd87ebaf..0c908b079 100644 --- a/frontend/packages/vkt/src/styles/pages/_public-homepage.scss +++ b/frontend/packages/vkt/src/styles/pages/_public-homepage.scss @@ -35,4 +35,19 @@ padding: 3rem 2rem; } } + + & &__cards { + @include not-phone { + width: 100%; + } + } + + & &__level-description-card { + background-color: $color-blue-200; + padding: 2rem; + @include not-phone { + width: 50%; + height: 50rem; + } + } } From 5696630a4e2fa00196a654119af446cc7827aa28 Mon Sep 17 00:00:00 2001 From: Pyry Koivisto Date: Tue, 24 Sep 2024 20:06:35 +0300 Subject: [PATCH 031/248] VKT(Frontend): Refactor frontend URL structure [deploy] --- frontend/packages/vkt/src/enums/app.ts | 38 ++++++++++++++++++-------- 1 file changed, 27 insertions(+), 11 deletions(-) diff --git a/frontend/packages/vkt/src/enums/app.ts b/frontend/packages/vkt/src/enums/app.ts index e994a3ee1..892aa8abd 100644 --- a/frontend/packages/vkt/src/enums/app.ts +++ b/frontend/packages/vkt/src/enums/app.ts @@ -2,26 +2,42 @@ export enum AppConstants { CallerID = '1.2.246.562.10.00000000001.vkt', } +const excellentLevelRoutePrefix = '/vkt/erinomainen-taito'; +const excellentLevelEnrollmentRoute = + excellentLevelRoutePrefix + '/ilmoittaudu'; + export enum AppRoutes { PublicRoot = '/vkt', PublicHomePage = '/vkt/etusivu', - PublicEnrollment = '/vkt/ilmoittaudu', - PublicAuth = '/vkt/ilmoittaudu/:examEventId/tunnistaudu', - PublicEnrollmentContactDetails = '/vkt/ilmoittaudu/:examEventId/tiedot', - PublicEnrollmentEducationDetails = '/vkt/ilmoittaudu/:examEventId/koulutus', - PublicEnrollmentSelectExam = '/vkt/ilmoittaudu/:examEventId/tutkinto', - PublicEnrollmentPreview = '/vkt/ilmoittaudu/:examEventId/esikatsele', - PublicEnrollmentPaymentFail = '/vkt/ilmoittaudu/:examEventId/maksu/peruutettu', - PublicEnrollmentPaymentSuccess = '/vkt/ilmoittaudu/:examEventId/maksu/valmis', - PublicEnrollmentDoneQueued = '/vkt/ilmoittaudu/:examEventId/jono-valmis', - PublicEnrollmentDone = '/vkt/ilmoittaudu/:examEventId/valmis', + // Routes for excellent level + PublicExcellentLevelLanding = excellentLevelRoutePrefix, + PublicEnrollment = excellentLevelEnrollmentRoute, + PublicAuth = excellentLevelEnrollmentRoute + '/:examEventId/tunnistaudu', + PublicEnrollmentContactDetails = excellentLevelEnrollmentRoute + + '/:examEventId/tiedot', + PublicEnrollmentEducationDetails = excellentLevelEnrollmentRoute + + '/:examEventId/koulutus', + PublicEnrollmentSelectExam = excellentLevelEnrollmentRoute + + '/:examEventId/tutkinto', + PublicEnrollmentPreview = excellentLevelEnrollmentRoute + + '/:examEventId/esikatsele', + PublicEnrollmentPaymentFail = excellentLevelEnrollmentRoute + + '/:examEventId/maksu/peruutettu', + PublicEnrollmentPaymentSuccess = excellentLevelEnrollmentRoute + + '/:examEventId/maksu/valmis', + PublicEnrollmentDoneQueued = excellentLevelEnrollmentRoute + + '/:examEventId/jono-valmis', + PublicEnrollmentDone = excellentLevelEnrollmentRoute + '/:examEventId/valmis', + // Routes for good and satisfactory level - TODO + PublicGoodAndSatisfactoryLevelLanding = '/vkt/hyva-ja-tyydyttava-taito', + // Routes for clerk user ClerkHomePage = '/vkt/virkailija', ClerkExamEventCreatePage = '/vkt/virkailija/tutkintotilaisuus/luo', ClerkExamEventOverviewPage = '/vkt/virkailija/tutkintotilaisuus/:examEventId', ClerkEnrollmentOverviewPage = '/vkt/virkailija/tutkintotilaisuus/:examEventId/ilmoittautuminen', ClerkLocalLogoutPage = '/vkt/cas/localLogout', + // Miscellaneous AccessibilityStatementPage = '/vkt/saavutettavuusseloste', - PrivacyPolicyPage = '/vkt/tietosuojaseloste', LogoutSuccess = '/vkt/uloskirjautuminen-onnistui', NotFoundPage = '*', } From 862fa24e5f1409454bdf6ae4a62deaf8ff2d8b5d Mon Sep 17 00:00:00 2001 From: Pyry Koivisto Date: Wed, 25 Sep 2024 11:51:25 +0300 Subject: [PATCH 032/248] VKT(Frontend): Fix Cypress tests [deploy] --- frontend/packages/vkt/src/tests/cypress/support/commands.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/packages/vkt/src/tests/cypress/support/commands.ts b/frontend/packages/vkt/src/tests/cypress/support/commands.ts index deef88404..e0a75e627 100644 --- a/frontend/packages/vkt/src/tests/cypress/support/commands.ts +++ b/frontend/packages/vkt/src/tests/cypress/support/commands.ts @@ -5,7 +5,7 @@ Cypress.Commands.add('openPublicHomePage', () => { cy.window().then((win) => { win.sessionStorage.setItem('persist:root', '{}'); }); - cy.visit(AppRoutes.PublicHomePage); + cy.visit(AppRoutes.PublicExcellentLevelLanding); }); Cypress.Commands.add( From 5eb3afc0f7aad8b5464a7d877c8def7b098d0c21 Mon Sep 17 00:00:00 2001 From: Pyry Koivisto Date: Wed, 25 Sep 2024 11:57:52 +0300 Subject: [PATCH 033/248] VKT(Frontend): Always show navigation links [deploy] --- .../vkt/src/components/layouts/Header.tsx | 6 +---- .../__snapshots__/Header.test.tsx.snap | 23 ++++++++++++++++++- 2 files changed, 23 insertions(+), 6 deletions(-) diff --git a/frontend/packages/vkt/src/components/layouts/Header.tsx b/frontend/packages/vkt/src/components/layouts/Header.tsx index 520db4e88..4857acaf4 100644 --- a/frontend/packages/vkt/src/components/layouts/Header.tsx +++ b/frontend/packages/vkt/src/components/layouts/Header.tsx @@ -90,8 +90,6 @@ export const Header = (): JSX.Element => { const { isAuthenticated, isClerkUI, clerkUser, publicUser } = useAuthentication(); - const goodAndSatisfactoryLevelSupported = - useAppSelector(featureFlagsSelector).goodAndSatisfactoryLevel; const logoRedirectURL = isAuthenticated ? AppRoutes.ClerkHomePage : AppRoutes.PublicHomePage; @@ -164,9 +162,7 @@ export const Header = (): JSX.Element => {
{isAuthenticated && } - {isPublicUrl && goodAndSatisfactoryLevelSupported && ( - - )} + {isPublicUrl && }
{isAuthenticated && } diff --git a/frontend/packages/vkt/src/tests/jest/components/layouts/__snapshots__/Header.test.tsx.snap b/frontend/packages/vkt/src/tests/jest/components/layouts/__snapshots__/Header.test.tsx.snap index 5f449f0b4..728cf84ef 100644 --- a/frontend/packages/vkt/src/tests/jest/components/layouts/__snapshots__/Header.test.tsx.snap +++ b/frontend/packages/vkt/src/tests/jest/components/layouts/__snapshots__/Header.test.tsx.snap @@ -45,7 +45,28 @@ exports[`Header should render Header correctly 1`] = `
From 6fac8ee2335018269a5b2508051d509e5ab0c8b2 Mon Sep 17 00:00:00 2001 From: Pyry Koivisto Date: Wed, 25 Sep 2024 12:12:37 +0300 Subject: [PATCH 034/248] VKT(Frontend): Fix msw test setup [deploy] --- frontend/packages/vkt/src/tests/msw/handlers.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/frontend/packages/vkt/src/tests/msw/handlers.ts b/frontend/packages/vkt/src/tests/msw/handlers.ts index a04a7d71c..88dec2843 100644 --- a/frontend/packages/vkt/src/tests/msw/handlers.ts +++ b/frontend/packages/vkt/src/tests/msw/handlers.ts @@ -16,6 +16,7 @@ import { publicEnrollmentInitialisationWithFreeEnrollments, } from 'tests/msw/fixtures/publicEnrollmentInitialisation'; import { publicExamEvents11 } from 'tests/msw/fixtures/publicExamEvents11'; +import { AppRoutes } from 'enums/app'; export const handlers = [ http.get(APIEndpoints.ClerkUser, ({ cookies }) => { @@ -37,7 +38,10 @@ export const handlers = [ http.get(APIEndpoints.PublicEducation, ({ request }) => { if ( request.referrer.endsWith( - `/vkt/ilmoittaudu/${examEventIdWithKoskiEducationDetailsFound}/koulutus`, + AppRoutes.PublicEnrollmentEducationDetails.replace( + /:examEventId/, + `${examEventIdWithKoskiEducationDetailsFound}`, + ), ) ) { return new Response( From 74b25ed2cc851005e278376cc39e45bd18917d62 Mon Sep 17 00:00:00 2001 From: Pyry Koivisto Date: Thu, 26 Sep 2024 14:23:44 +0300 Subject: [PATCH 035/248] YKI(Frontend): Remove leftover styling rule --- .../packages/yki/src/styles/components/layouts/_header.scss | 4 ---- 1 file changed, 4 deletions(-) diff --git a/frontend/packages/yki/src/styles/components/layouts/_header.scss b/frontend/packages/yki/src/styles/components/layouts/_header.scss index 41bd2b369..1b956471f 100644 --- a/frontend/packages/yki/src/styles/components/layouts/_header.scss +++ b/frontend/packages/yki/src/styles/components/layouts/_header.scss @@ -52,10 +52,6 @@ } align-self: flex-end; justify-self: start; - - [role='tablist'] { - gap: 3rem; - } } &__language-select { From e1a3fd78470b87d8a530e3a27c59c68eb4a9b7aa Mon Sep 17 00:00:00 2001 From: Pyry Koivisto Date: Thu, 26 Sep 2024 14:23:02 +0300 Subject: [PATCH 036/248] VKT(Frontend): Use grid layout on desktop in header to improve layout behaviour with reduced screen width [deploy] --- .../vkt/src/components/layouts/Header.tsx | 8 ++-- .../styles/components/layouts/_header.scss | 47 ++++++++++++------- 2 files changed, 34 insertions(+), 21 deletions(-) diff --git a/frontend/packages/vkt/src/components/layouts/Header.tsx b/frontend/packages/vkt/src/components/layouts/Header.tsx index 4857acaf4..457e1f03b 100644 --- a/frontend/packages/vkt/src/components/layouts/Header.tsx +++ b/frontend/packages/vkt/src/components/layouts/Header.tsx @@ -141,7 +141,7 @@ export const Header = (): JSX.Element => { /> )} -
+
{isClerkUI ? ( { /> ) : ( { )}
-
+
{isAuthenticated && } {isPublicUrl && }
-
+
{isAuthenticated && } {!isPhone && ( Date: Fri, 27 Sep 2024 14:28:54 +0300 Subject: [PATCH 037/248] SHARED(Frontend): Introduce components for accessible mobile navigation menu --- frontend/package.json | 1 + frontend/packages/shared/CHANGELOG.MD | 5 + .../MobileNavigationMenu.scss | 30 +++++ .../MobileNavigationMenu.tsx | 123 ++++++++++++++++++ .../NavigationLinks/NavigationLinks.scss | 1 + .../NavigationLinks/NavigationLinks.tsx | 2 +- .../packages/shared/src/components/index.tsx | 4 + frontend/yarn.lock | 83 +++++++++++- 8 files changed, 247 insertions(+), 2 deletions(-) create mode 100644 frontend/packages/shared/src/components/MobileNavigationMenu/MobileNavigationMenu.scss create mode 100644 frontend/packages/shared/src/components/MobileNavigationMenu/MobileNavigationMenu.tsx diff --git a/frontend/package.json b/frontend/package.json index ff9c4f6f1..e341be630 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -28,6 +28,7 @@ "@emotion/react": "^11.13.0", "@emotion/styled": "^11.13.0", "@fontsource/roboto": "^5.0.14", + "@mui/base": "5.0.0-beta.58", "@mui/icons-material": "^5.16.7", "@mui/material": "^5.16.7", "@mui/system": "^5.16.7", diff --git a/frontend/packages/shared/CHANGELOG.MD b/frontend/packages/shared/CHANGELOG.MD index db4d7c429..0e41f31d7 100644 --- a/frontend/packages/shared/CHANGELOG.MD +++ b/frontend/packages/shared/CHANGELOG.MD @@ -9,6 +9,11 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ## [Released] +## [1.11.5] - 2024-09-27 + +### Added +- MobileNavigationMenuToggle and MobileNavigationMenuContents components + ## [1.11.4] - 2024-10-07 ### Fixed diff --git a/frontend/packages/shared/src/components/MobileNavigationMenu/MobileNavigationMenu.scss b/frontend/packages/shared/src/components/MobileNavigationMenu/MobileNavigationMenu.scss new file mode 100644 index 000000000..2ea3cd75d --- /dev/null +++ b/frontend/packages/shared/src/components/MobileNavigationMenu/MobileNavigationMenu.scss @@ -0,0 +1,30 @@ +@use '../../styles/abstracts/colors'; + +button.navigation-menu-toggle { + background-color: colors.$color-primary; + border: 0; +} + +.navigation-menu-contents { + ul { + list-style-type: none; + display: flex; + flex-direction: column; + padding: 1rem; + } + + li { + > a { + text-decoration: none; + } + padding: 1rem; + } + + li.active { + p { + color: colors.$color-secondary; + font-weight: 700; + } + border-left: 2px solid colors.$color-secondary; + } +} diff --git a/frontend/packages/shared/src/components/MobileNavigationMenu/MobileNavigationMenu.tsx b/frontend/packages/shared/src/components/MobileNavigationMenu/MobileNavigationMenu.tsx new file mode 100644 index 000000000..90eba8d73 --- /dev/null +++ b/frontend/packages/shared/src/components/MobileNavigationMenu/MobileNavigationMenu.tsx @@ -0,0 +1,123 @@ +import CloseIcon from '@mui/icons-material/Close'; +import MenuIcon from '@mui/icons-material/Menu'; +import { ClickAwayListener, Divider } from '@mui/material'; +import { FocusTrap } from '@mui/base/FocusTrap'; +import { Fragment } from 'react'; +import { Link } from 'react-router-dom'; + +import { Color } from '../../enums'; +import { NavigationLinksProps } from '../NavigationLinks/NavigationLinks'; +import { Text } from '../Text/Text'; + +import './MobileNavigationMenu.scss'; + +export const MobileNavigationMenuToggle = ({ + openStateLabel, + openStateAriaLabel, + closedStateLabel, + closedStateAriaLabel, + isOpen, + setIsOpen, +}: { + openStateLabel: string; + openStateAriaLabel: string; + closedStateLabel: string; + closedStateAriaLabel: string; + isOpen: boolean; + setIsOpen: (state: boolean) => void; +}) => { + const handleClick = () => { + setIsOpen(!isOpen); + }; + + return ( + + ); +}; + +interface MobileNavigationMenuProps extends NavigationLinksProps { + closeMenu: () => void; +} + +export const MobileNavigationMenuContents = ({ + navigationAriaLabel, + links, + closeMenu, +}: MobileNavigationMenuProps) => { + // TODO + // - positioning of menu + // - border-shadow to bottom of element.. perhaps use a wrapper like Paper or something + + const handleClickAway = (e: MouseEvent | TouchEvent) => { + // Prevent event default so that when user clicks on menu close button (outside actual menu contents), + // the menu isn't immediately opened again. + e.preventDefault(); + closeMenu(); + }; + + const handleEsc = (e: React.KeyboardEvent) => { + if (e.key === 'Escape') { + closeMenu(); + } + }; + + return ( + +
+ +
+
+ ); +}; diff --git a/frontend/packages/shared/src/components/NavigationLinks/NavigationLinks.scss b/frontend/packages/shared/src/components/NavigationLinks/NavigationLinks.scss index 688f40358..905ba60f0 100644 --- a/frontend/packages/shared/src/components/NavigationLinks/NavigationLinks.scss +++ b/frontend/packages/shared/src/components/NavigationLinks/NavigationLinks.scss @@ -11,6 +11,7 @@ li { padding-bottom: 2rem; + > a { text-decoration: none; } diff --git a/frontend/packages/shared/src/components/NavigationLinks/NavigationLinks.tsx b/frontend/packages/shared/src/components/NavigationLinks/NavigationLinks.tsx index 546a4c237..2a0a21aa6 100644 --- a/frontend/packages/shared/src/components/NavigationLinks/NavigationLinks.tsx +++ b/frontend/packages/shared/src/components/NavigationLinks/NavigationLinks.tsx @@ -3,7 +3,7 @@ import { Link } from 'react-router-dom'; import { Text } from '../Text/Text'; import './NavigationLinks.scss'; -interface NavigationLinksProps { +export interface NavigationLinksProps { navigationAriaLabel: string; links: Array; } diff --git a/frontend/packages/shared/src/components/index.tsx b/frontend/packages/shared/src/components/index.tsx index ccf99f818..59944d71f 100644 --- a/frontend/packages/shared/src/components/index.tsx +++ b/frontend/packages/shared/src/components/index.tsx @@ -53,3 +53,7 @@ export { NativeSelectWithLabel, } from './NativeSelect/NativeSelect'; export { NavigationLinks } from './NavigationLinks/NavigationLinks'; +export { + MobileNavigationMenuToggle, + MobileNavigationMenuContents, +} from './MobileNavigationMenu/MobileNavigationMenu'; diff --git a/frontend/yarn.lock b/frontend/yarn.lock index 844a539c0..4fca88224 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -1433,7 +1433,7 @@ __metadata: languageName: node linkType: hard -"@babel/runtime@npm:^7.23.9": +"@babel/runtime@npm:^7.23.9, @babel/runtime@npm:^7.25.0": version: 7.25.6 resolution: "@babel/runtime@npm:7.25.6" dependencies: @@ -1909,6 +1909,44 @@ __metadata: languageName: node linkType: hard +"@floating-ui/core@npm:^1.6.0": + version: 1.6.8 + resolution: "@floating-ui/core@npm:1.6.8" + dependencies: + "@floating-ui/utils": "npm:^0.2.8" + checksum: 87d52989c3d2cc80373bc153b7a40814db3206ce7d0b2a2bdfb63e2ff39ffb8b999b1b0ccf28e548000ebf863bf16e2bed45eab4c4d287a5dbe974ef22368d82 + languageName: node + linkType: hard + +"@floating-ui/dom@npm:^1.0.0": + version: 1.6.11 + resolution: "@floating-ui/dom@npm:1.6.11" + dependencies: + "@floating-ui/core": "npm:^1.6.0" + "@floating-ui/utils": "npm:^0.2.8" + checksum: 8579392ad10151474869e7640af169b0d7fc2df48d4da27b6dcb1a57202329147ed986b2972787d4b8cd550c87897271b2d9c4633c2ec7d0b3ad37ce1da636f1 + languageName: node + linkType: hard + +"@floating-ui/react-dom@npm:^2.1.1": + version: 2.1.2 + resolution: "@floating-ui/react-dom@npm:2.1.2" + dependencies: + "@floating-ui/dom": "npm:^1.0.0" + peerDependencies: + react: ">=16.8.0" + react-dom: ">=16.8.0" + checksum: 2a67dc8499674e42ff32c7246bded185bb0fdd492150067caf9568569557ac4756a67787421d8604b0f241e5337de10762aee270d9aeef106d078a0ff13596c4 + languageName: node + linkType: hard + +"@floating-ui/utils@npm:^0.2.8": + version: 0.2.8 + resolution: "@floating-ui/utils@npm:0.2.8" + checksum: 3e3ea3b2de06badc4baebdf358b3dbd77ccd9474a257a6ef237277895943db2acbae756477ec64de65a2a1436d94aea3107129a1feeef6370675bf2b161c1abc + languageName: node + linkType: hard + "@fontsource/roboto@npm:^5.0.14": version: 5.0.15 resolution: "@fontsource/roboto@npm:5.0.15" @@ -2295,6 +2333,28 @@ __metadata: languageName: node linkType: hard +"@mui/base@npm:5.0.0-beta.58": + version: 5.0.0-beta.58 + resolution: "@mui/base@npm:5.0.0-beta.58" + dependencies: + "@babel/runtime": "npm:^7.25.0" + "@floating-ui/react-dom": "npm:^2.1.1" + "@mui/types": "npm:^7.2.15" + "@mui/utils": "npm:6.0.0-rc.0" + "@popperjs/core": "npm:^2.11.8" + clsx: "npm:^2.1.1" + prop-types: "npm:^15.8.1" + peerDependencies: + "@types/react": ^17.0.0 || ^18.0.0 + react: ^17.0.0 || ^18.0.0 + react-dom: ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 151842a4b3421cb40dbcc88d22200ae3112174bcc160211baa3ac6f932e58eb13627c339a6bc457e3ccbcb279bf3605203d75b4e2cf0e4c195df59583ebbc4a6 + languageName: node + linkType: hard + "@mui/core-downloads-tracker@npm:^5.16.7": version: 5.16.7 resolution: "@mui/core-downloads-tracker@npm:5.16.7" @@ -2429,6 +2489,26 @@ __metadata: languageName: node linkType: hard +"@mui/utils@npm:6.0.0-rc.0": + version: 6.0.0-rc.0 + resolution: "@mui/utils@npm:6.0.0-rc.0" + dependencies: + "@babel/runtime": "npm:^7.25.0" + "@mui/types": "npm:^7.2.15" + "@types/prop-types": "npm:^15.7.12" + clsx: "npm:^2.1.1" + prop-types: "npm:^15.8.1" + react-is: "npm:^18.3.1" + peerDependencies: + "@types/react": ^17.0.0 || ^18.0.0 || ^19.0.0 + react: ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 9254808d615b0d2f20fdc866f4e90587d7c650ddfb2a989ec6aa553ce0d774dce296d246896bb608ef51e9b9192f83bb6bfd9e02353d9e5fe4cbeaec9b8a702a + languageName: node + linkType: hard + "@mui/utils@npm:^5.10.3": version: 5.15.3 resolution: "@mui/utils@npm:5.15.3" @@ -2614,6 +2694,7 @@ __metadata: "@emotion/react": "npm:^11.13.0" "@emotion/styled": "npm:^11.13.0" "@fontsource/roboto": "npm:^5.0.14" + "@mui/base": "npm:5.0.0-beta.58" "@mui/icons-material": "npm:^5.16.7" "@mui/material": "npm:^5.16.7" "@mui/system": "npm:^5.16.7" From edca07f93a1cc5fd2c3784e526b87bfc100a0900 Mon Sep 17 00:00:00 2001 From: Pyry Koivisto Date: Fri, 27 Sep 2024 14:48:40 +0300 Subject: [PATCH 038/248] VKT(Frontend): Use hamburger menu on mobile for navigation --- .../vkt/src/components/layouts/Header.tsx | 84 +++++++++++++++++-- .../styles/components/layouts/_header.scss | 12 ++- 2 files changed, 85 insertions(+), 11 deletions(-) diff --git a/frontend/packages/vkt/src/components/layouts/Header.tsx b/frontend/packages/vkt/src/components/layouts/Header.tsx index 457e1f03b..104fc2b6b 100644 --- a/frontend/packages/vkt/src/components/layouts/Header.tsx +++ b/frontend/packages/vkt/src/components/layouts/Header.tsx @@ -1,15 +1,24 @@ import { AppBar, Toolbar } from '@mui/material'; +import { TFunction } from 'i18next'; +import { useState } from 'react'; import { Link, matchPath, useLocation } from 'react-router-dom'; import { CookieBanner, LangSelector, + MobileNavigationMenuContents, + MobileNavigationMenuToggle, NavigationLinks, OPHClerkLogo, OPHLogoViewer, SkipLink, Text, } from 'shared/components'; -import { APIResponseStatus, AppLanguage, Direction } from 'shared/enums'; +import { + APIResponseStatus, + AppLanguage, + Direction, + I18nNamespace, +} from 'shared/enums'; import { useWindowProperties } from 'shared/hooks'; import { ClerkHeaderButtons } from 'components/layouts/clerkHeader/ClerkHeaderButtons'; @@ -33,10 +42,11 @@ import { featureFlagsSelector } from 'redux/selectors/featureFlags'; const isPathActive = (currentPath: string, route: AppRoutes) => !!matchPath({ path: route, end: false }, currentPath); -const PublicNavigationLinks = () => { - const translateCommon = useCommonTranslation(); - const { pathname } = useLocation(); - const { goodAndSatisfactoryLevel } = useAppSelector(featureFlagsSelector); +const getNavigationLinks = ( + pathname: string, + goodAndSatisfactoryLevel: boolean, + translateCommon: TFunction, +) => { const excellentLevelLink = { active: isPathActive(pathname, AppRoutes.PublicExcellentLevelLanding), label: translateCommon( @@ -68,6 +78,20 @@ const PublicNavigationLinks = () => { ] : [excellentLevelLink]; + return navigationLinks; +}; + +const PublicNavigationLinks = () => { + const translateCommon = useCommonTranslation(); + const { pathname } = useLocation(); + const { goodAndSatisfactoryLevel } = useAppSelector(featureFlagsSelector); + + const navigationLinks = getNavigationLinks( + pathname, + !!goodAndSatisfactoryLevel, + translateCommon, + ); + return ( { ); }; +const PublicMobileNavigationMenu = ({ + closeMenu, +}: { + closeMenu: () => void; +}) => { + const translateCommon = useCommonTranslation(); + const { pathname } = useLocation(); + const { goodAndSatisfactoryLevel } = useAppSelector(featureFlagsSelector); + + const navigationLinks = getNavigationLinks( + pathname, + !!goodAndSatisfactoryLevel, + translateCommon, + ); + + return ( + + ); +}; + export const Header = (): JSX.Element => { const dispatch = useAppDispatch(); const translateCommon = useCommonTranslation(); @@ -109,6 +159,7 @@ export const Header = (): JSX.Element => { } } }; + const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false); useInterval(heartBeat, 5000); // Every 5 seconds @@ -155,14 +206,28 @@ export const Header = (): JSX.Element => { direction={Direction.Horizontal} alt={translateCommon('ophLogoToFrontPageAlt')} currentLang={getCurrentLang()} - title={translateCommon('appNameAbbreviation')} + title={ + !isPhone + ? translateCommon('appNameAbbreviation') + : undefined + } /> )}
{isAuthenticated && } - {isPublicUrl && } + {isPublicUrl && !isPhone && } + {isPublicUrl && isPhone && ( + + )}
{isAuthenticated && } @@ -178,6 +243,11 @@ export const Header = (): JSX.Element => { )}
+ {isPhone && isMobileMenuOpen && ( + setIsMobileMenuOpen(false)} + /> + )} {!isClerkUI && ( a { @@ -49,9 +50,12 @@ &__navigation { @include not-phone { grid-area: navigation; + align-self: flex-end; + justify-self: start; + } + @include phone { + margin-left: auto; } - align-self: flex-end; - justify-self: start; } &__language-select { From ce17f1cca1888e15b651697f815207055f779383 Mon Sep 17 00:00:00 2001 From: Pyry Koivisto Date: Fri, 27 Sep 2024 14:59:45 +0300 Subject: [PATCH 039/248] VKT(Frontend): Fix Jest snapshots [deploy] --- .../components/layouts/__snapshots__/Header.test.tsx.snap | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/frontend/packages/vkt/src/tests/jest/components/layouts/__snapshots__/Header.test.tsx.snap b/frontend/packages/vkt/src/tests/jest/components/layouts/__snapshots__/Header.test.tsx.snap index 728cf84ef..61f87ac94 100644 --- a/frontend/packages/vkt/src/tests/jest/components/layouts/__snapshots__/Header.test.tsx.snap +++ b/frontend/packages/vkt/src/tests/jest/components/layouts/__snapshots__/Header.test.tsx.snap @@ -21,7 +21,7 @@ exports[`Header should render Header correctly 1`] = ` className="MuiToolbar-root MuiToolbar-gutters MuiToolbar-regular header__toolbar css-hyum1k-MuiToolbar-root" >