@aehrc/smart-forms-renderer 0.21.2 → 0.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/lib/components/FormComponents/DateTimeItems/CustomDateTimeItem/CustomDateTimeItem.js +0 -1
  2. package/lib/components/FormComponents/DateTimeItems/CustomDateTimeItem/CustomDateTimeItem.js.map +1 -1
  3. package/lib/components/FormComponents/DecimalItem/DecimalField.js +1 -1
  4. package/lib/components/FormComponents/DecimalItem/DecimalField.js.map +1 -1
  5. package/lib/components/FormComponents/DecimalItem/DecimalItem.js +2 -2
  6. package/lib/components/FormComponents/DecimalItem/DecimalItem.js.map +1 -1
  7. package/lib/hooks/useRenderingExtensions.d.ts +4 -3
  8. package/lib/hooks/useRenderingExtensions.js +4 -3
  9. package/lib/hooks/useRenderingExtensions.js.map +1 -1
  10. package/lib/hooks/useValidationFeedback.d.ts +1 -1
  11. package/lib/hooks/useValidationFeedback.js +10 -6
  12. package/lib/hooks/useValidationFeedback.js.map +1 -1
  13. package/lib/stores/questionnaireResponseStore.d.ts +3 -4
  14. package/lib/stores/questionnaireResponseStore.js +3 -5
  15. package/lib/stores/questionnaireResponseStore.js.map +1 -1
  16. package/lib/utils/debounce.d.ts +1 -1
  17. package/lib/utils/debounce.js +1 -1
  18. package/lib/utils/itemControl.d.ts +3 -3
  19. package/lib/utils/itemControl.js +14 -8
  20. package/lib/utils/itemControl.js.map +1 -1
  21. package/lib/utils/validateQuestionnaire.d.ts +44 -5
  22. package/lib/utils/validateQuestionnaire.js +278 -42
  23. package/lib/utils/validateQuestionnaire.js.map +1 -1
  24. package/package.json +1 -1
  25. package/src/components/FormComponents/DateTimeItems/CustomDateTimeItem/CustomDateTimeItem.tsx +0 -1
  26. package/src/components/FormComponents/DecimalItem/DecimalField.tsx +1 -0
  27. package/src/components/FormComponents/DecimalItem/DecimalItem.tsx +16 -3
  28. package/src/hooks/useRenderingExtensions.ts +7 -5
  29. package/src/hooks/useValidationFeedback.ts +20 -8
  30. package/src/stores/questionnaireResponseStore.ts +5 -9
  31. package/src/utils/debounce.ts +1 -1
  32. package/src/utils/itemControl.ts +18 -10
  33. package/src/utils/validateQuestionnaire.ts +352 -51
@@ -15,6 +15,6 @@
15
15
  * limitations under the License.
16
16
  */
17
17
 
18
- export const DEBOUNCE_DURATION = 200;
18
+ export const DEBOUNCE_DURATION = 150;
19
19
 
20
20
  export const AUTOCOMPLETE_DEBOUNCE_DURATION = 300;
@@ -312,17 +312,30 @@ export function getTextDisplayFlyover(qItem: QuestionnaireItem): string {
312
312
  }
313
313
 
314
314
  /**
315
- * Get entry format if its extension is present
316
- * i.e. DD-MM-YYYY for dates, HH:MM for times etc.
315
+ * Get regex validation for items with regex extensions
317
316
  *
318
317
  * @author Sean Fong
319
318
  */
320
- export function getRegexValidation(qItem: QuestionnaireItem): RegexValidation | null {
319
+ export function getRegexValidation(qItem: QuestionnaireItem): RegexValidation | undefined {
320
+ // Get regex expression from extension
321
+ const regexString = getRegexString(qItem);
322
+ if (regexString) {
323
+ return { expression: new RegExp(regexString), feedback: null };
324
+ }
325
+
326
+ // Get regex expression from item types if regex extensions not present
327
+ if (qItem.type === 'url') {
328
+ return { expression: new RegExp(/^\S*$/), feedback: 'URLs should not contain any whitespaces' };
329
+ }
330
+
331
+ return undefined;
332
+ }
333
+
334
+ export function getRegexString(qItem: QuestionnaireItem): string | null {
321
335
  const itemControl = qItem.extension?.find(
322
336
  (extension: Extension) => extension.url === 'http://hl7.org/fhir/StructureDefinition/regex'
323
337
  );
324
338
 
325
- // Get regex expression from extension
326
339
  if (itemControl) {
327
340
  const extensionString = itemControl.valueString;
328
341
  if (extensionString) {
@@ -336,14 +349,9 @@ export function getRegexValidation(qItem: QuestionnaireItem): RegexValidation |
336
349
  regexString = extensionString;
337
350
  }
338
351
 
339
- return { expression: new RegExp(regexString), feedback: null };
352
+ return regexString;
340
353
  }
341
354
  }
342
355
 
343
- // Get regex expression from item types if regex extensions not present
344
- if (qItem.type === 'url') {
345
- return { expression: new RegExp(/^\S*$/), feedback: 'URLs should not contain any whitespaces' };
346
- }
347
-
348
356
  return null;
349
357
  }
