@i4mi/fhir_questionnaire
Version:
TS Package for handling FHIR Questionnaire and generating QuestionnaireRespones.
949 lines • 98.7 kB
JavaScript
"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,