UNPKG

@i4mi/fhir_questionnaire

Version:

TS Package for handling FHIR Questionnaire and generating QuestionnaireRespones.

949 lines 98.7 kB
"use strict"; var __assign = (this && this.__assign) || function () { __assign = Object.assign || function(t) { for (var s, i = 1, n = arguments.length; i < n; i++) { s = arguments[i]; for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p]; } return t; }; return __assign.apply(this, arguments); }; var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) { if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) { if (ar || !(i in from)) { if (!ar) ar = Array.prototype.slice.call(from, 0, i); ar[i] = from[i]; } } return to.concat(ar || Array.prototype.slice.call(from)); }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.QuestionnaireData = void 0; var fhirpath_1 = __importDefault(require("fhirpath")); var fhir_r4_1 = require("@i4mi/fhir_r4"); var IQuestion_1 = require("./IQuestion"); var UNSELECT_OTHERS_EXTENSION = 'http://midata.coop/extensions/valueset-unselect-others'; var ITEM_CONTROL_EXTENSION = 'http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl'; var ITEM_CONTROL_EXTENSION_SYSTEM = 'http://hl7.org/fhir/questionnaire-item-control'; var MIN_VALUE_EXTENSION = 'http://hl7.org/fhir/StructureDefinition/minValue'; var MAX_VALUE_EXTENSION = 'http://hl7.org/fhir/StructureDefinition/maxValue'; var ENTRY_FORMAT_EXTENSION = 'http://hl7.org/fhir/StructureDefinition/entryFormat'; var SLIDER_STEP_VALUE_EXTENSION = 'http://hl7.org/fhir/StructureDefinition/questionnaire-sliderStepValue'; var UNIT_EXTENSION = 'http://hl7.org/fhir/StructureDefinition/questionnaire-unit'; var HIDDEN_EXTENSION = 'http://hl7.org/fhir/StructureDefinition/questionnaire-hidden'; var CALCULATED_EXPRESSION_EXTENSION = 'http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-calculatedExpression'; var QUESTIONNAIRERESPONSE_CODING_EXTENSION_URL = 'http://midata.coop/extensions/response-code'; var INITIAL_EXPRESSION_EXTENSION = 'http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-initialExpression'; var LANGUAGE_EXTENSION = 'http://hl7.org/fhir/StructureDefinition/translation'; var PRIMITIVE_VALUE_X = [ 'valueString', 'valueInteger', 'valueBoolean', 'valueDecimal', 'valueDate', 'valueDateTime', 'valueTime', 'valueUri' ]; var COMPLEX_VALUE_X = [ { type: 'valueCoding', isMatching: function (criterium, answerOption) { if (criterium.valueCoding && answerOption.code.valueCoding) { return criterium.valueCoding.system === answerOption.code.valueCoding.system && criterium.valueCoding.code === answerOption.code.valueCoding.code; } else { return false; } } }, { type: 'valueAttachment', isMatching: function (criterium, answerOption) { if (criterium.valueAttachment) { console.warn('Sorry, picking valueAttachment from AnswerOptions is currently not supported.', answerOption); } return false; } }, { type: 'valueReference', isMatching: function (criterium, answerOption) { if (criterium.valueReference && answerOption.code.valueReference) { return criterium.valueReference.reference === answerOption.code.valueReference.reference || criterium.valueReference.identifier === answerOption.code.valueReference.identifier; } else { return false; } } }, { type: 'valueQuantity', isMatching: function (criterium, answerOption) { if (criterium.valueQuantity && answerOption.code.valueQuantity) { return criterium.valueQuantity.value === answerOption.code.valueQuantity.value && (criterium.valueQuantity.unit === answerOption.code.valueQuantity.unit || criterium.valueQuantity.code === answerOption.code.valueQuantity.code); } else { return false; } } } ]; function checkIfDependingQuestionIsEnabled(_dependant, _depending, _answer) { // we start with true, when undefined or any with false var isEnabled = (_dependant.dependingQuestionsEnableBehaviour == fhir_r4_1.QuestionnaireEnableWhenBehavior.ALL); _depending.criteria.forEach(function (criterium) { var _a, _b; var evaluatesToTrue = false; var crit = ((_a = criterium.answer.valueCoding) === null || _a === void 0 ? void 0 : _a.code) || criterium.answer.valueDate || criterium.answer.valueDateTime || criterium.answer.valueTime || criterium.answer.valueDecimal || criterium.answer.valueString || criterium.answer.valueInteger || (criterium.answer.valueBoolean == undefined ? undefined : criterium.answer.valueBoolean); var answ = ((_b = _answer === null || _answer === void 0 ? void 0 : _answer.code.valueCoding) === null || _b === void 0 ? void 0 : _b.code) || (_answer === null || _answer === void 0 ? void 0 : _answer.code.valueDate) || (_answer === null || _answer === void 0 ? void 0 : _answer.code.valueDateTime) || (_answer === null || _answer === void 0 ? void 0 : _answer.code.valueDecimal) || (_answer === null || _answer === void 0 ? void 0 : _answer.code.valueString) || (_answer === null || _answer === void 0 ? void 0 : _answer.code.valueInteger) || ((_answer === null || _answer === void 0 ? void 0 : _answer.code.valueBoolean) == undefined ? undefined : _answer === null || _answer === void 0 ? void 0 : _answer.code.valueBoolean); if (crit != undefined) { evaluatesToTrue = evaluateAnswersForDependingQuestion(answ, crit, criterium.operator); } isEnabled = (_dependant.dependingQuestionsEnableBehaviour == fhir_r4_1.QuestionnaireEnableWhenBehavior.ALL) ? (evaluatesToTrue && isEnabled) // only true, when criteria before were true : (evaluatesToTrue || isEnabled); // true when evaluates to true or questions before were true }); return isEnabled; } /** * Evaluates a given answer with a criterium and an operator, for enabling and disabling depending questions. * @param _answer the given answer, as string (also for code etc) or as a number (when using GE, GT, LE & LT operator) * @param _criterium the criterium against which the given answer is compared * @param _operator defines if the answer and criterium must be equal or not equal etc. * @returns true if answer and criterium match with the given operator, false if not so. **/ function evaluateAnswersForDependingQuestion(_answer, _criterium, _operator) { // make sure we have both comparants as number if one is if (typeof _answer === 'number' && typeof _criterium !== 'number') { _criterium = Number(_criterium); } else if (typeof _criterium === 'number' && typeof _answer !== 'number') { _answer = Number(_answer); } switch (_operator) { case fhir_r4_1.QuestionnaireItemOperator.EXISTS: if (_criterium) { return Array.isArray(_answer) ? _answer.length > 0 : _answer != undefined; } else { return Array.isArray(_answer) ? _answer.length === 0 : _answer == undefined; } case fhir_r4_1.QuestionnaireItemOperator.E: return _answer === _criterium; case fhir_r4_1.QuestionnaireItemOperator.NE: return _answer != _criterium && _answer !== undefined; case fhir_r4_1.QuestionnaireItemOperator.GE: return _answer == undefined ? false : _answer >= _criterium; case fhir_r4_1.QuestionnaireItemOperator.LE: return _answer == undefined ? false : _answer <= _criterium; case fhir_r4_1.QuestionnaireItemOperator.GT: return _answer == undefined ? false : _answer > _criterium; case fhir_r4_1.QuestionnaireItemOperator.LT: return _answer == undefined ? false : _answer < _criterium; default: return false; } } /** * Recursively iterates through question items to check for completeness * @param _question the question that should be checked for completeness with all its subquestions * @param _onlyRequired parameter that indicates if only questions that are actually are marked * as required should be required * @param _markInvalid optional parameter, to specify if not completed questions * should be updated to be invalid (see isInvalid property) * @returns true if the question and all its subquestions are complete * false if at least one (sub)question is not complete */ function recursivelyCheckCompleteness(_question, _onlyRequired, _markInvalid) { var areAllComplete = true; _question.forEach(function (question) { if (!question.readOnly && question.isEnabled) { if (question.subItems) { if (_markInvalid) { var areSubItemsComplete = recursivelyCheckCompleteness(question.subItems, _onlyRequired, _markInvalid); areAllComplete = areSubItemsComplete && areAllComplete; } else { areAllComplete = (areAllComplete ? recursivelyCheckCompleteness(question.subItems, _onlyRequired, _markInvalid) : false); } } if (question.isEnabled && (question.required || !_onlyRequired) && question.type !== fhir_r4_1.QuestionnaireItemType.DISPLAY && question.type !== fhir_r4_1.QuestionnaireItemType.GROUP) { if (question.selectedAnswers === undefined || question.selectedAnswers.length === 0) { if (_markInvalid) question.isInvalid = true; areAllComplete = false; } } } }); return areAllComplete; } /** * Recursively iterates through nested IQuestions and extracts the given answers and adds * it to a given array as FHIR QuestionnaireResponseItem * @param questions an array of (possibly nested) IQuestions * @param responseItems the array to fill with the FHIR QuestionnaireResponseItems * @returns the given array * @throws an error if answers are not valid **/ function mapIQuestionToQuestionnaireResponseItem(_questions, _responseItems, _language) { _questions.forEach(function (question) { question.isInvalid = false; if (question.type === fhir_r4_1.QuestionnaireItemType.GROUP) { if (question.subItems && question.subItems.length > 0) { var labelText = question.label[_language]; _responseItems.push({ linkId: question.id, text: labelText ? labelText : undefined, item: mapIQuestionToQuestionnaireResponseItem(question.subItems, [], _language) }); } else { question.isInvalid = true; throw new Error("Invalid question set: IQuestion with id ".concat(question.id, " is group type, but has no subItems.")); } } else if (question.isEnabled) { // some validation if (question.required && question.selectedAnswers.length === 0) { question.isInvalid = true; throw new Error("Invalid answer set: IQuestion with id ".concat(question.id, " is mandatory, but not answered.")); } else if (!question.allowsMultipleAnswers && question.selectedAnswers.length > 1) { question.isInvalid = true; throw new Error("Invalid answer set: IQuestion with id ".concat(question.id, " allows only one answer, but has more.")); } else { var responseItem_1 = { linkId: question.id, text: question.label[_language], answer: new Array() }; question.selectedAnswers.forEach(function (answer) { if (answer.valueCoding) { // find translated display for answer valueCoding var answerDisplayAllLanguages = (question.answerOptions.find(function (answerOption) { return answerOption.code.valueCoding && answer.valueCoding && answerOption.code.valueCoding.code === answer.valueCoding.code; }) || { answer: '' }).answer; // some answer options (e.g. zip code locations) have only one language set var answerDisplay = answerDisplayAllLanguages ? answerDisplayAllLanguages[_language] ? answerDisplayAllLanguages[_language] : answerDisplayAllLanguages[Object.keys(answerDisplayAllLanguages)[0]] : ''; responseItem_1.answer.push({ valueCoding: { system: answer.valueCoding.system, code: answer.valueCoding.code, display: answerDisplay, extension: answer.valueCoding.extension } }); } else { responseItem_1.answer.push(answer); } if (question.subItems && question.subItems.length > 0) { answer.item = []; mapIQuestionToQuestionnaireResponseItem(question.subItems, answer.item, _language); } }); if (question.type === fhir_r4_1.QuestionnaireItemType.DISPLAY) responseItem_1.answer = undefined; // add to array _responseItems.push(responseItem_1); } } }); return _responseItems; } /* * Checks if a item has a given extension, and returns the value of the extension (when one of the following: * - valueCodeableConcept * - valueDuration * - valueString * - valueInteger * - valueDate * - valueExpression * - valueBoolean (CAVE: when the valueBoolean is FALSE, the method will return FALSE!) * ) * If the extension exists with another value, the method returns TRUE. * If the extension does not exist, the method returns UNDEFINED. */ function hasExtension(_extensionURL, _extensionSystem, _item) { var returnValue = undefined; if (_item.extension) { _item.extension.forEach(function (extension) { if (!returnValue && extension.url === _extensionURL) { if (_extensionSystem && extension.valueCodeableConcept && extension.valueCodeableConcept.coding) { extension.valueCodeableConcept.coding.forEach(function (coding) { if (coding.system === _extensionSystem) { returnValue = coding; } }); } if (extension.valueDuration && extension.valueDuration.system && extension.valueDuration.system === _extensionSystem) { returnValue = extension.valueDuration; } if (extension.valueInteger != undefined) { returnValue = extension.valueInteger; } if (extension.valueString) { returnValue = extension.valueString; } if (extension.valueBoolean != undefined) { returnValue = extension.valueBoolean; } if (extension.valueDate) { returnValue = extension.valueDate; } if (extension.valueExpression && extension.valueExpression.language && extension.valueExpression.language === _extensionSystem) { returnValue = extension.valueExpression; } return (returnValue != undefined ? returnValue : true); } }); } return returnValue; } /** * Extracts IQuestionOptions from a QuestionnaireItem * @param _FHIRItem the QuestionnaireItem to extract the options from * @returns an IQuestionOptions object */ function setOptionsFromExtensions(_FHIRItem) { var _a; var calculatedExpressionExtension = hasExtension(CALCULATED_EXPRESSION_EXTENSION, 'text/fhirpath', _FHIRItem); var initialExpressionExtension = hasExtension(INITIAL_EXPRESSION_EXTENSION, 'text/fhirpath', _FHIRItem); var returnValue = { min: hasExtension(MIN_VALUE_EXTENSION, undefined, _FHIRItem), max: hasExtension(MAX_VALUE_EXTENSION, undefined, _FHIRItem), format: hasExtension(ENTRY_FORMAT_EXTENSION, undefined, _FHIRItem), sliderStep: hasExtension(SLIDER_STEP_VALUE_EXTENSION, undefined, _FHIRItem), unit: hasExtension(UNIT_EXTENSION, 'https://ucum.org', _FHIRItem), calculatedExpression: calculatedExpressionExtension ? calculatedExpressionExtension.expression : undefined, initialExpression: initialExpressionExtension ? initialExpressionExtension.expression : undefined, controlTypes: [] }; // push itemcontrol extensions to controlTypes (_a = _FHIRItem.extension) === null || _a === void 0 ? void 0 : _a.forEach(function (extension) { var _a; if (extension.url === ITEM_CONTROL_EXTENSION) { var controlType = (_a = extension.valueCodeableConcept) === null || _a === void 0 ? void 0 : _a.coding[0].code; controlType && returnValue.controlTypes.push(controlType); } }); // TODO: legacy code, can be removed in version 1.0.0 var itemControlExtension = hasExtension(ITEM_CONTROL_EXTENSION, ITEM_CONTROL_EXTENSION_SYSTEM, _FHIRItem); if (itemControlExtension) { Object.values(IQuestion_1.ItemControlType).forEach(function (typeCode) { if (!returnValue.controlType && itemControlExtension.code === typeCode) { returnValue.controlType = typeCode; } }); } // TODO: end of legacy code return returnValue; } function recursivelyCheckTouched(item) { var _a; var touched = item.selectedAnswers.length > 0; (_a = item.subItems) === null || _a === void 0 ? void 0 : _a.forEach(function (subItem) { if (!touched) { touched = recursivelyCheckTouched(subItem); } }); return touched; } function getAvailableLanguages(_questionnaire) { var _a, _b, _c; function extractFromExtensions(languageKeys, extensions) { extensions === null || extensions === void 0 ? void 0 : extensions.forEach(function (extension) { var _a; if (extension.url === LANGUAGE_EXTENSION) { (_a = extension.extension) === null || _a === void 0 ? void 0 : _a.forEach(function (subExtension) { if (subExtension.url === 'lang') { var languageValue = subExtension.valueCode; if (languageValue) { languageKeys[languageValue] = true; } } }); } }); } var languageKeys = {}; extractFromExtensions(languageKeys, (_a = _questionnaire._title) === null || _a === void 0 ? void 0 : _a.extension); extractFromExtensions(languageKeys, (_b = _questionnaire._description) === null || _b === void 0 ? void 0 : _b.extension); _questionnaire.item && _questionnaire.item[0] && extractFromExtensions(languageKeys, (_c = _questionnaire.item[0]._text) === null || _c === void 0 ? void 0 : _c.extension); return Object.keys(languageKeys); } var QuestionnaireData = /** @class */ (function () { function QuestionnaireData(_questionnaire, _availableLanguages, _valueSets, _items, _hiddenFhirItems) { var _this = this; this.fhirQuestionnaire = _questionnaire; this.items = new Array(); this.hiddenFhirItems = new Array(); this.availableLanguages = _availableLanguages || getAvailableLanguages(_questionnaire); this.valueSets = {}; if (_valueSets) { this.valueSets = _valueSets; } else if (this.fhirQuestionnaire.contained) { // process contained valuesets // TODO: prepare for not contained valuesets this.fhirQuestionnaire.contained.forEach(function (resource) { if (resource.resourceType === 'ValueSet') { var valueSet = resource; if (valueSet.id) { _this.valueSets[valueSet.id] = valueSet; } } }); } if (_items) { this.items = _items; this.hiddenFhirItems = _hiddenFhirItems ? _hiddenFhirItems : []; } else { this.items = new Array(); this.resetResponse(); } } /** * Returns the storeable data of the object as a string, not including the questionnaire. * When rehydrating with a serialized QuestionnaireData string, you can create a new * QuestionnaireData object using the Questionnaire and then call .unserialize() on it, * passing the serialized string * @returns a string representing the QuestionnaireData object _without_ containing * - the fhir Questionnaire * @see unserialize() */ QuestionnaireData.prototype.serialize = function () { return JSON.stringify({ valueSets: this.valueSets, items: this.items, hiddenFhirItems: this.hiddenFhirItems, lastRestored: this.lastRestored, availableLanguages: this.availableLanguages, responseIdToSynchronize: this.responseIdToSynchronize }); }; /** * Populates the QuestionnaireData object with data from a previously serialized * QuestionnaireData Object. This can be either passed on as a string from serialize() or * as a JSON object that was created with JSON.stringify() from a QuestionnaireData * @param _data The serialized data from a QuestionnaireData as string or JSON * @throws An error if the data is passed as a string with no items property * (which is used to detect if it is a serialized QuestionnaireData) * @see serialize() */ QuestionnaireData.prototype.unserialize = function (_data) { var data = typeof _data === 'string' ? JSON.parse(_data) : _data; if (!Object.prototype.hasOwnProperty.call(data, 'items')) { console.warn('Can not unserialize, passed string seems not to be a serialized QuestionnaireData.', data); throw new Error('Can not unserialize, passed string seems not to be a serialized QuestionnaireData.'); } Object.assign(this, data); if (this.lastRestored) { this.lastRestored = new Date(this.lastRestored); } }; /** * Resets the response to the questionnaire **/ QuestionnaireData.prototype.resetResponse = function () { var _this = this; if (this.items.length > 0) { this.items = new Array(); } this.hiddenFhirItems = new Array(); var questionsDependencies = []; // helper array for dependingQuestions if (this.fhirQuestionnaire.item) { this.filterOutHiddenItems(this.fhirQuestionnaire.item).forEach(function (item) { // recursively process items var currentQuestion = _this.mapQuestionnaireItemToIQuestion(item); var dependingToQuestions = _this.linkDependingQuestions(item, currentQuestion); if (dependingToQuestions.length > 0) { questionsDependencies = questionsDependencies.concat(dependingToQuestions); } _this.items.push(currentQuestion); }); // now we stepped through all the items and the helper array is complete, we can add depending questions to their determinators questionsDependencies.forEach(function (question) { var determinator = _this.findQuestionById(question.id, _this.items); if (question.reference && determinator) { var existingDependingQuestion = determinator.dependingQuestions.find(function (q) { return q.dependingQuestion == question.reference; }); if (existingDependingQuestion && question.answer !== undefined) { existingDependingQuestion.criteria.push({ answer: question.answer, operator: question.operator }); } else { question.answer && determinator.dependingQuestions.push({ dependingQuestion: question.reference, criteria: [{ answer: question.answer, operator: question.operator }] }); } } }); } }; /** * Returns the questions array. **/ QuestionnaireData.prototype.getQuestions = function () { return this.items; }; /** * Updates the selected answer(s) of a question: adds the answer if it's not already selected * and removes it, if it was selected. * @param _question the IQuestion to which the answer belongs * @param _answer the selected / unselected QuestionnaireItemAnswerOption **/ QuestionnaireData.prototype.updateQuestionAnswers = function (_question, _answer) { _question.isInvalid = false; // assume it is not invalid anymore, until further check if (_answer === undefined || (_question.type === fhir_r4_1.QuestionnaireItemType.INTEGER && _answer.code.valueInteger == undefined) || (_question.type === fhir_r4_1.QuestionnaireItemType.STRING && _answer.code.valueString == '') || (_question.type === fhir_r4_1.QuestionnaireItemType.TEXT && _answer.code.valueString == '') || (_question.type === fhir_r4_1.QuestionnaireItemType.DATE && _answer.code.valueDate == '')) { // remove previous given answers _question.selectedAnswers.splice(0, _question.selectedAnswers.length); } else { var indexOfAnswer = _question.selectedAnswers.indexOf(_answer.code); if (_question.allowsMultipleAnswers) { // check if item is already selected if (indexOfAnswer >= 0) { // answer is already selected _question.selectedAnswers.splice(indexOfAnswer, 1); // remove answer } else { // if not already selected, we select it now _question.selectedAnswers.push(_answer.code); // and disable other answers when necessary if (_answer.disableOtherAnswers) { _answer.disableOtherAnswers.forEach(function (otherAnswer) { var indexOfOtherAnswer = _question.selectedAnswers.findIndex(function (selectedAnswer) { return selectedAnswer.valueCoding ? selectedAnswer.valueCoding.code === otherAnswer : undefined; }); if (indexOfOtherAnswer >= 0) { // otherAnswer is selected _question.selectedAnswers.splice(indexOfOtherAnswer, 1); // remove otherAnswer) } // no else needed, because we don't have to unselect already unselected answers }); } } } else { if (indexOfAnswer < 0) { _question.selectedAnswers[0] = _answer.code; } } } // we shouldn't have to do this in 2022, but if we don't vite will get confused and break everything // eslint-disable-next-line @typescript-eslint/no-this-alias var that = this; _question.dependingQuestions.forEach(function (dependingQuestion) { dependingQuestion.dependingQuestion.isEnabled = checkIfDependingQuestionIsEnabled(_question, dependingQuestion, _answer); // specification says that if an item is not enabled, every subitem is not enabled, // no matter what their own enableWhen says if (dependingQuestion.dependingQuestion.isEnabled === false) { that.recursivelyDisableSubItems(dependingQuestion.dependingQuestion); } else { that.recursivelyEnableSubitems(dependingQuestion.dependingQuestion); } }); }; QuestionnaireData.prototype.recursivelyDisableSubItems = function (subItem) { var _this = this; var _a; subItem.isEnabled = false; (_a = subItem.subItems) === null || _a === void 0 ? void 0 : _a.forEach(function (sI) { return _this.recursivelyDisableSubItems(sI); }); }; QuestionnaireData.prototype.recursivelyEnableSubitems = function (subItem) { var _this = this; var _a, _b; subItem.isEnabled = true; var fhirItem = this.findFhirItem(subItem.id); (_a = fhirItem === null || fhirItem === void 0 ? void 0 : fhirItem.enableWhen) === null || _a === void 0 ? void 0 : _a.forEach(function (enableWhen) { var determinator = _this.findQuestionById(enableWhen.question); if (determinator) { var ewDefinition = determinator.dependingQuestions.find(function (dq) { return dq.dependingQuestion.id === subItem.id; }); subItem.isEnabled = checkIfDependingQuestionIsEnabled(determinator, ewDefinition, { answer: {}, code: determinator.selectedAnswers[0] }); // TODO: handle this for when parent item is a multiple choice item } }); if (subItem.isEnabled) (_b = subItem.subItems) === null || _b === void 0 ? void 0 : _b.forEach(function (sI) { return _this.recursivelyEnableSubitems(sI); }); }; /** * Checks if a given IAnswerOption is already sel ected for a IQuestion. * It is checking for the code of the IAnswerOption, not the display string. * @param _question the IQuestion to which the answer belongs * @param _answer the IAnswerOption in question **/ QuestionnaireData.prototype.isAnswerOptionSelected = function (_question, _answer) { return _question.selectedAnswers.findIndex(function (selectedAnswer) { var _a, _b, _c, _d, _e; return (_answer.code.valueBoolean != undefined && selectedAnswer.valueBoolean === _answer.code.valueBoolean || _answer.code.valueDate != undefined && selectedAnswer.valueDate === _answer.code.valueDate || _answer.code.valueDateTime != undefined && selectedAnswer.valueDateTime === _answer.code.valueDateTime || _answer.code.valueDecimal != undefined && selectedAnswer.valueDecimal === _answer.code.valueDecimal || _answer.code.valueInteger != undefined && selectedAnswer.valueInteger === _answer.code.valueInteger || _answer.code.valueQuantity != undefined && (((_a = selectedAnswer.valueQuantity) === null || _a === void 0 ? void 0 : _a.value) === _answer.code.valueQuantity.value && ((_b = selectedAnswer.valueQuantity) === null || _b === void 0 ? void 0 : _b.system) === _answer.code.valueQuantity.system || ((_c = selectedAnswer.valueQuantity) === null || _c === void 0 ? void 0 : _c.unit) === _answer.code.valueQuantity.unit) || _answer.code.valueReference != undefined && selectedAnswer.valueReference === _answer.code.valueReference || _answer.code.valueString != undefined && selectedAnswer.valueString === _answer.code.valueString || _answer.code.valueTime != undefined && selectedAnswer.valueTime === _answer.code.valueTime || _answer.code.valueUri != undefined && selectedAnswer.valueUri === _answer.code.valueUri || _answer.code.valueAttachment != undefined && selectedAnswer.valueAttachment === _answer.code.valueAttachment || _answer.code.valueCoding != undefined && (((_d = selectedAnswer.valueCoding) === null || _d === void 0 ? void 0 : _d.code) === _answer.code.valueCoding.code && ((_e = selectedAnswer.valueCoding) === null || _e === void 0 ? void 0 : _e.system) === _answer.code.valueCoding.system)); }) > -1; }; /** * Returns the questionnaire title in a given language. * Falls back to default language of the questionnaire, * if the wanted language is not available. * @param _language the language code of the wanted language. **/ QuestionnaireData.prototype.getQuestionnaireTitle = function (_language) { var title = undefined; if (this.fhirQuestionnaire._title && this.fhirQuestionnaire._title.extension && this.availableLanguages.includes(_language)) { title = (0, fhir_r4_1.readI18N)(this.fhirQuestionnaire._title, _language); } return title || this.fhirQuestionnaire.title; }; /** * Returns the questionnaire description in a given language. * Falls back to default language of the questionnaire, * if the wanted language is not available. * @param _language the language code of the wanted language. **/ QuestionnaireData.prototype.getQuestionnaireDescription = function (_language) { var title = undefined; if (this.fhirQuestionnaire._description && this.fhirQuestionnaire._description.extension && this.availableLanguages.includes(_language)) { title = (0, fhir_r4_1.readI18N)(this.fhirQuestionnaire._description, _language); } return title || this.fhirQuestionnaire.description; }; /** * Processes a QuestionnaireResponse and parses the given answers to the local iQuestion array. Existing answers are overwritten. * @param _fhirResponse a QuestionnaireResponse that matches the Questionnaire. * @throws an error if the questionnaire response is not matching the questionnaire **/ QuestionnaireData.prototype.restoreAnswersFromQuestionnaireResponse = function (_fhirResponse) { var _this = this; var questionnaireUrl = _fhirResponse.questionnaire ? _fhirResponse.questionnaire.split('|')[0] : ''; if (this.fhirQuestionnaire.url && questionnaireUrl !== this.fhirQuestionnaire.url.split('|')[0]) { throw new Error('Invalid argument: QuestionnaireResponse does not match Questionnaire!'); } var recursivelyClearItems = function (_items) { _items.forEach(function (item) { item.selectedAnswers = []; if (item.subItems) { recursivelyClearItems(item.subItems); } }); }; recursivelyClearItems(this.items); var answerMatchingIQuestionItemWithFhirResponseItem = function (_fhirItems) { _fhirItems.forEach(function (answerItem) { var item = _this.findQuestionById(answerItem.linkId, _this.items); if (item) { item.selectedAnswers = []; if (item.answerOptions !== undefined && answerItem.answer) { answerItem.answer.forEach(function (answer) { var answerAsAnswerOption = _this.findAccordingAnswerOption(answer, item.answerOptions); if (answerAsAnswerOption) { _this.updateQuestionAnswers(item, answerAsAnswerOption); } else { item.selectedAnswers = answerItem.answer ? answerItem.answer : []; } }); } else if (answerItem.answer && answerItem.answer.length > 0) { if (item.allowsMultipleAnswers) { item.selectedAnswers = answerItem.answer; } else { item.selectedAnswers.push(answerItem.answer[0]); } } if (answerItem.item) { answerMatchingIQuestionItemWithFhirResponseItem(answerItem.item); } } else { console.warn('Item with linkId ' + answerItem.linkId + ' was found in QuestionnaireResponse, but does not exist in Questionnaire.'); } }); }; // only restore, if it is not already up to date if (_fhirResponse.item && _fhirResponse.item.length > 0 && (this.lastRestored == undefined || (_fhirResponse.authored && this.lastRestored < new Date(_fhirResponse.authored)))) { this.lastRestored = _fhirResponse.authored ? new Date(_fhirResponse.authored) : new Date(); this.responseIdToSynchronize = _fhirResponse.id; answerMatchingIQuestionItemWithFhirResponseItem(_fhirResponse.item); } }; /** * Gets the QuestionnaireResponse resource with all the currently set answers. * @param _language the shorthand for the language the QuestionnaireResponse (eg. 'de' or 'en'), * should be in the set of available languages * @param _options Options object that can contain zero, one or many of the following properties: * - date: the date when the Questionnaire was filled out (current date by default) * - includeID: boolean that determines if to include FHIR resource ID of a potential * previously restored QuestionnaireResponse (default: false) (if the previous response has * no id, the id of the generated response will be undefined) * - patient: a Reference to the FHIR Patient who filled out the Questionnaire * - midataExtensions: wether to include MIDATA extensions or not (default: false) * - reset: should the questionnaire be reseted after creating the response (default: false) * @returns a QuestionnaireResponse FHIR resource containing all the answers the user gave * @throws - an error if the QuestionnaireResponse is not valid for the corresponding * Questionnaire, e.g. when a required answer is missing * - an error if the _language given is not in the set of available languages **/ QuestionnaireData.prototype.getQuestionnaireResponse = function (_language, _options) { var _this = this; if (!this.availableLanguages.includes(_language)) { throw new Error('getQuestionnaireResponse(): Provided _language (' + _language + ') is not supported by this Questionnaire. (Supported languages: ' + this.availableLanguages + ').'); } var options = _options || {}; // usual questionnaire response var fhirResponse = { resourceType: 'QuestionnaireResponse', status: this.isResponseComplete(true) ? fhir_r4_1.QuestionnaireResponseStatus.COMPLETED : fhir_r4_1.QuestionnaireResponseStatus.IN_PROGRESS, text: { status: fhir_r4_1.NarrativeStatus.GENERATED, div: '' }, questionnaire: this.getQuestionnaireURLwithVersion(), authored: options.date ? options.date.toISOString() : new Date().toISOString(), source: options.patient, id: options.includeID ? this.responseIdToSynchronize : undefined, item: mapIQuestionToQuestionnaireResponseItem(this.items, new Array(), _language) }; if (options.midataExtensions && this.fhirQuestionnaire.code) { fhirResponse.extension = [{ url: QUESTIONNAIRERESPONSE_CODING_EXTENSION_URL, valueCoding: this.fhirQuestionnaire.code[0] }]; } // stuff to do for hidden items with calculated expression var hiddenItemsWithCalculatedExpression = __spreadArray([], this.hiddenFhirItems, true).filter(function (i) { return i.item.options && i.item.options.calculatedExpression !== undefined; }); hiddenItemsWithCalculatedExpression.forEach(function (item) { var _a, _b; if (item.item.options && item.item.options.calculatedExpression) { try { var calculatedAnswer = {}; switch (item.item.type) { case fhir_r4_1.QuestionnaireItemType.INTEGER: calculatedAnswer = { valueInteger: fhirpath_1.default.evaluate(fhirResponse, item.item.options.calculatedExpression)[0] }; break; case fhir_r4_1.QuestionnaireItemType.DECIMAL: calculatedAnswer = { valueDecimal: fhirpath_1.default.evaluate(fhirResponse, item.item.options.calculatedExpression)[0] }; break; case fhir_r4_1.QuestionnaireItemType.QUANTITY: var initial = (_b = (_a = item.item.initial) === null || _a === void 0 ? void 0 : _a.find(function (i) { return i.valueQuantity != undefined; })) === null || _b === void 0 ? void 0 : _b.valueQuantity; if (initial) { calculatedAnswer = { valueQuantity: { value: fhirpath_1.default.evaluate(fhirResponse, item.item.options.calculatedExpression)[0], unit: initial.unit, system: initial.system, code: initial.code } }; } else { console.warn('Calculated answer for item type QUANTITY needs an initial element for defining the unit.'); } break; default: console.warn('Calculated answer for item type ' + item.item.type.toUpperCase() + ' is currently not implemented.', item.item); } if (item.item.allowsMultipleAnswers) { item.item.selectedAnswers.push(calculatedAnswer); } else { item.item.selectedAnswers = [calculatedAnswer]; } } catch (e) { throw new Error('Can not evaluate fhirpath expression for item ' + item.item.id + ': ' + item.item.options.calculatedExpression + '.'); } } if (item.parentLinkId) { var parentItem = _this.recursivelyFindId(item.parentLinkId, fhirResponse.item); if (parentItem) { if (parentItem.item) { parentItem.item.push(mapIQuestionToQuestionnaireResponseItem([item.item], new Array(), _language)[0]); } else { parentItem.item = [mapIQuestionToQuestionnaireResponseItem([item.item], new Array(), _language)[0]]; } } } else { fhirResponse.item.push(mapIQuestionToQuestionnaireResponseItem([item.item], new Array(), _language)[0]); } }); // generate narrative var narrativeDiv = '<div xmlns="http://www.w3.org/1999/xhtml">'; // narrativeDiv += '<style>' + // '.question {font-weight: bold;} ' + // 'ul {margin: 0} ' + // '.multiple-answers {display: inline; padding: 0} ' + // '.multiple-answers > li {display: inline; margin-left: 0.3em} ' + // '.multiple-answers > li:before {content: \'-\'; margin-right: 0.3em}' + // '</style>'; narrativeDiv += '<h4 class="title">' + this.getQuestionnaireTitle(_language) + '</h4>'; narrativeDiv += '<p class="status">Status: ' + fhirResponse.status + '</p>'; narrativeDiv += '<p class="created">Created: ' + new Date(fhirResponse.authored).toLocaleDateString() + '</p>'; narrativeDiv += '<p class="questionnaire-link">Questionnaire: ' + fhirResponse.questionnaire + '</p>'; if (options.patient) { narrativeDiv += '<p class="patient">Patient: ' + (options.patient.display ? options.patient.display : options.patient.reference) + '</p>'; } narrativeDiv += fhirResponse.item ? this.getNarrativeString(fhirResponse.item, true) : ''; narrativeDiv += '</div>'; fhirResponse.text = { status: fhir_r4_1.NarrativeStatus.GENERATED, div: narrativeDiv }; fhirResponse.item = this.recursivelyCleanEmptyArrays(fhirResponse.item); if (options.reset) { this.resetResponse(); } return __assign({}, fhirResponse); }; /** * Recursively searches for a QuestionnaireResponseItem in a deep array. * @param id the id of the item to find * @param items the array of items * @returns the QuestionnaireResponseItem if found, or undefined if no item matches */ QuestionnaireData.prototype.recursivelyFindId = function (id, items) { var _this = this; var itemWithId; items.forEach(function (i) { if (!itemWithId) { if (i.linkId === id) { itemWithId = i; } else if (i.item) { itemWithId = _this.recursivelyFindId(id, i.item); } } }); return itemWithId; }; /** * Recursively removes empty arrays on item (because we don't want empty arrays in QuestionnaireResponses) * @param _items the input array of items to iterate through * @returns the deep cleaned input array, or undefined if the input array was empty */ QuestionnaireData.prototype.recursivelyCleanEmptyArrays = function (_items) { var _this = this; if (_items !== undefined && _items.length > 0) { _items.forEach(function (item) { var _a; if (((_a = item.answer) === null || _a === void 0 ? void 0 : _a.length) === 0) { // also throw out empty answer arrays item.answer = undefined; } item.item = _this.recursivelyCleanEmptyArrays(item.item); }); return _items; } else { return undefined; } }; /** * Recursively generates the narrative html for QuestionnaireResponseItems. * @param _items the items to be represented * @param _topLevel? indicates if we are on the top level. do not set to true when calling recursively. * @returns a string containging html code that represents the QuestionnaireResponseItems */ QuestionnaireData.prototype.getNarrativeString = function (_items, _topLevel) { var _this = this; if (!_items || _items.length === 0) return ''; var narrativeString = _topLevel ? '<ul class="narrative questionnaire-response">' : '<ul class="sub-question">'; _items.map(function (i) { narrativeString += _this.getItemString(i); }); return narrativeString + '</ul>'; }; /** * Recursively generates the narrative html for a single QuestionnaireResponseItem. * @param _item the item * @returns a string containging html code that represents the QuestionnaireResponseItem */ QuestionnaireData.prototype.getItemString = function (_item) { var _a, _b; var parseAnswer = function (a) { var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k,