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/components/AudioRecorder/hooks.ts b/src/components/AudioRecorder/hooks.ts new file mode 100644 index 00000000..90db8b93 --- /dev/null +++ b/src/components/AudioRecorder/hooks.ts @@ -0,0 +1,19 @@ +// eslint-disable-next-line +import { useAudioRecorder as useAudioRecorderControl } from "react-audio-voice-recorder"; + +export interface RecorderControls { + startRecording: () => void; + stopRecording: () => void; + togglePauseResume: () => void; + recordingBlob?: Blob; + isRecording: boolean; + isPaused: boolean; + recordingTime: number; + mediaRecorder?: MediaRecorder; +} + +export function useAudioRecorder() { + const recorderControls: RecorderControls = useAudioRecorderControl(); + + return { recorderControls }; +} \ No newline at end of file diff --git a/src/components/AudioRecorder/index.tsx b/src/components/AudioRecorder/index.tsx new file mode 100644 index 00000000..6d300bb2 --- /dev/null +++ b/src/components/AudioRecorder/index.tsx @@ -0,0 +1,73 @@ +import { Trans } from '@lingui/macro'; +// eslint-disable-next-line +import { AudioRecorder as AudioRecorderControl } from 'react-audio-voice-recorder'; + +import { RecorderControls } from './hooks'; +import { S } from './styles'; +import { uuid4 } from '@beda.software/fhir-react'; +import { Upload, type UploadFile } from 'antd'; +import React from 'react'; +import { RcFile } from 'antd/lib/upload/interface'; + +interface AudioRecorderProps { + onChange: (url: RcFile) => Promise; + recorderControls: RecorderControls; +} + +export function AudioRecorder(props: AudioRecorderProps) { + const { recorderControls, onChange } = props; + + const onRecordingComplete = async (blob: Blob) => { + const uuid = uuid4(); + const audioFile = new File([blob], `${uuid}.webm`, { type: blob.type }) as RcFile; + audioFile.uid = uuid; + onChange(audioFile); + }; + + return ( + + + Capture in progress + + + + ); +} + +interface AudioPlayerProps { + files: UploadFile[]; + onRemove?: (file: UploadFile) => void; +} + +export function AudioPlayer(props: AudioPlayerProps) { + const { files, onRemove } = props; + + return ( + + + Listen to the audio + + {files.map((file) => ( + + + onRemove?.(file)} + /> + + ))} + + ); +} diff --git a/src/components/AudioRecorder/styles.ts b/src/components/AudioRecorder/styles.ts new file mode 100644 index 00000000..6e44e420 --- /dev/null +++ b/src/components/AudioRecorder/styles.ts @@ -0,0 +1,50 @@ +import styled, { css } from 'styled-components'; + +import { Text } from 'src/components/Typography'; + +export const S = { + Scriber: styled.div` + display: flex; + flex-direction: column; + gap: 8px 0; + + .audio-recorder { + width: 100%; + box-shadow: none; + border-radius: 30px; + background-color: ${({ theme }) => theme.neutralPalette.gray_2}; + padding: 3px 6px 3px 18px; + } + + .audio-recorder-timer, + .audio-recorder-status { + font-family: inherit; + color: ${({ theme }) => theme.neutralPalette.gray_12}; + } + + .audio-recorder-mic { + display: none; + } + + .audio-recorder-timer { + margin-left: 0; + } + + .audio-recorder-options { + filter: ${({ theme }) => (theme.mode === 'dark' ? `invert(100%)` : `invert(0%)`)}; + } + `, + Title: styled(Text)<{ $danger?: boolean }>` + font-weight: 700; + + ${({ $danger }) => + $danger && + css` + color: ${({ theme }) => theme.antdTheme?.red5}; + `} + `, + Audio: styled.audio` + height: 52px; + width: 100%; + `, +}; diff --git a/src/components/BaseQuestionnaireResponseForm/ReadonlyQuestionnaireResponseForm.tsx b/src/components/BaseQuestionnaireResponseForm/ReadonlyQuestionnaireResponseForm.tsx index e266d122..a0140bcc 100644 --- a/src/components/BaseQuestionnaireResponseForm/ReadonlyQuestionnaireResponseForm.tsx +++ b/src/components/BaseQuestionnaireResponseForm/ReadonlyQuestionnaireResponseForm.tsx @@ -18,7 +18,8 @@ import { QuestionReference } from './readonly-widgets/reference'; import { AnxietyScore, DepressionScore } from './readonly-widgets/score'; import { QuestionText, TextWithInput } from './readonly-widgets/string'; import { TimeRangePickerControl } from './readonly-widgets/TimeRangePickerControl'; -import { UploadFileControlReadOnly } from './widgets/UploadFileControl'; +import { UploadFile } from './readonly-widgets/UploadFile'; +import { AudioAttachment } from './readonly-widgets/AudioAttachment'; interface Props extends Partial { formData: QuestionnaireResponseFormData; @@ -66,7 +67,7 @@ export function ReadonlyQuestionnaireResponseForm(props: Props) { reference: QuestionReference, display: Display, boolean: QuestionBoolean, - attachment: UploadFileControlReadOnly, + attachment: UploadFile, ...questionItemComponents, }} itemControlQuestionItemComponents={{ @@ -74,6 +75,7 @@ export function ReadonlyQuestionnaireResponseForm(props: Props) { 'anxiety-score': AnxietyScore, 'depression-score': DepressionScore, 'input-inside-text': TextWithInput, + 'audio-recorder-uploader': AudioAttachment, ...itemControlQuestionItemComponents, }} > diff --git a/src/components/BaseQuestionnaireResponseForm/controls.tsx b/src/components/BaseQuestionnaireResponseForm/controls.tsx index d3212f54..9e0e754c 100644 --- a/src/components/BaseQuestionnaireResponseForm/controls.tsx +++ b/src/components/BaseQuestionnaireResponseForm/controls.tsx @@ -35,6 +35,7 @@ import { QuestionReference } from './widgets/reference'; import { ReferenceRadioButton } from './widgets/ReferenceRadioButton'; import { UploadFileControl } from './widgets/UploadFileControl'; import { TextWithMacroFill } from '../TextWithMacroFill'; +import { AudioRecorderUploader } from './widgets/AudioRecorderUploader'; export const itemComponents: QuestionItemComponentMapping = { text: QuestionText, @@ -66,6 +67,7 @@ export const itemControlComponents: ItemControlQuestionItemComponentMapping = { 'check-box': InlineChoice, 'input-inside-text': QuestionInputInsideText, 'markdown-editor': MDEditorControl, + 'audio-recorder-uploader': AudioRecorderUploader, }; export const groupControlComponents: ItemControlGroupItemComponentMapping = { diff --git a/src/components/BaseQuestionnaireResponseForm/readonly-widgets/AudioAttachment.tsx b/src/components/BaseQuestionnaireResponseForm/readonly-widgets/AudioAttachment.tsx new file mode 100644 index 00000000..55b0d61e --- /dev/null +++ b/src/components/BaseQuestionnaireResponseForm/readonly-widgets/AudioAttachment.tsx @@ -0,0 +1,34 @@ +import { Upload } from 'antd'; +import { QuestionItemProps } from 'sdc-qrf'; +import classNames from 'classnames'; + +import s from './ReadonlyWidgets.module.scss'; +import { S } from './ReadonlyWidgets.styles'; +import { useUploader } from '../widgets/UploadFileControl/hooks'; +import React from 'react'; + +export function AudioAttachment(props: QuestionItemProps) { + const { questionItem } = props; + const { text, hidden } = questionItem; + const { fileList } = useUploader(props); + + if (hidden) { + return null; + } + + return ( + + {text} + {fileList.length ? ( + fileList.map((file) => ( + + + + + )) + ) : ( + - + )} + + ); +} diff --git a/src/components/BaseQuestionnaireResponseForm/readonly-widgets/ReadonlyWidgets.styles.ts b/src/components/BaseQuestionnaireResponseForm/readonly-widgets/ReadonlyWidgets.styles.ts index a1d76e3a..965eae6c 100644 --- a/src/components/BaseQuestionnaireResponseForm/readonly-widgets/ReadonlyWidgets.styles.ts +++ b/src/components/BaseQuestionnaireResponseForm/readonly-widgets/ReadonlyWidgets.styles.ts @@ -16,4 +16,8 @@ export const S = { border-top: 1px solid ${({ theme }) => theme.neutralPalette.gray_4}; } `, + Audio: styled.audio` + height: 52px; + width: 100%; + `, }; diff --git a/src/components/BaseQuestionnaireResponseForm/readonly-widgets/UploadFile.tsx b/src/components/BaseQuestionnaireResponseForm/readonly-widgets/UploadFile.tsx new file mode 100644 index 00000000..0c097a7a --- /dev/null +++ b/src/components/BaseQuestionnaireResponseForm/readonly-widgets/UploadFile.tsx @@ -0,0 +1,28 @@ +import { Upload } from 'antd'; +import { QuestionItemProps } from 'sdc-qrf'; +import classNames from 'classnames'; + +import s from './ReadonlyWidgets.module.scss'; +import { S } from './ReadonlyWidgets.styles'; +import { useUploader } from '../widgets/UploadFileControl/hooks'; + +export function UploadFile(props: QuestionItemProps) { + const { questionItem } = props; + const { text, hidden } = questionItem; + const { fileList } = useUploader(props); + + if (hidden) { + return null; + } + + return ( + + {text} + {fileList.length ? ( + + ) : ( + - + )} + + ); +} diff --git a/src/components/BaseQuestionnaireResponseForm/widgets/AudioRecorderUploader/index.tsx b/src/components/BaseQuestionnaireResponseForm/widgets/AudioRecorderUploader/index.tsx new file mode 100644 index 00000000..ac92ee61 --- /dev/null +++ b/src/components/BaseQuestionnaireResponseForm/widgets/AudioRecorderUploader/index.tsx @@ -0,0 +1,99 @@ +import { AudioOutlined } from '@ant-design/icons'; +import { Trans } from '@lingui/macro'; +import { Form, UploadFile } from 'antd'; +import { useCallback, useState } from 'react'; +import { QuestionItemProps } from 'sdc-qrf'; + +import { AudioPlayer as AudioPlayerControl, AudioRecorder as AudioRecorderControl } from 'src/components/AudioRecorder'; +import { useAudioRecorder } from 'src/components/AudioRecorder/hooks'; + +import { useUploader } from '../UploadFileControl/hooks'; +import { RcFile } from 'antd/lib/upload/interface'; +import { isSuccess } from '@beda.software/remote-data'; +import { S } from './styles'; +import { UploadFileControl } from '../UploadFileControl'; + +export function AudioRecorderUploader(props: QuestionItemProps) { + const { questionItem } = props; + const [showScriber, setShowScriber] = useState(false); + + const { recorderControls } = useAudioRecorder(); + const { formItem, customRequest, onChange, fileList } = useUploader(props); + const hasFiles = fileList.length > 0; + + const onScribeChange = useCallback( + async (file: RcFile) => { + setShowScriber(false); + + const fileClone = new File([file], file.name, { + type: file.type, + }) as any as UploadFile; + fileClone.uid = file.uid; + fileClone.status = 'uploading'; + fileClone.percent = 0; + + onChange({ + fileList: [...fileList, fileClone], + file: fileClone, + }); + + const response = await customRequest({ file }); + + if (isSuccess(response)) { + fileClone.status = 'done'; + fileClone.url = response.data.uploadUrl; + fileClone.percent = 100; + + onChange({ + fileList: [...fileList, fileClone], + file: fileClone, + }); + } + }, + [fileList], + ); + + const renderContent = () => { + if (hasFiles) { + return ( + + + + ); + } + + if (showScriber) { + return ( + + + + ); + } + + return ( + <> + } + type="primary" + onClick={() => { + setShowScriber(true); + recorderControls.startRecording(); + }} + > + + Start scribe + + + + + ); + }; + + return {renderContent()}; +} diff --git a/src/components/BaseQuestionnaireResponseForm/widgets/AudioRecorderUploader/styles.ts b/src/components/BaseQuestionnaireResponseForm/widgets/AudioRecorderUploader/styles.ts new file mode 100644 index 00000000..3224ffee --- /dev/null +++ b/src/components/BaseQuestionnaireResponseForm/widgets/AudioRecorderUploader/styles.ts @@ -0,0 +1,13 @@ +import { Button } from 'antd'; +import styled from 'styled-components'; + +export const S = { + Container: styled.div` + border-radius: 10px; + padding: 12px 8px; + border: 1px solid ${({ theme }) => theme.neutralPalette.gray_4}; + `, + Button: styled(Button)` + width: 100%; + `, +}; diff --git a/src/components/BaseQuestionnaireResponseForm/widgets/UploadFileControl/UploadFileControl.stories.tsx b/src/components/BaseQuestionnaireResponseForm/widgets/UploadFileControl/UploadFileControl.stories.tsx index 39de58e3..6aef356d 100644 --- a/src/components/BaseQuestionnaireResponseForm/widgets/UploadFileControl/UploadFileControl.stories.tsx +++ b/src/components/BaseQuestionnaireResponseForm/widgets/UploadFileControl/UploadFileControl.stories.tsx @@ -4,6 +4,8 @@ import { ItemContext } from 'sdc-qrf/lib/types'; import { WithQuestionFormProviderDecorator, withColorSchemeDecorator } from 'src/storybook/decorators'; import { UploadFileControl } from './index'; +import { I18nProvider } from '@lingui/react'; +import { i18n } from '@lingui/core'; const meta: Meta = { title: 'Questionnaire / questions / UploadFileControl', @@ -14,17 +16,21 @@ const meta: Meta = { export default meta; type Story = StoryObj; +i18n.activate('en'); + export const Default: Story = { render: () => ( - + + + ), }; diff --git a/src/components/BaseQuestionnaireResponseForm/widgets/UploadFileControl/hooks.ts b/src/components/BaseQuestionnaireResponseForm/widgets/UploadFileControl/hooks.ts index fba13ebb..2713a756 100644 --- a/src/components/BaseQuestionnaireResponseForm/widgets/UploadFileControl/hooks.ts +++ b/src/components/BaseQuestionnaireResponseForm/widgets/UploadFileControl/hooks.ts @@ -1,7 +1,7 @@ import type { UploadFile } from 'antd'; import { notification } from 'antd'; import { Attachment } from 'fhir/r4b'; -import { useCallback, useEffect, useRef, useState } from 'react'; +import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { QuestionItemProps } from 'sdc-qrf'; import { formatError } from '@beda.software/fhir-react'; @@ -22,8 +22,9 @@ export function useUploader({ parentPath, questionItem }: QuestionItemProps) { const { linkId, repeats } = questionItem; const fieldName = [...parentPath, linkId]; const { formItem, value, onChange } = useFieldController(fieldName, questionItem); + const uid = useRef>({}); - const initialFileList: Array = (value ?? []).map((v: ValueAttachment) => { + const initialFileList: Array = useMemo(() => (value ?? []).map((v: ValueAttachment) => { const url = v.value.Attachment.url!; const file: UploadFile = { uid: url, @@ -31,9 +32,13 @@ export function useUploader({ parentPath, questionItem }: QuestionItemProps) { percent: 100, }; return file; - }); + }), [value]); const [fileList, setFileList] = useState>(initialFileList); + useEffect(() => { + setFileList(initialFileList); + }, [JSON.stringify(initialFileList)]); + useEffect(() => { (async () => { const result: Array = []; @@ -65,6 +70,7 @@ export function useUploader({ parentPath, questionItem }: QuestionItemProps) { async (options: CustomUploadRequestOption) => { const file: UploadFile = options.file as any; const response = await generateUploadUrl(file.name); + if (isSuccess(response)) { const { filename, uploadUrl } = response.data; uid.current[file.uid] = filename; @@ -72,6 +78,7 @@ export function useUploader({ parentPath, questionItem }: QuestionItemProps) { } else { notification.error({ message: formatError(response.error) }); } + return response; }, [uid], ); diff --git a/src/components/BaseQuestionnaireResponseForm/widgets/UploadFileControl/index.tsx b/src/components/BaseQuestionnaireResponseForm/widgets/UploadFileControl/index.tsx index b508ed42..1cf0ec57 100644 --- a/src/components/BaseQuestionnaireResponseForm/widgets/UploadFileControl/index.tsx +++ b/src/components/BaseQuestionnaireResponseForm/widgets/UploadFileControl/index.tsx @@ -3,12 +3,14 @@ import { Form, Upload } from 'antd'; import { QuestionItemProps } from 'sdc-qrf'; import { useUploader } from './hooks'; +import { Trans } from '@lingui/macro'; const { Dragger } = Upload; export function UploadFileControl(props: QuestionItemProps) { const { showDragger, formItem, customRequest, onChange, onRemove, fileList } = useUploader(props); const { helpText, repeats } = props.questionItem; + return ( {showDragger ? ( @@ -23,7 +25,9 @@ export function UploadFileControl(props: QuestionItemProps) {

