From 7e7c93bc97954a9d4b08be5253ce316a0dcf5986 Mon Sep 17 00:00:00 2001 From: Reuben Ringdal Date: Thu, 6 Mar 2025 12:44:06 +0100 Subject: [PATCH 1/2] move calculators to async thunk --- preview/skjema/q.json | 272 +++++++++++++++-- preview/skjema/responses/qr.json | 138 +++++++-- src/actions/thunks.ts | 46 +++ src/calculators/__tests__/__data__/index.ts | 6 + src/calculators/__tests__/__data__/q.json | 273 ++++++++++++++++++ .../__tests__/runFhirPathUpdater-spec.ts | 245 ++++++++++++++++ .../__tests__/runScoringCalculator-spec.ts | 159 ++++++++++ .../__tests__/scoringCalculators-spec.tsx | 72 +++++ src/calculators/runFhirPathUpdater.ts | 245 ++++++++++++++++ src/calculators/runScoringCalculator.ts | 77 +++++ src/hooks/useFhirPathQrUpdater.tsx | 151 ---------- src/hooks/useOnAnswerChange.tsx | 18 +- src/hooks/useScoringCalculator.ts | 95 ------ src/util/actionRequester.ts | 9 + 14 files changed, 1499 insertions(+), 307 deletions(-) create mode 100644 src/actions/thunks.ts create mode 100644 src/calculators/__tests__/__data__/index.ts create mode 100644 src/calculators/__tests__/__data__/q.json create mode 100644 src/calculators/__tests__/runFhirPathUpdater-spec.ts create mode 100644 src/calculators/__tests__/runScoringCalculator-spec.ts create mode 100644 src/calculators/__tests__/scoringCalculators-spec.tsx create mode 100644 src/calculators/runFhirPathUpdater.ts create mode 100644 src/calculators/runScoringCalculator.ts delete mode 100644 src/hooks/useFhirPathQrUpdater.tsx delete mode 100644 src/hooks/useScoringCalculator.ts diff --git a/preview/skjema/q.json b/preview/skjema/q.json index c0959ad0..6a0d1bca 100644 --- a/preview/skjema/q.json +++ b/preview/skjema/q.json @@ -1,33 +1,271 @@ { "resourceType": "Questionnaire", - "id": "211f35d2-700c-4e0c-81dd-a020d17c728b", - "name": "NHN_Test_Tabell_Repeterendegrupper", - "title": "Testskjema tabell repeterende grupper", + "language": "nb-NO", + "id": "3981d77d-f69f-4c9c-85cd-8b07b05d93d2", + "name": "NHN_Test_Scoore_Calculation", + "title": "Test kopi og regning på score", "version": "0.1", "status": "draft", "publisher": "NHN", "meta": { - "security": [ - { - "code": "3", - "display": "Helsehjelp (Full)", - "system": "urn:oid:2.16.578.1.12.4.1.1.7618" - } - ] + "profile": ["http://ehelse.no/fhir/StructureDefinition/sdf-Questionnaire"], + "tag": [{ "system": "urn:ietf:bcp:47", "code": "nb-NO", "display": "Bokmål" }], + "security": [{ "code": "3", "display": "Helsehjelp (Full)", "system": "urn:oid:2.16.578.1.12.4.1.1.7618" }] }, + "contact": [{ "name": "http://www.nhn.no" }], "subjectType": ["Patient"], + "extension": [ + { + "url": "http://helsenorge.no/fhir/StructureDefinition/sdf-sidebar", + "valueCoding": { "system": "http://helsenorge.no/fhir/ValueSet/sdf-sidebar", "code": "1" } + }, + { + "url": "http://helsenorge.no/fhir/StructureDefinition/sdf-information-message", + "valueCoding": { "system": "http://helsenorge.no/fhir/ValueSet/sdf-information-message", "code": "1" } + }, + { + "url": "http://helsenorge.no/fhir/StructureDefintion/sdf-itemControl-visibility", + "valueCodeableConcept": { + "coding": [ + { "system": "http://helsenorge.no/fhir/CodeSystem/AttachmentRenderOptions", "code": "hide-help", "display": "Hide help texts" }, + { + "system": "http://helsenorge.no/fhir/CodeSystem/AttachmentRenderOptions", + "code": "hide-sublabel", + "display": "Hide sublabel texts" + } + ] + } + } + ], + "date": "2025-03-05T00:00:00+01:00", "item": [ { - "linkId": "6f08b879-733e-4a85-fb3d-419b3b6b5df1", + "linkId": "0701f3cf-73f5-48f7-8317-24c1223264a8", "type": "group", - "text": "Group 1", - "repeats": true, + "text": "Input scoring and calculation and copying of scoringvalue", + "required": false, "item": [ { - "linkId": "26509baa-e8d8-43f1-8b2a-a6e275667fe4", - "type": "string", - "text": "mandatory", - "required": true + "linkId": "verdi1", + "type": "choice", + "text": "Verdi 1", + "code": [ + { "system": "http://ehelse.no/Score", "code": "score", "display": "score" }, + { "system": "http://ehelse.no/scoringFormulas", "code": "QS", "display": "Question score" } + ], + "required": false, + "answerOption": [ + { + "valueCoding": { + "id": "96651f43-5ac1-4b87-83b8-49879c32daa6", + "code": "100", + "system": "urn:uuid:5ca4194b-32df-409e-81e5-8d4d0c600ee3", + "display": "Ja", + "extension": [{ "url": "http://hl7.org/fhir/StructureDefinition/ordinalValue", "valueDecimal": 100 }] + } + }, + { + "valueCoding": { + "id": "049bb493-4ec3-4df8-855b-12be2307360b", + "code": "200", + "system": "urn:uuid:5ca4194b-32df-409e-81e5-8d4d0c600ee3", + "display": "Nei", + "extension": [{ "url": "http://hl7.org/fhir/StructureDefinition/ordinalValue", "valueDecimal": 200 }] + } + } + ] + }, + { + "linkId": "verdi2", + "type": "choice", + "text": "Verdi 2", + "code": [ + { "system": "http://ehelse.no/Score", "code": "score", "display": "score" }, + { "system": "http://ehelse.no/scoringFormulas", "code": "QS", "display": "Question score" } + ], + "required": false, + "answerOption": [ + { + "valueCoding": { + "id": "96651f43-5ac1-4b87-83b8-49879c32daa6", + "code": "100", + "system": "urn:uuid:5ca4194b-32df-409e-81e5-8d4d0c600ee3", + "display": "Ja", + "extension": [{ "url": "http://hl7.org/fhir/StructureDefinition/ordinalValue", "valueDecimal": 100 }] + } + }, + { + "valueCoding": { + "id": "049bb493-4ec3-4df8-855b-12be2307360b", + "code": "200", + "system": "urn:uuid:5ca4194b-32df-409e-81e5-8d4d0c600ee3", + "display": "Nei", + "extension": [{ "url": "http://hl7.org/fhir/StructureDefinition/ordinalValue", "valueDecimal": 200 }] + } + } + ] + }, + { + "linkId": "verdi3", + "type": "choice", + "text": "Verdi 3", + "code": [ + { "system": "http://ehelse.no/Score", "code": "score", "display": "score" }, + { "system": "http://ehelse.no/scoringFormulas", "code": "QS", "display": "Question score" } + ], + "required": false, + "answerOption": [ + { + "valueCoding": { + "id": "96651f43-5ac1-4b87-83b8-49879c32daa6", + "code": "100", + "system": "urn:uuid:5ca4194b-32df-409e-81e5-8d4d0c600ee3", + "display": "Ja", + "extension": [{ "url": "http://hl7.org/fhir/StructureDefinition/ordinalValue", "valueDecimal": 100 }] + } + }, + { + "valueCoding": { + "id": "049bb493-4ec3-4df8-855b-12be2307360b", + "code": "200", + "system": "urn:uuid:5ca4194b-32df-409e-81e5-8d4d0c600ee3", + "display": "Nei", + "extension": [{ "url": "http://hl7.org/fhir/StructureDefinition/ordinalValue", "valueDecimal": 200 }] + } + } + ] + }, + { + "linkId": "verdi4", + "type": "choice", + "text": "Verdi 4", + "code": [ + { "system": "http://ehelse.no/Score", "code": "score", "display": "score" }, + { "system": "http://ehelse.no/scoringFormulas", "code": "QS", "display": "Question score" } + ], + "required": false, + "answerOption": [ + { + "valueCoding": { + "id": "96651f43-5ac1-4b87-83b8-49879c32daa6", + "code": "100", + "system": "urn:uuid:5ca4194b-32df-409e-81e5-8d4d0c600ee3", + "display": "Ja", + "extension": [{ "url": "http://hl7.org/fhir/StructureDefinition/ordinalValue", "valueDecimal": 100 }] + } + }, + { + "valueCoding": { + "id": "049bb493-4ec3-4df8-855b-12be2307360b", + "code": "200", + "system": "urn:uuid:5ca4194b-32df-409e-81e5-8d4d0c600ee3", + "display": "Nei", + "extension": [{ "url": "http://hl7.org/fhir/StructureDefinition/ordinalValue", "valueDecimal": 200 }] + } + } + ] + }, + { + "linkId": "Delsum", + "type": "integer", + "text": "Delsum", + "code": [ + { "system": "http://ehelse.no/Score", "code": "score", "display": "score" }, + { "system": "http://ehelse.no/scoringFormulas", "code": "SS", "display": "Section score" } + ], + "required": false, + "readOnly": true + }, + { + "linkId": "Totalsum", + "type": "integer", + "text": "Totalsum", + "code": [ + { "system": "http://ehelse.no/Score", "code": "score", "display": "score" }, + { "system": "http://ehelse.no/scoringFormulas", "code": "TS", "display": "Total score" } + ], + "required": false, + "readOnly": true + }, + { + "linkId": "aritmetisk_gjennomsnitt", + "type": "integer", + "text": "Aritmetisk gjennomsnitt basert på totalsum.", + "required": false, + "extension": [ + { + "url": "http://ehelse.no/fhir/StructureDefinition/sdf-calculatedExpression", + "valueString": "QuestionnaireResponse.descendants().where(linkId='Totalsum').answer.value / 4" + } + ], + "readOnly": true + }, + { + "linkId": "kopiert_felt", + "type": "integer", + "text": "Kopi", + "required": false, + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl", + "valueCodeableConcept": { + "coding": [{ "system": "http://hl7.org/fhir/ValueSet/questionnaire-item-control", "code": "data-receiver" }] + } + }, + { + "url": "http://hl7.org/fhir/StructureDefinition/cqf-expression", + "valueString": "QuestionnaireResponse.descendants().where(linkId='Totalsum').answer.value" + } + ], + "readOnly": true, + "enableWhen": [{ "answerBoolean": true, "question": "Totalsum", "operator": "exists" }] + }, + { + "linkId": "aritmetisk_gjennomsnitt_kopi", + "type": "integer", + "text": "Aritmetisk gjennomsnitt basert på kopiert felt", + "required": false, + "extension": [ + { + "url": "http://ehelse.no/fhir/StructureDefinition/sdf-calculatedExpression", + "valueString": "QuestionnaireResponse.descendants().where(linkId='kopiert_felt').answer.value / 4" + } + ], + "readOnly": true + } + ] + }, + { + "linkId": "498140b3-7c26-4492-dea5-24c829552347", + "type": "group", + "text": "Regning - Fhir path", + "required": false, + "item": [ + { "linkId": "Tall1", "type": "integer", "text": "Tall 1", "required": false }, + { "linkId": "Tall2", "type": "integer", "text": "Tall 2", "required": false }, + { + "linkId": "Sum", + "type": "integer", + "text": "Sum", + "required": false, + "extension": [ + { + "url": "http://ehelse.no/fhir/StructureDefinition/sdf-calculatedExpression", + "valueString": "QuestionnaireResponse.descendants().where(linkId='Tall1').answer.value + QuestionnaireResponse.descendants().where(linkId='Tall2').answer.value" + } + ] + }, + { + "linkId": "Gjennomsnitt", + "type": "integer", + "text": "Gjennomsnitt", + "required": false, + "extension": [ + { + "url": "http://ehelse.no/fhir/StructureDefinition/sdf-calculatedExpression", + "valueString": "QuestionnaireResponse.descendants().where(linkId='Sum').answer.value / 2" + } + ] } ] } diff --git a/preview/skjema/responses/qr.json b/preview/skjema/responses/qr.json index 604987c1..4bd22e54 100644 --- a/preview/skjema/responses/qr.json +++ b/preview/skjema/responses/qr.json @@ -3,71 +3,145 @@ "status": "in-progress", "item": [ { - "linkId": "62314607-656b-47bb-8b8a-a3bcbd579eb7", - "text": "Initial", + "linkId": "0701f3cf-73f5-48f7-8317-24c1223264a8", + "text": "Input scoring and calculation and copying of scoringvalue", "item": [ { - "linkId": "d342579c-2805-49f6-8c92-f4c8ecd07750", - "text": "Choice 1", + "linkId": "verdi1", + "text": "Verdi 1", "answer": [ { "valueCoding": { - "system": "urn:oid:2.16.578.1.12.4.1.1102", - "code": "2", - "display": "Nei" + "code": "100", + "display": "Ja", + "system": "urn:uuid:5ca4194b-32df-409e-81e5-8d4d0c600ee3" } - }, + } + ] + }, + { + "linkId": "verdi2", + "text": "Verdi 2", + "answer": [ { "valueCoding": { - "code": "3", - "display": "Vet ikke", - "system": "urn:oid:2.16.578.1.12.4.1.1102" + "code": "200", + "display": "Nei", + "system": "urn:uuid:5ca4194b-32df-409e-81e5-8d4d0c600ee3" } } ] }, { - "linkId": "8cd87d20-1583-4f79-f238-c5c5607e314a", - "text": "Choice 1", + "linkId": "verdi3", + "text": "Verdi 3", "answer": [ { "valueCoding": { - "system": "urn:oid:2.16.578.1.12.4.1.1102", - "code": "2", - "display": "Nei" + "code": "100", + "display": "Ja", + "system": "urn:uuid:5ca4194b-32df-409e-81e5-8d4d0c600ee3" } } ] + }, + { + "linkId": "verdi4", + "text": "Verdi 4", + "answer": [ + { + "valueCoding": { + "code": "100", + "display": "Ja", + "system": "urn:uuid:5ca4194b-32df-409e-81e5-8d4d0c600ee3" + } + } + ] + }, + { + "linkId": "Delsum", + "text": "Delsum", + "answer": [ + { + "valueInteger": 500 + } + ] + }, + { + "linkId": "Totalsum", + "text": "Totalsum", + "answer": [ + { + "valueInteger": 500 + } + ] + }, + { + "linkId": "aritmetisk_gjennomsnitt", + "text": "Aritmetisk gjennomsnitt basert på totalsum.", + "answer": [ + { + "valueInteger": 125 + } + ] + }, + { + "linkId": "kopiert_felt", + "text": "Kopi", + "answer": [ + { + "valueInteger": 500 + } + ] + }, + { + "linkId": "aritmetisk_gjennomsnitt_kopi", + "text": "Aritmetisk gjennomsnitt basert på kopiert felt", + "answer": [ + { + "valueInteger": 125 + } + ] } ] }, { - "linkId": "f3ca60d7-678b-49c5-812d-d76b30a5cccb", - "text": "Initial - radio", + "linkId": "498140b3-7c26-4492-dea5-24c829552347", + "text": "Regning - Fhir path", "item": [ { - "linkId": "8c4ec2fd-e05d-40d0-8c8e-5d6672ec34b7", - "text": "Radio 1", + "linkId": "Tall1", + "text": "Tall 1", "answer": [ { - "valueCoding": { - "system": "urn:oid:2.16.578.1.12.4.1.1102", - "code": "2", - "display": "Nei" - } + "valueInteger": 10 } ] }, { - "linkId": "282c6123-dd00-4786-9543-8605a22aca3e", - "text": "radio 2", + "linkId": "Tall2", + "text": "Tall 2", "answer": [ { - "valueCoding": { - "system": "urn:oid:2.16.578.1.12.4.1.1102", - "code": "2", - "display": "Nei" - } + "valueInteger": 20 + } + ] + }, + { + "linkId": "Sum", + "text": "Sum", + "answer": [ + { + "valueInteger": 30 + } + ] + }, + { + "linkId": "Gjennomsnitt", + "text": "Gjennomsnitt", + "answer": [ + { + "valueInteger": 15 } ] } diff --git a/src/actions/thunks.ts b/src/actions/thunks.ts new file mode 100644 index 00000000..16752c01 --- /dev/null +++ b/src/actions/thunks.ts @@ -0,0 +1,46 @@ +import { createAsyncThunk } from '@reduxjs/toolkit'; + +import { runFhirPathQrUpdater } from '@/calculators/runFhirPathUpdater'; +import { runScoringCalculator } from '@/calculators/runScoringCalculator'; +import { AppDispatch, RootState } from '@/reducers'; +import { ActionRequester } from '@/util/actionRequester'; +import { FhirPathExtensions } from '@/util/FhirPathExtensions'; +import { ScoringCalculator } from '@/util/scoringCalculator'; + +export const runCalculatorsAction = createAsyncThunk( + 'questionnaireResponse/update', + async (_, { getState, dispatch, rejectWithValue }) => { + const state = getState(); + const questionnaire = state.refero.form.FormDefinition.Content; + const questionnaireResponse = state.refero.form.FormData.Content; + + if (!questionnaire || !questionnaireResponse) { + return rejectWithValue('Missing questionnaire or questionnaireResponse'); + } + + try { + const scoringActionRequester = new ActionRequester(questionnaire, questionnaireResponse); + + await runScoringCalculator(questionnaire, questionnaireResponse, scoringActionRequester, new ScoringCalculator(questionnaire)); + + scoringActionRequester.dispatchAllActions(dispatch); + + const updatedQuestionnaireResponse = getState().refero.form.FormData.Content; + if (!updatedQuestionnaireResponse) { + return rejectWithValue('Missing updated questionnaire response'); + } + const fhirActionRequester = new ActionRequester(questionnaire, updatedQuestionnaireResponse); + + await runFhirPathQrUpdater({ + questionnaire, + questionnaireResponse: updatedQuestionnaireResponse, + dispatch, + actionRequester: fhirActionRequester, + fhirPathUpdater: new FhirPathExtensions(questionnaire), + }); + fhirActionRequester.dispatchAllActions(dispatch); + } catch (error) { + return rejectWithValue(error); + } + } +); diff --git a/src/calculators/__tests__/__data__/index.ts b/src/calculators/__tests__/__data__/index.ts new file mode 100644 index 00000000..6dcbbe82 --- /dev/null +++ b/src/calculators/__tests__/__data__/index.ts @@ -0,0 +1,6 @@ +import * as fs from 'fs'; + +import { Questionnaire } from 'fhir/r4'; + +const q: Questionnaire = JSON.parse(fs.readFileSync(__dirname + '/q.json').toString()); +export default q; diff --git a/src/calculators/__tests__/__data__/q.json b/src/calculators/__tests__/__data__/q.json new file mode 100644 index 00000000..6a0d1bca --- /dev/null +++ b/src/calculators/__tests__/__data__/q.json @@ -0,0 +1,273 @@ +{ + "resourceType": "Questionnaire", + "language": "nb-NO", + "id": "3981d77d-f69f-4c9c-85cd-8b07b05d93d2", + "name": "NHN_Test_Scoore_Calculation", + "title": "Test kopi og regning på score", + "version": "0.1", + "status": "draft", + "publisher": "NHN", + "meta": { + "profile": ["http://ehelse.no/fhir/StructureDefinition/sdf-Questionnaire"], + "tag": [{ "system": "urn:ietf:bcp:47", "code": "nb-NO", "display": "Bokmål" }], + "security": [{ "code": "3", "display": "Helsehjelp (Full)", "system": "urn:oid:2.16.578.1.12.4.1.1.7618" }] + }, + "contact": [{ "name": "http://www.nhn.no" }], + "subjectType": ["Patient"], + "extension": [ + { + "url": "http://helsenorge.no/fhir/StructureDefinition/sdf-sidebar", + "valueCoding": { "system": "http://helsenorge.no/fhir/ValueSet/sdf-sidebar", "code": "1" } + }, + { + "url": "http://helsenorge.no/fhir/StructureDefinition/sdf-information-message", + "valueCoding": { "system": "http://helsenorge.no/fhir/ValueSet/sdf-information-message", "code": "1" } + }, + { + "url": "http://helsenorge.no/fhir/StructureDefintion/sdf-itemControl-visibility", + "valueCodeableConcept": { + "coding": [ + { "system": "http://helsenorge.no/fhir/CodeSystem/AttachmentRenderOptions", "code": "hide-help", "display": "Hide help texts" }, + { + "system": "http://helsenorge.no/fhir/CodeSystem/AttachmentRenderOptions", + "code": "hide-sublabel", + "display": "Hide sublabel texts" + } + ] + } + } + ], + "date": "2025-03-05T00:00:00+01:00", + "item": [ + { + "linkId": "0701f3cf-73f5-48f7-8317-24c1223264a8", + "type": "group", + "text": "Input scoring and calculation and copying of scoringvalue", + "required": false, + "item": [ + { + "linkId": "verdi1", + "type": "choice", + "text": "Verdi 1", + "code": [ + { "system": "http://ehelse.no/Score", "code": "score", "display": "score" }, + { "system": "http://ehelse.no/scoringFormulas", "code": "QS", "display": "Question score" } + ], + "required": false, + "answerOption": [ + { + "valueCoding": { + "id": "96651f43-5ac1-4b87-83b8-49879c32daa6", + "code": "100", + "system": "urn:uuid:5ca4194b-32df-409e-81e5-8d4d0c600ee3", + "display": "Ja", + "extension": [{ "url": "http://hl7.org/fhir/StructureDefinition/ordinalValue", "valueDecimal": 100 }] + } + }, + { + "valueCoding": { + "id": "049bb493-4ec3-4df8-855b-12be2307360b", + "code": "200", + "system": "urn:uuid:5ca4194b-32df-409e-81e5-8d4d0c600ee3", + "display": "Nei", + "extension": [{ "url": "http://hl7.org/fhir/StructureDefinition/ordinalValue", "valueDecimal": 200 }] + } + } + ] + }, + { + "linkId": "verdi2", + "type": "choice", + "text": "Verdi 2", + "code": [ + { "system": "http://ehelse.no/Score", "code": "score", "display": "score" }, + { "system": "http://ehelse.no/scoringFormulas", "code": "QS", "display": "Question score" } + ], + "required": false, + "answerOption": [ + { + "valueCoding": { + "id": "96651f43-5ac1-4b87-83b8-49879c32daa6", + "code": "100", + "system": "urn:uuid:5ca4194b-32df-409e-81e5-8d4d0c600ee3", + "display": "Ja", + "extension": [{ "url": "http://hl7.org/fhir/StructureDefinition/ordinalValue", "valueDecimal": 100 }] + } + }, + { + "valueCoding": { + "id": "049bb493-4ec3-4df8-855b-12be2307360b", + "code": "200", + "system": "urn:uuid:5ca4194b-32df-409e-81e5-8d4d0c600ee3", + "display": "Nei", + "extension": [{ "url": "http://hl7.org/fhir/StructureDefinition/ordinalValue", "valueDecimal": 200 }] + } + } + ] + }, + { + "linkId": "verdi3", + "type": "choice", + "text": "Verdi 3", + "code": [ + { "system": "http://ehelse.no/Score", "code": "score", "display": "score" }, + { "system": "http://ehelse.no/scoringFormulas", "code": "QS", "display": "Question score" } + ], + "required": false, + "answerOption": [ + { + "valueCoding": { + "id": "96651f43-5ac1-4b87-83b8-49879c32daa6", + "code": "100", + "system": "urn:uuid:5ca4194b-32df-409e-81e5-8d4d0c600ee3", + "display": "Ja", + "extension": [{ "url": "http://hl7.org/fhir/StructureDefinition/ordinalValue", "valueDecimal": 100 }] + } + }, + { + "valueCoding": { + "id": "049bb493-4ec3-4df8-855b-12be2307360b", + "code": "200", + "system": "urn:uuid:5ca4194b-32df-409e-81e5-8d4d0c600ee3", + "display": "Nei", + "extension": [{ "url": "http://hl7.org/fhir/StructureDefinition/ordinalValue", "valueDecimal": 200 }] + } + } + ] + }, + { + "linkId": "verdi4", + "type": "choice", + "text": "Verdi 4", + "code": [ + { "system": "http://ehelse.no/Score", "code": "score", "display": "score" }, + { "system": "http://ehelse.no/scoringFormulas", "code": "QS", "display": "Question score" } + ], + "required": false, + "answerOption": [ + { + "valueCoding": { + "id": "96651f43-5ac1-4b87-83b8-49879c32daa6", + "code": "100", + "system": "urn:uuid:5ca4194b-32df-409e-81e5-8d4d0c600ee3", + "display": "Ja", + "extension": [{ "url": "http://hl7.org/fhir/StructureDefinition/ordinalValue", "valueDecimal": 100 }] + } + }, + { + "valueCoding": { + "id": "049bb493-4ec3-4df8-855b-12be2307360b", + "code": "200", + "system": "urn:uuid:5ca4194b-32df-409e-81e5-8d4d0c600ee3", + "display": "Nei", + "extension": [{ "url": "http://hl7.org/fhir/StructureDefinition/ordinalValue", "valueDecimal": 200 }] + } + } + ] + }, + { + "linkId": "Delsum", + "type": "integer", + "text": "Delsum", + "code": [ + { "system": "http://ehelse.no/Score", "code": "score", "display": "score" }, + { "system": "http://ehelse.no/scoringFormulas", "code": "SS", "display": "Section score" } + ], + "required": false, + "readOnly": true + }, + { + "linkId": "Totalsum", + "type": "integer", + "text": "Totalsum", + "code": [ + { "system": "http://ehelse.no/Score", "code": "score", "display": "score" }, + { "system": "http://ehelse.no/scoringFormulas", "code": "TS", "display": "Total score" } + ], + "required": false, + "readOnly": true + }, + { + "linkId": "aritmetisk_gjennomsnitt", + "type": "integer", + "text": "Aritmetisk gjennomsnitt basert på totalsum.", + "required": false, + "extension": [ + { + "url": "http://ehelse.no/fhir/StructureDefinition/sdf-calculatedExpression", + "valueString": "QuestionnaireResponse.descendants().where(linkId='Totalsum').answer.value / 4" + } + ], + "readOnly": true + }, + { + "linkId": "kopiert_felt", + "type": "integer", + "text": "Kopi", + "required": false, + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl", + "valueCodeableConcept": { + "coding": [{ "system": "http://hl7.org/fhir/ValueSet/questionnaire-item-control", "code": "data-receiver" }] + } + }, + { + "url": "http://hl7.org/fhir/StructureDefinition/cqf-expression", + "valueString": "QuestionnaireResponse.descendants().where(linkId='Totalsum').answer.value" + } + ], + "readOnly": true, + "enableWhen": [{ "answerBoolean": true, "question": "Totalsum", "operator": "exists" }] + }, + { + "linkId": "aritmetisk_gjennomsnitt_kopi", + "type": "integer", + "text": "Aritmetisk gjennomsnitt basert på kopiert felt", + "required": false, + "extension": [ + { + "url": "http://ehelse.no/fhir/StructureDefinition/sdf-calculatedExpression", + "valueString": "QuestionnaireResponse.descendants().where(linkId='kopiert_felt').answer.value / 4" + } + ], + "readOnly": true + } + ] + }, + { + "linkId": "498140b3-7c26-4492-dea5-24c829552347", + "type": "group", + "text": "Regning - Fhir path", + "required": false, + "item": [ + { "linkId": "Tall1", "type": "integer", "text": "Tall 1", "required": false }, + { "linkId": "Tall2", "type": "integer", "text": "Tall 2", "required": false }, + { + "linkId": "Sum", + "type": "integer", + "text": "Sum", + "required": false, + "extension": [ + { + "url": "http://ehelse.no/fhir/StructureDefinition/sdf-calculatedExpression", + "valueString": "QuestionnaireResponse.descendants().where(linkId='Tall1').answer.value + QuestionnaireResponse.descendants().where(linkId='Tall2').answer.value" + } + ] + }, + { + "linkId": "Gjennomsnitt", + "type": "integer", + "text": "Gjennomsnitt", + "required": false, + "extension": [ + { + "url": "http://ehelse.no/fhir/StructureDefinition/sdf-calculatedExpression", + "valueString": "QuestionnaireResponse.descendants().where(linkId='Sum').answer.value / 2" + } + ] + } + ] + } + ] +} diff --git a/src/calculators/__tests__/runFhirPathUpdater-spec.ts b/src/calculators/__tests__/runFhirPathUpdater-spec.ts new file mode 100644 index 00000000..1038df12 --- /dev/null +++ b/src/calculators/__tests__/runFhirPathUpdater-spec.ts @@ -0,0 +1,245 @@ +import { Coding, Questionnaire, QuestionnaireItem, QuestionnaireResponse } from 'fhir/r4'; +import { describe, it, expect, vi, beforeEach } from 'vitest'; + +import { runFhirPathQrUpdater } from '../runFhirPathUpdater'; + +import { newAnswerValueAction } from '@/actions/newValue'; +import ItemType from '@/constants/itemType'; +import { AppDispatch } from '@/reducers'; +import * as utils from '@/util'; +import { ActionRequester } from '@/util/actionRequester'; +import * as extensionUtils from '@/util/extension'; +import { AnswerPad, FhirPathExtensions } from '@/util/FhirPathExtensions'; +import * as referoCore from '@/util/refero-core'; + +vi.mock('@/actions/newValue'); +vi.mock('@/util/extension'); +vi.mock('@/util/refero-core'); +vi.mock('@/util'); + +describe('runFhirPathQrUpdater', () => { + let mockDispatch: AppDispatch; + let mockFhirPathUpdater: FhirPathExtensions; + let mockQuestionnaire: Questionnaire; + let mockQuestionnaireResponse: QuestionnaireResponse; + let mockActionRequester: ActionRequester; + + beforeEach(() => { + mockDispatch = vi.fn(); + mockFhirPathUpdater = { + evaluateAllExpressions: vi.fn(), + calculateFhirScore: vi.fn(), + } as unknown as FhirPathExtensions; + + mockQuestionnaire = { item: [], resourceType: 'Questionnaire', status: 'draft' }; + mockQuestionnaireResponse = { item: [], resourceType: 'QuestionnaireResponse', status: 'in-progress' }; + mockActionRequester = { + addQuantityAnswer: vi.fn(), + addDecimalAnswer: vi.fn(), + addIntegerAnswer: vi.fn(), + addBooleanAnswer: vi.fn(), + addStringAnswer: vi.fn(), + addChoiceAnswer: vi.fn(), + addOpenChoiceAnswer: vi.fn(), + addDateTimeAnswer: vi.fn(), + addDateAnswer: vi.fn(), + addTimeAnswer: vi.fn(), + setNewAnswer: vi.fn(), + isCheckbox: vi.fn(), + } as unknown as ActionRequester; + + vi.clearAllMocks(); + }); + + it('should return early if any required parameters are missing', async () => { + await runFhirPathQrUpdater({ + questionnaire: null as unknown as Questionnaire, + questionnaireResponse: mockQuestionnaireResponse, + dispatch: mockDispatch, + fhirPathUpdater: mockFhirPathUpdater, + }); + + expect(mockFhirPathUpdater.evaluateAllExpressions).not.toHaveBeenCalled(); + }); + + it('should evaluate expressions and update the response', async () => { + const updatedResponse = { resourceType: 'QuestionnaireResponse' } as QuestionnaireResponse; + const scores: AnswerPad = { 'test-item': 10 }; + + mockFhirPathUpdater.evaluateAllExpressions = vi.fn().mockReturnValue(updatedResponse); + mockFhirPathUpdater.calculateFhirScore = vi.fn().mockReturnValue(scores); + + const mockItem: QuestionnaireItem = { linkId: 'test-item', type: ItemType.INTEGER }; + vi.mocked(referoCore.getQuestionnaireDefinitionItem).mockReturnValue(mockItem); + vi.mocked(referoCore.getResponseItemAndPathWithLinkId).mockReturnValue([ + { + item: { linkId: 'test-item' }, + path: [{ linkId: 'test-item', index: 0 }], + }, + ]); + + await runFhirPathQrUpdater({ + questionnaire: mockQuestionnaire, + questionnaireResponse: mockQuestionnaireResponse, + dispatch: mockDispatch, + fhirPathUpdater: mockFhirPathUpdater, + }); + + expect(mockFhirPathUpdater.evaluateAllExpressions).toHaveBeenCalledWith(mockQuestionnaireResponse); + expect(mockFhirPathUpdater.calculateFhirScore).toHaveBeenCalledWith(updatedResponse); + expect(referoCore.getQuestionnaireDefinitionItem).toHaveBeenCalledWith('test-item', mockQuestionnaire.item); + expect(referoCore.getResponseItemAndPathWithLinkId).toHaveBeenCalledWith('test-item', updatedResponse); + expect(mockDispatch).toHaveBeenCalledWith(newAnswerValueAction(expect.any(Object))); + }); + + it('should handle quantity type items', async () => { + const scores: AnswerPad = { 'quantity-item': 42 }; + const mockExtension: Coding = { + system: 'test-system', + code: 'test-code', + display: 'Test Unit', + }; + const mockItem: QuestionnaireItem = { linkId: 'quantity-item', type: ItemType.QUANTITY }; + + mockFhirPathUpdater.evaluateAllExpressions = vi.fn().mockReturnValue(mockQuestionnaireResponse); + mockFhirPathUpdater.calculateFhirScore = vi.fn().mockReturnValue(scores); + + vi.mocked(referoCore.getQuestionnaireDefinitionItem).mockReturnValue(mockItem); + vi.mocked(extensionUtils.getQuestionnaireUnitExtensionValue).mockReturnValue(mockExtension); + vi.mocked(referoCore.getResponseItemAndPathWithLinkId).mockReturnValue([ + { + item: { linkId: 'quantity-item' }, + path: [{ linkId: 'quantity-item', index: 0 }], + }, + ]); + vi.mocked(utils.getDecimalValue).mockReturnValue(42); + + await runFhirPathQrUpdater({ + questionnaire: mockQuestionnaire, + questionnaireResponse: mockQuestionnaireResponse, + dispatch: mockDispatch, + actionRequester: mockActionRequester, + fhirPathUpdater: mockFhirPathUpdater, + }); + + expect(mockActionRequester.addQuantityAnswer).toHaveBeenCalledWith( + 'quantity-item', + expect.objectContaining({ + unit: 'Test Unit', + system: 'test-system', + code: 'test-code', + value: 42, + }), + 0 + ); + }); + + it('should handle different item types with actionRequester', async () => { + const scores: AnswerPad = { + 'decimal-item': 10.5, + 'integer-item': 42, + 'boolean-item': true, + 'string-item': 'test', + 'choice-item': { system: 'test', code: 'code', display: 'Test Coding' }, + 'openchoice-item': 'open value', + 'datetime-item': '2023-01-01T12:00:00', + 'date-item': '2023-01-01', + 'time-item': '12:00:00', + }; + + mockFhirPathUpdater.evaluateAllExpressions = vi.fn().mockReturnValue(mockQuestionnaireResponse); + mockFhirPathUpdater.calculateFhirScore = vi.fn().mockReturnValue(scores); + + const mockPath = [{ linkId: '', index: 0 }]; + + // Mock different item types + vi.mocked(referoCore.getQuestionnaireDefinitionItem).mockImplementation(linkId => { + const typeMap: Record = { + 'decimal-item': ItemType.DECIMAL, + 'integer-item': ItemType.INTEGER, + 'boolean-item': ItemType.BOOLEAN, + 'string-item': ItemType.STRING, + 'choice-item': ItemType.CHOICE, + 'openchoice-item': ItemType.OPENCHOICE, + 'datetime-item': ItemType.DATETIME, + 'date-item': ItemType.DATE, + 'time-item': ItemType.TIME, + }; + + return { linkId, type: typeMap[linkId] } as QuestionnaireItem; + }); + + vi.mocked(referoCore.getResponseItemAndPathWithLinkId).mockImplementation(linkId => [ + { + item: { linkId }, + path: mockPath, + }, + ]); + + vi.mocked(utils.getDecimalValue).mockReturnValue(10.5); + + await runFhirPathQrUpdater({ + questionnaire: mockQuestionnaire, + questionnaireResponse: mockQuestionnaireResponse, + dispatch: mockDispatch, + actionRequester: mockActionRequester, + fhirPathUpdater: mockFhirPathUpdater, + }); + + expect(mockActionRequester.addDecimalAnswer).toHaveBeenCalledWith('decimal-item', 10.5, 0); + expect(mockActionRequester.addIntegerAnswer).toHaveBeenCalledWith('integer-item', 42, 0); + expect(mockActionRequester.addBooleanAnswer).toHaveBeenCalledWith('boolean-item', true, 0); + expect(mockActionRequester.addStringAnswer).toHaveBeenCalledWith('string-item', 'test', 0); + expect(mockActionRequester.addChoiceAnswer).toHaveBeenCalledWith( + 'choice-item', + { system: 'test', code: 'code', display: 'Test Coding' }, + 0 + ); + expect(mockActionRequester.addOpenChoiceAnswer).toHaveBeenCalledWith('openchoice-item', 'open value', 0); + expect(mockActionRequester.addDateTimeAnswer).toHaveBeenCalledWith('datetime-item', '2023-01-01T12:00:00', 0); + expect(mockActionRequester.addDateAnswer).toHaveBeenCalledWith('date-item', '2023-01-01', 0); + expect(mockActionRequester.addTimeAnswer).toHaveBeenCalledWith('time-item', '12:00:00', 0); + }); + + it('should handle checkbox items for choice type', async () => { + const scores: AnswerPad = { + 'checkbox-item': [ + { system: 'test', code: 'code1', display: 'Option 1' }, + { system: 'test', code: 'code2', display: 'Option 2' }, + ], + }; + + const mockItem: QuestionnaireItem = { linkId: 'checkbox-item', type: ItemType.CHOICE }; + + mockFhirPathUpdater.evaluateAllExpressions = vi.fn().mockReturnValue(mockQuestionnaireResponse); + mockFhirPathUpdater.calculateFhirScore = vi.fn().mockReturnValue(scores); + + vi.mocked(referoCore.getQuestionnaireDefinitionItem).mockReturnValue(mockItem); + vi.mocked(referoCore.getResponseItemAndPathWithLinkId).mockReturnValue([ + { + item: { linkId: 'checkbox-item' }, + path: [{ linkId: 'checkbox-item', index: 0 }], + }, + ]); + + mockActionRequester.isCheckbox = vi.fn().mockReturnValue(true); + + await runFhirPathQrUpdater({ + questionnaire: mockQuestionnaire, + questionnaireResponse: mockQuestionnaireResponse, + dispatch: mockDispatch, + actionRequester: mockActionRequester, + fhirPathUpdater: mockFhirPathUpdater, + }); + + expect(mockActionRequester.isCheckbox).toHaveBeenCalledWith(mockItem); + expect(mockActionRequester.setNewAnswer).toHaveBeenCalledWith( + 'checkbox-item', + expect.arrayContaining([ + { valueCoding: { system: 'test', code: 'code1', display: 'Option 1' } }, + { valueCoding: { system: 'test', code: 'code2', display: 'Option 2' } }, + ]), + 0 + ); + }); +}); diff --git a/src/calculators/__tests__/runScoringCalculator-spec.ts b/src/calculators/__tests__/runScoringCalculator-spec.ts new file mode 100644 index 00000000..0714cf37 --- /dev/null +++ b/src/calculators/__tests__/runScoringCalculator-spec.ts @@ -0,0 +1,159 @@ +import { Questionnaire, QuestionnaireResponse } from 'fhir/r4'; +import { describe, expect, vi, beforeEach } from 'vitest'; + +import { questionnaireHasScoring, runScoringCalculator, updateQuestionnaireResponseWithScore } from '../runScoringCalculator'; + +import ItemType from '@/constants/itemType'; +import { ActionRequester } from '@/util/actionRequester'; +import * as extensionUtil from '@/util/extension'; +import * as valueUtil from '@/util/index'; +import * as referoCore from '@/util/refero-core'; +import { ScoringCalculator } from '@/util/scoringCalculator'; + +describe('runScoringCalculator', () => { + let questionnaire: Questionnaire; + let questionnaireResponse: QuestionnaireResponse; + let actionRequester: ActionRequester; + let scoringCalculator: ScoringCalculator; + let mockScores: Record; + + beforeEach(() => { + questionnaire = { item: [], resourceType: 'Questionnaire', status: 'draft' } as Questionnaire; + questionnaireResponse = { item: [], resourceType: 'QuestionnaireResponse', status: 'in-progress' } as QuestionnaireResponse; + mockScores = { 'score-item-1': 10 }; + + actionRequester = { + addManyActions: vi.fn(), + } as unknown as ActionRequester; + + scoringCalculator = { + calculateScore: vi.fn().mockReturnValue(mockScores), + getIsScoringQuestionnaire: vi.fn().mockReturnValue(true), + } as unknown as ScoringCalculator; + + vi.spyOn(referoCore, 'getQuestionnaireDefinitionItem').mockImplementation(linkId => ({ + linkId, + type: ItemType.INTEGER, + })); + + vi.spyOn(referoCore, 'getResponseItemAndPathWithLinkId').mockImplementation(() => [ + { + item: { linkId: 'score-item-1' }, + path: [{ linkId: 'score-item-1', index: 0 }], + }, + ]); + + vi.spyOn(valueUtil, 'getDecimalValue').mockReturnValue(10); + }); + + it('should return empty array if any required parameter is missing', async () => { + expect(await runScoringCalculator(undefined, questionnaireResponse, actionRequester, scoringCalculator)).toEqual([]); + expect(await runScoringCalculator(questionnaire, undefined, actionRequester, scoringCalculator)).toEqual([]); + expect(await runScoringCalculator(questionnaire, questionnaireResponse, undefined, scoringCalculator)).toEqual([]); + expect(await runScoringCalculator(questionnaire, questionnaireResponse, actionRequester, undefined)).toEqual([]); + }); + + it('should return empty array if questionnaire does not have scoring', async () => { + vi.spyOn(scoringCalculator, 'getIsScoringQuestionnaire').mockReturnValue(false); + expect(await runScoringCalculator(questionnaire, questionnaireResponse, actionRequester, scoringCalculator)).toEqual([]); + }); + + it('should calculate scores and update questionnaire response', async () => { + const result = await runScoringCalculator(questionnaire, questionnaireResponse, actionRequester, scoringCalculator); + + expect(scoringCalculator.calculateScore).toHaveBeenCalledWith(questionnaireResponse); + expect(actionRequester.addManyActions).toHaveBeenCalled(); + expect(result.length).toBeGreaterThan(0); + }); +}); + +describe('updateQuestionnaireResponseWithScore', () => { + let questionnaire: Questionnaire; + let questionnaireResponse: QuestionnaireResponse; + let actionRequester: ActionRequester; + let scores: Record; + + beforeEach(() => { + questionnaire = { item: [], resourceType: 'Questionnaire', status: 'draft' } as Questionnaire; + questionnaireResponse = { item: [], resourceType: 'QuestionnaireResponse', status: 'in-progress' } as QuestionnaireResponse; + scores = { 'quantity-item': 10, 'decimal-item': 5.5, 'integer-item': 3 }; + + actionRequester = { + addManyActions: vi.fn(), + } as unknown as ActionRequester; + + vi.spyOn(referoCore, 'getQuestionnaireDefinitionItem').mockImplementation(linkId => { + if (linkId === 'quantity-item') { + return { linkId, type: ItemType.QUANTITY }; + } else if (linkId === 'decimal-item') { + return { linkId, type: ItemType.DECIMAL }; + } else { + return { linkId, type: ItemType.INTEGER }; + } + }); + + vi.spyOn(referoCore, 'getResponseItemAndPathWithLinkId').mockImplementation(() => [ + { + item: { + linkId: 'quantity-item', + }, + path: [{ linkId: 'quantity-item', index: 0 }], + }, + ]); + + vi.spyOn(extensionUtil, 'getQuestionnaireUnitExtensionValue').mockReturnValue({ + system: 'test-system', + code: 'test-code', + display: 'Test Unit', + }); + + vi.spyOn(valueUtil, 'getDecimalValue').mockReturnValue(10); + }); + + it('should handle quantity items correctly', () => { + const result = updateQuestionnaireResponseWithScore(scores, questionnaire, questionnaireResponse, actionRequester); + + expect( + result.some(action => action.payload && 'valueQuantity' in action.payload && action.payload?.valueQuantity?.unit === 'Test Unit') + ).toBeTruthy(); + }); + + it('should handle decimal items correctly', () => { + const result = updateQuestionnaireResponseWithScore(scores, questionnaire, questionnaireResponse, actionRequester); + + expect(result.some(action => action.payload && 'valueDecimal' in action.payload)).toBeTruthy(); + }); + + it('should handle integer items correctly', () => { + const result = updateQuestionnaireResponseWithScore(scores, questionnaire, questionnaireResponse, actionRequester); + + expect(result.some(action => action.payload && 'valueInteger' in action.payload)).toBeTruthy(); + }); + + it('should add actions to the actionRequester', () => { + updateQuestionnaireResponseWithScore(scores, questionnaire, questionnaireResponse, actionRequester); + expect(actionRequester.addManyActions).toHaveBeenCalled(); + }); +}); + +describe('questionnaireHasScoring', () => { + it('should return false if scoringCalculator is undefined', () => { + expect(questionnaireHasScoring(undefined)).toBe(false); + }); + + it('should return false if getIsScoringQuestionnaire returns false', () => { + const scoringCalculator = { + getIsScoringQuestionnaire: vi.fn().mockReturnValue(false), + } as unknown as ScoringCalculator; + + expect(questionnaireHasScoring(scoringCalculator)).toBe(false); + }); + + it('should return true if getIsScoringQuestionnaire returns true', () => { + const scoringCalculator = { + getIsScoringQuestionnaire: vi.fn().mockReturnValue(true), + } as unknown as ScoringCalculator; + + expect(questionnaireHasScoring(scoringCalculator)).toBe(true); + }); +}); diff --git a/src/calculators/__tests__/scoringCalculators-spec.tsx b/src/calculators/__tests__/scoringCalculators-spec.tsx new file mode 100644 index 00000000..514ed406 --- /dev/null +++ b/src/calculators/__tests__/scoringCalculators-spec.tsx @@ -0,0 +1,72 @@ +import { renderRefero, screen, waitFor, within } from '@test/test-utils'; +import userEvent from '@testing-library/user-event'; +import { Questionnaire } from 'fhir/r4'; + +import q from './__data__'; + +import { ReferoProps } from '@/types/referoProps'; + +describe('scoring calculations', () => { + describe('Input scoring and calculation and copying of scoringvalue', () => { + it('should render', async () => { + await createWrapper(q); + expect(screen.getByTestId('item_Gjennomsnitt')).toBeInTheDocument(); + }); + it('should calculate the based on the input values', async () => { + await createWrapper(q); + const input1 = screen.getByTestId('item_verdi1-0-radio-choice'); + const input2 = screen.getByTestId('item_verdi2-0-radio-choice'); + const input3 = screen.getByTestId('item_verdi3-1-radio-choice'); + const input4 = screen.getByTestId('item_verdi4-0-radio-choice'); + + const delsum = screen.getByTestId('item_Delsum-readonly'); + const totalsum = screen.getByTestId('item_Totalsum-readonly'); + const aritmetisk_gjennomsnitt = screen.getByTestId('item_aritmetisk_gjennomsnitt-readonly'); + const Kopi = screen.getByTestId('item_aritmetisk_gjennomsnitt_kopi-readonly'); + expect(delsum).toHaveTextContent('DelsumIkke besvart'); + expect(totalsum).toHaveTextContent('TotalsumIkke besvart'); + expect(aritmetisk_gjennomsnitt).toHaveTextContent('Aritmetisk gjennomsnitt basert på totalsum.Ikke besvar'); + expect(Kopi).toHaveTextContent('Aritmetisk gjennomsnitt basert på kopiert feltIkke besvart'); + + await waitFor(async () => await userEvent.click(within(input1).getByLabelText('Ja'))); + await waitFor(async () => await userEvent.click(within(input2).getByLabelText('Ja'))); + await waitFor(async () => await userEvent.click(within(input3).getByLabelText('Nei'))); + await waitFor(async () => await userEvent.click(within(input4).getByLabelText('Ja'))); + const kopiertFelt = screen.getByTestId('item_kopiert_felt-readonly'); + + expect(delsum).toHaveTextContent('Delsum500'); + + expect(totalsum).toHaveTextContent('Totalsum500'); + expect(aritmetisk_gjennomsnitt).toHaveTextContent('Aritmetisk gjennomsnitt basert på totalsum.125'); + expect(kopiertFelt).toHaveTextContent('Kopi500'); + expect(Kopi).toHaveTextContent('Aritmetisk gjennomsnitt basert på kopiert felt125'); + }); + }); + describe('only scoring', () => { + it('should calculate the based on the input values', async () => { + await createWrapper(q); + const tall1 = screen.getByLabelText('Tall 1'); + const tall2 = screen.getByLabelText('Tall 2'); + + const delsum = screen.getByLabelText('Sum'); + const average = screen.getByLabelText('Gjennomsnitt'); + + expect(delsum).not.toHaveValue(); + expect(average).not.toHaveValue(); + + await waitFor(async () => await userEvent.type(tall1, '10')); + await waitFor(async () => await userEvent.type(tall2, '20')); + + expect(tall1).toHaveValue(10); + expect(tall2).toHaveValue(20); + + expect(delsum).toHaveValue(30); + expect(average).toHaveValue(15); + }); + }); +}); + +// eslint-disable-next-line @typescript-eslint/explicit-function-return-type +async function createWrapper(q: Questionnaire, props?: Partial) { + return renderRefero({ questionnaire: q, props }); +} diff --git a/src/calculators/runFhirPathUpdater.ts b/src/calculators/runFhirPathUpdater.ts new file mode 100644 index 00000000..e574e958 --- /dev/null +++ b/src/calculators/runFhirPathUpdater.ts @@ -0,0 +1,245 @@ +import { Coding, Quantity, Questionnaire, QuestionnaireItem, QuestionnaireResponse } from 'fhir/r4'; + +import { newAnswerValueAction } from '@/actions/newValue'; +import ItemType from '@/constants/itemType'; +import { AppDispatch } from '@/reducers'; +import { getDecimalValue } from '@/util'; +import { ActionRequester } from '@/util/actionRequester'; +import { getQuestionnaireUnitExtensionValue } from '@/util/extension'; +import { AnswerPad, FhirPathExtensions } from '@/util/FhirPathExtensions'; +import { getQuestionnaireDefinitionItem, getResponseItemAndPathWithLinkId } from '@/util/refero-core'; + +type InputParams = { + questionnaire: Questionnaire; + questionnaireResponse: QuestionnaireResponse; + dispatch: AppDispatch; + actionRequester?: ActionRequester; + fhirPathUpdater?: FhirPathExtensions; +}; + +export const runFhirPathQrUpdater = async ({ + questionnaire, + questionnaireResponse, + dispatch, + actionRequester, + fhirPathUpdater, +}: InputParams): Promise => { + if (!questionnaire || !questionnaireResponse || !fhirPathUpdater) return; + + // Evaluate all expressions and get the updated response + const updatedResponse = fhirPathUpdater.evaluateAllExpressions(questionnaireResponse); + //TODO: Figure out a way to not run this on all changes + // if (JSON.stringify(updatedResponse) === JSON.stringify(questionnaireResponse)) { + // return; + // } + // Calculate FHIR scores using the same updated response + + const fhirScores = fhirPathUpdater.calculateFhirScore(updatedResponse); + + updateQuestionnaireResponseWithScore(fhirScores, questionnaire, dispatch, updatedResponse, actionRequester); +}; +const createQuantity = (item: QuestionnaireItem, extension: Coding, value: number): Quantity => { + return { + unit: extension.display, + system: extension.system, + code: extension.code, + value: getDecimalValue(item, value), + }; +}; +const updateQuestionnaireResponseWithScore = ( + scores: AnswerPad, + questionnaire: Questionnaire, + dispatch: AppDispatch, + questionnaireResponse: QuestionnaireResponse, + actionRequester?: ActionRequester +): void => { + for (const linkId in scores) { + const item = getQuestionnaireDefinitionItem(linkId, questionnaire.item); + if (!item) continue; + const itemsAndPaths = getResponseItemAndPathWithLinkId(linkId, questionnaireResponse); + const value = scores[linkId]; + switch (item.type) { + case ItemType.QUANTITY: { + const extension = getQuestionnaireUnitExtensionValue(item); + if (!extension) continue; + + for (const itemAndPath of itemsAndPaths) { + if (actionRequester) { + actionRequester.addQuantityAnswer( + linkId, + typeof value === 'string' || typeof value === 'number' + ? createQuantity(item, extension, value as number) + : (value as Quantity), + itemAndPath.path[0]?.index + ); + } else { + dispatch( + newAnswerValueAction({ + itemPath: itemAndPath.path, + newAnswer: [ + typeof value === 'string' || typeof value === 'number' + ? createQuantity(item, extension, value as number) + : (value as Quantity), + ], + item, + }) + ); + } + } + break; + } + case ItemType.DECIMAL: { + for (const itemAndPath of itemsAndPaths) { + const decimalValue = getDecimalValue(item, value as number); + if (actionRequester) { + actionRequester.addDecimalAnswer(linkId, decimalValue, itemAndPath.path[0]?.index); + } else { + dispatch( + newAnswerValueAction({ + itemPath: itemAndPath.path, + newAnswer: [{ valueDecimal: decimalValue }], + item, + }) + ); + } + } + break; + } + case ItemType.INTEGER: { + for (const itemAndPath of itemsAndPaths) { + if (actionRequester) { + actionRequester.addIntegerAnswer(linkId, value as number, itemAndPath.path[0]?.index); + } else { + dispatch( + newAnswerValueAction({ + itemPath: itemAndPath.path, + newAnswer: [{ valueInteger: value as number }], + item, + }) + ); + } + } + + break; + } + case ItemType.BOOLEAN: { + for (const itemAndPath of itemsAndPaths) { + if (actionRequester) { + actionRequester.addBooleanAnswer(linkId, value as boolean, itemAndPath.path[0]?.index); + } else { + dispatch( + newAnswerValueAction({ + itemPath: itemAndPath.path, + newAnswer: [{ valueBoolean: value as boolean }], + item, + }) + ); + } + } + break; + } + case ItemType.STRING: + case ItemType.TEXT: + for (const itemAndPath of itemsAndPaths) { + if (actionRequester) { + actionRequester.addStringAnswer(linkId, (value as string) ?? '', itemAndPath.path[0]?.index); + } else { + dispatch( + newAnswerValueAction({ + itemPath: itemAndPath.path, + newAnswer: [{ valueString: (value as string) ?? '' }], + item, + }) + ); + } + } + break; + case ItemType.CHOICE: { + for (const itemAndPath of itemsAndPaths) { + if (actionRequester) { + if (actionRequester.isCheckbox(item)) { + const answer = value ? (value as Coding[])?.map(x => ({ valueCoding: x })) : []; + actionRequester.setNewAnswer(linkId, answer, itemAndPath.path[0]?.index); + } else { + actionRequester.addChoiceAnswer(linkId, value as Coding, itemAndPath.path[0]?.index); + } + } else { + dispatch( + newAnswerValueAction({ + itemPath: itemAndPath.path, + newAnswer: [{ valueCoding: value as Coding }], + item, + }) + ); + } + } + break; + } + case ItemType.OPENCHOICE: { + for (const itemAndPath of itemsAndPaths) { + if (actionRequester) { + if (actionRequester.isCheckbox(item)) { + const answer = value ? (value as Coding[])?.map(x => (typeof x === 'string' ? { valueString: x } : { valueCoding: x })) : []; + actionRequester.setNewAnswer(linkId, answer, itemAndPath.path[0]?.index); + } else { + actionRequester.addOpenChoiceAnswer(linkId, value as Coding | string, itemAndPath.path[0]?.index); + } + } else { + dispatch( + newAnswerValueAction({ + itemPath: itemAndPath.path, + newAnswer: [typeof value === 'string' ? { valueString: value } : { valueCoding: value as Coding }], + item, + }) + ); + } + } + break; + } + case ItemType.DATETIME: + for (const itemAndPath of itemsAndPaths) { + if (actionRequester) { + actionRequester.addDateTimeAnswer(linkId, value as string, itemAndPath.path[0]?.index); + } else { + dispatch( + newAnswerValueAction({ + itemPath: itemAndPath.path, + newAnswer: [{ valueDateTime: value as string }], + item, + }) + ); + } + } + break; + case ItemType.DATE: + for (const itemAndPath of itemsAndPaths) { + if (actionRequester) { + actionRequester.addDateAnswer(linkId, value as string, itemAndPath.path[0]?.index); + } else { + dispatch( + newAnswerValueAction({ + itemPath: itemAndPath.path, + newAnswer: [{ valueDate: value as string }], + item, + }) + ); + } + } + break; + case ItemType.TIME: + for (const itemAndPath of itemsAndPaths) { + if (actionRequester) { + actionRequester.addTimeAnswer(linkId, value as string, itemAndPath.path[0]?.index); + } else { + dispatch( + newAnswerValueAction({ + itemPath: itemAndPath.path, + newAnswer: [{ valueTime: value as string }], + item, + }) + ); + } + } + } + } +}; diff --git a/src/calculators/runScoringCalculator.ts b/src/calculators/runScoringCalculator.ts new file mode 100644 index 00000000..aff5d5cf --- /dev/null +++ b/src/calculators/runScoringCalculator.ts @@ -0,0 +1,77 @@ +import { Quantity, Questionnaire, QuestionnaireResponse } from 'fhir/r4'; + +import { newValue, NewValuePayload } from '@/actions/newValue'; +import ItemType from '@/constants/itemType'; +import { getDecimalValue } from '@/util'; +import { ActionRequester } from '@/util/actionRequester'; +import { getQuestionnaireUnitExtensionValue } from '@/util/extension'; +import { getQuestionnaireDefinitionItem, getResponseItemAndPathWithLinkId } from '@/util/refero-core'; +import { AnswerPad, ScoringCalculator } from '@/util/scoringCalculator'; + +export const runScoringCalculator = async ( + questionnaire?: Questionnaire | null, + questionnaireResponse?: QuestionnaireResponse | null, + actionRequester?: ActionRequester, + scoringCalculator?: ScoringCalculator +): Promise<{ payload: NewValuePayload; type: string }[]> => { + const hasScoring = questionnaireHasScoring(scoringCalculator); + if (!questionnaire || !questionnaireResponse || !scoringCalculator || !hasScoring || !actionRequester) return []; + + // Calculate scores using the updated response + const scores = scoringCalculator.calculateScore(questionnaireResponse); + + return updateQuestionnaireResponseWithScore(scores, questionnaire, questionnaireResponse, actionRequester); +}; + +export const updateQuestionnaireResponseWithScore = ( + scores: AnswerPad, + questionnaire: Questionnaire, + questionnaireResponse: QuestionnaireResponse, + actionRequester: ActionRequester +): { payload: NewValuePayload; type: string }[] => { + const actions = []; + for (const linkId in scores) { + const item = getQuestionnaireDefinitionItem(linkId, questionnaire.item); + if (!item) continue; + const itemsAndPaths = getResponseItemAndPathWithLinkId(linkId, questionnaireResponse); + const value = scores[linkId]; + + switch (item.type) { + case ItemType.QUANTITY: { + const extension = getQuestionnaireUnitExtensionValue(item); + if (!extension) continue; + + const quantity: Quantity = { + unit: extension.display, + system: extension.system, + code: extension.code, + value: getDecimalValue(item, value), + }; + for (const itemAndPath of itemsAndPaths) { + actions.push(newValue({ itemPath: itemAndPath.path, valueQuantity: quantity, item })); + } + break; + } + case ItemType.DECIMAL: { + const decimalValue = getDecimalValue(item, value); + for (const itemAndPath of itemsAndPaths) { + actions.push(newValue({ itemPath: itemAndPath.path, valueDecimal: decimalValue, item })); + } + break; + } + case ItemType.INTEGER: { + const intValue = value !== undefined ? Math.round(value) : undefined; + for (const itemAndPath of itemsAndPaths) { + actions.push(newValue({ itemPath: itemAndPath.path, valueInteger: intValue, item })); + } + break; + } + } + } + actionRequester.addManyActions(actions); + return actions; +}; +export const questionnaireHasScoring = (scoringCalculator?: ScoringCalculator): boolean => { + if (!scoringCalculator) return false; + return scoringCalculator.getIsScoringQuestionnaire() ?? false; +}; diff --git a/src/hooks/useFhirPathQrUpdater.tsx b/src/hooks/useFhirPathQrUpdater.tsx deleted file mode 100644 index fb007585..00000000 --- a/src/hooks/useFhirPathQrUpdater.tsx +++ /dev/null @@ -1,151 +0,0 @@ -import { useEffect, useState } from 'react'; - -import { Coding, Quantity, Questionnaire, QuestionnaireItem, QuestionnaireResponse } from 'fhir/r4'; - -import ItemType from '@/constants/itemType'; -import { useAppSelector } from '@/reducers'; -import { getFormDefinition } from '@/reducers/form'; -import { getDecimalValue } from '@/util'; -import { ActionRequester } from '@/util/actionRequester'; -import { getQuestionnaireUnitExtensionValue } from '@/util/extension'; -import { AnswerPad, FhirPathExtensions } from '@/util/FhirPathExtensions'; -import { getQuestionnaireDefinitionItem, getResponseItemAndPathWithLinkId } from '@/util/refero-core'; - -export const useFhirPathQrUpdater = (): { - runFhirPathQrUpdater: ( - questionnaire: Questionnaire, - questionnaireResponse: QuestionnaireResponse, - actionRequester: ActionRequester - ) => void; -} => { - const formDefinition = useAppSelector(state => getFormDefinition(state)); - const [fhirPathUpdater, setFhirPathUpdater] = useState(); - - useEffect(() => { - if (formDefinition?.Content) { - setFhirPathUpdater(new FhirPathExtensions(formDefinition.Content)); - } - }, [formDefinition?.Content]); - - const runFhirPathQrUpdater = ( - questionnaire: Questionnaire, - questionnaireResponse: QuestionnaireResponse, - actionRequester: ActionRequester - ): void => { - if (!questionnaire || !questionnaireResponse || !fhirPathUpdater) return; - - // Evaluate all expressions and get the updated response - const updatedResponse = fhirPathUpdater.evaluateAllExpressions(questionnaireResponse); - //TODO: Figure out a way to not run this on all changes - // if (JSON.stringify(updatedResponse) === JSON.stringify(questionnaireResponse)) { - // return; - // } - // Calculate FHIR scores using the same updated response - - const fhirScores = fhirPathUpdater.calculateFhirScore(updatedResponse); - - updateQuestionnaireResponseWithScore(fhirScores, questionnaire, updatedResponse, actionRequester); - }; - const createQuantity = (item: QuestionnaireItem, extension: Coding, value: number): Quantity => { - return { - unit: extension.display, - system: extension.system, - code: extension.code, - value: getDecimalValue(item, value), - }; - }; - const updateQuestionnaireResponseWithScore = ( - scores: AnswerPad, - questionnaire: Questionnaire, - questionnaireResponse: QuestionnaireResponse, - actionRequester: ActionRequester - ): void => { - for (const linkId in scores) { - const item = getQuestionnaireDefinitionItem(linkId, questionnaire.item); - if (!item) continue; - const itemsAndPaths = getResponseItemAndPathWithLinkId(linkId, questionnaireResponse); - const value = scores[linkId]; - switch (item.type) { - case ItemType.QUANTITY: { - const extension = getQuestionnaireUnitExtensionValue(item); - if (!extension) continue; - - for (const itemAndPath of itemsAndPaths) { - actionRequester.addQuantityAnswer( - linkId, - typeof value === 'string' || typeof value === 'number' - ? createQuantity(item, extension, value as number) - : (value as Quantity), - itemAndPath.path[0]?.index - ); - } - break; - } - case ItemType.DECIMAL: { - for (const itemAndPath of itemsAndPaths) { - const decimalValue = getDecimalValue(item, value as number); - actionRequester.addDecimalAnswer(linkId, decimalValue, itemAndPath.path[0]?.index); - } - break; - } - case ItemType.INTEGER: { - for (const itemAndPath of itemsAndPaths) { - actionRequester.addIntegerAnswer(linkId, value as number, itemAndPath.path[0]?.index); - } - - break; - } - case ItemType.BOOLEAN: { - for (const itemAndPath of itemsAndPaths) { - actionRequester.addBooleanAnswer(linkId, value as boolean, itemAndPath.path[0]?.index); - } - break; - } - case ItemType.STRING: - case ItemType.TEXT: - for (const itemAndPath of itemsAndPaths) { - actionRequester.addStringAnswer(linkId, (value as string) ?? '', itemAndPath.path[0]?.index); - } - break; - case ItemType.CHOICE: { - for (const itemAndPath of itemsAndPaths) { - if (actionRequester.isCheckbox(item)) { - const answer = value ? (value as Coding[])?.map(x => ({ valueCoding: x })) : []; - actionRequester.setNewAnswer(linkId, answer, itemAndPath.path[0]?.index); - } else { - actionRequester.addChoiceAnswer(linkId, value as Coding, itemAndPath.path[0]?.index); - } - } - break; - } - case ItemType.OPENCHOICE: { - for (const itemAndPath of itemsAndPaths) { - if (actionRequester.isCheckbox(item)) { - const answer = value ? (value as Coding[])?.map(x => (typeof x === 'string' ? { valueString: x } : { valueCoding: x })) : []; - actionRequester.setNewAnswer(linkId, answer, itemAndPath.path[0]?.index); - } else { - actionRequester.addOpenChoiceAnswer(linkId, value as Coding | string, itemAndPath.path[0]?.index); - } - } - break; - } - case ItemType.DATETIME: - for (const itemAndPath of itemsAndPaths) { - actionRequester.addDateTimeAnswer(linkId, value as string, itemAndPath.path[0]?.index); - } - break; - case ItemType.DATE: - for (const itemAndPath of itemsAndPaths) { - actionRequester.addDateAnswer(linkId, value as string, itemAndPath.path[0]?.index); - } - break; - case ItemType.TIME: - for (const itemAndPath of itemsAndPaths) { - actionRequester.addTimeAnswer(linkId, value as string, itemAndPath.path[0]?.index); - } - } - } - }; - - return { runFhirPathQrUpdater }; -}; diff --git a/src/hooks/useOnAnswerChange.tsx b/src/hooks/useOnAnswerChange.tsx index a4dc8535..825a20aa 100644 --- a/src/hooks/useOnAnswerChange.tsx +++ b/src/hooks/useOnAnswerChange.tsx @@ -1,9 +1,6 @@ -/* eslint-disable @typescript-eslint/no-unused-expressions */ import { QuestionnaireItem, QuestionnaireResponseItemAnswer } from 'fhir/r4'; -import { useFhirPathQrUpdater } from './useFhirPathQrUpdater'; -import { useScoringCalculator } from './useScoringCalculator'; - +import { runCalculatorsAction } from '@/actions/thunks'; import { GlobalState, useAppDispatch } from '@/reducers'; import { ActionRequester, IActionRequester } from '@/util/actionRequester'; import { IQuestionnaireInspector, QuestionniareInspector } from '@/util/questionnaireInspector'; @@ -17,22 +14,19 @@ const useOnAnswerChange = ( ) => void ): ((state: GlobalState, item: QuestionnaireItem, answer?: QuestionnaireResponseItemAnswer) => void) => { const dispatch = useAppDispatch(); - const { runScoringCalculator } = useScoringCalculator(); - const { runFhirPathQrUpdater } = useFhirPathQrUpdater(); return (state: GlobalState, item: QuestionnaireItem, answer?: QuestionnaireResponseItemAnswer): void => { const questionnaire = state.refero.form.FormDefinition.Content; const questionnaireResponse = state.refero.form.FormData.Content; if (questionnaire && questionnaireResponse) { const actionRequester = new ActionRequester(questionnaire, questionnaireResponse); - runFhirPathQrUpdater(questionnaire, questionnaireResponse, actionRequester); - runScoringCalculator(questionnaire, questionnaireResponse, actionRequester); - + dispatch(runCalculatorsAction()); const questionnaireInspector = new QuestionniareInspector(questionnaire, questionnaireResponse); - onChange && answer && item && onChange(item, answer, actionRequester, questionnaireInspector); - for (const action of actionRequester.getActions()) { - dispatch(action); + if (onChange && answer && item) { + onChange(item, answer, actionRequester, questionnaireInspector); } + + actionRequester.dispatchAllActions(dispatch); } }; }; diff --git a/src/hooks/useScoringCalculator.ts b/src/hooks/useScoringCalculator.ts deleted file mode 100644 index fc1098d2..00000000 --- a/src/hooks/useScoringCalculator.ts +++ /dev/null @@ -1,95 +0,0 @@ -import { useEffect, useState } from 'react'; - -import { Quantity, Questionnaire, QuestionnaireResponse } from 'fhir/r4'; - -import { newDecimalValueAction, newIntegerValueAction, newQuantityValueAction } from '@/actions/newValue'; -import ItemType from '@/constants/itemType'; -import { useAppDispatch, useAppSelector } from '@/reducers'; -import { getFormDefinition } from '@/reducers/form'; -import { getDecimalValue } from '@/util'; -import { ActionRequester } from '@/util/actionRequester'; -import { getQuestionnaireUnitExtensionValue } from '@/util/extension'; -import { getQuestionnaireDefinitionItem, getResponseItemAndPathWithLinkId } from '@/util/refero-core'; -import { AnswerPad, ScoringCalculator } from '@/util/scoringCalculator'; - -export const useScoringCalculator = (): { - runScoringCalculator: ( - questionnaire?: Questionnaire | null, - questionnaireResponse?: QuestionnaireResponse | null, - actionRequester?: ActionRequester - ) => Promise; -} => { - const formDefinition = useAppSelector(state => getFormDefinition(state)); - const dispatch = useAppDispatch(); - const [scoringCalculator, setScoringCalculator] = useState(); - - useEffect(() => { - if (formDefinition?.Content) { - setScoringCalculator(new ScoringCalculator(formDefinition.Content)); - } - }, [formDefinition?.Content]); - - const runScoringCalculator = async ( - questionnaire?: Questionnaire | null, - questionnaireResponse?: QuestionnaireResponse | null, - actionRequester?: ActionRequester - ): Promise => { - if (!questionnaire || !questionnaireResponse || !scoringCalculator || !questionnaireHasScoring() || !actionRequester) return; - - // Calculate scores using the updated response - const scores = scoringCalculator.calculateScore(questionnaireResponse); - updateQuestionnaireResponseWithScore(scores, questionnaire, questionnaireResponse, actionRequester); - }; - const updateQuestionnaireResponseWithScore = ( - scores: AnswerPad, - questionnaire: Questionnaire, - questionnaireResponse: QuestionnaireResponse, - actionRequester: ActionRequester - ): void => { - for (const linkId in scores) { - const item = getQuestionnaireDefinitionItem(linkId, questionnaire.item); - if (!item) continue; - const itemsAndPaths = getResponseItemAndPathWithLinkId(linkId, questionnaireResponse); - const value = scores[linkId]; - - switch (item.type) { - case ItemType.QUANTITY: { - const extension = getQuestionnaireUnitExtensionValue(item); - if (!extension) continue; - - const quantity: Quantity = { - unit: extension.display, - system: extension.system, - code: extension.code, - value: getDecimalValue(item, value), - }; - for (const itemAndPath of itemsAndPaths) { - // eslint-disable-next-line @typescript-eslint/no-unused-expressions - actionRequester; - dispatch(newQuantityValueAction({ itemPath: itemAndPath.path, valueQuantity: quantity, item })); - } - break; - } - case ItemType.DECIMAL: { - const decimalValue = getDecimalValue(item, value); - for (const itemAndPath of itemsAndPaths) { - dispatch(newDecimalValueAction({ itemPath: itemAndPath.path, valueDecimal: decimalValue, item })); - } - break; - } - case ItemType.INTEGER: { - const intValue = value !== undefined ? Math.round(value) : undefined; - for (const itemAndPath of itemsAndPaths) { - dispatch(newIntegerValueAction({ itemPath: itemAndPath.path, valueInteger: intValue, item })); - } - break; - } - } - } - }; - const questionnaireHasScoring = (): boolean => { - if (!scoringCalculator) return false; - return scoringCalculator.getIsScoringQuestionnaire() ?? false; - }; - return { runScoringCalculator }; -}; diff --git a/src/util/actionRequester.ts b/src/util/actionRequester.ts index b4a3380c..f277ef34 100644 --- a/src/util/actionRequester.ts +++ b/src/util/actionRequester.ts @@ -21,6 +21,7 @@ import { newAnswerValueAction, } from '@/actions/newValue'; import itemControlConstants from '@/constants/itemcontrol'; +import { AppDispatch } from '@/reducers'; export interface IActionRequester { addIntegerAnswer(linkId: string, value: number, index?: number): void; @@ -239,4 +240,12 @@ export class ActionRequester implements IActionRequester { public addManyActions(actions: PayloadAction[]): void { this.actions.push(...actions); } + + /* + * @description: Dispatches all actions in the actions array and then clears the array. + */ + public dispatchAllActions(dispatch: AppDispatch): void { + this.actions.forEach(action => dispatch(action)); + this.actions = []; + } } From 79e967d684a440c3bb83c095e94502125f07459f Mon Sep 17 00:00:00 2001 From: Reuben Ringdal Date: Thu, 6 Mar 2025 14:14:26 +0100 Subject: [PATCH 2/2] version --- CHANGES.md | 4 ++++ package.json | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 369a9943..66eaf2b4 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,7 @@ +## 17.2.2-beta01 + +- fixed bug by moving the logic for the calculators into a action creator + ## 17.2.1 - Fixed a bug with standard table where the headervalue did not represent the correct value from the choice input elements diff --git a/package.json b/package.json index a61ffd19..b3c30946 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@helsenorge/refero", - "version": "17.2.1", + "version": "17.2.2-beta01", "description": "Refero is a library that uses a fhir r4 schema and creates a interactive form using helsenorge packages.", "keywords": [ "react",