diff --git a/frontend/packages/yki/public/i18n/fi-FI/common.json b/frontend/packages/yki/public/i18n/fi-FI/common.json
index 8ad2482e5d..7b655255b2 100644
--- a/frontend/packages/yki/public/i18n/fi-FI/common.json
+++ b/frontend/packages/yki/public/i18n/fi-FI/common.json
@@ -70,6 +70,8 @@
"langSelectorAriaLabel": "Kieli / Språk / Language"
},
"sessionState": {
+ "activeRegistrations": "Sinulla on 1 käynnissä oleva ilmoittautuminen.",
+ "continueToRegistration": "Siirry ilmoittautumissivulle.",
"logOut": "Kirjaudu ulos"
},
"lang": {
diff --git a/frontend/packages/yki/setupProxy.js b/frontend/packages/yki/setupProxy.js
index f976102595..65d5132b80 100644
--- a/frontend/packages/yki/setupProxy.js
+++ b/frontend/packages/yki/setupProxy.js
@@ -1019,7 +1019,7 @@ module.exports = function (app) {
}
);
- app.get('/yki/auth/user', (req, res) => {
+ app.get('/yki/api/user/identity', (req, res) => {
try {
res.set('Content-Type', 'application/json; charset=utf-8');
res.send(adminUser);
@@ -1029,6 +1029,16 @@ module.exports = function (app) {
}
});
+ app.get('/yki/api/user/open-registrations', (req, res) => {
+ try {
+ res.set('Content-Type', 'application/json; charset=utf-8');
+ res.send({ 'open-registrations': [ { exam_session_id: 25 }]});
+ } catch (err) {
+ printError(req, err);
+ res.status(404).send(err.message);
+ }
+ });
+
app.post('/yki/api/login-link', (req, res) => {
const mockCall = () => {
try {
diff --git a/frontend/packages/yki/src/components/layouts/SessionStateHeader.tsx b/frontend/packages/yki/src/components/layouts/SessionStateHeader.tsx
index 34919efb3c..4b7b9782a9 100644
--- a/frontend/packages/yki/src/components/layouts/SessionStateHeader.tsx
+++ b/frontend/packages/yki/src/components/layouts/SessionStateHeader.tsx
@@ -3,12 +3,13 @@ import {
Person as PersonIcon,
} from '@mui/icons-material';
import Button from '@mui/material/Button';
-import { FC } from 'react';
+import { FC, useEffect } from 'react';
+import { generatePath, Link, matchPath, useLocation } from 'react-router-dom';
import { Text } from 'shared/components';
-import { Color, Variant } from 'shared/enums';
+import { APIResponseStatus, Color, Variant } from 'shared/enums';
import { useCommonTranslation } from 'configs/i18n';
-import { useAppSelector } from 'configs/redux';
+import { useAppDispatch, useAppSelector } from 'configs/redux';
import { APIEndpoints } from 'enums/api';
import { AppRoutes } from 'enums/app';
import {
@@ -16,7 +17,9 @@ import {
EmailAuthenticatedSession,
SuomiFiAuthenticatedSession,
} from 'interfaces/session';
+import { loadUserOpenRegistrations } from 'redux/reducers/userOpenRegistrations';
import { sessionSelector } from 'redux/selectors/session';
+import { userOpenRegistrationsSelector } from 'redux/selectors/userOpenRegistrations';
const getUserName = (
user:
@@ -39,12 +42,50 @@ const generateLogoutURL = () => {
};
export const SessionStateHeader: FC = () => {
+ const location = useLocation();
const translateCommon = useCommonTranslation();
+ const userOpenRegistrations = useAppSelector(userOpenRegistrationsSelector);
const session = useAppSelector(sessionSelector).loggedInSession;
- if (!session || session.identity === null) return <>>;
+ const dispatch = useAppDispatch();
+ const hasNullIdentity = !session || session.identity === null;
+ const notOnRegistrationPage =
+ matchPath(AppRoutes.ExamSessionRegistration, location.pathname) === null;
+
+ useEffect(() => {
+ const needsUserOpenRegistrations =
+ !hasNullIdentity &&
+ notOnRegistrationPage &&
+ userOpenRegistrations.status === APIResponseStatus.NotStarted;
+
+ if (needsUserOpenRegistrations) {
+ dispatch(loadUserOpenRegistrations());
+ }
+ }, [hasNullIdentity, notOnRegistrationPage, userOpenRegistrations, dispatch]);
+
+ if (hasNullIdentity) return <>>;
return (
+ {notOnRegistrationPage &&
+ userOpenRegistrations.openRegistrations &&
+ userOpenRegistrations.openRegistrations['open-registrations'] &&
+ userOpenRegistrations.openRegistrations['open-registrations'][0] && (
+
+
+ {translateCommon('header.sessionState.activeRegistrations')}
+
+
+ {translateCommon('header.sessionState.continueToRegistration')}
+
+
+ )}
{getUserName(session)}
diff --git a/frontend/packages/yki/src/enums/api.ts b/frontend/packages/yki/src/enums/api.ts
index 9e948076e4..c0c821eb6d 100644
--- a/frontend/packages/yki/src/enums/api.ts
+++ b/frontend/packages/yki/src/enums/api.ts
@@ -13,7 +13,8 @@ export enum APIEndpoints {
Registration = '/yki/api/registration/:registrationId',
SubmitRegistration = '/yki/api/registration/:registrationId/submit',
SuomiFiAuthRedirect = '/yki/auth/',
- User = '/yki/auth/user',
+ User = '/yki/api/user/identity',
+ OpenRegistrations = '/yki/api/user/open-registrations',
}
export enum PaymentStatus {
diff --git a/frontend/packages/yki/src/interfaces/publicRegistration.ts b/frontend/packages/yki/src/interfaces/publicRegistration.ts
index 92b1c35dd4..4b96fd1cb3 100644
--- a/frontend/packages/yki/src/interfaces/publicRegistration.ts
+++ b/frontend/packages/yki/src/interfaces/publicRegistration.ts
@@ -73,3 +73,10 @@ export interface PublicRegistrationFormSubmitErrorResponse {
person_creation?: boolean;
};
}
+
+export interface UserOpenRegistration {
+ exam_session_id: number;
+}
+export interface UserOpenRegistrationsResponse {
+ 'open-registrations': Array;
+}
diff --git a/frontend/packages/yki/src/redux/reducers/userOpenRegistrations.ts b/frontend/packages/yki/src/redux/reducers/userOpenRegistrations.ts
new file mode 100644
index 0000000000..6e04c87627
--- /dev/null
+++ b/frontend/packages/yki/src/redux/reducers/userOpenRegistrations.ts
@@ -0,0 +1,45 @@
+import { createSlice, PayloadAction } from '@reduxjs/toolkit';
+import { APIResponseStatus } from 'shared/enums';
+
+import { UserOpenRegistrationsResponse } from 'interfaces/publicRegistration';
+
+interface UserOpenRegistrationsState {
+ status: APIResponseStatus;
+ openRegistrations?: UserOpenRegistrationsResponse;
+}
+
+const initialState: UserOpenRegistrationsState = {
+ status: APIResponseStatus.NotStarted,
+};
+
+const userOpenRegistrationsSlice = createSlice({
+ name: 'userOpenRegistrations',
+ initialState,
+ reducers: {
+ acceptUserOpenRegistrations(
+ state,
+ action: PayloadAction
+ ) {
+ state.status = APIResponseStatus.Success;
+ state.openRegistrations = action.payload;
+ },
+ loadUserOpenRegistrations(state) {
+ state.status = APIResponseStatus.InProgress;
+ },
+ rejectUserOpenRegistrations(state) {
+ state.status = APIResponseStatus.Error;
+ },
+ resetUserOpenRegistrations(state) {
+ state.status = APIResponseStatus.NotStarted;
+ state.openRegistrations = undefined;
+ },
+ },
+});
+
+export const userOpenRegistrationsReducer = userOpenRegistrationsSlice.reducer;
+export const {
+ acceptUserOpenRegistrations,
+ loadUserOpenRegistrations,
+ rejectUserOpenRegistrations,
+ resetUserOpenRegistrations,
+} = userOpenRegistrationsSlice.actions;
diff --git a/frontend/packages/yki/src/redux/sagas/index.ts b/frontend/packages/yki/src/redux/sagas/index.ts
index 5419b59c67..4519b239c0 100644
--- a/frontend/packages/yki/src/redux/sagas/index.ts
+++ b/frontend/packages/yki/src/redux/sagas/index.ts
@@ -8,6 +8,7 @@ import { watchPublicIdentification } from 'redux/sagas/publicIdentification';
import { watchRegistration } from 'redux/sagas/registration';
import { watchReservationRequest } from 'redux/sagas/reservation';
import { watchSession } from 'redux/sagas/session';
+import { watchUserOpenRegistrations } from 'redux/sagas/userOpenRegistrations';
export default function* rootSaga() {
yield all([
@@ -19,5 +20,6 @@ export default function* rootSaga() {
watchRegistration(),
watchReservationRequest(),
watchSession(),
+ watchUserOpenRegistrations(),
]);
}
diff --git a/frontend/packages/yki/src/redux/sagas/registration.ts b/frontend/packages/yki/src/redux/sagas/registration.ts
index ed2a0a3ba8..610c73a26d 100644
--- a/frontend/packages/yki/src/redux/sagas/registration.ts
+++ b/frontend/packages/yki/src/redux/sagas/registration.ts
@@ -24,6 +24,7 @@ import {
resetPublicRegistration,
submitPublicRegistration,
} from 'redux/reducers/registration';
+import { resetUserOpenRegistrations } from 'redux/reducers/userOpenRegistrations';
import { nationalitiesSelector } from 'redux/selectors/nationalities';
import { registrationSelector } from 'redux/selectors/registration';
import { SerializationUtils } from 'utils/serialization';
@@ -80,6 +81,7 @@ function* submitRegistrationFormSaga() {
}
);
yield put(acceptPublicRegistrationSubmission());
+ yield put(resetUserOpenRegistrations());
} catch (error) {
// eslint-disable-next-line no-console
console.error('caught error!', error);
diff --git a/frontend/packages/yki/src/redux/sagas/userOpenRegistrations.ts b/frontend/packages/yki/src/redux/sagas/userOpenRegistrations.ts
new file mode 100644
index 0000000000..6edf55dcef
--- /dev/null
+++ b/frontend/packages/yki/src/redux/sagas/userOpenRegistrations.ts
@@ -0,0 +1,30 @@
+import { call, put, takeLatest } from '@redux-saga/core/effects';
+import { AxiosResponse } from 'axios';
+
+import axiosInstance from 'configs/axios';
+import { APIEndpoints } from 'enums/api';
+import { UserOpenRegistrationsResponse } from 'interfaces/publicRegistration';
+import {
+ acceptUserOpenRegistrations,
+ loadUserOpenRegistrations,
+ rejectUserOpenRegistrations,
+} from 'redux/reducers/userOpenRegistrations';
+
+function* loadUserOpenRegistrationsSaga() {
+ try {
+ const response: AxiosResponse = yield call(
+ axiosInstance.get,
+ APIEndpoints.OpenRegistrations
+ );
+ yield put(acceptUserOpenRegistrations(response.data));
+ } catch (error) {
+ yield put(rejectUserOpenRegistrations());
+ }
+}
+
+export function* watchUserOpenRegistrations() {
+ yield takeLatest(
+ loadUserOpenRegistrations.type,
+ loadUserOpenRegistrationsSaga
+ );
+}
diff --git a/frontend/packages/yki/src/redux/selectors/userOpenRegistrations.ts b/frontend/packages/yki/src/redux/selectors/userOpenRegistrations.ts
new file mode 100644
index 0000000000..5b5ac0a432
--- /dev/null
+++ b/frontend/packages/yki/src/redux/selectors/userOpenRegistrations.ts
@@ -0,0 +1,4 @@
+import { RootState } from 'configs/redux';
+
+export const userOpenRegistrationsSelector = (state: RootState) =>
+ state.userOpenRegistrations;
diff --git a/frontend/packages/yki/src/redux/store/index.ts b/frontend/packages/yki/src/redux/store/index.ts
index ab22491206..5e1b374b75 100644
--- a/frontend/packages/yki/src/redux/store/index.ts
+++ b/frontend/packages/yki/src/redux/store/index.ts
@@ -11,6 +11,7 @@ import { publicIdentificationReducer } from 'redux/reducers/publicIdentification
import { registrationReducer } from 'redux/reducers/registration';
import { reservationReducer } from 'redux/reducers/reservation';
import { sessionReducer } from 'redux/reducers/session';
+import { userOpenRegistrationsReducer } from 'redux/reducers/userOpenRegistrations';
import rootSaga from 'redux/sagas/index';
const saga = createSagaMiddleware();
@@ -27,6 +28,7 @@ const store = configureStore({
registration: registrationReducer,
reservation: reservationReducer,
session: sessionReducer,
+ userOpenRegistrations: userOpenRegistrationsReducer,
},
middleware: [saga],
});
diff --git a/frontend/packages/yki/src/styles/components/layouts/_session-header.scss b/frontend/packages/yki/src/styles/components/layouts/_session-header.scss
index 2209bebedb..b85d950ed0 100644
--- a/frontend/packages/yki/src/styles/components/layouts/_session-header.scss
+++ b/frontend/packages/yki/src/styles/components/layouts/_session-header.scss
@@ -1,7 +1,7 @@
.session-header {
background: #f0f3f7;
justify-content: flex-end;
- padding: 2rem 6rem 2rem 2rem;
+ padding: 2rem 6rem;
@include phone {
align-items: flex-end;
@@ -15,4 +15,23 @@
height: 2rem;
width: 2rem;
}
+
+ & &__open-registrations {
+ margin-right: auto;
+
+ & a {
+ color: $color-secondary-dark;
+ font-size: 1.6rem;
+ font-weight: normal;
+ letter-spacing: 0;
+ line-height: 1.9rem;
+ margin: 0;
+ text-decoration: underline;
+ text-transform: initial;
+
+ @include phone {
+ font-size: 1.5rem;
+ }
+ }
+ }
}