diff --git a/resources/seeds/Client/a57d90e3-5f69-4b92-aa2e-2992180863c1.yaml b/resources/seeds/Client/a57d90e3-5f69-4b92-aa2e-2992180863c1.yaml index 11a11eb9..d5ec00ed 100644 --- a/resources/seeds/Client/a57d90e3-5f69-4b92-aa2e-2992180863c1.yaml +++ b/resources/seeds/Client/a57d90e3-5f69-4b92-aa2e-2992180863c1.yaml @@ -4,7 +4,7 @@ auth: refresh_token: true secret_required: false access_token_expiration: 36000 -type: smart-on-fhir +type: smart-on-fhir-practitioner smart: launch_uri: https://www.smartforms.io/launch name: SmartForms diff --git a/resources/seeds/Client/a57d90e3-5f69-4b92-aa2e-2992180863c2.yaml b/resources/seeds/Client/a57d90e3-5f69-4b92-aa2e-2992180863c2.yaml new file mode 100644 index 00000000..2261a9f9 --- /dev/null +++ b/resources/seeds/Client/a57d90e3-5f69-4b92-aa2e-2992180863c2.yaml @@ -0,0 +1,15 @@ +auth: + authorization_code: + redirect_uri: https://portal-xrp.digitalhealth.gov.au/FormsPortal/authorisation + refresh_token: true + secret_required: false + access_token_expiration: 36000 +type: smart-on-fhir-practitioner +smart: + name: ADHA CHAP Form + launch_uri: https://portal-xrp.digitalhealth.gov.au/FormsPortal/launch + description: ADHA CHAP Form +grant_types: + - authorization_code +id: a57d90e3-5f69-4b92-aa2e-2992180863c2 +resourceType: Client diff --git a/resources/seeds/Client/de26b280-d3fc-4db1-9df3-50f3f328b7a5.yaml b/resources/seeds/Client/de26b280-d3fc-4db1-9df3-50f3f328b7a5.yaml index b8723856..7a8e6c0d 100644 --- a/resources/seeds/Client/de26b280-d3fc-4db1-9df3-50f3f328b7a5.yaml +++ b/resources/seeds/Client/de26b280-d3fc-4db1-9df3-50f3f328b7a5.yaml @@ -4,7 +4,7 @@ auth: refresh_token: true secret_required: false access_token_expiration: 36000 -type: smart-on-fhir +type: smart-on-fhir-practitioner smart: name: Clinical guidelines launch_uri: https://beda.caresofa.com/ diff --git a/src/containers/PatientDetails/DocumentPrint/utils.ts b/src/containers/PatientDetails/DocumentPrint/utils.ts index 573f1153..71401eb1 100644 --- a/src/containers/PatientDetails/DocumentPrint/utils.ts +++ b/src/containers/PatientDetails/DocumentPrint/utils.ts @@ -1,41 +1,46 @@ import { QuestionnaireItem, QuestionnaireResponse } from 'fhir/r4b'; -import { evaluate } from 'src/utils'; +import { compileAsFirst } from 'src/utils'; -export function findQRItemValue(linkId: string, type = 'String') { - return `repeat(item).where(linkId='${linkId}').answer.value${type}`; -} +const qItemIsHidden = compileAsFirst( + "extension.where(url='http://hl7.org/fhir/StructureDefinition/questionnaire-hidden').exists() and extension.where(url='http://hl7.org/fhir/StructureDefinition/questionnaire-hidden').valueBoolean=true", +); + +const getQrItemValueByLinkIdAndType = (linkId: string, type: string) => + compileAsFirst(`repeat(item).where(linkId='${linkId}').answer.value${type}`); + +const questionnaireItemValueTypeMap: Record = { + display: 'String', + group: 'String', + text: 'String', + string: 'String', + decimal: 'Decimal', + integer: 'Integer', + date: 'Date', + dateTime: 'DateTime', + time: 'Time', + choice: 'Coding.display', + boolean: 'Boolean', + reference: 'Reference.display', + 'open-choice': '', + attachment: '', + quantity: '', + question: '', + url: '', +}; export function getQuestionnaireItemValue( questionnaireItem: QuestionnaireItem, questionnaireResponse: QuestionnaireResponse, ) { - switch (questionnaireItem.type) { - case 'display': - case 'group': - return ''; - case 'text': - case 'string': - return evaluate(questionnaireResponse, findQRItemValue(questionnaireItem.linkId, 'String'))[0]; - case 'decimal': - return evaluate(questionnaireResponse, findQRItemValue(questionnaireItem.linkId, 'Decimal'))[0]; - case 'integer': - return evaluate(questionnaireResponse, findQRItemValue(questionnaireItem.linkId, 'Integer'))[0]; - case 'date': - return evaluate(questionnaireResponse, findQRItemValue(questionnaireItem.linkId, 'Date'))[0]; - case 'dateTime': - return evaluate(questionnaireResponse, findQRItemValue(questionnaireItem.linkId, 'DateTime'))[0]; - case 'time': - return evaluate(questionnaireResponse, findQRItemValue(questionnaireItem.linkId, 'Time'))[0]; - case 'choice': - return evaluate(questionnaireResponse, findQRItemValue(questionnaireItem.linkId, 'Coding.display'))[0]; - case 'boolean': - return evaluate(questionnaireResponse, findQRItemValue(questionnaireItem.linkId, 'Boolean'))[0]; - case 'reference': - return evaluate(questionnaireResponse, findQRItemValue(questionnaireItem.linkId, 'Reference.display'))[0]; - default: - return evaluate(questionnaireResponse, findQRItemValue(questionnaireItem.linkId))[0]; + if (qItemIsHidden(questionnaireItem)) { + return undefined; } + + return getQrItemValueByLinkIdAndType( + questionnaireItem.linkId, + questionnaireItemValueTypeMap[questionnaireItem.type], + )(questionnaireResponse); } export function flattenQuestionnaireGroupItems(item: QuestionnaireItem): QuestionnaireItem[] { diff --git a/src/utils/__tests__/enableWhen/equal.test.ts b/src/utils/__tests__/enableWhen/equal.test.ts new file mode 100644 index 00000000..83178d96 --- /dev/null +++ b/src/utils/__tests__/enableWhen/equal.test.ts @@ -0,0 +1,152 @@ +import { + CONTROL_ITEM_LINK_ID, + generateQAndQRData, + QuestionnaireData, + testEnableWhenCases, + ENABLE_WHEN_TESTS_TITLE, +} from './utils'; + +const ENABLE_WHEN_EQUAL_QUESTIONAIRES: QuestionnaireData[] = [ + { + ...generateQAndQRData({ + type: 'integer', + enableWhen: [ + { + question: 'q1', + operator: '=', + answer: { integer: 1 }, + }, + ], + qrItem: [ + { + linkId: 'q1', + answer: [{ value: { integer: 1 } }], + }, + { + linkId: 'q2', + answer: [], + }, + { + linkId: CONTROL_ITEM_LINK_ID, + answer: [], + }, + ], + }), + }, + { + ...generateQAndQRData({ + type: 'integer', + enableWhen: [ + { + question: 'q1', + operator: '=', + answer: { string: 'test' }, + }, + ], + qrItem: [ + { + linkId: 'q1', + answer: [{ value: { string: 'test2' } }], + }, + { + linkId: 'q2', + answer: [], + }, + ], + }), + }, + { + ...generateQAndQRData({ + type: 'integer', + enableWhen: [ + { + question: 'q1', + operator: '=', + answer: { string: 'test1' }, + }, + { + question: 'q2', + operator: '=', + answer: { string: 'test2' }, + }, + ], + qrItem: [ + { + linkId: 'q1', + answer: [{ value: { string: 'test1' } }], + }, + { + linkId: 'q2', + answer: [{ value: { string: 'test2' } }], + }, + { + linkId: CONTROL_ITEM_LINK_ID, + answer: [], + }, + ], + }), + }, + { + ...generateQAndQRData({ + type: 'integer', + enableWhen: [ + { + question: 'q1', + operator: '=', + answer: { string: 'test1' }, + }, + { + question: 'q2', + operator: '=', + answer: { string: 'test2' }, + }, + ], + qrItem: [ + { + linkId: 'q1', + answer: [{ value: { string: 'asd' } }], + }, + { + linkId: 'q2', + answer: [{ value: { string: 'test2' } }], + }, + ], + }), + }, + { + ...generateQAndQRData({ + type: 'integer', + enableBehavior: 'any', + enableWhen: [ + { + question: 'q1', + operator: '=', + answer: { Coding: { code: 'test1', display: 'test1' } }, + }, + { + question: 'q2', + operator: '=', + answer: { Coding: { code: 'test2', display: 'test2' } }, + }, + ], + qrItem: [ + { + linkId: 'q1', + answer: [{ value: { Coding: { code: 'asd', display: 'asd' } } }], + }, + { + linkId: 'q2', + answer: [{ value: { Coding: { code: 'test2', display: 'Different display' } } }], + }, + { + linkId: CONTROL_ITEM_LINK_ID, + answer: [], + }, + ], + }), + }, +]; + +describe('Enable when: "="', () => { + test.each(ENABLE_WHEN_EQUAL_QUESTIONAIRES)(ENABLE_WHEN_TESTS_TITLE, testEnableWhenCases); +}); diff --git a/src/utils/__tests__/enableWhen/exists.test.ts b/src/utils/__tests__/enableWhen/exists.test.ts new file mode 100644 index 00000000..c2940bcc --- /dev/null +++ b/src/utils/__tests__/enableWhen/exists.test.ts @@ -0,0 +1,152 @@ +import { + CONTROL_ITEM_LINK_ID, + generateQAndQRData, + QuestionnaireData, + testEnableWhenCases, + ENABLE_WHEN_TESTS_TITLE, +} from './utils'; + +const ENABLE_WHEN_EXISTS_QUESTIONAIRES: QuestionnaireData[] = [ + { + ...generateQAndQRData({ + type: 'integer', + enableWhen: [ + { + question: 'q1', + operator: 'exists', + answer: { boolean: true }, + }, + ], + qrItem: [ + { + linkId: 'q1', + answer: [{ value: { string: 'test1' } }], + }, + { + linkId: 'q2', + answer: [], + }, + { + linkId: CONTROL_ITEM_LINK_ID, + answer: [], + }, + ], + }), + }, + { + ...generateQAndQRData({ + type: 'integer', + enableWhen: [ + { + question: 'q1', + operator: 'exists', + answer: { boolean: false }, + }, + ], + qrItem: [ + { + linkId: 'q1', + answer: [{ value: { string: 'test2' } }], + }, + { + linkId: 'q2', + answer: [], + }, + ], + }), + }, + { + ...generateQAndQRData({ + type: 'integer', + enableWhen: [ + { + question: 'q1', + operator: 'exists', + answer: { boolean: true }, + }, + { + question: 'q2', + operator: 'exists', + answer: { boolean: true }, + }, + ], + qrItem: [ + { + linkId: 'q1', + answer: [{ value: { string: 'test1' } }], + }, + { + linkId: 'q2', + answer: [{ value: { string: 'test2' } }], + }, + { + linkId: CONTROL_ITEM_LINK_ID, + answer: [], + }, + ], + }), + }, + { + ...generateQAndQRData({ + type: 'integer', + enableWhen: [ + { + question: 'q1', + operator: 'exists', + answer: { boolean: true }, + }, + { + question: 'q2', + operator: 'exists', + answer: { boolean: true }, + }, + ], + qrItem: [ + { + linkId: 'q1', + answer: [], + }, + { + linkId: 'q2', + answer: [{ value: { string: 'test2' } }], + }, + ], + }), + }, + { + ...generateQAndQRData({ + type: 'integer', + enableBehavior: 'any', + enableWhen: [ + { + question: 'q1', + operator: 'exists', + answer: { boolean: true }, + }, + { + question: 'q2', + operator: 'exists', + answer: { boolean: false }, + }, + ], + qrItem: [ + { + linkId: 'q1', + answer: [{ value: { Coding: { code: 'asd', display: 'asd' } } }], + }, + { + linkId: 'q2', + answer: [{ value: { Coding: { code: 'test2', display: 'test2' } } }], + }, + { + linkId: CONTROL_ITEM_LINK_ID, + answer: [], + }, + ], + }), + }, +]; + +describe('Enable when: "exists"', () => { + test.each(ENABLE_WHEN_EXISTS_QUESTIONAIRES)(ENABLE_WHEN_TESTS_TITLE, testEnableWhenCases); +}); diff --git a/src/utils/__tests__/enableWhen/gt.test.ts b/src/utils/__tests__/enableWhen/gt.test.ts new file mode 100644 index 00000000..8df46d3c --- /dev/null +++ b/src/utils/__tests__/enableWhen/gt.test.ts @@ -0,0 +1,152 @@ +import { + CONTROL_ITEM_LINK_ID, + generateQAndQRData, + QuestionnaireData, + testEnableWhenCases, + ENABLE_WHEN_TESTS_TITLE, +} from './utils'; + +const ENABLE_WHEN_GT_QUESTIONAIRES: QuestionnaireData[] = [ + { + ...generateQAndQRData({ + type: 'integer', + enableWhen: [ + { + question: 'q1', + operator: '>', + answer: { integer: 10 }, + }, + ], + qrItem: [ + { + linkId: 'q1', + answer: [{ value: { integer: 11 } }], + }, + { + linkId: 'q2', + answer: [], + }, + { + linkId: CONTROL_ITEM_LINK_ID, + answer: [], + }, + ], + }), + }, + { + ...generateQAndQRData({ + type: 'integer', + enableWhen: [ + { + question: 'q1', + operator: '>', + answer: { integer: 10 }, + }, + ], + qrItem: [ + { + linkId: 'q1', + answer: [{ value: { integer: 10 } }], + }, + { + linkId: 'q2', + answer: [], + }, + ], + }), + }, + { + ...generateQAndQRData({ + type: 'integer', + enableWhen: [ + { + question: 'q1', + operator: '>', + answer: { integer: 10 }, + }, + { + question: 'q2', + operator: '>', + answer: { integer: 5 }, + }, + ], + qrItem: [ + { + linkId: 'q1', + answer: [{ value: { integer: 15 } }], + }, + { + linkId: 'q2', + answer: [{ value: { integer: 6 } }], + }, + { + linkId: CONTROL_ITEM_LINK_ID, + answer: [], + }, + ], + }), + }, + { + ...generateQAndQRData({ + type: 'integer', + enableWhen: [ + { + question: 'q1', + operator: '>', + answer: { integer: 10 }, + }, + { + question: 'q2', + operator: '>', + answer: { integer: 5 }, + }, + ], + qrItem: [ + { + linkId: 'q1', + answer: [{ value: { integer: 11 } }], + }, + { + linkId: 'q2', + answer: [{ value: { integer: 5 } }], + }, + ], + }), + }, + { + ...generateQAndQRData({ + type: 'integer', + enableBehavior: 'any', + enableWhen: [ + { + question: 'q1', + operator: '>', + answer: { integer: 10 }, + }, + { + question: 'q2', + operator: '>', + answer: { integer: 5 }, + }, + ], + qrItem: [ + { + linkId: 'q1', + answer: [{ value: { integer: 10 } }], + }, + { + linkId: 'q2', + answer: [{ value: { integer: 6 } }], + }, + { + linkId: CONTROL_ITEM_LINK_ID, + answer: [], + }, + ], + }), + }, +]; + +describe('Enable when: ">"', () => { + test.each(ENABLE_WHEN_GT_QUESTIONAIRES)(ENABLE_WHEN_TESTS_TITLE, testEnableWhenCases); +}); diff --git a/src/utils/__tests__/enableWhen/gte.test.ts b/src/utils/__tests__/enableWhen/gte.test.ts new file mode 100644 index 00000000..8ff81bdc --- /dev/null +++ b/src/utils/__tests__/enableWhen/gte.test.ts @@ -0,0 +1,152 @@ +import { + CONTROL_ITEM_LINK_ID, + generateQAndQRData, + QuestionnaireData, + testEnableWhenCases, + ENABLE_WHEN_TESTS_TITLE, +} from './utils'; + +const ENABLE_WHEN_GTE_QUESTIONAIRES: QuestionnaireData[] = [ + { + ...generateQAndQRData({ + type: 'integer', + enableWhen: [ + { + question: 'q1', + operator: '>=', + answer: { integer: 10 }, + }, + ], + qrItem: [ + { + linkId: 'q1', + answer: [{ value: { integer: 10 } }], + }, + { + linkId: 'q2', + answer: [], + }, + { + linkId: CONTROL_ITEM_LINK_ID, + answer: [], + }, + ], + }), + }, + { + ...generateQAndQRData({ + type: 'integer', + enableWhen: [ + { + question: 'q1', + operator: '>=', + answer: { integer: 10 }, + }, + ], + qrItem: [ + { + linkId: 'q1', + answer: [{ value: { integer: 9 } }], + }, + { + linkId: 'q2', + answer: [], + }, + ], + }), + }, + { + ...generateQAndQRData({ + type: 'integer', + enableWhen: [ + { + question: 'q1', + operator: '>=', + answer: { integer: 10 }, + }, + { + question: 'q2', + operator: '>=', + answer: { integer: 5 }, + }, + ], + qrItem: [ + { + linkId: 'q1', + answer: [{ value: { integer: 10 } }], + }, + { + linkId: 'q2', + answer: [{ value: { integer: 5 } }], + }, + { + linkId: CONTROL_ITEM_LINK_ID, + answer: [], + }, + ], + }), + }, + { + ...generateQAndQRData({ + type: 'integer', + enableWhen: [ + { + question: 'q1', + operator: '>=', + answer: { integer: 10 }, + }, + { + question: 'q2', + operator: '>=', + answer: { integer: 5 }, + }, + ], + qrItem: [ + { + linkId: 'q1', + answer: [{ value: { integer: 9 } }], + }, + { + linkId: 'q2', + answer: [{ value: { integer: 5 } }], + }, + ], + }), + }, + { + ...generateQAndQRData({ + type: 'integer', + enableBehavior: 'any', + enableWhen: [ + { + question: 'q1', + operator: '>=', + answer: { integer: 10 }, + }, + { + question: 'q2', + operator: '>=', + answer: { integer: 5 }, + }, + ], + qrItem: [ + { + linkId: 'q1', + answer: [{ value: { integer: 1 } }], + }, + { + linkId: 'q2', + answer: [{ value: { integer: 5 } }], + }, + { + linkId: CONTROL_ITEM_LINK_ID, + answer: [], + }, + ], + }), + }, +]; + +describe('Enable when: ">="', () => { + test.each(ENABLE_WHEN_GTE_QUESTIONAIRES)(ENABLE_WHEN_TESTS_TITLE, testEnableWhenCases); +}); diff --git a/src/utils/__tests__/enableWhen/lt.test.ts b/src/utils/__tests__/enableWhen/lt.test.ts new file mode 100644 index 00000000..aaaad297 --- /dev/null +++ b/src/utils/__tests__/enableWhen/lt.test.ts @@ -0,0 +1,152 @@ +import { + CONTROL_ITEM_LINK_ID, + generateQAndQRData, + QuestionnaireData, + testEnableWhenCases, + ENABLE_WHEN_TESTS_TITLE, +} from './utils'; + +const ENABLE_WHEN_LT_QUESTIONAIRES: QuestionnaireData[] = [ + { + ...generateQAndQRData({ + type: 'integer', + enableWhen: [ + { + question: 'q1', + operator: '<', + answer: { integer: 10 }, + }, + ], + qrItem: [ + { + linkId: 'q1', + answer: [{ value: { integer: 1 } }], + }, + { + linkId: 'q2', + answer: [], + }, + { + linkId: CONTROL_ITEM_LINK_ID, + answer: [], + }, + ], + }), + }, + { + ...generateQAndQRData({ + type: 'integer', + enableWhen: [ + { + question: 'q1', + operator: '<', + answer: { integer: 10 }, + }, + ], + qrItem: [ + { + linkId: 'q1', + answer: [{ value: { integer: 10 } }], + }, + { + linkId: 'q2', + answer: [], + }, + ], + }), + }, + { + ...generateQAndQRData({ + type: 'integer', + enableWhen: [ + { + question: 'q1', + operator: '<', + answer: { integer: 10 }, + }, + { + question: 'q2', + operator: '<', + answer: { integer: 5 }, + }, + ], + qrItem: [ + { + linkId: 'q1', + answer: [{ value: { integer: 4 } }], + }, + { + linkId: 'q2', + answer: [{ value: { integer: 1 } }], + }, + { + linkId: CONTROL_ITEM_LINK_ID, + answer: [], + }, + ], + }), + }, + { + ...generateQAndQRData({ + type: 'integer', + enableWhen: [ + { + question: 'q1', + operator: '<', + answer: { integer: 10 }, + }, + { + question: 'q2', + operator: '<', + answer: { integer: 5 }, + }, + ], + qrItem: [ + { + linkId: 'q1', + answer: [{ value: { integer: 10 } }], + }, + { + linkId: 'q2', + answer: [{ value: { integer: 4 } }], + }, + ], + }), + }, + { + ...generateQAndQRData({ + type: 'integer', + enableBehavior: 'any', + enableWhen: [ + { + question: 'q1', + operator: '<', + answer: { integer: 10 }, + }, + { + question: 'q2', + operator: '<', + answer: { integer: 5 }, + }, + ], + qrItem: [ + { + linkId: 'q1', + answer: [{ value: { integer: 10 } }], + }, + { + linkId: 'q2', + answer: [{ value: { integer: 4 } }], + }, + { + linkId: CONTROL_ITEM_LINK_ID, + answer: [], + }, + ], + }), + }, +]; + +describe('Enable when: "<"', () => { + test.each(ENABLE_WHEN_LT_QUESTIONAIRES)(ENABLE_WHEN_TESTS_TITLE, testEnableWhenCases); +}); diff --git a/src/utils/__tests__/enableWhen/lte.test.ts b/src/utils/__tests__/enableWhen/lte.test.ts new file mode 100644 index 00000000..eddd54e8 --- /dev/null +++ b/src/utils/__tests__/enableWhen/lte.test.ts @@ -0,0 +1,152 @@ +import { + CONTROL_ITEM_LINK_ID, + generateQAndQRData, + QuestionnaireData, + testEnableWhenCases, + ENABLE_WHEN_TESTS_TITLE, +} from './utils'; + +const ENABLE_WHEN_LTE_QUESTIONAIRES: QuestionnaireData[] = [ + { + ...generateQAndQRData({ + type: 'integer', + enableWhen: [ + { + question: 'q1', + operator: '<=', + answer: { integer: 10 }, + }, + ], + qrItem: [ + { + linkId: 'q1', + answer: [{ value: { integer: 10 } }], + }, + { + linkId: 'q2', + answer: [], + }, + { + linkId: CONTROL_ITEM_LINK_ID, + answer: [], + }, + ], + }), + }, + { + ...generateQAndQRData({ + type: 'integer', + enableWhen: [ + { + question: 'q1', + operator: '<=', + answer: { integer: 10 }, + }, + ], + qrItem: [ + { + linkId: 'q1', + answer: [{ value: { integer: 11 } }], + }, + { + linkId: 'q2', + answer: [], + }, + ], + }), + }, + { + ...generateQAndQRData({ + type: 'integer', + enableWhen: [ + { + question: 'q1', + operator: '<=', + answer: { integer: 10 }, + }, + { + question: 'q2', + operator: '<=', + answer: { integer: 5 }, + }, + ], + qrItem: [ + { + linkId: 'q1', + answer: [{ value: { integer: 10 } }], + }, + { + linkId: 'q2', + answer: [{ value: { integer: 0 } }], + }, + { + linkId: CONTROL_ITEM_LINK_ID, + answer: [], + }, + ], + }), + }, + { + ...generateQAndQRData({ + type: 'integer', + enableWhen: [ + { + question: 'q1', + operator: '<=', + answer: { integer: 10 }, + }, + { + question: 'q2', + operator: '<=', + answer: { integer: 5 }, + }, + ], + qrItem: [ + { + linkId: 'q1', + answer: [{ value: { integer: 11 } }], + }, + { + linkId: 'q2', + answer: [{ value: { integer: 5 } }], + }, + ], + }), + }, + { + ...generateQAndQRData({ + type: 'integer', + enableBehavior: 'any', + enableWhen: [ + { + question: 'q1', + operator: '<=', + answer: { integer: 10 }, + }, + { + question: 'q2', + operator: '<=', + answer: { integer: 5 }, + }, + ], + qrItem: [ + { + linkId: 'q1', + answer: [{ value: { integer: 1 } }], + }, + { + linkId: 'q2', + answer: [{ value: { integer: 5 } }], + }, + { + linkId: CONTROL_ITEM_LINK_ID, + answer: [], + }, + ], + }), + }, +]; + +describe('Enable when: "<="', () => { + test.each(ENABLE_WHEN_LTE_QUESTIONAIRES)(ENABLE_WHEN_TESTS_TITLE, testEnableWhenCases); +}); diff --git a/src/utils/__tests__/enableWhen/notEqual.test.ts b/src/utils/__tests__/enableWhen/notEqual.test.ts new file mode 100644 index 00000000..b9feffe1 --- /dev/null +++ b/src/utils/__tests__/enableWhen/notEqual.test.ts @@ -0,0 +1,152 @@ +import { + CONTROL_ITEM_LINK_ID, + generateQAndQRData, + QuestionnaireData, + testEnableWhenCases, + ENABLE_WHEN_TESTS_TITLE, +} from './utils'; + +const ENABLE_WHEN_NOT_EQUAL_QUESTIONAIRES: QuestionnaireData[] = [ + { + ...generateQAndQRData({ + type: 'integer', + enableWhen: [ + { + question: 'q1', + operator: '!=', + answer: { integer: 1 }, + }, + ], + qrItem: [ + { + linkId: 'q1', + answer: [{ value: { integer: 1 } }], + }, + { + linkId: 'q2', + answer: [], + }, + ], + }), + }, + { + ...generateQAndQRData({ + type: 'integer', + enableWhen: [ + { + question: 'q1', + operator: '!=', + answer: { string: 'test' }, + }, + ], + qrItem: [ + { + linkId: 'q1', + answer: [{ value: { string: 'test2' } }], + }, + { + linkId: 'q2', + answer: [], + }, + { + linkId: CONTROL_ITEM_LINK_ID, + answer: [], + }, + ], + }), + }, + { + ...generateQAndQRData({ + type: 'integer', + enableWhen: [ + { + question: 'q1', + operator: '!=', + answer: { string: 'test1' }, + }, + { + question: 'q2', + operator: '!=', + answer: { string: 'test2' }, + }, + ], + qrItem: [ + { + linkId: 'q1', + answer: [{ value: { string: 'test1' } }], + }, + { + linkId: 'q2', + answer: [{ value: { string: 'test2' } }], + }, + ], + }), + }, + { + ...generateQAndQRData({ + type: 'integer', + enableWhen: [ + { + question: 'q1', + operator: '!=', + answer: { string: 'test1' }, + }, + { + question: 'q2', + operator: '!=', + answer: { string: 'test2' }, + }, + ], + qrItem: [ + { + linkId: 'q1', + answer: [{ value: { string: 'asd' } }], + }, + { + linkId: 'q2', + answer: [{ value: { string: 'test2' } }], + }, + { + linkId: CONTROL_ITEM_LINK_ID, + answer: [], + }, + ], + }), + }, + { + ...generateQAndQRData({ + type: 'integer', + enableBehavior: 'any', + enableWhen: [ + { + question: 'q1', + operator: '!=', + answer: { Coding: { code: 'test1', display: 'test1' } }, + }, + { + question: 'q2', + operator: '!=', + answer: { Coding: { code: 'test2', display: 'test2' } }, + }, + ], + qrItem: [ + { + linkId: 'q1', + answer: [{ value: { Coding: { code: 'asd', display: 'asd' } } }], + }, + { + linkId: 'q2', + answer: [{ value: { Coding: { code: 'test2', display: 'test2' } } }], + }, + { + linkId: CONTROL_ITEM_LINK_ID, + answer: [], + }, + ], + }), + }, +]; + +describe('Enable when: "!="', () => { + test.each(ENABLE_WHEN_NOT_EQUAL_QUESTIONAIRES)(ENABLE_WHEN_TESTS_TITLE, testEnableWhenCases); +}); diff --git a/src/utils/__tests__/enableWhen/utils.ts b/src/utils/__tests__/enableWhen/utils.ts new file mode 100644 index 00000000..238eecae --- /dev/null +++ b/src/utils/__tests__/enableWhen/utils.ts @@ -0,0 +1,89 @@ +import { + Questionnaire, + QuestionnaireItem, + QuestionnaireItemEnableWhen, + QuestionnaireResponse, + QuestionnaireResponseItem, +} from '@beda.software/aidbox-types'; + +import { evaluate, questionnaireItemsToValidationSchema } from 'src/utils'; + +export type QuestionnaireData = { + questionnaire: Questionnaire; + questionnaireResponse: QuestionnaireResponse; +}; + +export const CONTROL_ITEM_LINK_ID = 'control-item'; + +interface GenerateQAndQRDataProps { + type: QuestionnaireItem['type']; + enableWhen: QuestionnaireItemEnableWhen[]; + enableBehavior?: QuestionnaireItem['enableBehavior']; + qrItem: QuestionnaireResponseItem[]; +} +export function generateQAndQRData( + props: GenerateQAndQRDataProps, +): Pick { + const { type, enableWhen, enableBehavior, qrItem } = props; + + return { + questionnaire: { + resourceType: 'Questionnaire', + id: 'questionnaire', + title: 'Questionnaire', + status: 'active', + item: [ + { + linkId: 'q1', + type: type, + text: 'Item to check 1', + required: false, + }, + { + linkId: 'q2', + type: type, + text: 'Item to check 2', + required: false, + }, + { + linkId: CONTROL_ITEM_LINK_ID, + type: type, + text: 'Control item', + required: true, + enableWhen, + enableBehavior, + }, + ], + }, + questionnaireResponse: { + resourceType: 'QuestionnaireResponse', + status: 'completed', + item: qrItem, + }, + }; +} + +export const ENABLE_WHEN_TESTS_TITLE = 'Should check if CONTROL_ITEM_LINK_ID is required or not'; + +export async function testEnableWhenCases(questionnaireData: QuestionnaireData) { + const { questionnaire, questionnaireResponse } = questionnaireData; + + const qrValues: QuestionnaireResponseItem[] = evaluate(questionnaireResponse, `item`); + const values = qrValues.reduce( + (acc, item) => { + acc[item.linkId] = item.answer; + return acc; + }, + {} as Record, + ); + const schema = questionnaireItemsToValidationSchema(questionnaire.item!); + + // NOTE: A way to debug a schema errors + // try { + // schema.validateSync(values); + // } catch (e) { + // console.log('Test schema valiadtion errors:', e); + // } + + expect(schema.isValidSync(values)).toBeTruthy(); +} diff --git a/src/utils/enableWhen.ts b/src/utils/enableWhen.ts new file mode 100644 index 00000000..4d37872e --- /dev/null +++ b/src/utils/enableWhen.ts @@ -0,0 +1,93 @@ +import { getChecker } from 'sdc-qrf'; +import type { + QuestionnaireItemEnableWhenAnswer, + QuestionnaireItemAnswerOption, + QuestionnaireItemEnableWhen, +} from 'shared/src/contrib/aidbox'; +import * as yup from 'yup'; + +function getAnswerOptionsValues(answerOptionArray: QuestionnaireItemAnswerOption[]): Array<{ value: any }> { + return answerOptionArray.reduce>((acc, option) => { + if (option.value === undefined) { + return acc; + } + + return [...acc, { value: option.value }]; + }, []); +} + +interface IsEnableWhenItemSucceedProps { + answerOptionArray: QuestionnaireItemAnswerOption[] | undefined; + answer: QuestionnaireItemEnableWhenAnswer | undefined; + operator: string; +} +function isEnableWhenItemSucceed(props: IsEnableWhenItemSucceedProps): boolean { + const { answerOptionArray, answer, operator } = props; + + if (!answerOptionArray || answerOptionArray.length === 0 || !answer) { + return false; + } + + const answerOptionsWithValues = getAnswerOptionsValues(answerOptionArray); + if (answerOptionsWithValues.length === 0) { + return false; + } + + const checker = getChecker(operator); + return checker(answerOptionsWithValues, answer); +} + +interface GetEnableWhenItemSchemaProps extends GetQuestionItemEnableWhenSchemaProps { + currentIndex: number; + prevConditionResults?: boolean[]; +} +function getEnableWhenItemsSchema(props: GetEnableWhenItemSchemaProps): yup.AnySchema { + const { enableWhenItems, enableBehavior, currentIndex, schema, prevConditionResults } = props; + + const { question, operator, answer } = enableWhenItems[currentIndex]!; + + const isLastItem = currentIndex === enableWhenItems.length - 1; + + const conditionResults = prevConditionResults ? [...prevConditionResults] : []; + return yup.mixed().when(question, { + is: (answerOptionArray: QuestionnaireItemAnswerOption[]) => { + const isConditionSatisfied = isEnableWhenItemSucceed({ + answerOptionArray, + answer, + operator, + }); + + if (!enableBehavior || enableBehavior === 'all') { + return isConditionSatisfied; + } + + conditionResults.push(isConditionSatisfied); + + if (isLastItem) { + return conditionResults.some((result) => result); + } + + return true; + }, + then: () => + !isLastItem + ? getEnableWhenItemsSchema({ + enableWhenItems, + currentIndex: currentIndex + 1, + schema, + enableBehavior, + prevConditionResults: [...conditionResults], + }) + : schema, + otherwise: () => yup.mixed().nullable(), + }); +} + +interface GetQuestionItemEnableWhenSchemaProps { + enableWhenItems: QuestionnaireItemEnableWhen[]; + enableBehavior: string | undefined; + schema: yup.AnySchema; +} +export function getQuestionItemEnableWhenSchema(props: GetQuestionItemEnableWhenSchemaProps) { + return getEnableWhenItemsSchema({ ...props, currentIndex: 0 }); +} diff --git a/src/utils/questionnaire.ts b/src/utils/questionnaire.ts index b77cc647..c4e69044 100644 --- a/src/utils/questionnaire.ts +++ b/src/utils/questionnaire.ts @@ -13,6 +13,7 @@ import { import { parseFHIRTime } from '@beda.software/fhir-react'; import { formatHumanDate, formatHumanDateTime } from './date'; +import { getQuestionItemEnableWhenSchema } from './enableWhen'; import { evaluate } from './fhirpath'; export function getDisplay( @@ -100,21 +101,12 @@ export function questionnaireItemsToValidationSchema(questionnaireItems: Questio } else { schema = item.required ? yup.array().of(yup.mixed()).min(1).required() : yup.mixed().nullable(); } + if (item.enableWhen) { - item.enableWhen.forEach((itemEnableWhen) => { - const { question, operator, answer } = itemEnableWhen; - // TODO: handle all other operators - if (operator === '=') { - validationSchema[item.linkId] = yup.mixed().when(question, { - is: (answerOptionArray: QuestionnaireItemAnswerOption[]) => - answerOptionArray && - answerOptionArray.some( - (answerOption) => answerOption?.value?.Coding?.code === answer?.Coding?.code, - ), - then: () => schema, - otherwise: () => yup.mixed().nullable(), - }); - } + validationSchema[item.linkId] = getQuestionItemEnableWhenSchema({ + enableWhenItems: item.enableWhen, + enableBehavior: item.enableBehavior, + schema, }); } else { validationSchema[item.linkId] = schema;