From 52622d9a62063025c8abea25317ed264bc11786a Mon Sep 17 00:00:00 2001 From: Pyry Koivisto Date: Wed, 13 Nov 2024 12:55:08 +0200 Subject: [PATCH] VKT(Frontend): Examiner exam event page fine tuning, connect to redux state --- .../vkt/public/i18n/fi-FI/examiner.json | 2 +- .../vkt/src/interfaces/examinerExamEvent.ts | 13 ++ .../examiner/ExaminerExamEventCreatePage.tsx | 175 +++++++++++++----- .../redux/reducers/examinerExamEventUpsert.ts | 49 +++++ .../selectors/examinerExamEventUpsert.ts | 7 + .../packages/vkt/src/redux/store/index.ts | 2 + .../pages/_examiner-exam-event-page.scss | 7 +- 7 files changed, 211 insertions(+), 44 deletions(-) create mode 100644 frontend/packages/vkt/src/redux/reducers/examinerExamEventUpsert.ts create mode 100644 frontend/packages/vkt/src/redux/selectors/examinerExamEventUpsert.ts diff --git a/frontend/packages/vkt/public/i18n/fi-FI/examiner.json b/frontend/packages/vkt/public/i18n/fi-FI/examiner.json index 5006342da..b8f565cf5 100644 --- a/frontend/packages/vkt/public/i18n/fi-FI/examiner.json +++ b/frontend/packages/vkt/public/i18n/fi-FI/examiner.json @@ -33,7 +33,7 @@ }, "examinerExamEventCreate": { "description": { - "part1": "Lisää tutkintotilaisuus tästä. Voit luoda joko yksityisen tutkintotilaisuuden tai VKT:n ilmoittautumissivulla jullkisesti näkyvän tilaisuuden.", + "part1": "Lisää tutkintotilaisuus tästä. Voit luoda joko yksityisen tutkintotilaisuuden tai VKT:n ilmoittautumissivulla julkisesti näkyvän tilaisuuden.", "part2": "Voit vaihtaa tutkintotilaisuuden julkisuusasetuksia myöhemmin." }, "heading": "Tutkintotilaisuuden lisääminen", diff --git a/frontend/packages/vkt/src/interfaces/examinerExamEvent.ts b/frontend/packages/vkt/src/interfaces/examinerExamEvent.ts index 90ea1183a..16ce2ab34 100644 --- a/frontend/packages/vkt/src/interfaces/examinerExamEvent.ts +++ b/frontend/packages/vkt/src/interfaces/examinerExamEvent.ts @@ -7,6 +7,7 @@ import { ClerkEnrollmentAppointmentResponse, } from 'interfaces/clerkEnrollment'; import { MunicipalityCode } from 'interfaces/municipality'; +import { APIResponseStatus } from 'shared/enums'; export interface ExaminerExamEventResponse extends Omit< @@ -28,3 +29,15 @@ export interface ExaminerExamEvent extends WithId, WithVersion { registrationCloses?: Dayjs; enrollments: Array; } + +export interface ExaminerExamEventUpsert extends Omit { + id?: number; + examTime?: string; + addressDetails?: string; + otherDetails?: string; +} + +export interface ExaminerExamEventUpsertState { + status: APIResponseStatus; + examEvent: Partial; +} \ No newline at end of file diff --git a/frontend/packages/vkt/src/pages/examiner/ExaminerExamEventCreatePage.tsx b/frontend/packages/vkt/src/pages/examiner/ExaminerExamEventCreatePage.tsx index 40e91817b..bd409f3ee 100644 --- a/frontend/packages/vkt/src/pages/examiner/ExaminerExamEventCreatePage.tsx +++ b/frontend/packages/vkt/src/pages/examiner/ExaminerExamEventCreatePage.tsx @@ -11,7 +11,7 @@ import { RadioGroup, Typography, } from '@mui/material'; -import dayjs, { Dayjs } from 'dayjs'; +import dayjs from 'dayjs'; import { FC, useEffect, useState } from 'react'; import { useNavigate } from 'react-router'; import { @@ -32,13 +32,14 @@ import { APIResponseStatus, Color, CustomTextFieldErrors, + InputAutoComplete, Severity, TextFieldTypes, TextFieldVariant, Variant, } from 'shared/enums'; import { useToast } from 'shared/hooks'; -import { ComboBoxOption } from 'shared/interfaces'; +import { DateUtils } from 'shared/utils'; import { useCommonTranslation, @@ -49,8 +50,13 @@ import { useAppDispatch, useAppSelector } from 'configs/redux'; import { AppRoutes, ExamLanguage } from 'enums/app'; import { resetClerkNewExamDate } from 'redux/reducers/clerkNewExamDate'; import { loadExaminerDetails } from 'redux/reducers/examinerDetails'; +import { + resetExaminerExamEventUpsert, + updateExaminerExamEventUpsert, +} from 'redux/reducers/examinerExamEventUpsert'; import { clerkNewExamDateSelector } from 'redux/selectors/clerkNewExamDate'; import { examinerDetailsSelector } from 'redux/selectors/examinerDetails'; +import { examinerExamEventUpsertSelector } from 'redux/selectors/examinerExamEventUpsert'; import { ExamCreateEventUtils } from 'utils/examCreateEvent'; import { municipalityToOption } from 'utils/municipality'; @@ -94,7 +100,10 @@ const SelectIsPublic = () => { keyPrefix: 'vkt.component.examinerExamEventCreate', }); const translateCommon = useCommonTranslation(); - const [isPublic, setIsPublic] = useState(false); + const { isHidden } = useAppSelector( + examinerExamEventUpsertSelector, + ).examEvent; + const dispatch = useAppDispatch(); return (
@@ -108,9 +117,9 @@ const SelectIsPublic = () => { { - setIsPublic(checked); + dispatch(updateExaminerExamEventUpsert({ isHidden: !checked })); }} />
@@ -124,20 +133,30 @@ const SelectLanguage = ({ showErrors }: { showErrors: boolean }) => { keyPrefix: 'vkt.component.examinerExamEventCreate', }); const translateCommon = useCommonTranslation(); - const [examLanguage, setExamLanguage] = useState(''); - const hasRadioButtonError = showErrors && examLanguage === ''; + const { language } = useAppSelector( + examinerExamEventUpsertSelector, + ).examEvent; + const dispatch = useAppDispatch(); + + const hasRadioButtonError = showErrors && !language; return (
- - {t('labels.examLanguage')} + + + {t('labels.examLanguage')} + { - setExamLanguage(v as ExamLanguage); + dispatch( + updateExaminerExamEventUpsert({ + language: v as Exclude, + }), + ); }} >
@@ -147,7 +166,7 @@ const SelectLanguage = ({ showErrors }: { showErrors: boolean }) => { } label={translateCommon(`examLanguage.${ExamLanguage.FI}`)} - checked={examLanguage === ExamLanguage.FI} + checked={language === ExamLanguage.FI} className={`margin-left-sm ${ hasRadioButtonError && 'checkbox-error' }`} @@ -158,7 +177,7 @@ const SelectLanguage = ({ showErrors }: { showErrors: boolean }) => { } label={translateCommon(`examLanguage.${ExamLanguage.SV}`)} - checked={examLanguage === ExamLanguage.SV} + checked={language === ExamLanguage.SV} className={`margin-left-sm ${ hasRadioButtonError && 'checkbox-error' }`} @@ -185,7 +204,10 @@ const SelectMunicipality = ({ showErrors }: { showErrors: boolean }) => { const translateCommon = useCommonTranslation(); const translateMunicipality = useKoodistoMunicipalitiesTranslation(); const { examiner } = useAppSelector(examinerDetailsSelector); - const [municipality, setMunicipality] = useState(null); + const { municipality } = useAppSelector( + examinerExamEventUpsertSelector, + ).examEvent; + const dispatch = useAppDispatch(); if (!examiner) { return null; } @@ -198,15 +220,21 @@ const SelectMunicipality = ({ showErrors }: { showErrors: boolean }) => { helperText={translateCommon(CustomTextFieldErrors.Required)} showError={showErrors && !municipality} variant={TextFieldVariant.Outlined} - value={municipality} + value={ + municipality + ? municipalityToOption(municipality, translateMunicipality) + : null + } values={sortOptionsByLabels( examiner.municipalities.map((v) => municipalityToOption(v, translateMunicipality), ), )} onChange={(v) => { - setMunicipality( - v ? municipalityToOption({ code: v }, translateMunicipality) : null, + dispatch( + updateExaminerExamEventUpsert({ + municipality: v ? { code: v } : undefined, + }), ); }} /> @@ -219,24 +247,31 @@ const SelectDate = ({ showErrors }: { showErrors: boolean }) => { keyPrefix: 'vkt.component.examinerExamEventCreate', }); const translateCommon = useCommonTranslation(); - const [examDate, setExamDate] = useState(null); + const { date } = useAppSelector(examinerExamEventUpsertSelector).examEvent; + const dispatch = useAppDispatch(); + const error = showErrors && !date; return ( -
+
{t('labels.examDate')} { + dispatch(updateExaminerExamEventUpsert({ date: v || undefined })); + }} label={translateCommon('choose')} - value={examDate} + value={date || null} + showHelperText={error} + helperText={error && translateCommon(CustomTextFieldErrors.Required)} />
); @@ -246,12 +281,26 @@ const ExamTime = () => { const { t } = useExaminerTranslation({ keyPrefix: 'vkt.component.examinerExamEventCreate', }); + const { examTime } = useAppSelector( + examinerExamEventUpsertSelector, + ).examEvent; + const dispatch = useAppDispatch(); return ( { + const input = event.target.value; + if (DateUtils.parseTimeString(input)) { + dispatch(updateExaminerExamEventUpsert({ examTime: input })); + } else { + dispatch(updateExaminerExamEventUpsert({ examTime: undefined })); + } + }} /> ); }; @@ -260,12 +309,25 @@ const AddressDetails = () => { const { t } = useExaminerTranslation({ keyPrefix: 'vkt.component.examinerExamEventCreate', }); + const { addressDetails } = useAppSelector( + examinerExamEventUpsertSelector, + ).examEvent; + const dispatch = useAppDispatch(); return ( { + dispatch( + updateExaminerExamEventUpsert({ + addressDetails: event.target.value, + }), + ); + }} /> ); }; @@ -274,12 +336,24 @@ const OtherDetails = () => { const { t } = useExaminerTranslation({ keyPrefix: 'vkt.component.examinerExamEventCreate', }); + const { otherDetails } = useAppSelector( + examinerExamEventUpsertSelector, + ).examEvent; + const dispatch = useAppDispatch(); return ( { + dispatch( + updateExaminerExamEventUpsert({ + otherDetails: event.target.value, + }), + ); + }} /> ); }; @@ -289,9 +363,10 @@ const SelectRegistrationClosingDate = () => { keyPrefix: 'vkt.component.examinerExamEventCreate', }); const translateCommon = useCommonTranslation(); - const [registrationCloses, setRegistrationCloses] = useState( - null, - ); + const { registrationCloses } = useAppSelector( + examinerExamEventUpsertSelector, + ).examEvent; + const dispatch = useAppDispatch(); return (
@@ -305,9 +380,15 @@ const SelectRegistrationClosingDate = () => { { + dispatch( + updateExaminerExamEventUpsert({ + registrationCloses: v || undefined, + }), + ); + }} label={translateCommon('choose')} - value={registrationCloses} + value={registrationCloses || null} />
); @@ -318,17 +399,12 @@ const SelectMaxParticipants = ({ showErrors }: { showErrors: boolean }) => { keyPrefix: 'vkt.component.examinerExamEventCreate', }); const translateCommon = useCommonTranslation(); - const [maxParticipants, setMaxParticipants] = useState( - undefined, - ); - - const getErrorText = (value: number | undefined): string => { - return value === undefined - ? translateCommon('errors.customTextField.required') - : translateCommon('errors.customTextField.numberFormat'); - }; + const { maxParticipants } = useAppSelector( + examinerExamEventUpsertSelector, + ).examEvent; + const dispatch = useAppDispatch(); const maxParticipantsError = ExamCreateEventUtils.maxParticipantsHasError( - showErrors, + showErrors && maxParticipants !== undefined, maxParticipants, ); @@ -337,6 +413,7 @@ const SelectMaxParticipants = ({ showErrors }: { showErrors: boolean }) => { {t('labels.maxParticipants')} @@ -349,12 +426,19 @@ const SelectMaxParticipants = ({ showErrors }: { showErrors: boolean }) => { value={maxParticipants ?? ''} error={maxParticipantsError} showHelperText={maxParticipantsError} - helperText={getErrorText(maxParticipants)} + helperText={ + maxParticipantsError + ? translateCommon('errors.customTextField.numberFormat') + : '' + } variant={TextFieldVariant.Outlined} onChange={(event) => { const value = Number(event.target.value); - setMaxParticipants( - isNaN(value) || event.target.value === '' ? undefined : value, + dispatch( + updateExaminerExamEventUpsert({ + maxParticipants: + isNaN(value) || event.target.value === '' ? undefined : value, + }), ); }} /> @@ -371,6 +455,8 @@ export const ExaminerExamEventCreatePage: FC = () => { const navigate = useNavigate(); const { showToast } = useToast(); + // TODO Support creating and editing exam event details on same page? + // TODO Listen to actual examiner exam event create status const { status, id } = useAppSelector(clerkNewExamDateSelector); useEffect(() => { @@ -398,14 +484,19 @@ export const ExaminerExamEventCreatePage: FC = () => { const isLoading = status === APIResponseStatus.InProgress; const isSavingDisabled = isLoading; + // Reset state on unmount + useEffect(() => { + return () => { + dispatch(resetExaminerExamEventUpsert()); + }; + }, [dispatch]); + const onSave = () => { // eslint-disable-next-line no-console console.log('Tallennetaan...'); setShowErrors(true); }; - // TODO Toggle form error status on submit - return ( >, + ) { + state.examEvent = { ...state.examEvent, ...action.payload }; + }, + }, +}); + +export const { + acceptExaminerExamEventUpsert, + rejectExaminerExamEventUpsert, + resetExaminerExamEventUpsert, + startExaminerExamEventUpsert, + updateExaminerExamEventUpsert, +} = examinerExamEventUpsertSlice.actions; +export const examinerExamEventUpsertReducer = + examinerExamEventUpsertSlice.reducer; diff --git a/frontend/packages/vkt/src/redux/selectors/examinerExamEventUpsert.ts b/frontend/packages/vkt/src/redux/selectors/examinerExamEventUpsert.ts new file mode 100644 index 000000000..f14d19de6 --- /dev/null +++ b/frontend/packages/vkt/src/redux/selectors/examinerExamEventUpsert.ts @@ -0,0 +1,7 @@ +import { RootState } from 'configs/redux'; +import { ExaminerExamEventUpsertState } from 'interfaces/examinerExamEvent'; + +export const examinerExamEventUpsertSelector: ( + state: RootState, +) => ExaminerExamEventUpsertState = (state: RootState) => + state.examinerExamEventUpsert; diff --git a/frontend/packages/vkt/src/redux/store/index.ts b/frontend/packages/vkt/src/redux/store/index.ts index 34905fee6..5f19e3eb5 100644 --- a/frontend/packages/vkt/src/redux/store/index.ts +++ b/frontend/packages/vkt/src/redux/store/index.ts @@ -17,6 +17,7 @@ import { examinerDetailsReducer } from 'redux/reducers/examinerDetails'; import { examinerDetailsInitReducer } from 'redux/reducers/examinerDetailsInit'; import { examinerDetailsUpsertReducer } from 'redux/reducers/examinerDetailsUpsert'; import { examinerExamEventOverviewReducer } from 'redux/reducers/examinerExamEventOverview'; +import { examinerExamEventUpsertReducer } from 'redux/reducers/examinerExamEventUpsert'; import { featureFlagsReducer } from 'redux/reducers/featureFlags'; import { publicEducationReducer } from 'redux/reducers/publicEducation'; import { publicEnrollmentReducer } from 'redux/reducers/publicEnrollment'; @@ -57,6 +58,7 @@ const reducer = combineReducers({ examinerDetailsInit: examinerDetailsInitReducer, examinerDetailsUpsert: examinerDetailsUpsertReducer, examinerExamEventOverview: examinerExamEventOverviewReducer, + examinerExamEventUpsert: examinerExamEventUpsertReducer, clerkListExaminer: clerkListExaminerReducer, }); diff --git a/frontend/packages/vkt/src/styles/pages/_examiner-exam-event-page.scss b/frontend/packages/vkt/src/styles/pages/_examiner-exam-event-page.scss index 6aa5985fb..64ddb82dd 100644 --- a/frontend/packages/vkt/src/styles/pages/_examiner-exam-event-page.scss +++ b/frontend/packages/vkt/src/styles/pages/_examiner-exam-event-page.scss @@ -24,10 +24,15 @@ } } - & &__select-municipality { + & &__select-municipality, + & &__select-exam-date { // stylelint-disable-next-line > div.MuiFormControl-root { gap: 0.5rem; } } + + .error-label { + color: $color-red-500; + } }