= {
@@ -35,7 +35,7 @@ const noContent = (response: Response) => {
const redirectToLogin = () => {
const loginUrl = new URL(configuration.loginUrl);
loginUrl.searchParams.set('service', window.location.href);
- if (typeof window === 'undefined') {
+ if (isServer) {
redirect(loginUrl.toString());
} else {
window.location.replace(loginUrl.toString());
diff --git a/src/app/lib/mui-utils.tsx b/src/app/lib/mui-utils.tsx
index 027e8c43..bbe6ea12 100644
--- a/src/app/lib/mui-utils.tsx
+++ b/src/app/lib/mui-utils.tsx
@@ -11,9 +11,7 @@ export function withDefaultProps>(
const ComponentWithDefaultProps = forwardRef<
ComponentRef>,
P
- >((props, ref) => (
-
- ));
+ >((props, ref) => );
ComponentWithDefaultProps.displayName = displayName;
return ComponentWithDefaultProps;
diff --git a/src/app/haku/[oid]/hakukohde/[hakukohde]/valinnan-hallinta/lib/laskenta-state.test.ts b/src/app/lib/state/laskenta-state.test.ts
similarity index 66%
rename from src/app/haku/[oid]/hakukohde/[hakukohde]/valinnan-hallinta/lib/laskenta-state.test.ts
rename to src/app/lib/state/laskenta-state.test.ts
index c9ce41e9..f107c5f4 100644
--- a/src/app/haku/[oid]/hakukohde/[hakukohde]/valinnan-hallinta/lib/laskenta-state.test.ts
+++ b/src/app/lib/state/laskenta-state.test.ts
@@ -1,16 +1,18 @@
import { expect, test, vi, describe, afterEach, beforeEach } from 'vitest';
import {
createLaskentaMachine,
- LaskentaEvents,
- LaskentaStates,
+ LaskentaEventType,
+ LaskentaState,
StartLaskentaParams,
} from './laskenta-state';
import { client } from '@/app/lib/http-client';
import { Tila } from '@/app/lib/types/kouta-types';
-import { translateName } from '@/app/lib/localization/translation-utils';
import { createActor, waitFor } from 'xstate';
+import { range } from 'remeda';
-describe('Laskenta states', async () => {
+const LASKENTA_URL = 'urlmistatulosladataan';
+
+describe('Laskenta state', async () => {
const LASKENTAPARAMS: StartLaskentaParams = {
haku: {
oid: 'haku-oid',
@@ -22,17 +24,18 @@ describe('Laskenta states', async () => {
nimi: { fi: 'Haku' },
tila: Tila.JULKAISTU,
},
- hakukohde: {
- oid: 'hakukohde-oid',
- hakuOid: 'haku-oid',
- nimi: { fi: 'hakukohde' },
- jarjestyspaikkaHierarkiaNimi: { fi: 'Paikka' },
- organisaatioNimi: {},
- organisaatioOid: 'organisaatio-oid',
- voikoHakukohteessaOllaHarkinnanvaraisestiHakeneita: false,
- },
+ hakukohteet: [
+ {
+ oid: 'hakukohde-oid',
+ hakuOid: 'haku-oid',
+ nimi: { fi: 'hakukohde' },
+ jarjestyspaikkaHierarkiaNimi: { fi: 'Paikka' },
+ organisaatioNimi: {},
+ organisaatioOid: 'organisaatio-oid',
+ voikoHakukohteessaOllaHarkinnanvaraisestiHakeneita: false,
+ },
+ ],
sijoitellaanko: false,
- translateEntity: translateName,
};
let actor = createActor(createLaskentaMachine(LASKENTAPARAMS, vi.fn()));
@@ -51,15 +54,15 @@ describe('Laskenta states', async () => {
buildDummyLaskentaStart(),
);
vi.spyOn(client, 'get').mockImplementation(() => buildSeurantaTiedot());
- await actor.send({ type: LaskentaEvents.START });
- await actor.send({ type: LaskentaEvents.CONFIRM });
+ await actor.send({ type: LaskentaEventType.START });
+ actor.send({ type: LaskentaEventType.CONFIRM });
const state = await waitFor(actor, (state) =>
state.matches({
- [LaskentaStates.PROCESSING]: LaskentaStates.PROCESSING_WAITING,
+ [LaskentaState.PROCESSING]: LaskentaState.PROCESSING_WAITING,
}),
);
expect(state.context.laskenta.runningLaskenta?.loadingUrl).toEqual(
- 'urlmistatulosladataan',
+ LASKENTA_URL,
);
expect(
state.context.laskenta.runningLaskenta?.startedNewLaskenta,
@@ -74,16 +77,21 @@ describe('Laskenta states', async () => {
vi.spyOn(client, 'post').mockImplementationOnce(() =>
buildDummyLaskentaStart(),
);
- vi.spyOn(client, 'get').mockImplementation(() =>
- buildSeurantaTiedot(true, 1),
- );
- await actor.send({ type: LaskentaEvents.START });
- await actor.send({ type: LaskentaEvents.CONFIRM });
+ vi.spyOn(client, 'get').mockImplementation((url) => {
+ if (url.toString().includes('seuranta')) {
+ return buildSeurantaTiedot(true, 1);
+ } else if (url.toString().includes('yhteenveto')) {
+ return buildYhteenveto('VALMIS', 1);
+ }
+ return Promise.reject();
+ });
+ actor.send({ type: LaskentaEventType.START });
+ actor.send({ type: LaskentaEventType.CONFIRM });
const state = await waitFor(actor, (state) =>
- state.matches(LaskentaStates.IDLE),
+ state.matches(LaskentaState.IDLE),
);
expect(state.context.laskenta.runningLaskenta?.loadingUrl).toEqual(
- 'urlmistatulosladataan',
+ LASKENTA_URL,
);
expect(
state.context.laskenta.runningLaskenta?.startedNewLaskenta,
@@ -99,10 +107,10 @@ describe('Laskenta states', async () => {
vi.spyOn(client, 'post').mockRejectedValueOnce(
() => new Error('testerror'),
);
- await actor.send({ type: LaskentaEvents.START });
- await actor.send({ type: LaskentaEvents.CONFIRM });
+ actor.send({ type: LaskentaEventType.START });
+ actor.send({ type: LaskentaEventType.CONFIRM });
const state = await waitFor(actor, (state) =>
- state.matches(LaskentaStates.IDLE),
+ state.matches(LaskentaState.IDLE),
);
expect(state.context.error).toBeDefined();
});
@@ -114,13 +122,13 @@ describe('Laskenta states', async () => {
vi.spyOn(client, 'get').mockImplementationOnce(() =>
buildSeurantaTiedot(true, 0, 1),
);
- await actor.send({ type: LaskentaEvents.START });
- await actor.send({ type: LaskentaEvents.CONFIRM });
+ actor.send({ type: LaskentaEventType.START });
+ actor.send({ type: LaskentaEventType.CONFIRM });
const state = await waitFor(actor, (state) =>
- state.matches(LaskentaStates.IDLE),
+ state.matches(LaskentaState.IDLE),
);
expect(state.context.laskenta.runningLaskenta?.loadingUrl).toEqual(
- 'urlmistatulosladataan',
+ LASKENTA_URL,
);
expect(
state.context.laskenta.runningLaskenta?.startedNewLaskenta,
@@ -135,7 +143,7 @@ describe('Laskenta states', async () => {
const buildDummyLaskentaStart = () => {
const laskenta = {
- latausUrl: 'urlmistatulosladataan',
+ latausUrl: LASKENTA_URL,
lisatiedot: { luotiinkoUusiLaskenta: true },
};
return Promise.resolve({ headers: new Headers(), data: laskenta });
@@ -154,3 +162,30 @@ const buildSeurantaTiedot = (
};
return Promise.resolve({ headers: new Headers(), data: seuranta });
};
+
+const buildYhteenveto = (
+ tila: 'VALMIS' | 'PERUUTETTU',
+ hakukohteitaValmiina = 0,
+ hakukohteitaKeskeytetty = 0,
+) => {
+ const yhteenvetoValmiit = range(0, hakukohteitaValmiina).map(
+ (hakukohdeOid) => ({
+ hakukohdeOid,
+ tila: 'VALMIS',
+ }),
+ );
+
+ const yhteenvatKeskeytetty = range(0, hakukohteitaKeskeytetty).map(
+ (hakukohdeOid) => ({
+ hakukohdeOid,
+ tila: 'VIRHE',
+ }),
+ );
+
+ const yhteenveto = {
+ hakukohteet: [...yhteenvetoValmiit, ...yhteenvatKeskeytetty],
+ tila,
+ };
+
+ return Promise.resolve({ headers: new Headers(), data: yhteenveto });
+};
diff --git a/src/app/lib/state/laskenta-state.ts b/src/app/lib/state/laskenta-state.ts
new file mode 100644
index 00000000..cb6f5377
--- /dev/null
+++ b/src/app/lib/state/laskenta-state.ts
@@ -0,0 +1,453 @@
+'use client';
+
+import { ActorRefFrom, assign, fromPromise, setup, SnapshotFrom } from 'xstate';
+import {
+ Valinnanvaihe,
+ ValinnanvaiheTyyppi,
+} from '@/app/lib/types/valintaperusteet-types';
+import { Haku, Hakukohde } from '@/app/lib/types/kouta-types';
+import {
+ getLaskennanSeurantaTiedot,
+ getLaskennanYhteenveto,
+ kaynnistaLaskenta,
+ keskeytaLaskenta,
+} from '@/app/lib/valintalaskenta-service';
+import useToaster, { Toast } from '@/app/hooks/useToaster';
+import {
+ LaskentaSummary,
+ LaskentaStart,
+ SeurantaTiedot,
+ LaskentaErrorSummary,
+} from '@/app/lib/types/laskenta-types';
+import { prop } from 'remeda';
+import { useMemo } from 'react';
+import { sijoitellaankoHaunHakukohteetLaskennanYhteydessa } from '@/app/lib/kouta';
+import { useMachine, useSelector } from '@xstate/react';
+import { HaunAsetukset } from '@/app/lib/types/haun-asetukset';
+
+const POLLING_INTERVAL = 5000;
+
+export type Laskenta = {
+ errorMessage?: string | string[] | null;
+ calculatedTime?: Date | number | null;
+ runningLaskenta?: LaskentaStart;
+};
+
+const laskentaReducer = (state: Laskenta, action: Laskenta): Laskenta => {
+ return Object.assign({}, state, action);
+};
+
+export type StartLaskentaParams = {
+ haku: Haku;
+ hakukohteet: Array;
+ valinnanvaiheTyyppi?: ValinnanvaiheTyyppi;
+ sijoitellaanko: boolean;
+ valinnanvaiheNumber?: number;
+ valinnanvaiheNimi?: string;
+};
+
+export type LaskentaContext = {
+ laskenta: Laskenta;
+ startLaskentaParams: StartLaskentaParams;
+ seurantaTiedot: SeurantaTiedot | null;
+ errorSummary: LaskentaErrorSummary | null;
+ summary: LaskentaSummary | null;
+ error: Error | null;
+};
+
+export enum LaskentaState {
+ IDLE = 'IDLE',
+ INITIALIZED = 'INITIALIZED',
+ WAITING_CONFIRMATION = 'WAITING_CONFIRMATION',
+ STARTING = 'STARTING',
+ PROCESSING = 'PROCESSING',
+ PROCESSING_FETCHING = 'FETCHING',
+ PROCESSING_WAITING = 'WAITING',
+ PROCESSING_DETERMINE_POLL_COMPLETION = 'DETERMINE_POLL_COMPLETION',
+ FETCHING_SUMMARY = 'FETCHING_SUMMARY',
+ DETERMINE_SUMMARY = 'DETERMINE_SUMMARY',
+ // Laskennassa tai sen pyynnöissä tapahtui virhe, eikä laskenta siksi valmistunut
+ ERROR = 'ERROR',
+ // Laskenta valmistui, mutta yhteenvedossa on virheitä
+ COMPLETED_WITH_ERRORS = 'COMPLETED_WITH_ERRORS',
+ COMPLETED = 'COMPLETED',
+ CANCELING = 'CANCELING',
+}
+
+export enum LaskentaEventType {
+ START = 'START',
+ CONFIRM = 'CONFIRM',
+ CANCEL = 'CANCEL',
+ RESET_RESULTS = 'RESET_RESULTS',
+}
+
+export type LaskentaEvent = {
+ type: LaskentaEventType;
+};
+
+export type LaskentaStateTags =
+ | 'stopped'
+ | 'started'
+ | 'completed'
+ | 'canceling';
+
+export type LaskentaMachineSnapshot = SnapshotFrom<
+ ReturnType
+>;
+
+export type LaskentaActorRef = ActorRefFrom<
+ ReturnType
+>;
+
+export const createLaskentaMachine = (
+ params: StartLaskentaParams,
+ addToast: (toast: Toast) => void,
+) => {
+ const valinnanvaiheSelected: boolean = Boolean(params.valinnanvaiheNimi);
+ const keyPartValinnanvaihe = valinnanvaiheSelected
+ ? `-valinnanvaihe_${params.valinnanvaiheNumber ?? 0}`
+ : '';
+
+ const initialContext = {
+ laskenta: {},
+ startLaskentaParams: params,
+ seurantaTiedot: null,
+ // Laskennan yhteenveto
+ summary: null,
+ // Laskennan yhteenvedon virheilmoitukset
+ errorSummary: null,
+ // Mahdollinen virheolio. Huom! errorSummary voi sisältää jotain, vaikka error on null
+ error: null,
+ };
+
+ const machineKey = `haku_${params.haku.oid}-hakukohteet_${params.hakukohteet.map(prop('oid')).join('_')}${keyPartValinnanvaihe}`;
+ return setup({
+ types: {
+ context: {} as LaskentaContext,
+ tags: 'stopped' as LaskentaStateTags,
+ },
+ actors: {
+ startLaskenta: fromPromise(
+ ({ input }: { input: StartLaskentaParams }) => {
+ return kaynnistaLaskenta({
+ haku: input.haku,
+ hakukohteet: input.hakukohteet,
+ valinnanvaiheTyyppi: input.valinnanvaiheTyyppi,
+ sijoitellaankoHaunHakukohteetLaskennanYhteydessa:
+ input.sijoitellaanko,
+ valinnanvaihe: input.valinnanvaiheNumber,
+ });
+ },
+ ),
+ pollLaskenta: fromPromise(({ input }: { input: Laskenta }) => {
+ if (input.runningLaskenta) {
+ return getLaskennanSeurantaTiedot(input.runningLaskenta.loadingUrl);
+ }
+ return Promise.reject(
+ Error(
+ 'Tried to fetch seurantatiedot without having access to started laskenta',
+ ),
+ );
+ }),
+ fetchSummary: fromPromise(({ input }: { input: Laskenta }) => {
+ if (input.runningLaskenta) {
+ return getLaskennanYhteenveto(input.runningLaskenta.loadingUrl);
+ }
+ return Promise.reject(
+ Error('Tried to fetch summary without having access to laskenta'),
+ );
+ }),
+ stopLaskenta: fromPromise(({ input }: { input: Laskenta }) => {
+ if (input.runningLaskenta) {
+ return keskeytaLaskenta({
+ laskentaUuid: input.runningLaskenta.loadingUrl,
+ });
+ }
+ return Promise.reject(
+ Error(
+ 'Failed to stop laskenta without having access to started laskenta',
+ ),
+ );
+ }),
+ },
+ }).createMachine({
+ /** @xstate-layout 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-cKFQPn-3PN8-Bp9uQ-05CoFkgscHKVBU9EyCJtlsNQQFPa3stzNDKLhEK49eiPCdgMd4OCG64qZtlZes1V7zVKuVMJoyiVqOKL4B4FYqjvCSuuD0OFXiHh6iYF0-8TAsqeS9acZNnn8scS2B4FIcHJRqFg7+e4qQAmku8TsuhyzgksRCtOUczZQvNBYXquEgS1mxslNC+jT7aUPFyaSR9cKvH0JY1uK8152pjg65cbheoBxMOCQOfg9AV3XG2XQ65EVzI5IEQIQA */
+ id: `LaskentaMachine-${machineKey}`,
+ initial: LaskentaState.IDLE,
+ context: initialContext,
+ states: {
+ [LaskentaState.IDLE]: {
+ tags: ['stopped'],
+ initial: LaskentaState.INITIALIZED,
+ on: {
+ [LaskentaEventType.START]: {
+ target: LaskentaState.WAITING_CONFIRMATION,
+ },
+ [LaskentaEventType.RESET_RESULTS]: {
+ target: '#INITIALIZED',
+ },
+ },
+ states: {
+ previous: { type: 'history' },
+ [LaskentaState.INITIALIZED]: {
+ id: LaskentaState.INITIALIZED,
+ actions: assign(initialContext),
+ },
+ [LaskentaState.ERROR]: {
+ id: LaskentaState.ERROR,
+ },
+ [LaskentaState.COMPLETED_WITH_ERRORS]: {
+ id: LaskentaState.COMPLETED_WITH_ERRORS,
+ tags: ['completed'],
+ },
+ [LaskentaState.COMPLETED]: {
+ id: LaskentaState.COMPLETED,
+ tags: ['completed'],
+ entry: [
+ assign({
+ laskenta: ({ context }) =>
+ laskentaReducer(context.laskenta, {
+ calculatedTime: new Date(),
+ }),
+ }),
+ ({ context }) => {
+ const key = `laskenta-completed-for-${machineKey}`;
+ const message = valinnanvaiheSelected
+ ? 'valinnanhallinta.valmisvalinnanvaihe'
+ : 'valinnanhallinta.valmis';
+ const messageParams = valinnanvaiheSelected
+ ? {
+ nimi: context.startLaskentaParams.valinnanvaiheNimi ?? '',
+ }
+ : undefined;
+ addToast({ key, message, type: 'success', messageParams });
+ },
+ ],
+ },
+ },
+ },
+ [LaskentaState.WAITING_CONFIRMATION]: {
+ tags: ['stopped'],
+ on: {
+ [LaskentaEventType.CONFIRM]: {
+ target: LaskentaState.STARTING,
+ actions: assign(initialContext),
+ },
+ [LaskentaEventType.CANCEL]: {
+ target: 'IDLE.previous',
+ },
+ },
+ },
+ [LaskentaState.STARTING]: {
+ tags: ['started'],
+ invoke: {
+ src: 'startLaskenta',
+ input: ({ context }) => context.startLaskentaParams,
+ onDone: {
+ target: LaskentaState.PROCESSING,
+ actions: assign({
+ laskenta: ({ event, context }) =>
+ laskentaReducer(context.laskenta, {
+ runningLaskenta: event.output,
+ }),
+ }),
+ },
+ onError: {
+ target: '#ERROR',
+ actions: assign({
+ error: ({ event }) => event.error as Error,
+ }),
+ },
+ },
+ },
+ [LaskentaState.PROCESSING]: {
+ tags: ['started'],
+ initial: LaskentaState.PROCESSING_FETCHING,
+ on: {
+ [LaskentaEventType.CANCEL]: '#CANCELING',
+ },
+ states: {
+ [LaskentaState.PROCESSING_FETCHING]: {
+ invoke: {
+ src: 'pollLaskenta',
+ input: ({ context }) => context.laskenta,
+ onDone: {
+ target: LaskentaState.PROCESSING_DETERMINE_POLL_COMPLETION,
+ actions: assign({
+ seurantaTiedot: ({ event }) => event.output,
+ }),
+ },
+ onError: {
+ target: '#ERROR',
+ actions: assign({
+ error: ({ event }) => event.error as Error,
+ }),
+ },
+ },
+ },
+ [LaskentaState.PROCESSING_WAITING]: {
+ after: {
+ [POLLING_INTERVAL]: LaskentaState.PROCESSING_FETCHING,
+ },
+ },
+ [LaskentaState.PROCESSING_DETERMINE_POLL_COMPLETION]: {
+ always: [
+ {
+ guard: ({ context }) =>
+ context.seurantaTiedot?.tila === 'VALMIS' ||
+ context.seurantaTiedot?.tila === 'PERUUTETTU',
+ target: '#FETCHING_SUMMARY',
+ },
+ {
+ target: LaskentaState.PROCESSING_WAITING,
+ },
+ ],
+ },
+ [LaskentaState.CANCELING]: {
+ id: LaskentaState.CANCELING,
+ tags: ['canceling'],
+ invoke: {
+ src: 'stopLaskenta',
+ input: ({ context }) => context.laskenta,
+ onDone: {
+ target: '#FETCHING_SUMMARY',
+ },
+ onError: {
+ target: '#ERROR',
+ },
+ },
+ },
+ },
+ },
+ [LaskentaState.FETCHING_SUMMARY]: {
+ tags: ['started'],
+ id: LaskentaState.FETCHING_SUMMARY,
+ invoke: {
+ src: 'fetchSummary',
+ input: ({ context }) => context.laskenta,
+ onDone: {
+ target: LaskentaState.DETERMINE_SUMMARY,
+ actions: assign({
+ summary: ({ event }) => event.output,
+ // TODO: Poista errorSummary, kun virheiden esittäminen on yhdenmukaistettu myös "valinnan hallinta"-näkymässä
+ errorSummary: ({ event }) =>
+ event.output?.hakukohteet
+ ?.filter((hk) =>
+ hk.ilmoitukset?.some((i) => i.tyyppi === 'VIRHE'),
+ )
+ .map((hakukohde) => {
+ return {
+ hakukohdeOid: hakukohde.hakukohdeOid,
+ notifications: hakukohde.ilmoitukset?.map(
+ (i) => i.otsikko,
+ ),
+ };
+ })[0],
+ seurantaTiedot: ({ event, context }) => {
+ const hakukohteitaYhteensa =
+ context.seurantaTiedot?.hakukohteitaYhteensa ?? 0;
+
+ const hakukohteitaValmiina =
+ event.output?.hakukohteet?.filter(
+ (hk) => hk.tila === 'VALMIS',
+ )?.length ?? 0;
+ const hakukohteitaKeskeytetty =
+ hakukohteitaYhteensa - hakukohteitaValmiina;
+ const tila = event.output.tila ?? context?.seurantaTiedot?.tila;
+
+ return context.seurantaTiedot
+ ? {
+ ...context.seurantaTiedot,
+ tila,
+ hakukohteitaValmiina:
+ hakukohteitaYhteensa - hakukohteitaKeskeytetty,
+ hakukohteitaKeskeytetty,
+ }
+ : context.seurantaTiedot;
+ },
+ }),
+ },
+ onError: {
+ target: '#ERROR',
+ actions: assign({
+ error: ({ event }) => event.error as Error,
+ }),
+ },
+ },
+ },
+ [LaskentaState.DETERMINE_SUMMARY]: {
+ tags: ['started'],
+ always: [
+ {
+ guard: ({ context }) =>
+ (context.seurantaTiedot != null &&
+ context.seurantaTiedot.hakukohteitaKeskeytetty > 0) ||
+ (context.errorSummary?.notifications?.length ?? 0) > 0,
+ target: '#COMPLETED_WITH_ERRORS',
+ actions: assign({
+ laskenta: ({ context }) =>
+ laskentaReducer(context.laskenta, {
+ errorMessage: context.errorSummary?.notifications,
+ }),
+ }),
+ },
+ {
+ target: '#COMPLETED',
+ },
+ ],
+ },
+ },
+ });
+};
+
+type LaskentaStateParams = {
+ haku: Haku;
+ haunAsetukset: HaunAsetukset;
+ hakukohteet: Hakukohde | Array;
+ vaihe?: Valinnanvaihe;
+ valinnanvaiheNumber?: number;
+ addToast: (toast: Toast) => void;
+};
+
+export const useLaskentaState = ({
+ haku,
+ haunAsetukset,
+ hakukohteet,
+ vaihe,
+ valinnanvaiheNumber,
+}: LaskentaStateParams) => {
+ const { addToast } = useToaster();
+
+ const laskentaMachine = useMemo(() => {
+ return createLaskentaMachine(
+ {
+ haku,
+ hakukohteet: Array.isArray(hakukohteet) ? hakukohteet : [hakukohteet],
+ sijoitellaanko: sijoitellaankoHaunHakukohteetLaskennanYhteydessa(
+ haku,
+ haunAsetukset,
+ ),
+ ...(vaihe && valinnanvaiheNumber
+ ? {
+ valinnanvaiheTyyppi: vaihe.tyyppi,
+ valinnanvaiheNumber,
+ valinnanvaiheNimi: vaihe.nimi,
+ }
+ : {}),
+ },
+ addToast,
+ );
+ }, [haku, hakukohteet, haunAsetukset, vaihe, valinnanvaiheNumber, addToast]);
+
+ return useMachine(laskentaMachine);
+};
+
+export const useLaskentaError = (actorRef: LaskentaActorRef) => {
+ const error = useSelector(actorRef, (state) => state.context.error);
+ const laskenta = useSelector(actorRef, (state) => state.context.laskenta);
+ const hasError = laskenta.errorMessage != null || error;
+
+ return hasError
+ ? (laskenta.errorMessage ?? '' + (error?.message ?? error))
+ : '';
+};
diff --git a/src/app/lib/theme.tsx b/src/app/lib/theme.tsx
index 0e93d170..26850b9f 100644
--- a/src/app/lib/theme.tsx
+++ b/src/app/lib/theme.tsx
@@ -27,15 +27,6 @@ export const notLarge = (theme: Theme) => theme.breakpoints.down('lg');
export const THEME_OVERRIDES: ThemeOptions = {
components: {
- MuiInputBase: {
- styleOverrides: {
- root: {
- borderColor: ophColors.grey800,
- borderRadius: '2px',
- height: '48px',
- },
- },
- },
MuiDialog: {
defaultProps: {
fullWidth: true,
diff --git a/src/app/lib/types/laskenta-types.ts b/src/app/lib/types/laskenta-types.ts
index 46d78737..4824e933 100644
--- a/src/app/lib/types/laskenta-types.ts
+++ b/src/app/lib/types/laskenta-types.ts
@@ -66,10 +66,11 @@ export type LaskettuValinnanVaiheModel = {
};
export type SeurantaTiedot = {
- tila: 'VALMIS' | 'MENEILLAAN';
+ tila: 'VALMIS' | 'MENEILLAAN' | 'ALOITTAMATTA' | 'PERUUTETTU';
hakukohteitaYhteensa: number;
hakukohteitaValmiina: number;
hakukohteitaKeskeytetty: number;
+ jonosija: number | null;
};
export type LaskentaStart = {
@@ -79,7 +80,19 @@ export type LaskentaStart = {
export type LaskentaErrorSummary = {
hakukohdeOid: string;
- notifications: string[] | undefined;
+ notifications: Array | undefined;
+};
+
+export type LaskentaSummary = {
+ tila: 'PERUUTETTU' | 'VALMIS';
+ hakukohteet: Array<{
+ hakukohdeOid: string;
+ tila: 'TEKEMATTA' | 'VALMIS' | 'VIRHE';
+ ilmoitukset: Array<{ otsikko: string; tyyppi: string }>;
+ }>;
+ ilmoitus?: {
+ otsikko: string;
+ };
};
export enum ValintakoeOsallistuminenTulos {
diff --git a/src/app/lib/valintalaskenta-service.ts b/src/app/lib/valintalaskenta-service.ts
index 0f831a1e..72da8abc 100644
--- a/src/app/lib/valintalaskenta-service.ts
+++ b/src/app/lib/valintalaskenta-service.ts
@@ -11,7 +11,7 @@ import {
HakijaryhmanHakija,
HakukohteenHakijaryhma,
JarjestyskriteeriTila,
- LaskentaErrorSummary,
+ LaskentaSummary,
LaskentaStart,
LaskettuValinnanVaiheModel,
SeurantaTiedot,
@@ -38,9 +38,9 @@ import {
HarkinnanvaraisestiHyvaksytty,
} from './types/harkinnanvaraiset-types';
import { queryOptions } from '@tanstack/react-query';
-import { TranslatedName } from './localization/localization-types';
import { getFullnameOfHakukohde, Haku, Hakukohde } from './types/kouta-types';
import { ValinnanvaiheTyyppi } from './types/valintaperusteet-types';
+import { translateName } from './localization/translation-utils';
const formSearchParamsForStartLaskenta = ({
laskentaUrl,
@@ -49,24 +49,22 @@ const formSearchParamsForStartLaskenta = ({
valinnanvaiheTyyppi,
sijoitellaankoHaunHakukohteetLaskennanYhteydessa,
valinnanvaihe,
- translateEntity,
}: {
laskentaUrl: URL;
haku: Haku;
- hakukohde: Hakukohde;
+ hakukohde?: Hakukohde;
valinnanvaiheTyyppi?: ValinnanvaiheTyyppi;
sijoitellaankoHaunHakukohteetLaskennanYhteydessa: boolean;
valinnanvaihe?: number;
- translateEntity: (translateable: TranslatedName) => string;
}): URL => {
laskentaUrl.searchParams.append(
'erillishaku',
'' + sijoitellaankoHaunHakukohteetLaskennanYhteydessa,
);
- laskentaUrl.searchParams.append('haunnimi', translateEntity(haku.nimi));
+ laskentaUrl.searchParams.append('haunnimi', translateName(haku.nimi));
laskentaUrl.searchParams.append(
'nimi',
- getFullnameOfHakukohde(hakukohde, translateEntity),
+ hakukohde ? getFullnameOfHakukohde(hakukohde, translateName) : '',
);
if (valinnanvaihe && valinnanvaiheTyyppi !== ValinnanvaiheTyyppi.VALINTAKOE) {
laskentaUrl.searchParams.append('valinnanvaihe', '' + valinnanvaihe);
@@ -81,34 +79,39 @@ const formSearchParamsForStartLaskenta = ({
};
type LaskentaStatusResponseData = {
+ latausUrl: string;
lisatiedot: {
luotiinkoUusiLaskenta: boolean;
};
- latausUrl: string;
};
-export const kaynnistaLaskenta = async (
- haku: Haku,
- hakukohde: Hakukohde,
- valinnanvaiheTyyppi: ValinnanvaiheTyyppi,
- sijoitellaankoHaunHakukohteetLaskennanYhteydessa: boolean,
- valinnanvaihe: number,
- translateEntity: (translateable: TranslatedName) => string,
-): Promise => {
+export const kaynnistaLaskenta = async ({
+ haku,
+ hakukohteet,
+ valinnanvaiheTyyppi,
+ sijoitellaankoHaunHakukohteetLaskennanYhteydessa,
+ valinnanvaihe,
+}: {
+ haku: Haku;
+ hakukohteet: Array;
+ valinnanvaiheTyyppi?: ValinnanvaiheTyyppi;
+ sijoitellaankoHaunHakukohteetLaskennanYhteydessa: boolean;
+ valinnanvaihe?: number;
+}): Promise => {
+ const singleHakukohde = hakukohteet.length === 1 ? hakukohteet[0] : undefined;
const laskentaUrl = formSearchParamsForStartLaskenta({
laskentaUrl: new URL(
- `${configuration.valintalaskentaServiceUrl}valintalaskentakerralla/haku/${haku.oid}/tyyppi/HAKUKOHDE/whitelist/true?`,
+ `${configuration.valintalaskentakerrallaUrl}/haku/${haku.oid}/tyyppi/HAKUKOHDE/whitelist/true?`,
),
haku,
- hakukohde,
+ hakukohde: singleHakukohde,
valinnanvaiheTyyppi: valinnanvaiheTyyppi,
sijoitellaankoHaunHakukohteetLaskennanYhteydessa,
valinnanvaihe,
- translateEntity,
});
const response = await client.post(
laskentaUrl.toString(),
- [hakukohde.oid],
+ hakukohteet.map(prop('oid')),
);
return {
startedNewLaskenta: response.data?.lisatiedot?.luotiinkoUusiLaskenta,
@@ -116,55 +119,23 @@ export const kaynnistaLaskenta = async (
};
};
-export const kaynnistaLaskentaHakukohteenValinnanvaiheille = async (
- haku: Haku,
- hakukohde: Hakukohde,
- sijoitellaankoHaunHakukohteetLaskennanYhteydessa: boolean,
- translateEntity: (translateable: TranslatedName) => string,
-): Promise => {
- const laskentaUrl = formSearchParamsForStartLaskenta({
- laskentaUrl: new URL(
- `${configuration.valintalaskentaServiceUrl}valintalaskentakerralla/haku/${haku.oid}/tyyppi/HAKUKOHDE/whitelist/true?`,
- ),
- haku,
- hakukohde,
- sijoitellaankoHaunHakukohteetLaskennanYhteydessa,
- translateEntity,
- });
- const response = await client.post(
- laskentaUrl.toString(),
- [hakukohde.oid],
+export const keskeytaLaskenta = async ({
+ laskentaUuid,
+}: {
+ laskentaUuid: string;
+}): Promise => {
+ await client.delete(
+ `${configuration.valintalaskentakerrallaUrl}/haku/${laskentaUuid}`,
);
- return {
- startedNewLaskenta: response.data?.lisatiedot?.luotiinkoUusiLaskenta,
- loadingUrl: response.data?.latausUrl,
- };
};
-export const getLaskennanTilaHakukohteelle = async (
+export const getLaskennanYhteenveto = async (
loadingUrl: string,
-): Promise => {
- const response = await client.get<{
- hakukohteet: Array<{
- hakukohdeOid: string;
- ilmoitukset: [{ otsikko: string; tyyppi: string }] | null;
- }>;
- }>(
- `${configuration.valintalaskentaServiceUrl}valintalaskentakerralla/status/${loadingUrl}/yhteenveto`,
+): Promise => {
+ const response = await client.get(
+ `${configuration.valintalaskentakerrallaUrl}/status/${loadingUrl}/yhteenveto`,
);
- return response.data?.hakukohteet
- ?.filter((hk) => hk.ilmoitukset?.some((i) => i.tyyppi === 'VIRHE'))
- .map(
- (hakukohde: {
- hakukohdeOid: string;
- ilmoitukset: [{ otsikko: string }] | null;
- }) => {
- return {
- hakukohdeOid: hakukohde.hakukohdeOid,
- notifications: hakukohde.ilmoitukset?.map((i) => i.otsikko),
- };
- },
- )[0];
+ return response.data;
};
export const getHakukohteenLasketutValinnanvaiheet = async (
@@ -236,6 +207,7 @@ export const getLaskennanSeurantaTiedot = async (loadingUrl: string) => {
hakukohteitaYhteensa: response.data?.hakukohteitaYhteensa,
hakukohteitaValmiina: response.data?.hakukohteitaValmiina,
hakukohteitaKeskeytetty: response.data?.hakukohteitaKeskeytetty,
+ jonosija: response.data?.jonosija,
};
};
diff --git a/src/app/lib/valintaperusteet.test.ts b/src/app/lib/valintaperusteet.test.ts
index 279770a0..65101ae8 100644
--- a/src/app/lib/valintaperusteet.test.ts
+++ b/src/app/lib/valintaperusteet.test.ts
@@ -88,7 +88,7 @@ test('laskenta is not used for valinnanvaihe when jonos are not using laskenta',
expect(isLaskentaUsedForValinnanvaihe(vaihe)).toBeFalsy();
});
-test('laskenta is not used for valinnanvaihe when jonos best before date has passed ', () => {
+test('laskenta is not used for valinnanvaihe when jonos best before date has passed', () => {
const vaihe: Valinnanvaihe = {
aktiivinen: true,
jonot: [
diff --git a/src/app/lib/xstate-utils.ts b/src/app/lib/xstate-utils.ts
new file mode 100644
index 00000000..35f75f64
--- /dev/null
+++ b/src/app/lib/xstate-utils.ts
@@ -0,0 +1,19 @@
+import { useMachine } from '@xstate/react';
+
+import { createBrowserInspector } from '@statelyai/inspect';
+import { isServer } from '@tanstack/react-query';
+import { xstateInspect } from './configuration';
+
+type UseMachineParams = Parameters;
+
+const inspect =
+ xstateInspect && isServer
+ ? undefined
+ : (createBrowserInspector()?.inspect ?? undefined);
+
+export const useXstateMachine = (
+ machine: UseMachineParams[0],
+ options: UseMachineParams[1] = {},
+) => {
+ return useMachine(machine, { inspect, ...options });
+};
diff --git a/src/app/lokalisaatio/fi.json b/src/app/lokalisaatio/fi.json
index bfb06607..c5ac2364 100644
--- a/src/app/lokalisaatio/fi.json
+++ b/src/app/lokalisaatio/fi.json
@@ -410,6 +410,21 @@
"valintalaskenta-save-success": "Valintalaskennan tietojen tallentaminen onnistui",
"valintalaskenta-delete-error": "Valintalaskennan muokkauksen poistaminen epäonnistui",
"valintalaskenta-delete-success": "Valintalaskennan muokkauksen poistaminen onnistui",
- "pistesyotto": "Pistesyöttö"
+ "pistesyotto": "Pistesyöttö",
+ "suorita-valintalaskenta": "Suorita valintalaskenta",
+ "keskeyta-valintalaskenta": "Keskeytä valintalaskenta",
+ "valintalaskenta": "Valintalaskenta",
+ "sulje-laskennan-tiedot": "Sulje laskennan tiedot",
+ "piilota-suorittamattomat-hakukohteet": "Piilota suorittamattomat hakukohteet",
+ "nayta-suorittamattomat-hakukohteet": "Näytä suorittamattomat hakukohteet",
+ "syy": "Syy",
+ "valintalaskenta-epaonnistui": "Valintalaskenta epäonnistui",
+ "hakukohteita-valmiina": "Hakukohteita valmiina",
+ "suorittamattomia-hakukohteita": "Suorittamattomia hakukohteita",
+ "keskeytetaan-laskentaa": "Keskeytetään laskentaa...",
+ "kaynnistetaan-laskentaa": "Käynnistetään laskentaa...",
+ "tehtava-on-laskennassa-jonosijalla": "Tehtävä on laskennassa jonosijalla",
+ "tehtava-on-laskennassa-parhaillaan": "Tehtävä on laskennassa parhaillaan",
+ "laskenta-on-paattynyt": "Laskenta on päättynyt"
}
}
diff --git a/tests/e2e/hakukohde-tabs.spec.ts b/tests/e2e/hakukohde-tabs.spec.ts
index 4d48fd85..d441fd41 100644
--- a/tests/e2e/hakukohde-tabs.spec.ts
+++ b/tests/e2e/hakukohde-tabs.spec.ts
@@ -56,10 +56,10 @@ test('navigates to hakukohde tabs', async ({ page }) => {
await expect(hakukohdeNavItems).toHaveCount(3);
await hakukohdeNavItems.first().click();
await expect(page.locator('span.organisaatioLabel')).toHaveText(
- 'Tampereen yliopisto, Rakennetun ympäristön tiedekunta',
+ 'Tampereen yliopisto, Tekniikan ja luonnontieteiden tiedekunta',
);
await expect(page.locator('span.hakukohdeLabel')).toHaveText(
- 'Finnish MAOL competition route, Technology, Sustainable Urban Development, Bachelor and Master of Science (Technology) (3 + 2 yrs)',
+ 'Finnish MAOL competition route, Computing and Electrical Engineering, Science and Engineering, Bachelor and Master of Science (Technology) (3 + 2 yrs)',
);
await expect(page.locator('h3')).toHaveText('Valintatapajonot');
});
diff --git a/tests/e2e/henkilo.spec.ts b/tests/e2e/henkilo.spec.ts
index e4583435..b6fa8eee 100644
--- a/tests/e2e/henkilo.spec.ts
+++ b/tests/e2e/henkilo.spec.ts
@@ -24,10 +24,11 @@ import { forEachObj, isShallowEqual } from 'remeda';
import { NDASH } from '@/app/lib/constants';
const HAKUKOHDE_OID = '1.2.246.562.20.00000000000000045105';
+const NUKETTAJA_HAKEMUS_OID = '1.2.246.562.11.00000000000001796027';
const VALINNAN_TULOS_RESULT = hakemusValinnanTulosFixture({
hakukohdeOid: HAKUKOHDE_OID,
- hakemusOid: '1.2.246.562.11.00000000000001796027',
+ hakemusOid: NUKETTAJA_HAKEMUS_OID,
valintatapajonoOid: '17093042998533736417074016063604',
henkiloOid: '1.2.246.562.24.69259807406',
valinnantila: SijoittelunTila.HYVAKSYTTY,
@@ -50,15 +51,13 @@ test.beforeEach(async ({ page }) => {
await page.route(configuration.hakemuksetUrl, (route) => {
return route.fulfill({
- json: HAKENEET.filter(
- ({ oid }) => oid === '1.2.246.562.11.00000000000001793706',
- ),
+ json: HAKENEET.filter(({ oid }) => oid === NUKETTAJA_HAKEMUS_OID),
});
});
await page.route(
configuration.hakemuksenLasketutValinnanvaiheetUrl({
hakuOid: '1.2.246.562.29.00000000000000045102',
- hakemusOid: '1.2.246.562.11.00000000000001796027',
+ hakemusOid: NUKETTAJA_HAKEMUS_OID,
}),
(route) =>
route.fulfill({
@@ -68,7 +67,7 @@ test.beforeEach(async ({ page }) => {
await page.route(
configuration.hakemuksenSijoitteluajonTuloksetUrl({
hakuOid: '1.2.246.562.29.00000000000000045102',
- hakemusOid: '1.2.246.562.11.00000000000001796027',
+ hakemusOid: NUKETTAJA_HAKEMUS_OID,
}),
(route) => {
return route.fulfill({
@@ -79,7 +78,7 @@ test.beforeEach(async ({ page }) => {
await page.route(
configuration.hakemuksenValinnanTulosUrl({
- hakemusOid: '1.2.246.562.11.00000000000001796027',
+ hakemusOid: NUKETTAJA_HAKEMUS_OID,
}),
(route) => {
return route.fulfill({
@@ -203,7 +202,7 @@ test('Displays selected henkilö info with hakutoive but without valintalaskenta
await page.route(
configuration.hakemuksenLasketutValinnanvaiheetUrl({
hakuOid: '1.2.246.562.29.00000000000000045102',
- hakemusOid: '1.2.246.562.11.00000000000001796027',
+ hakemusOid: NUKETTAJA_HAKEMUS_OID,
}),
(route) =>
route.fulfill({
@@ -216,7 +215,7 @@ test('Displays selected henkilö info with hakutoive but without valintalaskenta
await page.route(
configuration.hakemuksenSijoitteluajonTuloksetUrl({
hakuOid: '1.2.246.562.29.00000000000000045102',
- hakemusOid: '1.2.246.562.11.00000000000001796027',
+ hakemusOid: NUKETTAJA_HAKEMUS_OID,
}),
(route) => {
return route.fulfill({
@@ -227,7 +226,7 @@ test('Displays selected henkilö info with hakutoive but without valintalaskenta
await page.route(
configuration.hakemuksenValinnanTulosUrl({
- hakemusOid: '1.2.246.562.11.00000000000001796027',
+ hakemusOid: NUKETTAJA_HAKEMUS_OID,
}),
(route) => {
return route.fulfill({
@@ -325,7 +324,7 @@ test('Displays selected henkilö hakutoiveet with laskenta results only', async
await page.route(
configuration.hakemuksenSijoitteluajonTuloksetUrl({
hakuOid: '1.2.246.562.29.00000000000000045102',
- hakemusOid: '1.2.246.562.11.00000000000001796027',
+ hakemusOid: NUKETTAJA_HAKEMUS_OID,
}),
(route) => {
return route.fulfill({
@@ -336,7 +335,7 @@ test('Displays selected henkilö hakutoiveet with laskenta results only', async
await page.route(
configuration.hakemuksenValinnanTulosUrl({
- hakemusOid: '1.2.246.562.11.00000000000001796027',
+ hakemusOid: NUKETTAJA_HAKEMUS_OID,
}),
(route) => {
return route.fulfill({
@@ -451,7 +450,7 @@ test('Sends valintalaskenta save request with right values and shows success not
page,
}) => {
const muokkausUrl = configuration.jarjestyskriteeriMuokkausUrl({
- hakemusOid: '1.2.246.562.11.00000000000001796027',
+ hakemusOid: NUKETTAJA_HAKEMUS_OID,
valintatapajonoOid: '17093042998533736417074016063604',
jarjestyskriteeriPrioriteetti: 0,
});
@@ -500,7 +499,7 @@ test('Sends valintalaskenta save request with right values and shows success not
test('Show notification on valintalaskenta save error', async ({ page }) => {
await page.route(
configuration.jarjestyskriteeriMuokkausUrl({
- hakemusOid: '1.2.246.562.11.00000000000001796027',
+ hakemusOid: NUKETTAJA_HAKEMUS_OID,
valintatapajonoOid: '17093042998533736417074016063604',
jarjestyskriteeriPrioriteetti: 0,
}),
@@ -636,149 +635,345 @@ test('Show notification on valinta save error', async ({ page }) => {
).toBeVisible();
});
-test('Displays pistesyöttö', async ({ page }) => {
- await page.goto(
- '/valintojen-toteuttaminen/haku/1.2.246.562.29.00000000000000045102/henkilo/1.2.246.562.11.00000000000001796027',
- );
+test.describe('Pistesyöttö', () => {
+ test('Displays pistesyöttö', async ({ page }) => {
+ await page.goto(
+ '/valintojen-toteuttaminen/haku/1.2.246.562.29.00000000000000045102/henkilo/1.2.246.562.11.00000000000001796027',
+ );
- await expectAllSpinnersHidden(page);
+ await expectAllSpinnersHidden(page);
- const pistesyottoHeading = page.getByRole('heading', { name: 'Pistesyöttö' });
- await expect(pistesyottoHeading).toBeVisible();
+ const pistesyottoHeading = page.getByRole('heading', {
+ name: 'Pistesyöttö',
+ });
+ await expect(pistesyottoHeading).toBeVisible();
- const nakkikoe = page.getByRole('region', {
- name: 'Nakkikoe, oletko nakkisuojassa?',
- });
+ const nakkikoe = page.getByRole('region', {
+ name: 'Nakkikoe, oletko nakkisuojassa?',
+ });
- await expect(nakkikoe).toBeVisible();
+ await expect(nakkikoe).toBeVisible();
- const nakkiKyllaInput = nakkikoe.getByText('Kyllä');
- await expect(nakkiKyllaInput).toBeVisible();
- const nakkiOsallistuiInput = nakkikoe.getByText('Osallistui', {
- exact: true,
- });
- await expect(nakkiOsallistuiInput).toBeVisible();
+ const nakkiKyllaInput = nakkikoe.getByText('Kyllä');
+ await expect(nakkiKyllaInput).toBeVisible();
+ const nakkiOsallistuiInput = nakkikoe.getByText('Osallistui', {
+ exact: true,
+ });
+ await expect(nakkiOsallistuiInput).toBeVisible();
- const nakkiTallennaButton = nakkikoe.getByRole('button', {
- name: 'Tallenna',
- });
+ const nakkiTallennaButton = nakkikoe.getByRole('button', {
+ name: 'Tallenna',
+ });
- await expect(nakkiTallennaButton).toBeEnabled();
+ await expect(nakkiTallennaButton).toBeEnabled();
- const koksakoe = page.getByRole('region', {
- name: `Köksäkokeen arvosana 4${NDASH}10`,
- });
+ const koksakoe = page.getByRole('region', {
+ name: `Köksäkokeen arvosana 4${NDASH}10`,
+ });
- await expect(koksakoe).toBeVisible();
+ await expect(koksakoe).toBeVisible();
- const koksaPisteetInput = koksakoe.getByLabel('Pisteet');
- await expect(koksaPisteetInput).toBeVisible();
- const koksaOsallistuiInput = koksakoe.getByText('Osallistui', {
- exact: true,
- });
- await expect(koksaOsallistuiInput).toBeVisible();
+ const koksaPisteetInput = koksakoe.getByLabel('Pisteet');
+ await expect(koksaPisteetInput).toBeVisible();
+ const koksaOsallistuiInput = koksakoe.getByText('Osallistui', {
+ exact: true,
+ });
+ await expect(koksaOsallistuiInput).toBeVisible();
- const koksaTallennaButton = koksakoe.getByRole('button', {
- name: 'Tallenna',
+ const koksaTallennaButton = koksakoe.getByRole('button', {
+ name: 'Tallenna',
+ });
+ await expect(koksaTallennaButton).toBeEnabled();
});
- await expect(koksaTallennaButton).toBeEnabled();
-});
-test('Sends right data when saving pistesyotto and shows success toast', async ({
- page,
-}) => {
- const pisteetSaveUrl = configuration.koostetutPistetiedotHakukohteelleUrl({
- hakuOid: '1.2.246.562.29.00000000000000045102',
- hakukohdeOid: HAKUKOHDE_OID,
- });
+ test('Sends right data when saving pistesyotto and shows success toast', async ({
+ page,
+ }) => {
+ const pisteetSaveUrl = configuration.koostetutPistetiedotHakukohteelleUrl({
+ hakuOid: '1.2.246.562.29.00000000000000045102',
+ hakukohdeOid: HAKUKOHDE_OID,
+ });
- await page.route(pisteetSaveUrl, (route) => {
- if (route.request().method() === 'PUT') {
- return route.fulfill({
- status: 200,
- });
- }
- });
+ await page.route(pisteetSaveUrl, (route) => {
+ if (route.request().method() === 'PUT') {
+ return route.fulfill({
+ status: 200,
+ });
+ }
+ });
- await page.goto(
- '/valintojen-toteuttaminen/haku/1.2.246.562.29.00000000000000045102/henkilo/1.2.246.562.11.00000000000001796027',
- );
+ await page.goto(
+ '/valintojen-toteuttaminen/haku/1.2.246.562.29.00000000000000045102/henkilo/1.2.246.562.11.00000000000001796027',
+ );
- await expectAllSpinnersHidden(page);
+ await expectAllSpinnersHidden(page);
- const pistesyottoHeading = page.getByRole('heading', { name: 'Pistesyöttö' });
- await expect(pistesyottoHeading).toBeVisible();
+ const pistesyottoHeading = page.getByRole('heading', {
+ name: 'Pistesyöttö',
+ });
+ await expect(pistesyottoHeading).toBeVisible();
- const nakkikoe = page.getByRole('region', {
- name: 'Nakkikoe, oletko nakkisuojassa?',
- });
+ const nakkikoe = page.getByRole('region', {
+ name: 'Nakkikoe, oletko nakkisuojassa?',
+ });
- await selectOption(page, 'Osallistumisen tila', 'Ei osallistunut', nakkikoe);
+ await selectOption(
+ page,
+ 'Osallistumisen tila',
+ 'Ei osallistunut',
+ nakkikoe,
+ );
- const nakkiTallennaButton = nakkikoe.getByRole('button', {
- name: 'Tallenna',
+ const nakkiTallennaButton = nakkikoe.getByRole('button', {
+ name: 'Tallenna',
+ });
+
+ const [saveRes] = await Promise.all([
+ page.waitForRequest(
+ (request) =>
+ request.url() === pisteetSaveUrl && request.method() === 'PUT',
+ ),
+ nakkiTallennaButton.click(),
+ ]);
+
+ expect(saveRes.postDataJSON()).toMatchObject([
+ {
+ oid: '1.2.246.562.11.00000000000001796027',
+ personOid: '1.2.246.562.24.69259807406',
+ firstNames: 'Ruhtinas',
+ lastName: 'Nukettaja',
+ additionalData: {
+ 'nakki-osallistuminen': ValintakoeOsallistuminenTulos.EI_OSALLISTUNUT,
+ },
+ },
+ ]);
+
+ await expect(page.getByText('Tiedot tallennettu')).toBeVisible();
});
- const [saveRes] = await Promise.all([
- page.waitForRequest(
- (request) =>
- request.url() === pisteetSaveUrl && request.method() === 'PUT',
- ),
- nakkiTallennaButton.click(),
- ]);
+ test('Saving pistesyöttö shows error toast', async ({ page }) => {
+ const pisteetSaveUrl = configuration.koostetutPistetiedotHakukohteelleUrl({
+ hakuOid: '1.2.246.562.29.00000000000000045102',
+ hakukohdeOid: HAKUKOHDE_OID,
+ });
- expect(saveRes.postDataJSON()).toMatchObject([
- {
- oid: '1.2.246.562.11.00000000000001796027',
- personOid: '1.2.246.562.24.69259807406',
- firstNames: 'Ruhtinas',
- lastName: 'Nukettaja',
- additionalData: {
- 'nakki-osallistuminen': ValintakoeOsallistuminenTulos.EI_OSALLISTUNUT,
- },
- },
- ]);
+ await page.route(pisteetSaveUrl, (route) => {
+ if (route.request().method() === 'PUT') {
+ return route.fulfill({
+ status: 400,
+ });
+ }
+ });
- await expect(page.getByText('Tiedot tallennettu')).toBeVisible();
-});
+ await page.goto(
+ '/valintojen-toteuttaminen/haku/1.2.246.562.29.00000000000000045102/henkilo/1.2.246.562.11.00000000000001796027',
+ );
-test('Saving pistesyöttö shows error toast', async ({ page }) => {
- const pisteetSaveUrl = configuration.koostetutPistetiedotHakukohteelleUrl({
- hakuOid: '1.2.246.562.29.00000000000000045102',
- hakukohdeOid: HAKUKOHDE_OID,
- });
+ const nakkikoe = page.getByRole('region', {
+ name: 'Nakkikoe, oletko nakkisuojassa?',
+ });
- await page.route(pisteetSaveUrl, (route) => {
- if (route.request().method() === 'PUT') {
- return route.fulfill({
- status: 400,
- });
- }
+ await selectOption(
+ page,
+ 'Osallistumisen tila',
+ 'Ei osallistunut',
+ nakkikoe,
+ );
+
+ const nakkiTallennaButton = nakkikoe.getByRole('button', {
+ name: 'Tallenna',
+ });
+
+ await Promise.all([
+ page.waitForRequest(
+ (request) =>
+ request.url() === pisteetSaveUrl && request.method() === 'PUT',
+ ),
+ nakkiTallennaButton.click(),
+ ]);
+
+ await expect(
+ page.getByText('Tietojen tallentamisessa tapahtui virhe.'),
+ ).toBeVisible();
});
+});
+const startLaskenta = async (page: Page) => {
await page.goto(
'/valintojen-toteuttaminen/haku/1.2.246.562.29.00000000000000045102/henkilo/1.2.246.562.11.00000000000001796027',
);
- const nakkikoe = page.getByRole('region', {
- name: 'Nakkikoe, oletko nakkisuojassa?',
+ const valintalaskentaButton = page.getByRole('button', {
+ name: 'Suorita valintalaskenta',
});
- await selectOption(page, 'Osallistumisen tila', 'Ei osallistunut', nakkikoe);
+ await expect(valintalaskentaButton).toBeVisible();
+ await valintalaskentaButton.click();
- const nakkiTallennaButton = nakkikoe.getByRole('button', {
- name: 'Tallenna',
+ await page.getByRole('button', { name: 'Kyllä' }).click();
+};
+
+test.describe('Valintalaskenta', () => {
+ test('shows error when starting valintalaskenta fails', async ({ page }) => {
+ await page.route(
+ '*/**/resources/valintalaskentakerralla/haku/1.2.246.562.29.00000000000000045102/tyyppi/HAKUKOHDE/whitelist/true**',
+ async (route) => {
+ await route.fulfill({ status: 500, body: 'Unknown error' });
+ },
+ );
+ await startLaskenta(page);
+
+ const error = page.getByText('Valintalaskenta epäonnistuiUnknown error');
+ await expect(error).toBeVisible();
+ });
+
+ test('succesfully starts laskenta and shows yhteenveto errors', async ({
+ page,
+ }) => {
+ await page.route(
+ '*/**/resources/valintalaskentakerralla/haku/1.2.246.562.29.00000000000000045102/tyyppi/HAKUKOHDE/whitelist/true**',
+ async (route) => {
+ const started = {
+ lisatiedot: {
+ luotiinkoUusiLaskenta: true,
+ },
+ latausUrl: '12345abs',
+ };
+ await route.fulfill({
+ json: started,
+ });
+ },
+ );
+ await page.route(
+ '*/**/valintalaskenta-laskenta-service/resources/seuranta/yhteenveto/12345abs',
+ async (route) => {
+ const seuranta = {
+ tila: 'VALMIS',
+ hakukohteitaYhteensa: 1,
+ hakukohteitaValmiina: 1,
+ hakukohteitaKeskeytetty: 0,
+ };
+ await route.fulfill({
+ json: seuranta,
+ });
+ },
+ );
+ await page.route(
+ '*/**/resources/valintalaskentakerralla/status/12345abs/yhteenveto',
+ async (route) => {
+ await route.fulfill({
+ json: {
+ hakukohteet: [
+ {
+ hakukohdeOid: '1.2.246.562.20.00000000000000045105',
+ tila: 'VIRHE',
+ ilmoitukset: [
+ {
+ otsikko: 'Unknown Error',
+ },
+ ],
+ },
+ ],
+ },
+ });
+ },
+ );
+ await startLaskenta(page);
+ await expect(
+ page.getByText(
+ 'Laskenta on päättynyt. Hakukohteita valmiina 0/1. Suorittamattomia hakukohteita 1.',
+ ),
+ ).toBeVisible();
+
+ await page
+ .getByRole('button', { name: 'Näytä suorittamattomat hakukohteet' })
+ .click();
+
+ await expect(
+ page.getByText(
+ `Tampereen yliopisto, Rakennetun ympäristön tiedekunta ${NDASH} Finnish MAOL competition route, Technology, Sustainable Urban Development, Bachelor and Master of Science (Technology) (3 + 2 yrs) (1.2.246.562.20.00000000000000045105)`,
+ ),
+ ).toBeVisible();
+ await expect(page.getByText('Unknown Error')).toBeVisible();
+
+ await expect(
+ page.getByRole('button', { name: 'Suorita valintalaskenta' }),
+ ).toBeHidden();
+
+ await page.getByRole('button', { name: 'Sulje laskennan tiedot' }).click();
+
+ await expect(
+ page.getByRole('button', { name: 'Suorita valintalaskenta' }),
+ ).toBeVisible();
});
- await Promise.all([
- page.waitForRequest(
- (request) =>
- request.url() === pisteetSaveUrl && request.method() === 'PUT',
- ),
- nakkiTallennaButton.click(),
- ]);
+ test('shows results with success toast', async ({ page }) => {
+ await page.route(
+ '*/**/resources/valintalaskentakerralla/haku/1.2.246.562.29.00000000000000045102/tyyppi/HAKUKOHDE/whitelist/true**',
+ async (route) => {
+ const started = {
+ lisatiedot: {
+ luotiinkoUusiLaskenta: true,
+ },
+ latausUrl: '12345abs',
+ };
+ await route.fulfill({
+ json: started,
+ });
+ },
+ );
+ await page.route(
+ '*/**/valintalaskenta-laskenta-service/resources/seuranta/yhteenveto/12345abs',
+ async (route) => {
+ await route.fulfill({
+ json: {
+ tila: 'VALMIS',
+ hakukohteitaYhteensa: 1,
+ hakukohteitaValmiina: 1,
+ hakukohteitaKeskeytetty: 0,
+ },
+ });
+ },
+ );
+ await page.route(
+ '*/**/resources/valintalaskentakerralla/status/12345abs/yhteenveto',
+ async (route) => {
+ await route.fulfill({
+ json: {
+ tila: 'VALMIS',
+ hakukohteet: [
+ {
+ hakukohdeOid: '1.2.246.562.20.00000000000000045105',
+ tila: 'VALMIS',
+ ilmoitukset: [],
+ },
+ ],
+ },
+ });
+ },
+ );
+ await startLaskenta(page);
+ await expect(
+ page.getByText(
+ 'Laskenta on päättynyt. Hakukohteita valmiina 1/1. Suorittamattomia hakukohteita 0.',
+ ),
+ ).toBeVisible();
+ await expect(
+ page.getByText('Laskenta suoritettu onnistuneesti'),
+ ).toBeVisible();
- await expect(
- page.getByText('Tietojen tallentamisessa tapahtui virhe.'),
- ).toBeVisible();
+ await expect(
+ page.getByRole('button', { name: 'Näytä suorittamattomat hakukohteet' }),
+ ).toBeHidden();
+
+ await expect(
+ page.getByRole('button', { name: 'Suorita valintalaskenta' }),
+ ).toBeHidden();
+
+ await page.getByRole('button', { name: 'Sulje laskennan tiedot' }).click();
+
+ await expect(
+ page.getByRole('button', { name: 'Suorita valintalaskenta' }),
+ ).toBeVisible();
+ });
});
diff --git a/tests/e2e/valinnan-hallinta.spec.ts b/tests/e2e/valinnan-hallinta.spec.ts
index 355a8a3d..ccdae0ca 100644
--- a/tests/e2e/valinnan-hallinta.spec.ts
+++ b/tests/e2e/valinnan-hallinta.spec.ts
@@ -102,8 +102,10 @@ test('shows success toast when laskenta completes', async ({ page }) => {
async (route) => {
await route.fulfill({
json: {
+ tila: 'VALMIS',
hakukohteet: [
{
+ tila: 'VALMIS',
hakukohde: '1.2.246.562.20.00000000000000045105',
ilmoitukset: [],
},