-

Click or drag file to this area to upload

+

+ Click or drag file to this area to upload +

{helpText}

) : ( @@ -37,12 +41,3 @@ export function UploadFileControl(props: QuestionItemProps) {
); } - -export function UploadFileControlReadOnly(props: QuestionItemProps) { - const { formItem, fileList } = useUploader(props); - return ( - - - - ); -} diff --git a/src/components/ChangesDiff/ChangesDiff.stories.tsx b/src/components/ChangesDiff/ChangesDiff.stories.tsx index 8d7a8267..510c1ac2 100644 --- a/src/components/ChangesDiff/ChangesDiff.stories.tsx +++ b/src/components/ChangesDiff/ChangesDiff.stories.tsx @@ -2,7 +2,7 @@ import { Meta, StoryObj } from '@storybook/react'; import { WithQuestionFormProviderDecorator, withColorSchemeDecorator } from 'src/storybook/decorators'; -import { ChangesDiff, Props } from './index'; +import { ChangesDiff, ChangesDiffProps } from './index'; const meta: Meta = { title: 'components / ChangesDiff', @@ -21,7 +21,7 @@ export const Deletions: Story = { render: () => , }; -const props1: Props = { +const props1: ChangesDiffProps = { id: '6d2d6fe6-beba-4ce6-9a9f-dd0d4b06d4e5', activityCode: 'CREATE', recorded: '2023-07-19T14:16:19.825125Z', @@ -48,7 +48,7 @@ const props1: Props = { ], }; -const props2: Props = { +const props2: ChangesDiffProps = { id: '27613e5f-e8dd-4a9c-8d36-dfd2f1089d5e', activityCode: 'UPDATE', recorded: '2023-07-19T14:16:19.825125Z', diff --git a/src/components/ChangesDiff/index.tsx b/src/components/ChangesDiff/index.tsx index e3e16af1..9ead712d 100644 --- a/src/components/ChangesDiff/index.tsx +++ b/src/components/ChangesDiff/index.tsx @@ -5,39 +5,44 @@ import { Text } from 'src/components/Typography'; import { formatHumanDateTime } from 'src/utils/date'; import { S } from './ChangesDiff.styles'; +import { CSSProperties } from 'react'; -interface Change { +export interface ChangesDiffChange { key: string; title: string; valueBefore: string | null; valueAfter: string | null; } -export interface Props { +export interface ChangesDiffProps { id: string; - changes: Change[]; - activityCode: string; - recorded: string; - author: string[]; + changes: ChangesDiffChange[]; + activityCode?: string; + recorded?: string; + author?: string[]; + className?: string | undefined; + style?: CSSProperties | undefined; } -export function ChangesDiff(props: Props) { - const { changes, id, activityCode, recorded, author = [] } = props; +export function ChangesDiff(props: ChangesDiffProps) { + const { changes, id, activityCode, recorded, author = [], className, style } = props; const codesMapping = { CREATE: t`Created`, UPDATE: t`Updated`, }; - const activity = codesMapping[activityCode]; - const date = formatHumanDateTime(recorded); + const activity = activityCode ? codesMapping[activityCode] : null; + const date = recorded ? formatHumanDateTime(recorded) : null; const by = author.join(', '); return ( - - - - {activity} {date} by {by} - - + + {activity ? ( + + + {activity} {date} by {by} + + + ) : null} {changes.map((item) => (
{item.title} 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/locale/en/messages.po b/src/locale/en/messages.po index 2827dd5c..58a8431e 100644 --- a/src/locale/en/messages.po +++ b/src/locale/en/messages.po @@ -13,7 +13,7 @@ msgstr "" "Language-Team: \n" "Plural-Forms: \n" -#: src/uberComponents/ResourceListPage/index.tsx:167 +#: src/uberComponents/ResourceListPage/index.tsx:176 msgid "{0, plural, one {Selected # item} other {Selected # items}}" msgstr "" @@ -31,7 +31,7 @@ msgstr "" #: src/containers/PatientList/index.tsx:103 #: src/containers/PractitionerList/index.tsx:121 #: src/containers/Prescriptions/index.tsx:148 -#: src/uberComponents/ResourceListPage/index.tsx:227 +#: src/uberComponents/ResourceListPage/index.tsx:237 msgid "Actions" msgstr "" @@ -110,7 +110,7 @@ msgstr "" #: src/components/ModalNewPatient/index.tsx:16 #: src/components/ModalNewPatient/index.tsx:20 -#: src/containers/PatientResourceListExample/index.tsx:75 +#: src/containers/PatientResourceListExample/index.tsx:76 msgid "Add patient" msgstr "" @@ -249,6 +249,7 @@ msgstr "" msgid "Cancelled" msgstr "" +#: src/components/AudioRecorder/index.tsx:30 #: src/containers/EncounterDetails/AIScribe/index.tsx:93 msgid "Capture in progress" msgstr "" @@ -261,6 +262,10 @@ msgstr "" msgid "Clear filters" msgstr "" +#: src/components/BaseQuestionnaireResponseForm/widgets/UploadFileControl/index.tsx:29 +msgid "Click or drag file to this area to upload" +msgstr "" + #: src/containers/PractitionerDetails/PractitionerOverview/index.tsx:92 msgid "Clinician successfully updated" msgstr "" @@ -352,7 +357,7 @@ msgstr "" msgid "Create practitioner" msgstr "" -#: src/components/ChangesDiff/index.tsx:27 +#: src/components/ChangesDiff/index.tsx:30 #: src/containers/PatientDetails/PatientOverviewDynamic/components/PatientNoteListCard/NoteList/index.tsx:45 msgid "Created" msgstr "" @@ -424,7 +429,7 @@ msgstr "" msgid "Delete" msgstr "" -#: src/containers/PatientResourceListExample/index.tsx:77 +#: src/containers/PatientResourceListExample/index.tsx:78 msgid "Delete patients" msgstr "" @@ -606,7 +611,7 @@ msgid "Fill" msgstr "" #: src/containers/PatientList/searchBarUtils.ts:10 -#: src/containers/PatientResourceListExample/index.tsx:66 +#: src/containers/PatientResourceListExample/index.tsx:67 msgid "Find patient" msgstr "" @@ -736,6 +741,10 @@ msgstr "" msgid "Last name" msgstr "" +#: src/components/AudioRecorder/index.tsx:58 +msgid "Listen to the audio" +msgstr "" + #: src/containers/SignIn/index.tsx:89 msgid "Log in" msgstr "" @@ -748,7 +757,7 @@ msgstr "" #~ msgid "Log in as Practitioner" #~ msgstr "" -#: src/components/BaseLayout/Sidebar/SidebarBottom/index.tsx:96 +#: src/components/BaseLayout/Sidebar/SidebarBottom/context.tsx:42 msgid "Log out" msgstr "" @@ -833,7 +842,7 @@ msgstr "" #: src/containers/PatientDetails/PatientDocumentDetails/index.tsx:148 #: src/containers/PatientDetails/PatientDocumentDetails/index.tsx:173 #: src/containers/QuestionnaireBuilder/QuestionnaireItemSettings/index.tsx:137 -#: src/utils/questionnaire.ts:62 +#: src/utils/questionnaire.ts:63 msgid "No" msgstr "" @@ -846,7 +855,7 @@ msgstr "" #: src/containers/PractitionerList/index.tsx:101 #: src/containers/QuestionnaireBuilder/QuestionnaireItemSettings/controls.tsx:500 #: src/containers/QuestionnaireList/index.tsx:102 -#: src/uberComponents/ResourceListPage/index.tsx:191 +#: src/uberComponents/ResourceListPage/index.tsx:200 msgid "No data" msgstr "" @@ -1076,7 +1085,7 @@ msgstr "" #~ msgid "Reset password" #~ msgstr "" -#: src/uberComponents/ResourceListPage/index.tsx:160 +#: src/uberComponents/ResourceListPage/index.tsx:169 msgid "Reset selection" msgstr "" @@ -1232,6 +1241,7 @@ msgstr "" msgid "Start date" msgstr "" +#: src/components/BaseQuestionnaireResponseForm/widgets/AudioRecorderUploader/index.tsx:84 #: src/containers/EncounterDetails/index.tsx:101 msgid "Start scribe" msgstr "" @@ -1289,9 +1299,9 @@ msgid "Subject Type" msgstr "" #: src/containers/QuestionnaireBuilder/PromptForm.tsx:104 -#: src/uberComponents/ResourceListPage/actions.tsx:94 -#: src/uberComponents/ResourceListPage/actions.tsx:127 -#: src/uberComponents/ResourceListPage/actions.tsx:172 +#: src/uberComponents/ResourceListPage/actions.tsx:96 +#: src/uberComponents/ResourceListPage/actions.tsx:130 +#: src/uberComponents/ResourceListPage/actions.tsx:176 msgid "Submit" msgstr "" @@ -1301,9 +1311,9 @@ msgstr "" #~ msgid "Successfully saved" #~ msgstr "" -#: src/uberComponents/ResourceListPage/actions.tsx:88 -#: src/uberComponents/ResourceListPage/actions.tsx:122 -#: src/uberComponents/ResourceListPage/actions.tsx:168 +#: src/uberComponents/ResourceListPage/actions.tsx:90 +#: src/uberComponents/ResourceListPage/actions.tsx:125 +#: src/uberComponents/ResourceListPage/actions.tsx:172 msgid "Successfully submitted" msgstr "" @@ -1336,11 +1346,11 @@ msgstr "" msgid "Text with macro" msgstr "" -#: src/containers/App/index.tsx:128 +#: src/containers/App/index.tsx:129 msgid "Thank you for filling out the questionnaire. Now you can close this page." msgstr "" -#: src/containers/App/index.tsx:127 +#: src/containers/App/index.tsx:128 msgid "Thank you!" msgstr "" @@ -1428,7 +1438,7 @@ msgstr "" msgid "Upcoming appointment" msgstr "" -#: src/components/ChangesDiff/index.tsx:28 +#: src/components/ChangesDiff/index.tsx:31 msgid "Updated" msgstr "" @@ -1483,7 +1493,7 @@ msgstr "" #: src/containers/PatientDetails/PatientDocumentDetails/index.tsx:147 #: src/containers/PatientDetails/PatientDocumentDetails/index.tsx:172 #: src/containers/QuestionnaireBuilder/QuestionnaireItemSettings/index.tsx:136 -#: src/utils/questionnaire.ts:62 +#: src/utils/questionnaire.ts:63 msgid "Yes" msgstr "" diff --git a/src/locale/es/messages.po b/src/locale/es/messages.po index 830c8751..cc1273d9 100644 --- a/src/locale/es/messages.po +++ b/src/locale/es/messages.po @@ -13,7 +13,7 @@ msgstr "" "Language-Team: \n" "Plural-Forms: \n" -#: src/uberComponents/ResourceListPage/index.tsx:167 +#: src/uberComponents/ResourceListPage/index.tsx:176 msgid "{0, plural, one {Selected # item} other {Selected # items}}" msgstr "" @@ -31,7 +31,7 @@ msgstr "Acción" #: src/containers/PatientList/index.tsx:103 #: src/containers/PractitionerList/index.tsx:121 #: src/containers/Prescriptions/index.tsx:148 -#: src/uberComponents/ResourceListPage/index.tsx:227 +#: src/uberComponents/ResourceListPage/index.tsx:237 msgid "Actions" msgstr "Acciones" @@ -110,7 +110,7 @@ msgstr "Añadir Orden" #: src/components/ModalNewPatient/index.tsx:16 #: src/components/ModalNewPatient/index.tsx:20 -#: src/containers/PatientResourceListExample/index.tsx:75 +#: src/containers/PatientResourceListExample/index.tsx:76 msgid "Add patient" msgstr "Añadir paciente" @@ -249,6 +249,7 @@ msgstr "Cancelar solicitud de medicamento" msgid "Cancelled" msgstr "Cancelado" +#: src/components/AudioRecorder/index.tsx:30 #: src/containers/EncounterDetails/AIScribe/index.tsx:93 msgid "Capture in progress" msgstr "Ejecución en curso" @@ -261,6 +262,10 @@ msgstr "Características" msgid "Clear filters" msgstr "" +#: src/components/BaseQuestionnaireResponseForm/widgets/UploadFileControl/index.tsx:29 +msgid "Click or drag file to this area to upload" +msgstr "" + #: src/containers/PractitionerDetails/PractitionerOverview/index.tsx:92 msgid "Clinician successfully updated" msgstr "Profesional actualizado exitosamente" @@ -352,7 +357,7 @@ msgstr "Crear un encuentro" msgid "Create practitioner" msgstr "Crear un profesional" -#: src/components/ChangesDiff/index.tsx:27 +#: src/components/ChangesDiff/index.tsx:30 #: src/containers/PatientDetails/PatientOverviewDynamic/components/PatientNoteListCard/NoteList/index.tsx:45 msgid "Created" msgstr "Creado" @@ -424,7 +429,7 @@ msgstr "Predeterminado" msgid "Delete" msgstr "Eliminar" -#: src/containers/PatientResourceListExample/index.tsx:77 +#: src/containers/PatientResourceListExample/index.tsx:78 msgid "Delete patients" msgstr "" @@ -602,7 +607,7 @@ msgid "Fill" msgstr "Rellenar" #: src/containers/PatientList/searchBarUtils.ts:10 -#: src/containers/PatientResourceListExample/index.tsx:66 +#: src/containers/PatientResourceListExample/index.tsx:67 msgid "Find patient" msgstr "Buscar paciente" @@ -732,6 +737,10 @@ msgstr "Etiqueta" msgid "Last name" msgstr "Apellido" +#: src/components/AudioRecorder/index.tsx:58 +msgid "Listen to the audio" +msgstr "" + #: src/containers/SignIn/index.tsx:89 msgid "Log in" msgstr "Iniciar sesión" @@ -740,7 +749,7 @@ msgstr "Iniciar sesión" msgid "Log in as demo patient" msgstr "Iniciar sesión como paciente demo" -#: src/components/BaseLayout/Sidebar/SidebarBottom/index.tsx:96 +#: src/components/BaseLayout/Sidebar/SidebarBottom/context.tsx:42 msgid "Log out" msgstr "Cerrar sesión" @@ -825,7 +834,7 @@ msgstr "Nuevo agendamiento" #: src/containers/PatientDetails/PatientDocumentDetails/index.tsx:148 #: src/containers/PatientDetails/PatientDocumentDetails/index.tsx:173 #: src/containers/QuestionnaireBuilder/QuestionnaireItemSettings/index.tsx:137 -#: src/utils/questionnaire.ts:62 +#: src/utils/questionnaire.ts:63 msgid "No" msgstr "No" @@ -838,7 +847,7 @@ msgstr "No" #: src/containers/PractitionerList/index.tsx:101 #: src/containers/QuestionnaireBuilder/QuestionnaireItemSettings/controls.tsx:500 #: src/containers/QuestionnaireList/index.tsx:102 -#: src/uberComponents/ResourceListPage/index.tsx:191 +#: src/uberComponents/ResourceListPage/index.tsx:200 msgid "No data" msgstr "Sin datos" @@ -1060,7 +1069,7 @@ msgstr "Requerido" msgid "Reset" msgstr "Restablecer" -#: src/uberComponents/ResourceListPage/index.tsx:160 +#: src/uberComponents/ResourceListPage/index.tsx:169 msgid "Reset selection" msgstr "" @@ -1212,6 +1221,7 @@ msgstr "Comenzar" msgid "Start date" msgstr "Fecha de inicio" +#: src/components/BaseQuestionnaireResponseForm/widgets/AudioRecorderUploader/index.tsx:84 #: src/containers/EncounterDetails/index.tsx:101 msgid "Start scribe" msgstr "Iniciar escritura" @@ -1269,9 +1279,9 @@ msgid "Subject Type" msgstr "Tipo de sujeto" #: src/containers/QuestionnaireBuilder/PromptForm.tsx:104 -#: src/uberComponents/ResourceListPage/actions.tsx:94 -#: src/uberComponents/ResourceListPage/actions.tsx:127 -#: src/uberComponents/ResourceListPage/actions.tsx:172 +#: src/uberComponents/ResourceListPage/actions.tsx:96 +#: src/uberComponents/ResourceListPage/actions.tsx:130 +#: src/uberComponents/ResourceListPage/actions.tsx:176 msgid "Submit" msgstr "Enviar" @@ -1281,9 +1291,9 @@ msgstr "Enviar" #~ msgid "Successfully saved" #~ msgstr "" -#: src/uberComponents/ResourceListPage/actions.tsx:88 -#: src/uberComponents/ResourceListPage/actions.tsx:122 -#: src/uberComponents/ResourceListPage/actions.tsx:168 +#: src/uberComponents/ResourceListPage/actions.tsx:90 +#: src/uberComponents/ResourceListPage/actions.tsx:125 +#: src/uberComponents/ResourceListPage/actions.tsx:172 msgid "Successfully submitted" msgstr "" @@ -1316,11 +1326,11 @@ msgstr "Texto (por defecto)" msgid "Text with macro" msgstr "Texto con macro" -#: src/containers/App/index.tsx:128 +#: src/containers/App/index.tsx:129 msgid "Thank you for filling out the questionnaire. Now you can close this page." msgstr "Gracias por completar el cuestionario. Ahora puedes cerrar esta página." -#: src/containers/App/index.tsx:127 +#: src/containers/App/index.tsx:128 msgid "Thank you!" msgstr "¡Gracias!" @@ -1408,7 +1418,7 @@ msgstr "Desconocido" msgid "Upcoming appointment" msgstr "Próxima cita" -#: src/components/ChangesDiff/index.tsx:28 +#: src/components/ChangesDiff/index.tsx:31 msgid "Updated" msgstr "Actualizado" @@ -1463,7 +1473,7 @@ msgstr "Bienvenido a" #: src/containers/PatientDetails/PatientDocumentDetails/index.tsx:147 #: src/containers/PatientDetails/PatientDocumentDetails/index.tsx:172 #: src/containers/QuestionnaireBuilder/QuestionnaireItemSettings/index.tsx:136 -#: src/utils/questionnaire.ts:62 +#: src/utils/questionnaire.ts:63 msgid "Yes" msgstr "Sí" diff --git a/src/locale/ru/messages.po b/src/locale/ru/messages.po index 4b69a9ad..17a5da79 100644 --- a/src/locale/ru/messages.po +++ b/src/locale/ru/messages.po @@ -13,7 +13,7 @@ msgstr "" "Language-Team: \n" "Plural-Forms: \n" -#: src/uberComponents/ResourceListPage/index.tsx:167 +#: src/uberComponents/ResourceListPage/index.tsx:176 msgid "{0, plural, one {Selected # item} other {Selected # items}}" msgstr "" @@ -31,7 +31,7 @@ msgstr "" #: src/containers/PatientList/index.tsx:103 #: src/containers/PractitionerList/index.tsx:121 #: src/containers/Prescriptions/index.tsx:148 -#: src/uberComponents/ResourceListPage/index.tsx:227 +#: src/uberComponents/ResourceListPage/index.tsx:237 msgid "Actions" msgstr "Действия" @@ -110,7 +110,7 @@ msgstr "" #: src/components/ModalNewPatient/index.tsx:16 #: src/components/ModalNewPatient/index.tsx:20 -#: src/containers/PatientResourceListExample/index.tsx:75 +#: src/containers/PatientResourceListExample/index.tsx:76 msgid "Add patient" msgstr "Добавить пациенда" @@ -249,6 +249,7 @@ msgstr "" msgid "Cancelled" msgstr "" +#: src/components/AudioRecorder/index.tsx:30 #: src/containers/EncounterDetails/AIScribe/index.tsx:93 msgid "Capture in progress" msgstr "" @@ -261,6 +262,10 @@ msgstr "" msgid "Clear filters" msgstr "" +#: src/components/BaseQuestionnaireResponseForm/widgets/UploadFileControl/index.tsx:29 +msgid "Click or drag file to this area to upload" +msgstr "" + #: src/containers/PractitionerDetails/PractitionerOverview/index.tsx:92 msgid "Clinician successfully updated" msgstr "" @@ -352,7 +357,7 @@ msgstr "" msgid "Create practitioner" msgstr "" -#: src/components/ChangesDiff/index.tsx:27 +#: src/components/ChangesDiff/index.tsx:30 #: src/containers/PatientDetails/PatientOverviewDynamic/components/PatientNoteListCard/NoteList/index.tsx:45 msgid "Created" msgstr "" @@ -424,7 +429,7 @@ msgstr "" msgid "Delete" msgstr "" -#: src/containers/PatientResourceListExample/index.tsx:77 +#: src/containers/PatientResourceListExample/index.tsx:78 msgid "Delete patients" msgstr "" @@ -606,7 +611,7 @@ msgid "Fill" msgstr "" #: src/containers/PatientList/searchBarUtils.ts:10 -#: src/containers/PatientResourceListExample/index.tsx:66 +#: src/containers/PatientResourceListExample/index.tsx:67 msgid "Find patient" msgstr "Найти пациента" @@ -736,6 +741,10 @@ msgstr "" msgid "Last name" msgstr "" +#: src/components/AudioRecorder/index.tsx:58 +msgid "Listen to the audio" +msgstr "" + #: src/containers/SignIn/index.tsx:89 msgid "Log in" msgstr "" @@ -748,7 +757,7 @@ msgstr "" #~ msgid "Log in as Practitioner" #~ msgstr "" -#: src/components/BaseLayout/Sidebar/SidebarBottom/index.tsx:96 +#: src/components/BaseLayout/Sidebar/SidebarBottom/context.tsx:42 msgid "Log out" msgstr "Выйти" @@ -833,7 +842,7 @@ msgstr "" #: src/containers/PatientDetails/PatientDocumentDetails/index.tsx:148 #: src/containers/PatientDetails/PatientDocumentDetails/index.tsx:173 #: src/containers/QuestionnaireBuilder/QuestionnaireItemSettings/index.tsx:137 -#: src/utils/questionnaire.ts:62 +#: src/utils/questionnaire.ts:63 msgid "No" msgstr "" @@ -846,7 +855,7 @@ msgstr "" #: src/containers/PractitionerList/index.tsx:101 #: src/containers/QuestionnaireBuilder/QuestionnaireItemSettings/controls.tsx:500 #: src/containers/QuestionnaireList/index.tsx:102 -#: src/uberComponents/ResourceListPage/index.tsx:191 +#: src/uberComponents/ResourceListPage/index.tsx:200 msgid "No data" msgstr "Нет данных" @@ -1076,7 +1085,7 @@ msgstr "Сбросить" #~ msgid "Reset password" #~ msgstr "Сбросить пароль" -#: src/uberComponents/ResourceListPage/index.tsx:160 +#: src/uberComponents/ResourceListPage/index.tsx:169 msgid "Reset selection" msgstr "" @@ -1232,6 +1241,7 @@ msgstr "" msgid "Start date" msgstr "Начало периода" +#: src/components/BaseQuestionnaireResponseForm/widgets/AudioRecorderUploader/index.tsx:84 #: src/containers/EncounterDetails/index.tsx:101 msgid "Start scribe" msgstr "" @@ -1289,9 +1299,9 @@ msgid "Subject Type" msgstr "" #: src/containers/QuestionnaireBuilder/PromptForm.tsx:104 -#: src/uberComponents/ResourceListPage/actions.tsx:94 -#: src/uberComponents/ResourceListPage/actions.tsx:127 -#: src/uberComponents/ResourceListPage/actions.tsx:172 +#: src/uberComponents/ResourceListPage/actions.tsx:96 +#: src/uberComponents/ResourceListPage/actions.tsx:130 +#: src/uberComponents/ResourceListPage/actions.tsx:176 msgid "Submit" msgstr "" @@ -1301,9 +1311,9 @@ msgstr "" #~ msgid "Successfully saved" #~ msgstr "" -#: src/uberComponents/ResourceListPage/actions.tsx:88 -#: src/uberComponents/ResourceListPage/actions.tsx:122 -#: src/uberComponents/ResourceListPage/actions.tsx:168 +#: src/uberComponents/ResourceListPage/actions.tsx:90 +#: src/uberComponents/ResourceListPage/actions.tsx:125 +#: src/uberComponents/ResourceListPage/actions.tsx:172 msgid "Successfully submitted" msgstr "" @@ -1336,11 +1346,11 @@ msgstr "" msgid "Text with macro" msgstr "" -#: src/containers/App/index.tsx:128 +#: src/containers/App/index.tsx:129 msgid "Thank you for filling out the questionnaire. Now you can close this page." msgstr "" -#: src/containers/App/index.tsx:127 +#: src/containers/App/index.tsx:128 msgid "Thank you!" msgstr "" @@ -1428,7 +1438,7 @@ msgstr "" msgid "Upcoming appointment" msgstr "" -#: src/components/ChangesDiff/index.tsx:28 +#: src/components/ChangesDiff/index.tsx:31 msgid "Updated" msgstr "" @@ -1483,7 +1493,7 @@ msgstr "" #: src/containers/PatientDetails/PatientDocumentDetails/index.tsx:147 #: src/containers/PatientDetails/PatientDocumentDetails/index.tsx:172 #: src/containers/QuestionnaireBuilder/QuestionnaireItemSettings/index.tsx:136 -#: src/utils/questionnaire.ts:62 +#: src/utils/questionnaire.ts:63 msgid "Yes" msgstr "" 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;