From 69eac16243197b3676f4ed9e653433ce6855dc74 Mon Sep 17 00:00:00 2001 From: Pyry Koivisto Date: Fri, 25 Oct 2024 14:52:15 +0300 Subject: [PATCH] SHARED:VKT(Frontend): Examiner pages WIP --- .../src/components/ComboBox/ComboBox.tsx | 68 +++++++ .../packages/shared/src/components/index.tsx | 1 + .../packages/vkt/public/i18n/fi-FI/clerk.json | 12 +- .../vkt/public/i18n/fi-FI/common.json | 5 +- .../clerkEnrollment/overview/MoveModal.tsx | 2 +- .../clerkExamEvent/ClerkExamEventGrid.tsx | 4 +- .../clerkExamEvent/overview/TopControls.tsx | 2 +- .../clerkHeader/ClerkNavigationLinks.tsx | 58 +++--- frontend/packages/vkt/src/enums/api.ts | 4 + frontend/packages/vkt/src/enums/app.ts | 26 ++- .../vkt/src/hooks/useAuthentication.ts | 18 +- .../packages/vkt/src/interfaces/clerkUser.ts | 9 + .../vkt/src/interfaces/examinerDetails.ts | 32 +++ .../vkt/src/interfaces/municipality.ts | 4 + .../vkt/src/interfaces/publicExaminer.ts | 9 +- .../src/pages/ClerkExamEventCreatePage.tsx | 2 +- .../src/pages/ClerkExamEventOverviewPage.tsx | 2 +- ...mePage.tsx => ClerkExcellentLevelPage.tsx} | 2 +- .../ClerkGoodAndSatisfactoryLevelPage.tsx | 65 ++++++ .../src/pages/examiner/ExaminerHomePage.tsx | 185 ++++++++++++++++++ .../pages/examiner/ExaminerRedirectPage.tsx | 27 +++ .../src/pages/examiner/ExaminerRootPage.tsx | 18 ++ .../vkt/src/redux/reducers/clerkUser.ts | 25 ++- .../vkt/src/redux/reducers/examinerDetails.ts | 42 ++++ .../src/redux/reducers/examinerDetailsInit.ts | 38 ++++ .../vkt/src/redux/sagas/examinerDetails.ts | 37 ++++ .../src/redux/sagas/examinerDetailsInit.ts | 28 +++ .../packages/vkt/src/redux/sagas/index.ts | 4 + .../vkt/src/redux/selectors/clerkUser.ts | 4 +- .../src/redux/selectors/examinerDetails.ts | 6 + .../redux/selectors/examinerDetailsInit.ts | 6 + .../packages/vkt/src/redux/store/index.ts | 4 + .../packages/vkt/src/routers/AppRouter.tsx | 61 +++++- .../vkt/src/styles/pages/_clerk-homepage.scss | 7 + .../styles/pages/_examiner-details-page.scss | 32 +++ frontend/packages/vkt/src/styles/styles.scss | 1 + frontend/packages/vkt/webpack.config.js | 8 +- 37 files changed, 774 insertions(+), 84 deletions(-) create mode 100644 frontend/packages/vkt/src/interfaces/examinerDetails.ts create mode 100644 frontend/packages/vkt/src/interfaces/municipality.ts rename frontend/packages/vkt/src/pages/{ClerkHomePage.tsx => ClerkExcellentLevelPage.tsx} (95%) create mode 100644 frontend/packages/vkt/src/pages/ClerkGoodAndSatisfactoryLevelPage.tsx create mode 100644 frontend/packages/vkt/src/pages/examiner/ExaminerHomePage.tsx create mode 100644 frontend/packages/vkt/src/pages/examiner/ExaminerRedirectPage.tsx create mode 100644 frontend/packages/vkt/src/pages/examiner/ExaminerRootPage.tsx create mode 100644 frontend/packages/vkt/src/redux/reducers/examinerDetails.ts create mode 100644 frontend/packages/vkt/src/redux/reducers/examinerDetailsInit.ts create mode 100644 frontend/packages/vkt/src/redux/sagas/examinerDetails.ts create mode 100644 frontend/packages/vkt/src/redux/sagas/examinerDetailsInit.ts create mode 100644 frontend/packages/vkt/src/redux/selectors/examinerDetails.ts create mode 100644 frontend/packages/vkt/src/redux/selectors/examinerDetailsInit.ts create mode 100644 frontend/packages/vkt/src/styles/pages/_examiner-details-page.scss diff --git a/frontend/packages/shared/src/components/ComboBox/ComboBox.tsx b/frontend/packages/shared/src/components/ComboBox/ComboBox.tsx index 053a473d5..a005695bc 100644 --- a/frontend/packages/shared/src/components/ComboBox/ComboBox.tsx +++ b/frontend/packages/shared/src/components/ComboBox/ComboBox.tsx @@ -1,6 +1,9 @@ +import CheckBoxIcon from '@mui/icons-material/CheckBox'; +import CheckBoxOutlineBlankIcon from '@mui/icons-material/CheckBoxOutlineBlank'; import { Autocomplete, AutocompleteProps, + Checkbox, createFilterOptions, FilterOptionsState, FormControl, @@ -167,3 +170,68 @@ export const LabeledComboBox = ({ ); }; + +type AutoCompleteMultipleComboBox = AutocompleteProps< + ComboBoxOption, + true, + false, + false +>; +export const LabeledMultipleCheckboxDropdown = ({ + id, + label, + helperText, + showError, + values, + variant, + value, + onChange, + ...rest +}: Omit & + Omit & { + id: string; + }) => { + const errorStyles = showError ? { color: 'error.main' } : {}; + const icon = ; + const checkedIcon = ; + return ( + + + { + const { key, ...optionProps } = props; + return ( +
  • + + {option?.label} +
  • + ); + }} + renderInput={(params) => ( + + )} + onChange={onChange} + {...rest} + /> + {showError && {helperText}} +
    + ); +}; diff --git a/frontend/packages/shared/src/components/index.tsx b/frontend/packages/shared/src/components/index.tsx index 127b51bc4..7393b039a 100644 --- a/frontend/packages/shared/src/components/index.tsx +++ b/frontend/packages/shared/src/components/index.tsx @@ -6,6 +6,7 @@ export { valueAsOption, ComboBox, LabeledComboBox, + LabeledMultipleCheckboxDropdown, } from './ComboBox/ComboBox'; export type { AutocompleteValue } from './ComboBox/ComboBox'; export { CustomButton } from './CustomButton/CustomButton'; diff --git a/frontend/packages/vkt/public/i18n/fi-FI/clerk.json b/frontend/packages/vkt/public/i18n/fi-FI/clerk.json index bb25e50f8..1094d515a 100644 --- a/frontend/packages/vkt/public/i18n/fi-FI/clerk.json +++ b/frontend/packages/vkt/public/i18n/fi-FI/clerk.json @@ -180,14 +180,18 @@ "header": { "backToOph": "Takaisin Opintopolkuun", "logOut": "Kirjaudu ulos", - "navTabs": { - "examEvents": "Tutkintotilaisuudet" + "navigationLinks": { + "excellentLevel": "Erinomainen taito", + "goodAndSatisfactoryLevel": "Hyvä ja tyydyttävä taito" } } }, "pages": { - "homepage": { - "title": "Valtionhallinnon kielitutkinnot" + "excellentLevel": { + "title": "Erinomaisen taidon kielitutkinnot" + }, + "goodAndSatisfactoryLevel": { + "title": "Hyvän ja tyydyttävän taidon kielitutkinnot" } } } diff --git a/frontend/packages/vkt/public/i18n/fi-FI/common.json b/frontend/packages/vkt/public/i18n/fi-FI/common.json index 5dbd7e469..19981257a 100644 --- a/frontend/packages/vkt/public/i18n/fi-FI/common.json +++ b/frontend/packages/vkt/public/i18n/fi-FI/common.json @@ -151,9 +151,12 @@ "clerkEnrollmentOverview": "Virkailija ilmoittautuminen", "clerkExamEventCreate": "Virkailija lisää tutkintopäivä", "clerkExamOverview": "Virkailija tutkintosivu", - "clerkHomepage": "Virkailija", + "clerkExcellentLevel": "Virkailija - erinomaisen taidon tutkinnot", + "clerkGoodAndSatisfactoryLevel": "Virkailija - hyvän ja tyydyttävän taidon tutkinnot", "contactDetails": "Ilmoittautuminen - täytä yhteystietosi", "educationDetails": "Ilmoittautuminen - koulutustiedot", + "examinerHomePage": "Tutkinnon vastaanottaja - hyvän ja tyydyttävän taidon tutkinnot", + "examinerDetails": "Tutkinnon vastaanottaja - omat tiedot", "excellentLevelLanding": "Erinomaisen taidon tutkinnot", "done": "Ilmoittautuminen - valmis", "frontPage": "Etusivu", diff --git a/frontend/packages/vkt/src/components/clerkEnrollment/overview/MoveModal.tsx b/frontend/packages/vkt/src/components/clerkEnrollment/overview/MoveModal.tsx index 37b22c8e1..2d1f39834 100644 --- a/frontend/packages/vkt/src/components/clerkEnrollment/overview/MoveModal.tsx +++ b/frontend/packages/vkt/src/components/clerkEnrollment/overview/MoveModal.tsx @@ -73,7 +73,7 @@ export const MoveModal: FC = ({ enrollment, onCancel }) => { }); dispatch(resetMoveEnrollment()); dispatch(resetClerkListExamEvent()); - navigate(AppRoutes.ClerkHomePage, { replace: true }); + navigate(AppRoutes.ClerkExcellentLevelPage, { replace: true }); } }, [dispatch, navigate, showToast, t, moveStatus]); diff --git a/frontend/packages/vkt/src/components/clerkExamEvent/ClerkExamEventGrid.tsx b/frontend/packages/vkt/src/components/clerkExamEvent/ClerkExamEventGrid.tsx index f37d68981..69aaba8c0 100644 --- a/frontend/packages/vkt/src/components/clerkExamEvent/ClerkExamEventGrid.tsx +++ b/frontend/packages/vkt/src/components/clerkExamEvent/ClerkExamEventGrid.tsx @@ -13,7 +13,9 @@ import { export const ClerkExamEventGrid = () => { // I18 - const { t } = useClerkTranslation({ keyPrefix: 'vkt.pages.homepage' }); + const { t } = useClerkTranslation({ + keyPrefix: 'vkt.pages.excellentLevel', + }); // Redux const { status } = useAppSelector(clerkListExamEventsSelector); diff --git a/frontend/packages/vkt/src/components/clerkExamEvent/overview/TopControls.tsx b/frontend/packages/vkt/src/components/clerkExamEvent/overview/TopControls.tsx index 779833993..33144b3a0 100644 --- a/frontend/packages/vkt/src/components/clerkExamEvent/overview/TopControls.tsx +++ b/frontend/packages/vkt/src/components/clerkExamEvent/overview/TopControls.tsx @@ -12,7 +12,7 @@ export const TopControls: FC = () => { return (
    } diff --git a/frontend/packages/vkt/src/components/layouts/clerkHeader/ClerkNavigationLinks.tsx b/frontend/packages/vkt/src/components/layouts/clerkHeader/ClerkNavigationLinks.tsx index e7e325155..bbe7d7ba1 100644 --- a/frontend/packages/vkt/src/components/layouts/clerkHeader/ClerkNavigationLinks.tsx +++ b/frontend/packages/vkt/src/components/layouts/clerkHeader/ClerkNavigationLinks.tsx @@ -1,4 +1,4 @@ -//import { useNavigate } from 'react-router-dom'; +import { useLocation } from 'react-router-dom'; import { NavigationLinks } from 'shared/components'; import { useClerkTranslation, useCommonTranslation } from 'configs/i18n'; @@ -6,28 +6,16 @@ import { useAppSelector } from 'configs/redux'; import { AppRoutes } from 'enums/app'; import { clerkUserSelector } from 'redux/selectors/clerkUser'; -/*const getTabForPath = (path: string) => { - if (path === AppRoutes.ClerkHomePage) { - return HeaderNavTab.ExamEvents; - } else { - return false; - } -};*/ +const ExaminerNavigationLinks = () => { + const { oid } = useAppSelector(clerkUserSelector); -const AdminNavigationLinks = () => { const { t } = useClerkTranslation({ keyPrefix: 'vkt.component.header.navigationLinks', }); const translateCommon = useCommonTranslation(); - //const navigate = useNavigate(); - const excellentLevelLink = { - active: false, - href: AppRoutes.ClerkExcellentLevelPage, - label: t('excellentLevel'), - }; const goodAndSatisfactoryLevelLink = { active: true, - href: AppRoutes.ClerkExcellentLevelPage, + href: AppRoutes.ExaminerDetailsPage.replace(/:oid/, oid), label: t('goodAndSatisfactoryLevel'), }; @@ -36,29 +24,27 @@ const AdminNavigationLinks = () => { navigationAriaLabel={translateCommon( 'header.accessibility.mainNavigation', )} - links={[excellentLevelLink, goodAndSatisfactoryLevelLink]} + links={[goodAndSatisfactoryLevelLink]} /> ); }; -export const ClerkNavigationLinks = (): JSX.Element => { +const AdminNavigationLinks = () => { const { t } = useClerkTranslation({ keyPrefix: 'vkt.component.header.navigationLinks', }); const translateCommon = useCommonTranslation(); - //const navigate = useNavigate(); - const { isAdmin, isExaminer } = useAppSelector(clerkUserSelector); + const { pathname } = useLocation(); const excellentLevelLink = { - active: false, + active: pathname.startsWith(AppRoutes.ClerkExcellentLevelPage), href: AppRoutes.ClerkExcellentLevelPage, label: t('excellentLevel'), }; - // TODO Need to return different link for examiner and admin - // For examiner, the link should go to their own details - // For admin, the link should go to examiner listing const goodAndSatisfactoryLevelLink = { - active: true, - href: AppRoutes.ClerkExcellentLevelPage, + active: + pathname.startsWith(AppRoutes.ClerkGoodAndSatisfactoryLevelPage) || + pathname.startsWith(AppRoutes.ExaminerRoot), + href: AppRoutes.ClerkGoodAndSatisfactoryLevelPage, label: t('goodAndSatisfactoryLevel'), }; @@ -67,13 +53,19 @@ export const ClerkNavigationLinks = (): JSX.Element => { navigationAriaLabel={translateCommon( 'header.accessibility.mainNavigation', )} - links={ - isAdmin - ? [excellentLevelLink, goodAndSatisfactoryLevelLink] - : isExaminer - ? [goodAndSatisfactoryLevelLink] - : [] - } + links={[excellentLevelLink, goodAndSatisfactoryLevelLink]} /> ); }; + +export const ClerkNavigationLinks = (): JSX.Element => { + const { isAdmin, isExaminer } = useAppSelector(clerkUserSelector); + + if (isAdmin) { + return ; + } else if (isExaminer) { + return ; + } + + return <>; +}; diff --git a/frontend/packages/vkt/src/enums/api.ts b/frontend/packages/vkt/src/enums/api.ts index c1794057d..ba5098321 100644 --- a/frontend/packages/vkt/src/enums/api.ts +++ b/frontend/packages/vkt/src/enums/api.ts @@ -15,6 +15,9 @@ export enum APIEndpoints { FeatureFlags = '/vkt/api/v1/featureFlags', UploadPostPolicy = '/vkt/api/v1/uploadPostPolicy/:examEventId', ClerkRefreshKoskiEducationDetails = '/vkt/api/v1/clerk/enrollment/:enrollmentId/refreshKoskiEducationDetails', + // TODO Consider using prefix /examiner instead of /tv + ExaminerDetails = '/vkt/api/v1/tv/:oid', + ExaminerDetailsInit = '/vkt/api/v1/tv/:oid/init', } /** @@ -46,4 +49,5 @@ export enum APIError { TicketValidationError = 'ticketValidationError', FileUploadError = 'fileUploadError', userAttachmentsMissing = 'userAttachmentsMissing', + ExaminerNotFound = 'examinerNotFound', } diff --git a/frontend/packages/vkt/src/enums/app.ts b/frontend/packages/vkt/src/enums/app.ts index 892aa8abd..4e4d9a594 100644 --- a/frontend/packages/vkt/src/enums/app.ts +++ b/frontend/packages/vkt/src/enums/app.ts @@ -5,6 +5,7 @@ export enum AppConstants { const excellentLevelRoutePrefix = '/vkt/erinomainen-taito'; const excellentLevelEnrollmentRoute = excellentLevelRoutePrefix + '/ilmoittaudu'; +const clerkExcellentLevelRoutePrefix = '/vkt/virkailija/erinomainen-taito'; export enum AppRoutes { PublicRoot = '/vkt', @@ -30,11 +31,22 @@ export enum AppRoutes { 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', + ClerkRoot = '/vkt/virkailija/', + // Routes for clerk user / excellent level + ClerkExcellentLevelPage = clerkExcellentLevelRoutePrefix, + ClerkExamEventCreatePage = clerkExcellentLevelRoutePrefix + + '/tutkintotilaisuus/luo', + ClerkExamEventOverviewPage = clerkExcellentLevelRoutePrefix + + '/tutkintotilaisuus/:examEventId', + ClerkEnrollmentOverviewPage = clerkExcellentLevelRoutePrefix + + '/tutkintotilaisuus/:examEventId/ilmoittautuminen', + // Routes for clerk user / good and satisfactory level + ClerkGoodAndSatisfactoryLevelPage = '/vkt/virkailija/hyva-ja-tyydyttava-taito', + // Routes for examiner + ExaminerRoot = '/vkt/tv', + ExaminerHomePage = '/vkt/tv/:oid', + ExaminerDetailsPage = '/vkt/tv/:oid/omat-tiedot', + // Other clerk and examiner routes ClerkLocalLogoutPage = '/vkt/cas/localLogout', // Miscellaneous AccessibilityStatementPage = '/vkt/saavutettavuusseloste', @@ -57,10 +69,6 @@ export enum ExamEventToggleFilter { Passed = 'passed', } -export enum HeaderNavTab { - ExamEvents = 'examEvents', -} - export enum UIMode { Edit = 'edit', View = 'view', diff --git a/frontend/packages/vkt/src/hooks/useAuthentication.ts b/frontend/packages/vkt/src/hooks/useAuthentication.ts index 5dd7b506a..b3e484d14 100644 --- a/frontend/packages/vkt/src/hooks/useAuthentication.ts +++ b/frontend/packages/vkt/src/hooks/useAuthentication.ts @@ -14,12 +14,13 @@ export const useAuthentication = () => { const publicUser = useAppSelector(publicUserSelector); const activeURL = window.location.href; - const isClerkURL = activeURL.includes(AppRoutes.ClerkHomePage); - const isPublicURL = !isClerkURL; + const isClerkURL = activeURL.includes(AppRoutes.ClerkRoot); + const isExaminerURL = activeURL.includes(AppRoutes.ExaminerRoot); + const isPublicURL = !isClerkURL && !isExaminerURL; useEffect(() => { if (clerkUser.status === APIResponseStatus.NotStarted) { - if (isClerkURL) { + if (isClerkURL || isExaminerURL) { dispatch(loadClerkUser()); } } @@ -28,11 +29,18 @@ export const useAuthentication = () => { dispatch(loadPublicUser()); } } - }, [clerkUser.status, publicUser.status, isClerkURL, isPublicURL, dispatch]); + }, [ + clerkUser.status, + publicUser.status, + isClerkURL, + isExaminerURL, + isPublicURL, + dispatch, + ]); return { isAuthenticated: clerkUser.isAuthenticated, - isClerkUI: isClerkURL, + isClerkUI: isClerkURL || isExaminerURL, publicUser, clerkUser, }; diff --git a/frontend/packages/vkt/src/interfaces/clerkUser.ts b/frontend/packages/vkt/src/interfaces/clerkUser.ts index e34798c09..eb744e008 100644 --- a/frontend/packages/vkt/src/interfaces/clerkUser.ts +++ b/frontend/packages/vkt/src/interfaces/clerkUser.ts @@ -1,3 +1,12 @@ +import { APIResponseStatus } from "shared/enums"; + export interface ClerkUser { oid: string; + isAdmin: boolean; + isExaminer: boolean; +} + +export interface ClerkUserState extends ClerkUser { + status: APIResponseStatus; + isAuthenticated: boolean; } diff --git a/frontend/packages/vkt/src/interfaces/examinerDetails.ts b/frontend/packages/vkt/src/interfaces/examinerDetails.ts new file mode 100644 index 000000000..b5148e74e --- /dev/null +++ b/frontend/packages/vkt/src/interfaces/examinerDetails.ts @@ -0,0 +1,32 @@ +import { APIResponseStatus } from 'shared/enums'; +import { WithId } from 'shared/interfaces'; +import { Municipality } from 'interfaces/municipality'; + +export interface ExaminerDetailsState { + status: APIResponseStatus; + examiner?: ExaminerDetails; + oid?: string; + initialized?: boolean; +} + +export interface ExaminerDetails extends WithId { + oid: string; + lastName: string; + firstName: string; + email: string; + phoneNumber: string; + examLanguageFinnish: boolean; + examLanguageSwedish: boolean; + municipalities: Array; + isPublic: boolean; +} + +export type ExaminerDetailsInit = Pick< + ExaminerDetails, + 'oid' | 'lastName' | 'firstName' +>; + +export interface ExaminerDetailsInitState { + status: APIResponseStatus; + initData?: ExaminerDetailsInit; +} diff --git a/frontend/packages/vkt/src/interfaces/municipality.ts b/frontend/packages/vkt/src/interfaces/municipality.ts new file mode 100644 index 000000000..2314fc45d --- /dev/null +++ b/frontend/packages/vkt/src/interfaces/municipality.ts @@ -0,0 +1,4 @@ +export interface Municipality { + fi: string; + sv: string; +} diff --git a/frontend/packages/vkt/src/interfaces/publicExaminer.ts b/frontend/packages/vkt/src/interfaces/publicExaminer.ts index 61422f217..6e0c95fec 100644 --- a/frontend/packages/vkt/src/interfaces/publicExaminer.ts +++ b/frontend/packages/vkt/src/interfaces/publicExaminer.ts @@ -3,11 +3,8 @@ import { APIResponseStatus } from 'shared/enums'; import { WithId } from 'shared/interfaces'; import { ExamLanguage } from 'enums/app'; +import { Municipality } from './municipality'; -interface PublicMunicipality { - fi: string; - sv: string; -} interface PublicExaminerExamDate { examDate: Dayjs; @@ -21,7 +18,7 @@ interface PublicExaminerExamDateResponse extends Omit; + municipalities: Array; examDates: Array; } @@ -29,7 +26,7 @@ export interface PublicExaminerResponse extends WithId { lastName: string; firstName: string; languages: Array; - municipalities: Array; + municipalities: Array; examDates: Array; } diff --git a/frontend/packages/vkt/src/pages/ClerkExamEventCreatePage.tsx b/frontend/packages/vkt/src/pages/ClerkExamEventCreatePage.tsx index fabb80d02..662ff9b86 100644 --- a/frontend/packages/vkt/src/pages/ClerkExamEventCreatePage.tsx +++ b/frontend/packages/vkt/src/pages/ClerkExamEventCreatePage.tsx @@ -32,7 +32,7 @@ const BackButton = () => { return ( } className="color-secondary-dark" diff --git a/frontend/packages/vkt/src/pages/ClerkExamEventOverviewPage.tsx b/frontend/packages/vkt/src/pages/ClerkExamEventOverviewPage.tsx index 074936737..dfdcd5711 100644 --- a/frontend/packages/vkt/src/pages/ClerkExamEventOverviewPage.tsx +++ b/frontend/packages/vkt/src/pages/ClerkExamEventOverviewPage.tsx @@ -55,7 +55,7 @@ export const ClerkExamEventOverviewPage: FC = () => { severity: Severity.Error, description: t('toasts.notFound'), }); - navigate(AppRoutes.ClerkHomePage); + navigate(AppRoutes.ClerkExcellentLevelPage); } }, [ overviewStatus, diff --git a/frontend/packages/vkt/src/pages/ClerkHomePage.tsx b/frontend/packages/vkt/src/pages/ClerkExcellentLevelPage.tsx similarity index 95% rename from frontend/packages/vkt/src/pages/ClerkHomePage.tsx rename to frontend/packages/vkt/src/pages/ClerkExcellentLevelPage.tsx index ef004c4d0..0d85cf71c 100644 --- a/frontend/packages/vkt/src/pages/ClerkHomePage.tsx +++ b/frontend/packages/vkt/src/pages/ClerkExcellentLevelPage.tsx @@ -8,7 +8,7 @@ import { resetClerkExamEventOverview } from 'redux/reducers/clerkExamEventOvervi import { loadExamEvents } from 'redux/reducers/clerkListExamEvent'; import { clerkListExamEventsSelector } from 'redux/selectors/clerkListExamEvent'; -export const ClerkHomePage: FC = () => { +export const ClerkExcellentLevelPage: FC = () => { const dispatch = useAppDispatch(); const { status } = useAppSelector(clerkListExamEventsSelector); diff --git a/frontend/packages/vkt/src/pages/ClerkGoodAndSatisfactoryLevelPage.tsx b/frontend/packages/vkt/src/pages/ClerkGoodAndSatisfactoryLevelPage.tsx new file mode 100644 index 000000000..684070f88 --- /dev/null +++ b/frontend/packages/vkt/src/pages/ClerkGoodAndSatisfactoryLevelPage.tsx @@ -0,0 +1,65 @@ +import { Box, Grid, Paper } from '@mui/material'; +import { FC, useEffect } from 'react'; +import { H1 } from 'shared/components'; +import { APIResponseStatus } from 'shared/enums'; + +import { ClerkExamEventListing } from 'components/clerkExamEvent/listing/ClerkExamEventListing'; +import { PublicExamEventGridSkeleton } from 'components/skeletons/PublicExamEventGridSkeleton'; +import { useClerkTranslation } from 'configs/i18n'; +import { useAppDispatch, useAppSelector } from 'configs/redux'; +import { resetClerkExamEventOverview } from 'redux/reducers/clerkExamEventOverview'; +import { loadExamEvents } from 'redux/reducers/clerkListExamEvent'; +import { clerkListExamEventsSelector } from 'redux/selectors/clerkListExamEvent'; + +export const ClerkGoodAndSatisfactoryLevelPage: FC = () => { + // I18 + const { t } = useClerkTranslation({ + keyPrefix: 'vkt.pages.goodAndSatisfactoryLevel', + }); + + const dispatch = useAppDispatch(); + const { status } = useAppSelector(clerkListExamEventsSelector); + + const examinersLoading = false; + + useEffect(() => { + if (status === APIResponseStatus.NotStarted) { + dispatch(loadExamEvents()); + } + }, [dispatch, status]); + + useEffect(() => { + dispatch(resetClerkExamEventOverview()); + }, [dispatch]); + // TODO Listing of examiners + // TODO Listing of exam events of good and satisfactory level + + return ( + + + +

    + {t('title')} +

    +
    + + + {examinersLoading ? ( + + ) : ( + + )} + + {' '} +
    +
    + ); +}; diff --git a/frontend/packages/vkt/src/pages/examiner/ExaminerHomePage.tsx b/frontend/packages/vkt/src/pages/examiner/ExaminerHomePage.tsx new file mode 100644 index 000000000..68b93aecd --- /dev/null +++ b/frontend/packages/vkt/src/pages/examiner/ExaminerHomePage.tsx @@ -0,0 +1,185 @@ +import { + Box, + Checkbox, + Divider, + FormControlLabel, + FormGroup, + Grid, + Paper, +} from '@mui/material'; +import { FC, useEffect } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { + H1, + H2, + LabeledMultipleCheckboxDropdown, + LabeledTextField, + Text, +} from 'shared/components'; +import { + APIResponseStatus, + InputAutoComplete, + TextFieldTypes, + TextFieldVariant, +} from 'shared/enums'; + +import { useAppDispatch, useAppSelector } from 'configs/redux'; +import { AppRoutes } from 'enums/app'; +import { loadExaminerDetails } from 'redux/reducers/examinerDetails'; +import { loadExaminerDetailsInit } from 'redux/reducers/examinerDetailsInit'; +import { clerkUserSelector } from 'redux/selectors/clerkUser'; +import { examinerDetailsSelector } from 'redux/selectors/examinerDetails'; +import { examinerDetailsInitSelector } from 'redux/selectors/examinerDetailsInit'; + +const InitializeExaminerDetails = () => { + const { initData } = useAppSelector(examinerDetailsInitSelector); + + return ( + +
    +

    Henkilötiedot

    + Tiedoista vain nimet näkyvät julkisessa listauksessa. +
    +
    + + Sukunimi: + + {initData?.lastName} +
    +
    + + Etunimi: + + {initData?.lastName} +
    + + +
    + +

    Tutkinnon perustiedot

    + Nämä tiedot näkyvät julkisessa listauksessa. +
    +
    + + + Tutkinnon kieli * + + + + } label="suomi" /> + } label="ruotsi" /> + +
    +
    +
    + +
    +
    +
    + ); +}; + +export const ExaminerDetailsPage = () => { + const dispatch = useAppDispatch(); + const { + oid, + status: examinerDetailsStatus, + initialized, + } = useAppSelector(examinerDetailsSelector); + const { status: initStatus } = useAppSelector(examinerDetailsInitSelector); + useEffect(() => { + if (examinerDetailsStatus === APIResponseStatus.NotStarted && oid) { + dispatch(loadExaminerDetails(oid)); + } + }, [dispatch, examinerDetailsStatus, oid]); + + useEffect(() => { + if ( + initialized === false && + initStatus === APIResponseStatus.NotStarted && + oid + ) { + dispatch(loadExaminerDetailsInit(oid)); + } + }); + + return ( + + + +

    Omat tiedot

    +
    + + {initialized === false && } + +
    +
    + ); +}; + +export const ExaminerHomePage: FC = () => { + const navigate = useNavigate(); + + const dispatch = useAppDispatch(); + const clerkUser = useAppSelector(clerkUserSelector); + const { oid, status, examiner, initialized } = useAppSelector( + examinerDetailsSelector, + ); + useEffect(() => { + if ( + oid && + (status === APIResponseStatus.NotStarted || + (status === APIResponseStatus.Success && oid !== examiner?.oid)) + ) { + dispatch(loadExaminerDetails(oid)); + } + }, [dispatch, status, oid, examiner?.oid]); + + // If examiner data is not initialized, redirect user to initialize the data + useEffect(() => { + if (initialized === false && oid) { + navigate(AppRoutes.ExaminerDetailsPage.replace(/:oid/, oid)); + } + }, [initialized, navigate, clerkUser.isExaminer, oid]); + + return ( + + + +

    Hyvän ja tyydyttävän taidon kielitutkinnot

    +
    +
    +
    + ); +}; diff --git a/frontend/packages/vkt/src/pages/examiner/ExaminerRedirectPage.tsx b/frontend/packages/vkt/src/pages/examiner/ExaminerRedirectPage.tsx new file mode 100644 index 000000000..5f8efee30 --- /dev/null +++ b/frontend/packages/vkt/src/pages/examiner/ExaminerRedirectPage.tsx @@ -0,0 +1,27 @@ +import { useEffect } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { APIResponseStatus } from 'shared/enums'; + +import { useAppSelector } from 'configs/redux'; +import { AppRoutes } from 'enums/app'; +import { clerkUserSelector } from 'redux/selectors/clerkUser'; + +export const ExaminerRedirectPage = () => { + const navigate = useNavigate(); + // Use OID from authentication details of logged in user to redirect to correct examiner pages. + // Note that this might be misleading if this page is accessed by OPH clerk instead of examiner. + // However, this should not ordinarily happen. + const { oid, status } = useAppSelector(clerkUserSelector); + useEffect(() => { + if (status === APIResponseStatus.Success) { + // eslint-disable-next-line no-console + console.log( + 'Redirecting to .....', + AppRoutes.ExaminerHomePage.replace(/:oid/, oid), + ); + navigate(AppRoutes.ExaminerHomePage.replace(/:oid/, oid)); + } + }); + + return
    ; +}; diff --git a/frontend/packages/vkt/src/pages/examiner/ExaminerRootPage.tsx b/frontend/packages/vkt/src/pages/examiner/ExaminerRootPage.tsx new file mode 100644 index 000000000..5ebe73757 --- /dev/null +++ b/frontend/packages/vkt/src/pages/examiner/ExaminerRootPage.tsx @@ -0,0 +1,18 @@ +import { ReactNode, useEffect } from 'react'; +import { useParams } from 'react-router-dom'; + +import { useAppDispatch } from 'configs/redux'; +import { setExaminerOid } from 'redux/reducers/examinerDetails'; + +export const ExaminerRootPage = ({ children }: { children: ReactNode }) => { + const { oid } = useParams(); + + const dispatch = useAppDispatch(); + useEffect(() => { + if (oid) { + dispatch(setExaminerOid(oid)); + } + }, [oid, dispatch]); + + return children; +}; diff --git a/frontend/packages/vkt/src/redux/reducers/clerkUser.ts b/frontend/packages/vkt/src/redux/reducers/clerkUser.ts index d2e1dd07f..d584b11c1 100644 --- a/frontend/packages/vkt/src/redux/reducers/clerkUser.ts +++ b/frontend/packages/vkt/src/redux/reducers/clerkUser.ts @@ -1,16 +1,13 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit'; import { APIResponseStatus } from 'shared/enums'; -import { ClerkUser } from 'interfaces/clerkUser'; - -interface ClerkUserState extends ClerkUser { - status: APIResponseStatus; - isAuthenticated: boolean; -} +import { ClerkUser, ClerkUserState } from 'interfaces/clerkUser'; const initialState: ClerkUserState = { status: APIResponseStatus.NotStarted, isAuthenticated: false, + isAdmin: false, + isExaminer: false, oid: '', }; @@ -21,15 +18,15 @@ const clerkUserSlice = createSlice({ loadClerkUser(state) { state.status = APIResponseStatus.InProgress; }, - rejectClerkUser(state) { - state.status = APIResponseStatus.Error; - state.isAuthenticated = initialState.isAuthenticated; - state.oid = initialState.oid; + rejectClerkUser(_) { + return { ...initialState, status: APIResponseStatus.Error }; }, - storeClerkUser(state, action: PayloadAction) { - state.status = APIResponseStatus.Success; - state.isAuthenticated = true; - state.oid = action.payload.oid; + storeClerkUser(_, action: PayloadAction) { + return { + ...action.payload, + status: APIResponseStatus.Success, + isAuthenticated: true, + }; }, }, }); diff --git a/frontend/packages/vkt/src/redux/reducers/examinerDetails.ts b/frontend/packages/vkt/src/redux/reducers/examinerDetails.ts new file mode 100644 index 000000000..e681edcab --- /dev/null +++ b/frontend/packages/vkt/src/redux/reducers/examinerDetails.ts @@ -0,0 +1,42 @@ +import { createSlice, PayloadAction } from '@reduxjs/toolkit'; +import { APIResponseStatus } from 'shared/enums'; + +import { + ExaminerDetails, + ExaminerDetailsState, +} from 'interfaces/examinerDetails'; + +const initialState: ExaminerDetailsState = { + status: APIResponseStatus.NotStarted, +}; + +const examinerDetailsSlice = createSlice({ + name: 'examinerDetails', + initialState, + reducers: { + loadExaminerDetails(state, _action: PayloadAction) { + state.status = APIResponseStatus.InProgress; + state.initialized = undefined; + }, + rejectExaminerDetails(state, action: PayloadAction) { + state.status = APIResponseStatus.Error; + state.initialized = action.payload; + }, + storeExaminerDetails(state, action: PayloadAction) { + state.status = APIResponseStatus.Success; + state.examiner = action.payload; + state.initialized = undefined; + }, + setExaminerOid(state, action: PayloadAction) { + state.oid = action.payload; + }, + }, +}); + +export const examinerDetailsReducer = examinerDetailsSlice.reducer; +export const { + loadExaminerDetails, + rejectExaminerDetails, + storeExaminerDetails, + setExaminerOid, +} = examinerDetailsSlice.actions; diff --git a/frontend/packages/vkt/src/redux/reducers/examinerDetailsInit.ts b/frontend/packages/vkt/src/redux/reducers/examinerDetailsInit.ts new file mode 100644 index 000000000..d6c00a3e4 --- /dev/null +++ b/frontend/packages/vkt/src/redux/reducers/examinerDetailsInit.ts @@ -0,0 +1,38 @@ +import { createSlice, PayloadAction } from '@reduxjs/toolkit'; +import { APIResponseStatus } from 'shared/enums'; + +import { + ExaminerDetailsInit, + ExaminerDetailsInitState, +} from 'interfaces/examinerDetails'; + +const initialState: ExaminerDetailsInitState = { + status: APIResponseStatus.NotStarted, +}; + +const examinerDetailsInitSlice = createSlice({ + name: 'examinerDetailsInit', + initialState, + reducers: { + loadExaminerDetailsInit(state, _action: PayloadAction) { + state.status = APIResponseStatus.InProgress; + }, + rejectExaminerDetailsInit(state) { + state.status = APIResponseStatus.Error; + }, + storeExaminerDetailsInit( + state, + action: PayloadAction, + ) { + state.status = APIResponseStatus.Success; + state.initData = action.payload; + }, + }, +}); + +export const examinerDetailsInitReducer = examinerDetailsInitSlice.reducer; +export const { + loadExaminerDetailsInit, + rejectExaminerDetailsInit, + storeExaminerDetailsInit, +} = examinerDetailsInitSlice.actions; diff --git a/frontend/packages/vkt/src/redux/sagas/examinerDetails.ts b/frontend/packages/vkt/src/redux/sagas/examinerDetails.ts new file mode 100644 index 000000000..fb916041a --- /dev/null +++ b/frontend/packages/vkt/src/redux/sagas/examinerDetails.ts @@ -0,0 +1,37 @@ +import { PayloadAction } from '@reduxjs/toolkit'; +import { AxiosResponse, isAxiosError } from 'axios'; +import { call, put, takeLatest } from 'redux-saga/effects'; + +import axiosInstance from 'configs/axios'; +import { APIEndpoints, APIError } from 'enums/api'; +import { ExaminerDetails } from 'interfaces/examinerDetails'; +import { + loadExaminerDetails, + rejectExaminerDetails, + storeExaminerDetails, +} from 'redux/reducers/examinerDetails'; + +function* loadExaminerDetailsSaga(action: PayloadAction) { + try { + const response: AxiosResponse = yield call( + axiosInstance.get, + APIEndpoints.ExaminerDetails.replace(/:oid/, action.payload), + ); + yield put(storeExaminerDetails(response.data)); + } catch (error) { + let initialized = true; + if (isAxiosError(error)) { + const errorCode = error.response?.data?.errorCode; + // eslint-disable-next-line no-console + console.log('moiccuuuuu! errorCode', errorCode); + if (errorCode === APIError.ExaminerNotFound) { + initialized = false; + } + } + yield put(rejectExaminerDetails(initialized)); + } +} + +export function* watchExaminerDetails() { + yield takeLatest(loadExaminerDetails.type, loadExaminerDetailsSaga); +} diff --git a/frontend/packages/vkt/src/redux/sagas/examinerDetailsInit.ts b/frontend/packages/vkt/src/redux/sagas/examinerDetailsInit.ts new file mode 100644 index 000000000..863ca59c3 --- /dev/null +++ b/frontend/packages/vkt/src/redux/sagas/examinerDetailsInit.ts @@ -0,0 +1,28 @@ +import { PayloadAction } from '@reduxjs/toolkit'; +import { AxiosResponse } from 'axios'; +import { call, put, takeLatest } from 'redux-saga/effects'; + +import axiosInstance from 'configs/axios'; +import { APIEndpoints } from 'enums/api'; +import { ExaminerDetailsInit } from 'interfaces/examinerDetails'; +import { + loadExaminerDetailsInit, + rejectExaminerDetailsInit, + storeExaminerDetailsInit, +} from 'redux/reducers/examinerDetailsInit'; + +function* loadExaminerDetailsInitSaga(action: PayloadAction) { + try { + const response: AxiosResponse = yield call( + axiosInstance.get, + APIEndpoints.ExaminerDetailsInit.replace(/:oid/, action.payload), + ); + yield put(storeExaminerDetailsInit(response.data)); + } catch (error) { + yield put(rejectExaminerDetailsInit()); + } +} + +export function* watchExaminerDetailsInit() { + yield takeLatest(loadExaminerDetailsInit.type, loadExaminerDetailsInitSaga); +} diff --git a/frontend/packages/vkt/src/redux/sagas/index.ts b/frontend/packages/vkt/src/redux/sagas/index.ts index fc1264fe5..0b893ad20 100644 --- a/frontend/packages/vkt/src/redux/sagas/index.ts +++ b/frontend/packages/vkt/src/redux/sagas/index.ts @@ -5,6 +5,8 @@ import { watchClerkExamEventOverview } from 'redux/sagas/clerkExamEventOverview' import { watchListExamEvents } from 'redux/sagas/clerkListExamEvent'; import { watchClerkNewExamDate } from 'redux/sagas/clerkNewExamDate'; import { watchClerkUser } from 'redux/sagas/clerkUser'; +import { watchExaminerDetails } from 'redux/sagas/examinerDetails'; +import { watchExaminerDetailsInit } from 'redux/sagas/examinerDetailsInit'; import { watchFeatureFlags } from 'redux/sagas/featureFlags'; import { watchPublicEducation } from 'redux/sagas/publicEducation'; import { watchPublicEnrollments } from 'redux/sagas/publicEnrollment'; @@ -27,5 +29,7 @@ export default function* rootSaga() { watchFileUpload(), watchPublicEducation(), watchPublicExaminers(), + watchExaminerDetails(), + watchExaminerDetailsInit(), ]); } diff --git a/frontend/packages/vkt/src/redux/selectors/clerkUser.ts b/frontend/packages/vkt/src/redux/selectors/clerkUser.ts index 5ca91ba7f..e641ce0a0 100644 --- a/frontend/packages/vkt/src/redux/selectors/clerkUser.ts +++ b/frontend/packages/vkt/src/redux/selectors/clerkUser.ts @@ -1,3 +1,5 @@ import { RootState } from 'configs/redux'; +import { ClerkUserState } from 'interfaces/clerkUser'; -export const clerkUserSelector = (state: RootState) => state.clerkUser; +export const clerkUserSelector = (state: RootState): ClerkUserState => + state.clerkUser; diff --git a/frontend/packages/vkt/src/redux/selectors/examinerDetails.ts b/frontend/packages/vkt/src/redux/selectors/examinerDetails.ts new file mode 100644 index 000000000..cedc58227 --- /dev/null +++ b/frontend/packages/vkt/src/redux/selectors/examinerDetails.ts @@ -0,0 +1,6 @@ +import { RootState } from 'configs/redux'; +import { ExaminerDetailsState } from 'interfaces/examinerDetails'; + +export const examinerDetailsSelector: ( + state: RootState, +) => ExaminerDetailsState = (state: RootState) => state.examinerDetails; diff --git a/frontend/packages/vkt/src/redux/selectors/examinerDetailsInit.ts b/frontend/packages/vkt/src/redux/selectors/examinerDetailsInit.ts new file mode 100644 index 000000000..aa29dc2c1 --- /dev/null +++ b/frontend/packages/vkt/src/redux/selectors/examinerDetailsInit.ts @@ -0,0 +1,6 @@ +import { RootState } from 'configs/redux'; +import { ExaminerDetailsInitState } from 'interfaces/examinerDetails'; + +export const examinerDetailsInitSelector: ( + state: RootState, +) => ExaminerDetailsInitState = (state: RootState) => state.examinerDetailsInit; diff --git a/frontend/packages/vkt/src/redux/store/index.ts b/frontend/packages/vkt/src/redux/store/index.ts index b7b6ff057..e8a514f05 100644 --- a/frontend/packages/vkt/src/redux/store/index.ts +++ b/frontend/packages/vkt/src/redux/store/index.ts @@ -10,6 +10,8 @@ import { clerkExamEventOverviewReducer } from 'redux/reducers/clerkExamEventOver import { clerkListExamEventReducer } from 'redux/reducers/clerkListExamEvent'; import { clerkNewExamDateReducer } from 'redux/reducers/clerkNewExamDate'; import { clerkUserReducer } from 'redux/reducers/clerkUser'; +import { examinerDetailsReducer } from 'redux/reducers/examinerDetails'; +import { examinerDetailsInitReducer } from 'redux/reducers/examinerDetailsInit'; import { featureFlagsReducer } from 'redux/reducers/featureFlags'; import { publicEducationReducer } from 'redux/reducers/publicEducation'; import { publicEnrollmentReducer } from 'redux/reducers/publicEnrollment'; @@ -40,6 +42,8 @@ const reducer = combineReducers({ publicFileUpload: publicFileUploadReducer, publicEducation: publicEducationReducer, publicExaminer: publicExaminerReducer, + examinerDetails: examinerDetailsReducer, + examinerDetailsInit: examinerDetailsInitReducer, }); const persistedReducer = persistReducer(persistConfig, reducer); diff --git a/frontend/packages/vkt/src/routers/AppRouter.tsx b/frontend/packages/vkt/src/routers/AppRouter.tsx index 5c2507e5d..4b89b562e 100644 --- a/frontend/packages/vkt/src/routers/AppRouter.tsx +++ b/frontend/packages/vkt/src/routers/AppRouter.tsx @@ -26,7 +26,14 @@ import { AccessibilityStatementPage } from 'pages/AccessibilityStatementPage'; import { ClerkEnrollmentOverviewPage } from 'pages/ClerkEnrollmentOverviewPage'; import { ClerkExamEventCreatePage } from 'pages/ClerkExamEventCreatePage'; import { ClerkExamEventOverviewPage } from 'pages/ClerkExamEventOverviewPage'; -import { ClerkHomePage } from 'pages/ClerkHomePage'; +import { ClerkExcellentLevelPage } from 'pages/ClerkExcellentLevelPage'; +import { ClerkGoodAndSatisfactoryLevelPage } from 'pages/ClerkGoodAndSatisfactoryLevelPage'; +import { + ExaminerDetailsPage, + ExaminerHomePage, +} from 'pages/examiner/ExaminerHomePage'; +import { ExaminerRedirectPage } from 'pages/examiner/ExaminerRedirectPage'; +import { ExaminerRootPage } from 'pages/examiner/ExaminerRootPage'; import { PublicEnrollmentPage } from 'pages/excellentLevel/PublicEnrollmentPage'; import { PublicExcellentLevelLandingPage } from 'pages/excellentLevel/PublicExcellentLevelLandingPage'; import { PublicGoodAndSatisfactoryLevelLandingPage } from 'pages/goodAndSatisfactoryLevel/PublicGoodAndSatisfactoryLevelLandingPage'; @@ -208,10 +215,18 @@ export const AppRouter: FC = () => { } /> - + + + + } + /> + + } /> @@ -239,6 +254,44 @@ export const AppRouter: FC = () => { } /> + + + + } + /> + + + + + } + /> + + + + + + } + /> + + + + + + } + /> + { return merge([ getDefaults(), - { devServer: { headers: { 'Access-Control-Allow-Origin': '*' } } }, + { + devServer: { + headers: { 'Access-Control-Allow-Origin': '*' }, + // Needed to allow direct navigation to URLs where segments contain dots (eg. OIDs) + historyApiFallback: { disableDotRule: true }, + }, + }, ]); };