From aa9f78cc1b5093ef9d70af921c63d1051813ae14 Mon Sep 17 00:00:00 2001 From: janadh Date: Fri, 10 Jan 2025 11:29:47 +0800 Subject: [PATCH] - Fixed #1069 by adding support for Quantity Max and Min value extensions. --- apps/smart-forms-app/tsconfig.json | 4 +- .../src/hooks/useValidationFeedback.ts | 29 ++- .../src/utils/itemControl.ts | 105 ++++++++++- .../src/utils/validateQuestionnaire.ts | 173 +++++++++++++++++- 4 files changed, 301 insertions(+), 10 deletions(-) diff --git a/apps/smart-forms-app/tsconfig.json b/apps/smart-forms-app/tsconfig.json index 8e7b780ab..2c0d44fdf 100644 --- a/apps/smart-forms-app/tsconfig.json +++ b/apps/smart-forms-app/tsconfig.json @@ -21,7 +21,9 @@ "noUnusedLocals": true, "noUnusedParameters": true, "noFallthroughCasesInSwitch": true, - "forceConsistentCasingInFileNames": true + "forceConsistentCasingInFileNames": true, + /* Debugging */ + "sourceMap": true }, "include": ["src", "cypress"], "references": [{ "path": "./tsconfig.node.json" }] diff --git a/packages/smart-forms-renderer/src/hooks/useValidationFeedback.ts b/packages/smart-forms-renderer/src/hooks/useValidationFeedback.ts index 8904519c7..c4c43a98a 100644 --- a/packages/smart-forms-renderer/src/hooks/useValidationFeedback.ts +++ b/packages/smart-forms-renderer/src/hooks/useValidationFeedback.ts @@ -22,6 +22,10 @@ import { getMaxValueFeedback, getMinValue, getMinValueFeedback, + getMinQuantityValue, + getMinQuantityValueFeedback, + getMaxQuantityValue, + getMaxQuantityValueFeedback, getRegexValidation } from '../utils/itemControl'; import { structuredDataCapture } from 'fhir-sdc-helpers'; @@ -33,6 +37,8 @@ function useValidationFeedback(qItem: QuestionnaireItem, input: string): string const maxDecimalPlaces = structuredDataCapture.getMaxDecimalPlaces(qItem); const minValue = getMinValue(qItem); const maxValue = getMaxValue(qItem); + const minQuantityValue = getMinQuantityValue(qItem);//gets the minQuantity value from the questionaire item + const maxQuantityValue = getMaxQuantityValue(qItem);//gets the maxQuantity value from the questionaire item const invalidType = getInputInvalidType({ qItem, @@ -42,12 +48,17 @@ function useValidationFeedback(qItem: QuestionnaireItem, input: string): string maxLength, maxDecimalPlaces, minValue, - maxValue + maxValue, + minQuantityValue,// Min Quantity validation type + maxQuantityValue// Max Quantity validation type }); if (!invalidType) { return ''; } + else { + //invalid type exists, so we proceed + } if (invalidType === ValidationResult.regex && regexValidation) { return `Input should match the specified regex ${regexValidation.expression}`; @@ -86,6 +97,22 @@ function useValidationFeedback(qItem: QuestionnaireItem, input: string): string return maxValueFeedback ?? `Input exceeds permitted maximum value of ${maxValue}.`; } + //Test min quantity + if ( + invalidType === ValidationResult.minQuantityValue && + (typeof minQuantityValue === 'number') + ) { + const minQuantityFeedback = getMinQuantityValueFeedback(qItem);//get the feedback for minquantity if it exists + return minQuantityFeedback ?? `Input is lower than the expected minimum quantity value of ${minQuantityValue}.`; + } + //Test max quantity + if ( + invalidType === ValidationResult.maxQuantityValue && + (typeof maxQuantityValue === 'number') + ) { + const maxQuantityFeedback = getMaxQuantityValueFeedback(qItem);//get the feedback for maxquantity if it exists + return maxQuantityFeedback ?? `Input exceeds permitted maximum quantity value of ${maxQuantityValue}.`; + } return ''; } diff --git a/packages/smart-forms-renderer/src/utils/itemControl.ts b/packages/smart-forms-renderer/src/utils/itemControl.ts index 76e9ba3be..a6f599bf7 100644 --- a/packages/smart-forms-renderer/src/utils/itemControl.ts +++ b/packages/smart-forms-renderer/src/utils/itemControl.ts @@ -15,7 +15,7 @@ * limitations under the License. */ -import type { Coding, Extension, QuestionnaireItem, QuestionnaireItemAnswerOption } from 'fhir/r4'; +import type { Coding, Extension, Quantity, QuestionnaireItem, QuestionnaireItemAnswerOption } from 'fhir/r4'; import type { RegexValidation } from '../interfaces/regex.interface'; import { structuredDataCapture } from 'fhir-sdc-helpers'; import { default as htmlParse } from 'html-react-parser'; @@ -470,3 +470,106 @@ export function getMaxValueFeedback(qItem: QuestionnaireItem) { return null; } + + + + +/** + * Check if the item has a sdc-questionnaire-minQuantity and minQuantity extension + * @author Janardhan Vignarajan + * @export + * @param {QuestionnaireItem} qItem + * @return {*} {(number | undefined)} + */ +export function getMinQuantityValue(qItem: QuestionnaireItem) : number | undefined { + const itemControl = qItem.extension?.find( + (extension: Extension) => + extension.url === 'http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-minQuantity' + ); + + if (itemControl && itemControl.valueQuantity) { //check if valueQuantity exists in the extension + if (itemControl.valueQuantity.value) {//check if valueQuantity.value exists in the extension + return itemControl.valueQuantity.value; + } + } + return undefined; +} + + +/** + * Check if the item has a sdc-questionnaire-minQuantity feedback extension + * + * @author Janardhan Vignarajan + * @export + * @param {QuestionnaireItem} qItem + * @return {*} + */ +export function getMinQuantityValueFeedback(qItem: QuestionnaireItem) { + const itemControl = qItem.extension?.find( + (extension: Extension) => + extension.url === 'https://smartforms.csiro.au/ig/StructureDefinition/minQuantityValue-feedback' + ); + if (itemControl) { + const extensionString = itemControl.valueString; + if (extensionString) { + return extensionString; + } + } + + return null; +} + + + +/** +* Check if the item has a sdc-questionnaire-maxQuantity extension + * + * @author Janardhan Vignarajan + * @export + * @param {QuestionnaireItem} qItem + * @return {*} {(number | undefined)} + */ +export function getMaxQuantityValue(qItem: QuestionnaireItem) : number | undefined { + const itemControl = qItem.extension?.find( + (extension: Extension) => + extension.url === 'http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-maxQuantity' + ); + + if (itemControl && itemControl.valueQuantity) { //check if valueQuantity exists in the extension + if (itemControl.valueQuantity.value) {//check if valueQuantity.value exists in the extension + return itemControl.valueQuantity.value; + } + } + return undefined; +} + + +/** + * Check if the item has a sdc-questionnaire-maxQuantity Feedback extension + * + * @author Janardhan Vignarajan + + * @export + * @param {QuestionnaireItem} qItem + * @return {*} + */ +export function getMaxQuantityValueFeedback(qItem: QuestionnaireItem) { + const itemControl = qItem.extension?.find( + (extension: Extension) => + extension.url === 'https://smartforms.csiro.au/ig/StructureDefinition/maxQuantityValue-feedback' + ); + if (itemControl) { + const extensionString = itemControl.valueString; + if (extensionString) { + return extensionString; + } + } + + return null; +} + + + + + + diff --git a/packages/smart-forms-renderer/src/utils/validateQuestionnaire.ts b/packages/smart-forms-renderer/src/utils/validateQuestionnaire.ts index d6ff0d696..49e14459a 100644 --- a/packages/smart-forms-renderer/src/utils/validateQuestionnaire.ts +++ b/packages/smart-forms-renderer/src/utils/validateQuestionnaire.ts @@ -18,6 +18,7 @@ import type { OperationOutcome, OperationOutcomeIssue, + Quantity, Questionnaire, QuestionnaireItem, QuestionnaireResponse, @@ -33,7 +34,9 @@ import { getMinValue, getRegexString, getRegexValidation, - getShortText + getShortText, + getMinQuantityValue,//import for Quantity Value + getMaxQuantityValue//import for Quantity Value } from './itemControl'; import { structuredDataCapture } from 'fhir-sdc-helpers'; import type { RegexValidation } from '../interfaces/regex.interface'; @@ -80,7 +83,9 @@ export enum ValidationResult { minValueIncompatUnits = 'minValueIncompatUnits', // The units provided in the Quantity cannot be converted to the min Quantity units maxValueIncompatUnits = 'maxValueIncompatUnits', // The units provided in the Quantity cannot be converted to the max Quantity units invalidUnit = 'invalidUnit', // The unit provided was not among the list selected (or did not have all the properties defined in the unit coding) - invalidUnitValueSet = 'invalidUnitValueSet' // The unit provided was not in the provided valueset + invalidUnitValueSet = 'invalidUnitValueSet', // The unit provided was not in the provided valueset + minQuantityValue = 'minQuantityValue', // Minimum Quantity value constraint violated + maxQuantityValue = 'maxQuantityValue' // Maximum Quantity value constraint violated } interface ValidateQuestionnaireParams { @@ -339,11 +344,11 @@ function validateSingleItem( } } - // Validate regex, maxLength and minLength + // Validate regex, maxLength and minLength, maxQuantity and minQuantity if (qrItem.answer) { for (const [i, answer] of qrItem.answer.entries()) { // Your code here, you can use 'index' and 'answer' as needed - if (answer.valueString || answer.valueInteger || answer.valueDecimal || answer.valueUri) { + if (answer.valueString || answer.valueInteger || answer.valueDecimal || answer.valueUri || answer.valueQuantity) { const invalidInputType = getInputInvalidType({ qItem, input: getInputInString(answer), @@ -352,7 +357,9 @@ function validateSingleItem( maxLength: qItem.maxLength, maxDecimalPlaces: structuredDataCapture.getMaxDecimalPlaces(qItem), minValue: getMinValue(qItem), - maxValue: getMaxValue(qItem) + maxValue: getMaxValue(qItem), + minQuantityValue: getMinQuantityValue(qItem), + maxQuantityValue: getMaxQuantityValue(qItem) }); if (invalidInputType) { @@ -365,6 +372,10 @@ function validateSingleItem( invalidItems[qItem.linkId]?.issue ); } + else // if not invalid input types found + { + //do nothing + } } } } @@ -385,6 +396,9 @@ function getInputInString(answer?: QuestionnaireResponseItemAnswer) { return answer.valueDecimal.toString(); } else if (answer.valueUri) { return answer.valueUri; + } else if (answer.valueQuantity && answer.valueQuantity.value) //return the valueQuantity as string + { + return answer.valueQuantity.value.toString(); } return ''; @@ -399,6 +413,9 @@ interface GetInputInvalidTypeParams { maxDecimalPlaces?: number; minValue?: string | number; maxValue?: string | number; + minQuantityValue?: number; + maxQuantityValue?: number; + } export function getInputInvalidType( @@ -412,7 +429,9 @@ export function getInputInvalidType( maxLength, maxDecimalPlaces, minValue, - maxValue + maxValue, + minQuantityValue, + maxQuantityValue } = getInputInvalidTypeParams; if (input) { @@ -448,6 +467,26 @@ export function getInputInvalidType( return ValidationResult.maxValue; } } + //if minQuantityValue exists then check the value and validate + if (minQuantityValue) { + const minQuantityValueError = checkMinQuantityValue(qItem, input, minQuantityValue); + if (minQuantityValueError !== null) { + return ValidationResult.minQuantityValue; + } + else { + //No error, do nothing + } + } + //if maxQuantityValue exists then check the value and validate + if (maxQuantityValue) { + const maxQuantityValueError = checkMaxQuantityValue(qItem, input, maxQuantityValue); + if (maxQuantityValueError !== null) { + return ValidationResult.maxQuantityValue; + } + else { + //No error, do nothing + } + } } return null; @@ -547,6 +586,83 @@ function checkMaxValue( return null; } +/** + * Checks for Minimum Quantity Value and returns the validation results + * + * @param {QuestionnaireItem} qItem + * @param {string} input + * @param {number} minQuantityValue + * @return {*} {(ValidationResult.minQuantityValue | null)} + */ +function checkMinQuantityValue( + qItem: QuestionnaireItem, + input: string, + minQuantityValue: number +): ValidationResult.minQuantityValue | null { + + + + switch (qItem.type) { + case 'quantity': + + const precision = getDecimalPrecision(qItem); + const decimalValue = precision + ? parseDecimalStringToFloat(input, precision) + : parseFloat(input); + + if (decimalValue < minQuantityValue) { + return ValidationResult.minQuantityValue; + } + + break; + default: + return null; + } + + return null; + + +} + +/** + * Checks for Maxmim Quantity Value and returns the validation results + * + * @param {QuestionnaireItem} qItem + * @param {string} input + * @param {number} maxQuantityValue + * @return {*} {(ValidationResult.maxQuantityValue | null)} + */ +function checkMaxQuantityValue( + qItem: QuestionnaireItem, + input: string, + maxQuantityValue: number +): ValidationResult.maxQuantityValue | null { + + switch (qItem.type) { + case 'quantity': + + const precision = getDecimalPrecision(qItem); + const decimalValue = precision + ? parseDecimalStringToFloat(input, precision) + : parseFloat(input); + + if (decimalValue > maxQuantityValue) { + return ValidationResult.maxQuantityValue; + } + + + break; + + + default: + return null; + } + + return null; + +} + + function createValidationOperationOutcome( error: ValidationResult, qItem: QuestionnaireItem, @@ -656,7 +772,7 @@ function createValidationOperationOutcomeIssue( case ValidationResult.maxLength: { detailsText = `${fieldDisplayText}: Exceeded maximum of ${ qItem.maxLength - } characters, received '${getInputInString(qrItem.answer?.[answerIndex])}'`; + } characters, received '${getInputInString(qrItem.answer?.[answerIndex])}'`; return { severity: 'error', code: 'business-rule', @@ -736,6 +852,49 @@ function createValidationOperationOutcomeIssue( } }; } + //Validation result error handling for min quantity extension + case ValidationResult.minQuantityValue: { + detailsText = `${fieldDisplayText}: Expected the minimum value ${getMinQuantityValue( + qItem + )}, received '${getInputInString(qrItem.answer?.[answerIndex])}'`; + return { + severity: 'error', + code: 'business-rule', + expression: [locationExpression], + details: { + coding: [ + { + system: errorCodeSystem, + code: error, + display: 'Too small' + } + ], + text: detailsText + } + }; + } + //Validation result error handling for max quantity extension + + case ValidationResult.maxQuantityValue: { + detailsText = `${fieldDisplayText}: Exceeded the maximum value ${getMaxQuantityValue( + qItem + )}, received '${getInputInString(qrItem.answer?.[answerIndex])}'`; + return { + severity: 'error', + code: 'business-rule', + expression: [locationExpression], + details: { + coding: [ + { + system: errorCodeSystem, + code: error, + display: 'Too big' + } + ], + text: detailsText + } + }; + } // mark unknown issues as fatal default: {