Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/scoring2 #383

Draft
wants to merge 13 commits into
base: master
Choose a base branch
from
2 changes: 0 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,6 @@
"@types/node": "^20.14.8",
"@types/react-collapse": "^5.0.4",
"@types/react-dom": "^18.3.0",
"@types/redux-mock-store": "^1.0.6",
"@types/rollup-plugin-generate-package-json": "^3.2.9",
"@types/rollup-plugin-peer-deps-external": "^2.2.5",
"@types/uuid": "^2.0.35",
Expand Down Expand Up @@ -111,7 +110,6 @@
"pretty-quick": "^4.0.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"redux-mock-store": "^1.5.4",
"redux-thunk": "^3.1.0",
"resolve": "^1.22.8",
"rollup-plugin-copy": "^3.5.0",
Expand Down
2 changes: 1 addition & 1 deletion src/actions/generateQuestionnaireResponse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ export function createQuestionnaireResponseItem(item: QuestionnaireItem): Questi

return responseItem;
}
function evaluateCalculatedExpressions(questionnaire: Questionnaire, response: QuestionnaireResponse): QuestionnaireResponse {
export function evaluateCalculatedExpressions(questionnaire: Questionnaire, response: QuestionnaireResponse): QuestionnaireResponse {
function traverseItems(qItems: QuestionnaireItem[], qrItems: QuestionnaireResponseItem[]): void {
qItems.forEach((qItem, index) => {
const qrItem = qrItems[index];
Expand Down
24 changes: 23 additions & 1 deletion src/actions/newValue.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Coding, QuestionnaireItem, Attachment, QuestionnaireResponseItem, Quantity } from 'fhir/r4';
import { Coding, QuestionnaireItem, Attachment, QuestionnaireResponseItem, Quantity, QuestionnaireResponseItemAnswer } from 'fhir/r4';
import { createAction, PayloadAction } from '@reduxjs/toolkit';

import { AppDispatch, GlobalState } from '../reducers';
Expand All @@ -11,6 +11,8 @@ export const NEW_CODINGSTRING_VALUE: NEW_CODINGSTRING_VALUE = 'refero/NEW_CODING
export type REMOVE_CODINGSTRING_VALUE = 'refero/REMOVE_CODINGSTRING_VALUE';
export const REMOVE_CODINGSTRING_VALUE: REMOVE_CODINGSTRING_VALUE = 'refero/REMOVE_CODINGSTRING_VALUE';
export const REMOVE_CODING_VALUE = 'refero/REMOVE_CODING_VALUE';
export type NEW_ANSWER_VALUE = 'refero/NEW_ANSWER_VALUE';
export const NEW_ANSWER_VALUE: NEW_ANSWER_VALUE = 'refero/NEW_ANSWER_VALUE';
export type ADD_REPEAT_ITEM = 'refero/ADD_REPEAT_ITEM';
export const ADD_REPEAT_ITEM: ADD_REPEAT_ITEM = 'refero/ADD_REPEAT_ITEM';
export type DELETE_REPEAT_ITEM = 'refero/DELETE_REPEAT_ITEM';
Expand All @@ -34,6 +36,7 @@ export type NewValuePayload = {
item?: QuestionnaireItem;
responseItems?: Array<QuestionnaireResponseItem>;
multipleAnswers?: boolean;
newAnswer?: QuestionnaireResponseItemAnswer[];
};
export type RemoveAttachmentPayload = Pick<NewValuePayload, 'itemPath' | 'valueAttachment' | 'item'>;
export type NewAttachmentPayload = Pick<NewValuePayload, 'itemPath' | 'valueAttachment' | 'item' | 'multipleAnswers'>;
Expand All @@ -51,6 +54,8 @@ export type DateItemPayload = Pick<NewValuePayload, 'itemPath' | 'valueDate' | '
export type TimeItemPayload = Pick<NewValuePayload, 'itemPath' | 'valueTime' | 'item'>;
export type DateTimeItemPayload = Pick<NewValuePayload, 'itemPath' | 'valueDateTime' | 'item'>;
export type DeleteRepeatItemPayload = Pick<NewValuePayload, 'itemPath' | 'item'>;
export type AnswerValueItemPayload = Pick<NewValuePayload, 'itemPath' | 'item' | 'newAnswer'>;

export const newValue = createAction<NewValuePayload>(NEW_VALUE);

export const newAttachmentAction = createAction<NewAttachmentPayload>(NEW_VALUE);
Expand Down Expand Up @@ -109,6 +114,16 @@ export function newBooleanValueAsync(itemPath: Array<Path>, value: boolean, item
return await Promise.resolve(getState());
};
}
export const newAnswerValueAction = createAction<AnswerValueItemPayload>(NEW_ANSWER_VALUE);
// /*
// * @deprecated this will be removed in a future version, use newCodingValueAction instead
// */
// export const newCodingsValue = (
// itemPath: CodingValueItemPayload['itemPath'],
// value: CodingValueItemPayload['valueCoding'],
// item: CodingValueItemPayload['item'],
// multipleAnswers?: CodingValueItemPayload['multipleAnswers']
// ): PayloadAction<AnswerValueItemPayload> => newCodingsValueAction({ itemPath, valueCodings: value, item, multipleAnswers });

export const newCodingValueAction = createAction<CodingValueItemPayload>(NEW_VALUE);
/*
Expand Down Expand Up @@ -307,6 +322,13 @@ export function newDateTimeValueAsync(itemPath: Array<Path>, value: string, item
}

export const addRepeatItemAction = createAction<RepeatItemPayload>(ADD_REPEAT_ITEM);

export const addRepeatItemAsync = (parentPath?: Path[], item?: QuestionnaireItem, responseItems?: QuestionnaireResponseItem[]) => {
return async (dispatch: AppDispatch, getState: () => GlobalState): Promise<GlobalState> => {
dispatch(addRepeatItemAction({ parentPath, item, responseItems }));
return await Promise.resolve(getState());
};
};
/*
* @deprecated this will be removed in a future version, use addRepeatItemAction instead
*/
Expand Down
18 changes: 10 additions & 8 deletions src/components/formcomponents/choice/choice.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -72,15 +72,16 @@ export const Choice = (props: ChoiceProps): JSX.Element | null => {
// }, [item]);

const getPDFValue = (): string => {
const getDataReceiverValue = (answer: Array<QuestionnaireResponseItemAnswer>): (string | undefined)[] => {
return answer.map((el: QuestionnaireResponseItemAnswer) => {
if (el && el.valueCoding && el.valueCoding.display) {
return el.valueCoding.display;
}
});
};
if (isDataReceiver(item)) {
return getDataReceiverValue(answer as Array<QuestionnaireResponseItemAnswer>).join(', ');
return Array.isArray(answer)
? answer
.map((el: QuestionnaireResponseItemAnswer) => {
if (el && el.valueCoding && el.valueCoding.display) {
return el.valueCoding.display;
}
})
?.join(', ')
: (answer && answer.valueCoding && answer.valueCoding.display) || resources?.ikkeBesvart || '';
}
const value = getAnswerValue();
if (!value || value.length === 0) {
Expand Down Expand Up @@ -158,6 +159,7 @@ export const Choice = (props: ChoiceProps): JSX.Element | null => {

const renderComponentBasedOnType = (): JSX.Element | null => {
const pdfValue = getPDFValue();

const itemControlValue = getItemControlValue(item);
if (!itemControlValue) return null;

Expand Down
8 changes: 6 additions & 2 deletions src/components/formcomponents/repeat/RepeatButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@ import Button from '@helsenorge/designsystem-react/components/Button';
import Icon from '@helsenorge/designsystem-react/components/Icon';
import PlusLarge from '@helsenorge/designsystem-react/components/Icons/PlusLarge';

import { addRepeatItemAction } from '../../../actions/newValue';
import { addRepeatItemAsync } from '../../../actions/newValue';
import { useAppDispatch } from '../../../reducers';
import { getRepeatsTextExtension } from '../../../util/extension';
import { Path } from '../../../util/refero-core';
import { useExternalRenderContext } from '@/context/externalRenderContext';
import useOnAnswerChange from '@/hooks/useOnAnswerChange';

interface Props {
item?: QuestionnaireItem;
Expand All @@ -20,9 +21,12 @@ interface Props {
export const RepeatButton = ({ item, parentPath, responseItems, disabled }: Props): JSX.Element => {
const dispatch = useAppDispatch();
const { resources } = useExternalRenderContext();
const onAnswerChange = useOnAnswerChange();
const onAddRepeatItem = (): void => {
if (dispatch && item) {
dispatch(addRepeatItemAction({ parentPath, item, responseItems }));
dispatch(addRepeatItemAsync(parentPath, item, responseItems))?.then(newState => {
return onAnswerChange && onAnswerChange(newState, item);
});
}
};
const text = getRepeatsTextExtension(item);
Expand Down
1 change: 0 additions & 1 deletion src/constants/scoringItemType.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,5 @@ export enum ScoringItemType {
TOTAL_SCORE = 'TOTAL_SCORE',
SECTION_SCORE = 'SECTION_SCORE',
QUESTION_SCORE = 'QUESTION_SCORE',
QUESTION_FHIRPATH_SCORE = 'QUESTION_FHIRPATH_SCORE',
NONE = 'NONE',
}
150 changes: 150 additions & 0 deletions src/hooks/useFhirPathQrUpdater.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
import ItemType from '@/constants/itemType';
import { GlobalState } 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';
import { Coding, Quantity, Questionnaire, QuestionnaireItem, QuestionnaireResponse } from 'fhir/r4';
import { useEffect, useState } from 'react';
import { useSelector } from 'react-redux';

export const useFhirPathQrUpdater = (): {
runFhirPathQrUpdater: (
questionnaire: Questionnaire,
questionnaireResponse: QuestionnaireResponse,
actionRequester: ActionRequester
) => void;
} => {
const formDefinition = useSelector((state: GlobalState) => getFormDefinition(state));
const [fhirPathUpdater, setFhirPathUpdater] = useState<FhirPathExtensions | undefined>();

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 };
};
67 changes: 4 additions & 63 deletions src/hooks/useGetAnswer.ts
Original file line number Diff line number Diff line change
@@ -1,75 +1,16 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import ItemType from '@/constants/itemType';
import { GlobalState } from '@/reducers';
import { findQuestionnaireItem, getResponseItemWithPathSelector, questionnaireResponseSelector } from '@/reducers/selectors';
import { getCalculatedExpressionExtension, getCopyExtension } from '@/util/extension';
import { evaluateFhirpathExpressionToGetString } from '@/util/fhirpathHelper';
import { getResponseItemWithPathSelector } from '@/reducers/selectors';
import { getAnswerFromResponseItem, Path } from '@/util/refero-core';
import { Extension, QuestionnaireItem, QuestionnaireResponse, QuestionnaireResponseItem, QuestionnaireResponseItemAnswer } from 'fhir/r4';
import { QuestionnaireResponseItem, QuestionnaireResponseItemAnswer } from 'fhir/r4';

import { useSelector } from 'react-redux';

function getAnswerIfDataReceiver(
questionnaireResponse: QuestionnaireResponse | null | undefined,
item: QuestionnaireItem,
extension: Extension
): QuestionnaireResponseItemAnswer | QuestionnaireResponseItemAnswer[] | undefined {
const result = evaluateFhirpathExpressionToGetString(extension, questionnaireResponse);
const processedResult = getCalculatedExpressionExtension(item) ? result.map((res: any) => res?.value ?? res) : result;

return getQuestionnaireResponseItemAnswer(item.type, processedResult);
}

function getQuestionnaireResponseItemAnswer(
type: string,
result: any[]
): QuestionnaireResponseItemAnswer | QuestionnaireResponseItemAnswer[] {
if (type === ItemType.BOOLEAN) {
return { valueBoolean: result[0] };
}
return result.map((answer: any) => {
switch (String(type)) {
case ItemType.TEXT:
case ItemType.STRING:
return { valueString: answer };
case ItemType.INTEGER:
return { valueInteger: answer };
case ItemType.DECIMAL:
return { valueDecimal: answer };

case ItemType.QUANTITY:
return { valueQuantity: answer };
case ItemType.DATETIME:
return { valueDateTime: answer };
case ItemType.DATE:
return { valueDate: answer };
case ItemType.TIME:
return { valueTime: answer };
default: {
if (typeof answer === 'string') {
return { valueString: answer };
} else {
return { valueCoding: answer };
}
}
}
});
}

export const useGetAnswer = (
linkId?: string,
_linkId?: string,
path?: Path[]
): QuestionnaireResponseItemAnswer | QuestionnaireResponseItemAnswer[] | undefined => {
const questionnaireResponse = useSelector<GlobalState, QuestionnaireResponse | null | undefined>(questionnaireResponseSelector);
const item = useSelector<GlobalState, QuestionnaireItem | undefined>(state => findQuestionnaireItem(state, linkId));
const responseItem = useSelector<GlobalState, QuestionnaireResponseItem | undefined>(state =>
getResponseItemWithPathSelector(state, path)
);
const dataRecieverExtension = item && getCopyExtension(item);

const answer = dataRecieverExtension
? getAnswerIfDataReceiver(questionnaireResponse, item, dataRecieverExtension)
: getAnswerFromResponseItem(responseItem);

return answer;
return getAnswerFromResponseItem(responseItem);
};
Loading
Loading