@@ -16,6 +16,8 @@
16
16
  */
17
17
 
18
18
  import type {
19
+ OperationOutcome,
20
+ OperationOutcomeIssue,
19
21
  Questionnaire,
20
22
  QuestionnaireItem,
21
23
  QuestionnaireResponse,
@@ -25,16 +27,55 @@ import type {
25
27
  import { getQrItemsIndex, mapQItemsIndex } from './mapItem';
26
28
  import type { EnableWhenExpressions, EnableWhenItems } from '../interfaces';
27
29
  import { isHiddenByEnableWhen } from './qItem';
28
- import { getRegexValidation } from './itemControl';
30
+ import { getRegexString, getRegexValidation, getShortText } from './itemControl';
29
31
  import { structuredDataCapture } from 'fhir-sdc-helpers';
30
32
  import type { RegexValidation } from '../interfaces/regex.interface';
31
33
 
32
- export type InvalidType = 'regex' | 'minLength' | 'maxLength' | 'required';
34
+ export enum ValidationResult {
35
+ unknown = 'unknown', // Unknown validation result
36
+ questionnaireNotFound = 'questionnaireNotFound', // The Questionnaire referenced by the QR was not found
37
+ questionnaireInactive = 'questionnaireInactive', // The QuestionnaireResponse.authored is outside the defined Questionnaire.effectivePeriod
38
+ questionnaireDraft = 'questionnaireDraft', // The Questionnaire.status was not active (draft)
39
+ questionnaireRetired = 'questionnaireRetired', // The Questionnaire.status was not active (retired)
40
+ invalidLinkId = 'invalidLinkId', // LinkId was not found in the questionnaire
41
+ invalidType = 'invalidType', // The Question Type should not be included in Response (or Definition) data
42
+ invalidAnswerType = 'invalidAnswerType', // The Answer does not conform to the Item.Type value required (also taking into consideration the fhir-type extension)
43
+ invalidAnswerOption = 'invalidAnswerOption', // A set of valid AnswerOptions were provided in the definition, but the value entered was not in the set
44
+ exclusiveAnswerOption = 'exclusiveAnswerOption', // The selected answer cannot be used in conjunction with other answers in this multi-select choice option
45
+ invalidUrlValue = 'invalidUrlValue', // URL value not formatted correctly as a UUID, or relative/absolute URL
46
+ groupShouldNotHaveAnswers = 'groupShouldNotHaveAnswers', // A Group Item should not use the `answer` child, it should use the `item` child
47
+ required = 'required', // There was no answer provided for a mandatory field
48
+ invariant = 'invariant', // A FHIRPATH validation expression did not pass
49
+ invariantExecution = 'invariantExecution', // A FHIRPATH validation expression failed to execute
50
+ repeats = 'repeats', // There was more than one answer provided for an item with repeats = false (which is the default)
51
+ minCount = 'minCount', // Minimum number of answers required for the item was not provided
52
+ maxCount = 'maxCount', // Number of answers provided exceeded the maximum permitted
53
+ minValue = 'minValue', // Minimum value constraint violated
54
+ maxValue = 'maxValue', // Maximum value constraint violated
55
+ maxDecimalPlaces = 'maxDecimalPlaces', // Maximum decimal places constraint violated
56
+ minLength = 'minLength', // Minimum length constraint violated
57
+ maxLength = 'maxLength', // Maximum length constraint violated
58
+ invalidNewLine = 'invalidNewLine', // 'string' type items cannot include newline characters, use a 'text' type for these
59
+ invalidCoding = 'invalidCoding', // Coding value not valid in the ValueSet
60
+ tsError = 'tsError', // Error accessing the Terminology Server
61
+ maxAttachmentSize = 'maxAttachmentSize', // Maximum attachment size constraint violated
62
+ attachmentSizeInconsistent = 'attachmentSizeInconsistent', // The Size of the attachment data and the reported size are different
63
+ invalidAttachmentType = 'invalidAttachmentType', // Attachment type constraint violated
64
+ displayAnswer = 'displayAnswer', // Display Items should not have an answer provided
65
+ regex = 'regex', // The answer does not match the provided regex expression
66
+ regexTimeout = 'regexTimeout', // Evaluating the regex expression timed out
67
+ invalidRefValue = 'invalidRefValue', // The Reference value was not a valid URL value (relative or absolute)
68
+ invalidRefResourceType = 'invalidRefResourceType', // The Reference value did not refer to a valid FHIR resource type
69
+ invalidRefResourceTypeRestriction = 'invalidRefResourceTypeRestriction', // The Reference value was not
70
+ minValueIncompatUnits = 'minValueIncompatUnits', // The units provided in the Quantity cannot be converted to the min Quantity units
71
+ maxValueIncompatUnits = 'maxValueIncompatUnits', // The units provided in the Quantity cannot be converted to the max Quantity units
72
+ invalidUnit = 'invalidUnit', // The unit provided was not among the list selected (or did not have all the properties defined in the unit coding)
73
+ invalidUnitValueSet = 'invalidUnitValueSet' // The unit provided was not in the provided valueset
74
+ }
33
75
 
34
76
  interface ValidateQuestionnaireParams {
35
77
  questionnaire: Questionnaire;
36
78
  questionnaireResponse: QuestionnaireResponse;
37
- invalidItems: Record<string, InvalidType>;
38
79
  enableWhenIsActivated: boolean;
39
80
  enableWhenItems: EnableWhenItems;
40
81
  enableWhenExpressions: EnableWhenExpressions;
@@ -48,11 +89,10 @@ interface ValidateQuestionnaireParams {
48
89
  */
49
90
  export function validateQuestionnaire(
50
91
  params: ValidateQuestionnaireParams
51
- ): Record<string, InvalidType> {
92
+ ): Record<string, OperationOutcome> {
52
93
  const {
53
94
  questionnaire,
54
95
  questionnaireResponse,
55
- invalidItems,
56
96
  enableWhenIsActivated,
57
97
  enableWhenItems,
58
98
  enableWhenExpressions
@@ -64,7 +104,7 @@ export function validateQuestionnaire(
64
104
  !questionnaireResponse.item ||
65
105
  questionnaireResponse.item.length === 0
66
106
  ) {
67
- return invalidItems;
107
+ return {};
68
108
  }
69
109
 
70
110
  const qItemsIndexMap = mapQItemsIndex(questionnaire);
@@ -74,13 +114,19 @@ export function validateQuestionnaire(
74
114
  qItemsIndexMap
75
115
  );
76
116
 
117
+ const invalidItems: Record<string, OperationOutcome> = {};
118
+ let qrItemIndex = 0;
77
119
  for (const [index, topLevelQItem] of questionnaire.item.entries()) {
120
+ let repeatGroupInstances: number | null = null;
78
121
  let topLevelQRItem = topLevelQRItemsByIndex[index] ?? {
79
122
  linkId: topLevelQItem.linkId,
80
123
  text: topLevelQItem.text
81
124
  };
82
125
 
126
+ // topLevelQRItem being an array means this item is a repeat group
127
+ const isRepeatGroup = Array.isArray(topLevelQRItem);
83
128
  if (Array.isArray(topLevelQRItem)) {
129
+ repeatGroupInstances = topLevelQRItem.length;
84
130
  topLevelQRItem = {
85
131
  linkId: topLevelQItem.linkId,
86
132
  text: topLevelQItem.text,
@@ -88,14 +134,26 @@ export function validateQuestionnaire(
88
134
  };
89
135
  }
90
136
 
137
+ const locationExpression = `QuestionnaireResponse.item`;
91
138
  validateItemRecursive({
92
139
  qItem: topLevelQItem,
93
140
  qrItem: topLevelQRItem,
94
- invalidItems: invalidItems,
141
+ qrItemIndex,
142
+ locationExpression,
143
+ invalidItems,
95
144
  enableWhenIsActivated,
96
145
  enableWhenItems,
97
- enableWhenExpressions
146
+ enableWhenExpressions,
147
+ isRepeatGroupInstance: false
98
148
  });
149
+
150
+ // Increment qrItemIndex
151
+ // If its a repeat group, increment by the number of instances so qrItemIndex is correct once we reach the next item
152
+ if (isRepeatGroup && typeof repeatGroupInstances === 'number') {
153
+ qrItemIndex += repeatGroupInstances;
154
+ } else {
155
+ qrItemIndex++;
156
+ }
99
157
  }
100
158
 
101
159
  return invalidItems;
@@ -104,22 +162,29 @@ export function validateQuestionnaire(
104
162
  interface ValidateItemRecursiveParams {
105
163
  qItem: QuestionnaireItem;
106
164
  qrItem: QuestionnaireResponseItem;
107
- invalidItems: Record<string, InvalidType>;
165
+ qrItemIndex: number;
166
+ locationExpression: string;
167
+ invalidItems: Record<string, OperationOutcome>;
108
168
  enableWhenIsActivated: boolean;
109
169
  enableWhenItems: EnableWhenItems;
110
170
  enableWhenExpressions: EnableWhenExpressions;
171
+ isRepeatGroupInstance: boolean;
111
172
  }
112
173
 
113
174
  function validateItemRecursive(params: ValidateItemRecursiveParams) {
114
175
  const {
115
176
  qItem,
116
177
  qrItem,
178
+ qrItemIndex,
117
179
  invalidItems,
118
180
  enableWhenIsActivated,
119
181
  enableWhenItems,
120
- enableWhenExpressions
182
+ enableWhenExpressions,
183
+ isRepeatGroupInstance
121
184
  } = params;
185
+ let { locationExpression } = params;
122
186
 
187
+ // If item is hidden by enableWhen, skip validation
123
188
  if (
124
189
  isHiddenByEnableWhen({
125
190
  linkId: qItem.linkId,
@@ -131,11 +196,27 @@ function validateItemRecursive(params: ValidateItemRecursiveParams) {
131
196
  return;
132
197
  }
133
198
 
134
- // FIXME repeat groups not working
199
+ // Validate repeat groups
135
200
  if (qItem.type === 'group' && qItem.repeats) {
136
- return;
201
+ if (!isRepeatGroupInstance) {
202
+ validateRepeatGroupRecursive({
203
+ qItem,
204
+ qrItem,
205
+ qrItemIndex,
206
+ locationExpression,
207
+ invalidItems,
208
+ enableWhenIsActivated,
209
+ enableWhenItems,
210
+ enableWhenExpressions,
211
+ isRepeatGroupInstance: false
212
+ });
213
+ return;
214
+ }
137
215
  }
138
216
 
217
+ locationExpression = `${locationExpression}[${qrItemIndex}]`;
218
+
219
+ // Recursively validate groups with child items
139
220
  const childQItems = qItem.item;
140
221
  if (childQItems && childQItems.length > 0) {
141
222
  const childQrItems = qrItem?.item ?? [];
@@ -143,13 +224,23 @@ function validateItemRecursive(params: ValidateItemRecursiveParams) {
143
224
  const indexMap = mapQItemsIndex(qItem);
144
225
  const qrItemsByIndex = getQrItemsIndex(childQItems, childQrItems, indexMap);
145
226
 
227
+ // Check if group is required and has no answers
146
228
  if (qItem.type === 'group' && qItem.required) {
147
229
  if (!qrItem || qrItemsByIndex.length === 0) {
148
- invalidItems[qItem.linkId] = 'required';
230
+ invalidItems[qItem.linkId] = createValidationOperationOutcome(
231
+ ValidationResult.required,
232
+ qItem,
233
+ qrItem,
234
+ null,
235
+ locationExpression,
236
+ invalidItems[qItem.linkId]?.issue
237
+ );
149
238
  }
150
239
  }
151
240
 
241
+ // Loop through child items
152
242
  for (const [index, childQItem] of childQItems.entries()) {
243
+ const childLocationExpression = `${locationExpression}.item`;
153
244
  let childQRItem = qrItemsByIndex[index] ?? {
154
245
  linkId: childQItem.linkId,
155
246
  text: childQItem.text
@@ -166,72 +257,114 @@ function validateItemRecursive(params: ValidateItemRecursiveParams) {
166
257
  validateItemRecursive({
167
258
  qItem: childQItem,
168
259
  qrItem: childQRItem,
260
+ qrItemIndex: index,
261
+ locationExpression: childLocationExpression,
169
262
  invalidItems: invalidItems,
170
263
  enableWhenIsActivated,
171
264
  enableWhenItems,
172
- enableWhenExpressions
265
+ enableWhenExpressions,
266
+ isRepeatGroupInstance: false
173
267
  });
174
268
  }
175
269
  }
176
270
 
177
- validateSingleItem(qItem, qrItem, invalidItems);
271
+ // Validate the item, note that this can be either a group or a non-group
272
+ validateSingleItem(qItem, qrItem, invalidItems, locationExpression);
273
+ }
274
+
275
+ function validateRepeatGroupRecursive(params: ValidateItemRecursiveParams) {
276
+ const {
277
+ qItem,
278
+ qrItem,
279
+ qrItemIndex,
280
+ locationExpression,
281
+ invalidItems,
282
+ enableWhenIsActivated,
283
+ enableWhenItems,
284
+ enableWhenExpressions
285
+ } = params;
286
+
287
+ if (!qItem.item || qItem.item.length === 0 || !qrItem.item || qrItem.item.length === 0) {
288
+ return;
289
+ }
290
+
291
+ // Get repeat group answers
292
+ const repeatGroupAnswers = qrItem.item ?? [];
293
+ for (const [index, repeatGroupAnswer] of repeatGroupAnswers.entries()) {
294
+ // Because the item is a repeat group and might have multiple answer instances, we need to increment the qItemIndex by the instanceIndex
295
+ const updatedQrItemIndex = qrItemIndex + index;
296
+
297
+ validateItemRecursive({
298
+ qItem: qItem,
299
+ qrItem: repeatGroupAnswer,
300
+ qrItemIndex: updatedQrItemIndex,
301
+ locationExpression: locationExpression,
302
+ invalidItems: invalidItems,
303
+ enableWhenIsActivated,
304
+ enableWhenItems,
305
+ enableWhenExpressions,
306
+ isRepeatGroupInstance: true
307
+ });
308
+ }
178
309
  }
179
310
 
180
311
  function validateSingleItem(
181
312
  qItem: QuestionnaireItem,
182
313
  qrItem: QuestionnaireResponseItem,
183
- invalidItems: Record<string, InvalidType>
314
+ invalidItems: Record<string, OperationOutcome>,
315
+ locationExpression: string
184
316
  ) {
185
- // Validate item.required
317
+ // Validate item.required first before every other validation check
186
318
  if (qItem.type !== 'display') {
187
319
  if (qItem.required && !qrItem.answer) {
188
- invalidItems[qItem.linkId] = 'required';
320
+ invalidItems[qItem.linkId] = createValidationOperationOutcome(
321
+ ValidationResult.required,
322
+ qItem,
323
+ qrItem,
324
+ null,
325
+ locationExpression,
326
+ invalidItems[qItem.linkId]?.issue
327
+ );
328
+
189
329
  return invalidItems;
190
330
  }
191
331
  }
192
332
 
193
333
  // Validate regex, maxLength and minLength
194
334
  if (qrItem.answer) {
195
- for (const answer of qrItem.answer) {
335
+ for (const [i, answer] of qrItem.answer.entries()) {
336
+ // Your code here, you can use 'index' and 'answer' as needed
196
337
  if (answer.valueString || answer.valueInteger || answer.valueDecimal || answer.valueUri) {
197
338
  const invalidInputType = getInputInvalidType(
198
339
  getInputInString(answer),
199
340
  getRegexValidation(qItem),
200
- structuredDataCapture.getMinLength(qItem) ?? null,
201
- qItem.maxLength ?? null
341
+ structuredDataCapture.getMinLength(qItem),
342
+ qItem.maxLength,
343
+ structuredDataCapture.getMaxDecimalPlaces(qItem)
202
344
  );
203
345
 
204
- if (!invalidInputType) {
205
- continue;
206
- }
207
-
208
- // Assign invalid type and break - stop checking other answers if is a repeat item
209
- switch (invalidInputType) {
210
- case 'regex':
211
- invalidItems[qItem.linkId] = 'regex';
212
- break;
213
- case 'minLength':
214
- invalidItems[qItem.linkId] = 'minLength';
215
- break;
216
- case 'maxLength':
217
- invalidItems[qItem.linkId] = 'maxLength';
218
- break;
346
+ if (invalidInputType) {
347
+ invalidItems[qItem.linkId] = createValidationOperationOutcome(
348
+ invalidInputType,
349
+ qItem,
350
+ qrItem,
351
+ i,
352
+ locationExpression,
353
+ invalidItems[qItem.linkId]?.issue
354
+ );
219
355
  }
220
- break;
221
356
  }
222
357
  }
223
-
224
- // Reached the end of the loop and no invalid input type found
225
- // If a required item is filled, remove the required invalid type
226
- if (qItem.required && invalidItems[qItem.linkId] && invalidItems[qItem.linkId] === 'required') {
227
- delete invalidItems[qItem.linkId];
228
- }
229
358
  }
230
359
 
231
360
  return invalidItems;
232
361
  }
233
362
 
234
- function getInputInString(answer: QuestionnaireResponseItemAnswer) {
363
+ function getInputInString(answer?: QuestionnaireResponseItemAnswer) {
364
+ if (!answer) {
365
+ return '';
366
+ }
367
+
235
368
  if (answer.valueString) {
236
369
  return answer.valueString;
237
370
  } else if (answer.valueInteger) {
@@ -247,23 +380,191 @@ function getInputInString(answer: QuestionnaireResponseItemAnswer) {
247
380
 
248
381
  export function getInputInvalidType(
249
382
  input: string,
250
- regexValidation: RegexValidation | null,
251
- minLength: number | null,
252
- maxLength: number | null
253
- ): InvalidType | null {
383
+ regexValidation?: RegexValidation,
384
+ minLength?: number,
385
+ maxLength?: number,
386
+ maxDecimalPlaces?: number
387
+ ): ValidationResult | null {
254
388
  if (input) {
255
389
  if (regexValidation && !regexValidation.expression.test(input)) {
256
- return 'regex';
390
+ return ValidationResult.regex;
257
391
  }
258
392
 
259
393
  if (minLength && input.length < minLength) {
260
- return 'minLength';
394
+ return ValidationResult.minLength;
261
395
  }
262
396
 
263
397
  if (maxLength && input.length > maxLength) {
264
- return 'maxLength';
398
+ return ValidationResult.maxLength;
399
+ }
400
+
401
+ if (maxDecimalPlaces) {
402
+ const decimalPlaces = input.split('.')[1]?.length ?? 0;
403
+ if (decimalPlaces > maxDecimalPlaces) {
404
+ return ValidationResult.maxDecimalPlaces;
405
+ }
265
406
  }
266
407
  }
267
408
 
268
409
  return null;
269
410
  }
411
+
412
+ function createValidationOperationOutcome(
413
+ error: ValidationResult,
414
+ qItem: QuestionnaireItem,
415
+ qrItem: QuestionnaireResponseItem,
416
+ answerIndex: number | null,
417
+ locationExpression: string,
418
+ existingOperationOutcomeIssues: OperationOutcomeIssue[] = []
419
+ ): OperationOutcome {
420
+ return {
421
+ resourceType: 'OperationOutcome',
422
+ issue: existingOperationOutcomeIssues.concat(
423
+ createValidationOperationOutcomeIssue(error, qItem, qrItem, answerIndex, locationExpression)
424
+ )
425
+ };
426
+ }
427
+
428
+ function createValidationOperationOutcomeIssue(
429
+ error: ValidationResult,
430
+ qItem: QuestionnaireItem,
431
+ qrItem: QuestionnaireResponseItem,
432
+ answerIndex: number | null,
433
+ locationExpression: string
434
+ ): OperationOutcomeIssue {
435
+ const errorCodeSystem = 'http://fhir.forms-lab.com/CodeSystem/errors';
436
+ let detailsText = '';
437
+ let fieldDisplayText =
438
+ qrItem?.text ?? getShortText(qItem) ?? qItem?.text ?? qItem.linkId ?? qrItem.linkId;
439
+ if (!fieldDisplayText && fieldDisplayText.endsWith(':')) {
440
+ fieldDisplayText = fieldDisplayText.substring(0, fieldDisplayText.length - 1);
441
+ }
442
+
443
+ answerIndex = answerIndex ?? 0;
444
+
445
+ // create operationOutcomeIssue based on error
446
+ switch (error) {
447
+ case ValidationResult.required: {
448
+ if (qItem.type === 'group') {
449
+ detailsText = `${fieldDisplayText}: Mandatory group does not have answer(s)`;
450
+ } else {
451
+ detailsText = `${fieldDisplayText}: Mandatory field does not have an answer`;
452
+ }
453
+
454
+ return {
455
+ severity: 'error',
456
+ code: 'required',
457
+ expression: [locationExpression],
458
+ details: {
459
+ coding: [
460
+ {
461
+ system: errorCodeSystem,
462
+ code: error,
463
+ display: 'Required'
464
+ }
465
+ ],
466
+ text: detailsText
467
+ }
468
+ };
469
+ }
470
+
471
+ case ValidationResult.regex: {
472
+ detailsText = `${fieldDisplayText}: The value '${getInputInString(qrItem.answer?.[answerIndex])}' does not match the defined format.`;
473
+ if (structuredDataCapture.getEntryFormat(qItem)) {
474
+ detailsText += ` ${structuredDataCapture.getEntryFormat(qItem)}`;
475
+ }
476
+
477
+ return {
478
+ severity: 'error',
479
+ code: 'invalid',
480
+ expression: [locationExpression],
481
+ details: {
482
+ coding: [
483
+ {
484
+ system: errorCodeSystem,
485
+ code: error,
486
+ display: 'Invalid format'
487
+ }
488
+ ],
489
+ text: detailsText
490
+ },
491
+ diagnostics: getRegexString(qItem) ?? undefined
492
+ };
493
+ }
494
+
495
+ case ValidationResult.minLength: {
496
+ detailsText = `${fieldDisplayText}: Expected the minimum value ${structuredDataCapture.getMinLength(qItem)} characters, received '${getInputInString(qrItem.answer?.[answerIndex])}'`;
497
+ return {
498
+ severity: 'error',
499
+ code: 'business-rule',
500
+ expression: [locationExpression],
501
+ details: {
502
+ coding: [
503
+ {
504
+ system: errorCodeSystem,
505
+ code: error,
506
+ display: 'Too short'
507
+ }
508
+ ],
509
+ text: detailsText
510
+ }
511
+ };
512
+ }
513
+
514
+ case ValidationResult.maxLength: {
515
+ detailsText = `${fieldDisplayText}: Exceeded maximum of ${qItem.maxLength} characters, received '${getInputInString(qrItem.answer?.[answerIndex])}'`;
516
+ return {
517
+ severity: 'error',
518
+ code: 'business-rule',
519
+ expression: [locationExpression],
520
+ details: {
521
+ coding: [
522
+ {
523
+ system: errorCodeSystem,
524
+ code: error,
525
+ display: 'Too long'
526
+ }
527
+ ],
528
+ text: detailsText
529
+ }
530
+ };
531
+ }
532
+
533
+ case ValidationResult.maxDecimalPlaces: {
534
+ detailsText = `${fieldDisplayText}: Exceeded maximum decimal places ${structuredDataCapture.getMaxDecimalPlaces(qItem)}, received '${getInputInString(qrItem.answer?.[answerIndex])}'`;
535
+ return {
536
+ severity: 'error',
537
+ code: 'business-rule',
538
+ expression: [locationExpression],
539
+ details: {
540
+ coding: [
541
+ {
542
+ system: errorCodeSystem,
543
+ code: error,
544
+ display: 'Too precise'
545
+ }
546
+ ],
547
+ text: detailsText
548
+ }
549
+ };
550
+ }
551
+
552
+ // mark unknown issues as fatal
553
+ default: {
554
+ return {
555
+ severity: 'error',
556
+ code: 'unknown',
557
+ expression: [locationExpression],
558
+ details: {
559
+ coding: [
560
+ {
561
+ system: errorCodeSystem,
562
+ code: 'unknown',
563
+ display: 'Unknown'
564
+ }
565
+ ]
566
+ }
567
+ };
568
+ }
569
+ }
570
+ }