diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/PdfConfig.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/PdfConfig.kt new file mode 100644 index 0000000000..819d95eb85 --- /dev/null +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/PdfConfig.kt @@ -0,0 +1,42 @@ +/* + * Copyright 2021-2024 Ona Systems, Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.smartregister.fhircore.engine.configuration + +import android.os.Parcelable +import kotlinx.parcelize.Parcelize +import kotlinx.serialization.Serializable +import org.smartregister.fhircore.engine.util.extension.interpolate + +@Serializable +@Parcelize +data class PdfConfig( + val title: String? = null, + val titleSuffix: String? = null, + val structureReference: String? = null, + val subjectReference: String? = null, + val questionnaireReferences: List = emptyList(), +) : java.io.Serializable, Parcelable { + + fun interpolate(computedValuesMap: Map) = + this.copy( + title = title?.interpolate(computedValuesMap), + titleSuffix = titleSuffix?.interpolate(computedValuesMap), + structureReference = structureReference?.interpolate(computedValuesMap), + subjectReference = subjectReference?.interpolate(computedValuesMap), + questionnaireReferences = questionnaireReferences.map { it.interpolate(computedValuesMap) }, + ) +} diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/QuestionnaireConfig.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/QuestionnaireConfig.kt index 0c5bc2f57c..89ff99b756 100644 --- a/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/QuestionnaireConfig.kt +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/QuestionnaireConfig.kt @@ -66,8 +66,6 @@ data class QuestionnaireConfig( val managingEntityRelationshipCode: String? = null, val uniqueIdAssignment: UniqueIdAssignmentConfig? = null, val linkIds: List? = null, - val htmlBinaryId: String? = null, - val htmlTitle: String? = null, ) : java.io.Serializable, Parcelable { fun interpolate(computedValuesMap: Map) = @@ -102,8 +100,6 @@ data class QuestionnaireConfig( uniqueIdAssignment?.copy(linkId = uniqueIdAssignment.linkId.interpolate(computedValuesMap)), linkIds = linkIds?.onEach { it.linkId.interpolate(computedValuesMap) }, saveButtonText = saveButtonText?.interpolate(computedValuesMap), - htmlBinaryId = htmlBinaryId?.interpolate(computedValuesMap), - htmlTitle = htmlTitle?.interpolate(computedValuesMap), ) } diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/app/ConfigService.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/app/ConfigService.kt index 00550f4a05..b2a856415e 100644 --- a/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/app/ConfigService.kt +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/app/ConfigService.kt @@ -98,12 +98,29 @@ interface ConfigService { description = "Search the status field" } - return listOf(activeGroupSearchParameter, flagStatusSearchParameter) + val medicationSortSearchParameter = + SearchParameter().apply { + url = MEDICATION_SORT_URL + addBase("Medication") + name = SORT_SEARCH_PARAM + code = SORT_SEARCH_PARAM + type = Enumerations.SearchParamType.NUMBER + expression = "Medication.extension.where(url = '$MEDICATION_SORT_URL').value" + description = "Search the sort field" + } + + return listOf( + activeGroupSearchParameter, + flagStatusSearchParameter, + medicationSortSearchParameter, + ) } companion object { const val ACTIVE_SEARCH_PARAM = "active" const val APP_VERSION = "AppVersion" const val STATUS_SEARCH_PARAM = "status" + const val SORT_SEARCH_PARAM = "sort" + const val MEDICATION_SORT_URL = "http://smartregister.org/SearchParameter/medication-sort" } } diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/domain/model/ActionConfig.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/domain/model/ActionConfig.kt index 0bd988d40b..3b9f5b3acd 100644 --- a/android/engine/src/main/java/org/smartregister/fhircore/engine/domain/model/ActionConfig.kt +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/domain/model/ActionConfig.kt @@ -22,6 +22,7 @@ import kotlinx.parcelize.Parcelize import kotlinx.serialization.Serializable import org.hl7.fhir.r4.model.Enumerations import org.hl7.fhir.r4.model.ResourceType +import org.smartregister.fhircore.engine.configuration.PdfConfig import org.smartregister.fhircore.engine.configuration.QuestionnaireConfig import org.smartregister.fhircore.engine.configuration.profile.ManagingEntityConfig import org.smartregister.fhircore.engine.configuration.workflow.ActionTrigger @@ -42,6 +43,7 @@ data class ActionConfig( val toolBarHomeNavigation: ToolBarHomeNavigation = ToolBarHomeNavigation.OPEN_DRAWER, val popNavigationBackStack: Boolean? = null, val multiSelectViewConfig: MultiSelectViewConfig? = null, + val pdfConfig: PdfConfig? = null, ) : Parcelable, java.io.Serializable { fun paramsBundle(computedValuesMap: Map = emptyMap()): Bundle = Bundle().apply { diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/domain/model/QuestionnaireType.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/domain/model/QuestionnaireType.kt index bf3fce59b7..ba490a4ec7 100644 --- a/android/engine/src/main/java/org/smartregister/fhircore/engine/domain/model/QuestionnaireType.kt +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/domain/model/QuestionnaireType.kt @@ -22,6 +22,7 @@ enum class QuestionnaireType { DEFAULT, EDIT, READ_ONLY, + SUMMARY, } fun QuestionnaireConfig.isDefault() = @@ -32,3 +33,6 @@ fun QuestionnaireConfig.isEditable() = fun QuestionnaireConfig.isReadOnly() = QuestionnaireType.valueOf(this.type) == QuestionnaireType.READ_ONLY + +fun QuestionnaireConfig.isSummary() = + QuestionnaireType.valueOf(this.type) == QuestionnaireType.SUMMARY diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/pdf/HtmlPopulator.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/pdf/HtmlPopulator.kt index bcdb51f226..7b49518eea 100644 --- a/android/engine/src/main/java/org/smartregister/fhircore/engine/pdf/HtmlPopulator.kt +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/pdf/HtmlPopulator.kt @@ -16,32 +16,55 @@ package org.smartregister.fhircore.engine.pdf +import java.util.Date import java.util.regex.Matcher import java.util.regex.Pattern import org.hl7.fhir.r4.model.BaseDateTimeType import org.hl7.fhir.r4.model.QuestionnaireResponse +import org.hl7.fhir.r4.model.QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent import org.smartregister.fhircore.engine.util.extension.allItems +import org.smartregister.fhircore.engine.util.extension.extractLogicalIdUuid import org.smartregister.fhircore.engine.util.extension.formatDate import org.smartregister.fhircore.engine.util.extension.makeItReadable import org.smartregister.fhircore.engine.util.extension.valueToString /** * HtmlPopulator class is responsible for processing an HTML template by replacing custom tags with - * data from a QuestionnaireResponse. The class uses various regex patterns to find and replace - * custom tags such as @is-not-empty, @answer-as-list, @answer, @submitted-date, and @contains. + * data from QuestionnaireResponses. The class uses various regex patterns to find and replace + * custom tags such as @is-not-empty, @answer-as-list, @answer, @submitted-date, @contains, + * and @is-questionnaire-submitted. * - * @property questionnaireResponse The QuestionnaireResponse object containing data for replacement. + * @property questionnaireResponses The QuestionnaireResponses object containing data for + * replacement. */ class HtmlPopulator( - private val questionnaireResponse: QuestionnaireResponse, + questionnaireResponses: List, ) { + private var answerMap: Map> + private var submittedDateMap: Map + private var questionnaireIds: List - // Map to store questionnaire response items keyed by their linkId - private val questionnaireResponseItemMap = - questionnaireResponse.allItems.associateBy( - keySelector = { it.linkId }, - valueTransform = { it.answer }, - ) + init { + val answerMap = mutableMapOf>() + val submittedDateMap = mutableMapOf() + val questionnaireIds = mutableListOf() + + questionnaireResponses.forEach { questionnaireResponse -> + val questionnaireId = questionnaireResponse.questionnaire.extractLogicalIdUuid() + questionnaireResponse.allItems + .associateBy( + keySelector = { "$questionnaireId/${it.linkId}" }, + valueTransform = { it.answer }, + ) + .let { answerMap.putAll(it) } + submittedDateMap[questionnaireId] = questionnaireResponse.meta.lastUpdated ?: Date() + questionnaireIds.add(questionnaireId) + } + + this.answerMap = answerMap + this.submittedDateMap = submittedDateMap + this.questionnaireIds = questionnaireIds + } /** * Populates the provided HTML template with data from the QuestionnaireResponse. @@ -77,6 +100,10 @@ class HtmlPopulator( val matcher = containsPattern.matcher(html.substring(i)) if (matcher.find()) processContains(i, html, matcher) else i++ } + html.startsWith("@is-questionnaire-submitted", i) -> { + val matcher = isQuestionnaireSubmittedPattern.matcher(html.substring(i)) + if (matcher.find()) processIsQuestionnaireSubmitted(i, html, matcher) else i++ + } else -> i++ } } @@ -94,7 +121,7 @@ class HtmlPopulator( private fun processIsNotEmpty(i: Int, html: StringBuilder, matcher: Matcher) { val linkId = matcher.group(1) val content = matcher.group(2) ?: "" - val doesAnswerExist = questionnaireResponseItemMap.getOrDefault(linkId, listOf()).isNotEmpty() + val doesAnswerExist = answerMap.getOrDefault(linkId, listOf()).isNotEmpty() if (doesAnswerExist) { html.replace(i, matcher.end() + i, content) // Start index is the index of '@' symbol, End index is the index after the ')' symbol. @@ -119,8 +146,7 @@ class HtmlPopulator( private fun processAnswerAsList(i: Int, html: StringBuilder, matcher: Matcher) { val linkId = matcher.group(1) val answerAsList = - questionnaireResponseItemMap.getOrDefault(linkId, listOf()).joinToString(separator = "") { - answer -> + answerMap.getOrDefault(linkId, listOf()).joinToString(separator = "") { answer -> "
  • ${answer.value.valueToString()}
  • " } html.replace(i, matcher.end() + i, answerAsList) @@ -137,7 +163,7 @@ class HtmlPopulator( val linkId = matcher.group(1) val dateFormat = matcher.group(2) val answer = - questionnaireResponseItemMap.getOrDefault(linkId, listOf()).joinToString { answer -> + answerMap.getOrDefault(linkId, listOf()).joinToString { answer -> if (dateFormat == null) { answer.value.valueToString() } else { @@ -155,12 +181,13 @@ class HtmlPopulator( * @param matcher The Matcher object for the regex pattern. */ private fun processSubmittedDate(i: Int, html: StringBuilder, matcher: Matcher) { - val dateFormat = matcher.group(1) + val questionnaireId = matcher.group(1) + val dateFormat = matcher.group(2) val date = if (dateFormat == null) { - questionnaireResponse.meta.lastUpdated.formatDate() + submittedDateMap.getOrDefault(questionnaireId, Date()).formatDate() } else { - questionnaireResponse.meta.lastUpdated.formatDate(dateFormat) + submittedDateMap.getOrDefault(questionnaireId, Date()).formatDate(dateFormat) } html.replace(i, matcher.end() + i, date) } @@ -178,7 +205,7 @@ class HtmlPopulator( val indicator = matcher.group(2) ?: "" val content = matcher.group(3) ?: "" val doesAnswerExist = - questionnaireResponseItemMap.getOrDefault(linkId, listOf()).any { + answerMap.getOrDefault(linkId, listOf()).any { when { it.hasValueCoding() -> it.valueCoding.code == indicator it.hasValueStringType() -> it.valueStringType.value.contains(indicator) @@ -199,14 +226,39 @@ class HtmlPopulator( } } + /** + * Processes the @is-questionnaire-submitted tag by checking if the corresponding + * [QuestionnaireResponse] exists. Replaces the tag with the content if the indicator is true, + * otherwise removes the tag. + * + * @param i The starting index of the tag in the HTML. + * @param html The StringBuilder containing the HTML. + * @param matcher The Matcher object for the regex pattern. + */ + private fun processIsQuestionnaireSubmitted(i: Int, html: StringBuilder, matcher: Matcher) { + val id = matcher.group(1) + val content = matcher.group(2) ?: "" + val doesQuestionnaireExists = questionnaireIds.contains(id) + if (doesQuestionnaireExists) { + html.replace(i, matcher.end() + i, content) + } else { + html.replace(i, matcher.end() + i, "") + } + } + companion object { // Compile regex patterns for different tags private val isNotEmptyPattern = Pattern.compile("@is-not-empty\\('([^']+)'\\)((?s).*?)@is-not-empty\\('\\1'\\)") private val answerAsListPattern = Pattern.compile("@answer-as-list\\('([^']+)'\\)") private val answerPattern = Pattern.compile("@answer\\('([^']+)'(?:,'([^']+)')?\\)") - private val submittedDatePattern = Pattern.compile("@submitted-date(?:\\('([^']+)'\\))?") + private val submittedDatePattern = + Pattern.compile("@submitted-date\\('([^']+)'(?:,'([^']+)')?\\)") private val containsPattern = Pattern.compile("@contains\\('([^']+)','([^']+)'\\)((?s).*?)@contains\\('\\1'\\)") + private val isQuestionnaireSubmittedPattern = + Pattern.compile( + "@is-questionnaire-submitted\\('([^']+)'\\)((?s).*?)@is-questionnaire-submitted\\('\\1'\\)", + ) } } diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/rulesengine/RulesFactory.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/rulesengine/RulesFactory.kt index 97e6a5912b..1ff6e122b1 100644 --- a/android/engine/src/main/java/org/smartregister/fhircore/engine/rulesengine/RulesFactory.kt +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/rulesengine/RulesFactory.kt @@ -50,6 +50,7 @@ import org.smartregister.fhircore.engine.domain.model.RepositoryResourceData import org.smartregister.fhircore.engine.domain.model.RuleConfig import org.smartregister.fhircore.engine.domain.model.ServiceMemberIcon import org.smartregister.fhircore.engine.domain.model.ServiceStatus +import org.smartregister.fhircore.engine.rulesengine.services.DateService import org.smartregister.fhircore.engine.rulesengine.services.LocationService import org.smartregister.fhircore.engine.util.DispatcherProvider import org.smartregister.fhircore.engine.util.SharedPreferenceKey @@ -102,6 +103,7 @@ constructor( put(DATA, mutableMapOf().apply { putAll(params) }) put(LOCATION_SERVICE, locationService) put(SERVICE, rulesEngineService) + put(DATE_SERVICE, DateService) } if (repositoryResourceData != null) { with(repositoryResourceData) { @@ -711,6 +713,7 @@ constructor( companion object { private const val SERVICE = "service" private const val LOCATION_SERVICE = "locationService" + private const val DATE_SERVICE = "dateService" private const val INCLUSIVE_SIX_DIGIT_MINIMUM = 100000 private const val INCLUSIVE_SIX_DIGIT_MAXIMUM = 999999 private const val DEFAULT_REGEX = "(?<=^|,)[\\s,]*(\\w[\\w\\s]*)(?=[\\s,]*$|,)" diff --git a/android/engine/src/main/res/values/styles.xml b/android/engine/src/main/res/values/styles.xml index fd15edd98b..eb95cda512 100644 --- a/android/engine/src/main/res/values/styles.xml +++ b/android/engine/src/main/res/values/styles.xml @@ -63,6 +63,7 @@ @style/TextAppearance.Material3.BodyLarge @style/AppTheme.MediaImageStyle @style/AppTheme.HelpHeaderStyle + @style/AppTheme.NextButtonCircularProgressIndicatorStyle + +