Skip to content

Commit

Permalink
Implement Save draft MVP functionality (#3596)
Browse files Browse the repository at this point in the history
* Update alert dialigue to have save draft, dismiss changes and cancel CTAs

* Add filter by status when fetching QR from db
Update save draft questionnaire response to have subject and questionnaire values

* Run spotless Apply

* Update unit tests

* Run spotlessApply

* Update CHANGELOG.md

* Added kdoc

* Add questionnaire view model save draft unit tests

* Run spotlessApply

* Add documentation for save as draft feature

* Handle saving drafts of paginated questionnaires
close form when draft is saved

* Add unit tests for saving paginated forms as draft
  • Loading branch information
Rkareko authored Nov 13, 2024
1 parent 4217119 commit e794590
Show file tree
Hide file tree
Showing 10 changed files with 274 additions and 11 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1. Added a new class (PdfGenerator) for generating PDF documents from HTML content using Android's WebView and PrintManager
2. Introduced a new class (HtmlPopulator) to populate HTML templates with data from a Questionnaire Response
3. Implemented functionality to launch PDF generation using a configuration setup
- Added Save draft MVP functionality

## [1.1.0] - 2024-02-15

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ object AlertDialogue {
@StringRes confirmButtonText: Int = R.string.questionnaire_alert_confirm_button_title,
neutralButtonListener: ((d: DialogInterface) -> Unit)? = null,
@StringRes neutralButtonText: Int = R.string.questionnaire_alert_neutral_button_title,
negativeButtonListener: ((d: DialogInterface) -> Unit)? = null,
@StringRes negativeButtonText: Int = R.string.questionnaire_alert_negative_button_title,
cancellable: Boolean = false,
options: Array<AlertDialogListItem>? = null,
): AlertDialog {
Expand All @@ -71,6 +73,9 @@ object AlertDialogue {
confirmButtonListener?.let {
setPositiveButton(confirmButtonText) { d, _ -> confirmButtonListener.invoke(d) }
}
negativeButtonListener?.let {
setNegativeButton(negativeButtonText) { d, _ -> negativeButtonListener.invoke(d) }
}
options?.run { setSingleChoiceItems(options.map { it.value }.toTypedArray(), -1, null) }
}
.show()
Expand Down Expand Up @@ -172,6 +177,8 @@ object AlertDialogue {
@StringRes confirmButtonText: Int,
neutralButtonListener: ((d: DialogInterface) -> Unit),
@StringRes neutralButtonText: Int,
negativeButtonListener: ((d: DialogInterface) -> Unit),
@StringRes negativeButtonText: Int,
cancellable: Boolean = true,
options: List<AlertDialogListItem>? = null,
): AlertDialog {
Expand All @@ -184,6 +191,8 @@ object AlertDialogue {
confirmButtonText = confirmButtonText,
neutralButtonListener = neutralButtonListener,
neutralButtonText = neutralButtonText,
negativeButtonListener = negativeButtonListener,
negativeButtonText = negativeButtonText,
cancellable = cancellable,
options = options?.toTypedArray(),
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import org.hl7.fhir.r4.model.Expression
import org.hl7.fhir.r4.model.IdType
import org.hl7.fhir.r4.model.Questionnaire
import org.hl7.fhir.r4.model.QuestionnaireResponse
import org.hl7.fhir.r4.model.QuestionnaireResponse.QuestionnaireResponseStatus
import org.hl7.fhir.r4.model.StringType
import org.smartregister.fhircore.engine.configuration.LinkIdType
import org.smartregister.fhircore.engine.configuration.QuestionnaireConfig
Expand Down Expand Up @@ -292,3 +293,19 @@ suspend fun Questionnaire.prepopulateUniqueIdAssignment(
}
}
}

/**
* Determines the [QuestionnaireResponse.Status] depending on the [saveDraft] and [isEditable]
* values contained in the [QuestionnaireConfig]
*
* returns [COMPLETED] when [isEditable] is [true] returns [INPROGRESS] when [saveDraft] is [true]
*/
fun QuestionnaireConfig.questionnaireResponseStatus(): String? {
return if (this.isEditable()) {
QuestionnaireResponseStatus.COMPLETED.toCode()
} else if (this.saveDraft) {
QuestionnaireResponseStatus.INPROGRESS.toCode()
} else {
null
}
}
11 changes: 7 additions & 4 deletions android/engine/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -65,11 +65,12 @@
<string name="error_saving_form">Error encountered cannot save form</string>
<string name="form_progress_message">Processing data. Please wait</string>
<string name="questionnaire_alert_back_pressed_message">Are you sure you want to go back?</string>
<string name="questionnaire_in_progress_alert_back_pressed_message">Are you sure you want to discard the answers?</string>
<string name="questionnaire_alert_back_pressed_title">Discard changes</string>
<string name="questionnaire_alert_back_pressed_button_title">Discard</string>
<string name="questionnaire_alert_back_pressed_save_draft_button_title">Save partial draft</string>
<string name="questionnaire_in_progress_alert_back_pressed_message">If you leave without saving, all your changes will not be saved</string>
<string name="questionnaire_alert_back_pressed_title">You have unsaved changes</string>
<string name="questionnaire_alert_back_pressed_button_title">Discard changes</string>
<string name="questionnaire_alert_back_pressed_save_draft_button_title">Save as draft</string>
<string name="questionnaire_alert_neutral_button_title">Cancel</string>
<string name="questionnaire_alert_negative_button_title">Discard Changes</string>
<string name="questionnaire_alert_confirm_button_title">Yes</string>
<string name="questionnaire_alert_invalid_message">Given details have validation errors. Resolve errors and submit again</string>
<string name="questionnaire_alert_invalid_title">Validation Failed</string>
Expand Down Expand Up @@ -198,4 +199,6 @@
<string name="unsynced_data_present">There\'s some un-synced data</string>
<string name="missing_supervisor_contact">Supervisor contact missing or the provided phone number is invalid</string>
<string name="apply_filter">APPLY FILTER</string>
<string name="questionnaire_save_draft_title">Save draft changes</string>
<string name="questionnaire_save_draft_message">Do you want to save draft changes?</string>
</resources>
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,8 @@ class AlertDialogueTest : ActivityRobolectricTest() {
confirmButtonText = R.string.questionnaire_alert_back_pressed_save_draft_button_title,
neutralButtonListener = {},
neutralButtonText = R.string.questionnaire_alert_back_pressed_button_title,
negativeButtonListener = {},
negativeButtonText = R.string.questionnaire_alert_negative_button_title,
)
val dialog = shadowOf(ShadowAlertDialog.getLatestAlertDialog())

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -471,4 +471,21 @@ class QuestionnaireExtensionTest : RobolectricTest() {
barCodeItemValue?.primitiveValue(),
)
}

@Test
fun testQuestionnaireResponseStatusReturnsCompletedWhenIsEditableIsTrue() {
val questionnaireConfig =
QuestionnaireConfig(id = "patient-reg-config", type = QuestionnaireType.EDIT.name)
Assert.assertEquals("completed", questionnaireConfig.questionnaireResponseStatus())
}

fun testQuestionnaireResponseStatusReturnsInProgressWhenSaveDraftIsTrue() {
val questionnaireConfig = QuestionnaireConfig(id = "patient-reg-config", saveDraft = true)
Assert.assertEquals("in-progress", questionnaireConfig.questionnaireResponseStatus())
}

fun testQuestionnaireResponseStatusReturnsNullWhenBothSaveDraftAndIsEditableAreFalse() {
val questionnaireConfig = QuestionnaireConfig(id = "patient-reg-config", saveDraft = true)
Assert.assertEquals("in-progress", questionnaireConfig.questionnaireResponseStatus())
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -366,16 +366,20 @@ class QuestionnaireActivity : BaseMultiLanguageActivity() {
confirmButtonListener = {
lifecycleScope.launch {
retrieveQuestionnaireResponse()?.let { questionnaireResponse ->
viewModel.saveDraftQuestionnaire(questionnaireResponse)
viewModel.saveDraftQuestionnaire(questionnaireResponse, questionnaireConfig)
finish()
}
}
},
confirmButtonText =
org.smartregister.fhircore.engine.R.string
.questionnaire_alert_back_pressed_save_draft_button_title,
neutralButtonListener = { finish() },
neutralButtonListener = {},
neutralButtonText =
org.smartregister.fhircore.engine.R.string.questionnaire_alert_back_pressed_button_title,
org.smartregister.fhircore.engine.R.string.questionnaire_alert_neutral_button_title,
negativeButtonListener = { finish() },
negativeButtonText =
org.smartregister.fhircore.engine.R.string.questionnaire_alert_negative_button_title,
)
} else {
AlertDialogue.showConfirmAlert(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ import org.smartregister.fhircore.engine.util.extension.isIn
import org.smartregister.fhircore.engine.util.extension.logErrorMessages
import org.smartregister.fhircore.engine.util.extension.packRepeatedGroups
import org.smartregister.fhircore.engine.util.extension.prepopulateWithComputedConfigValues
import org.smartregister.fhircore.engine.util.extension.questionnaireResponseStatus
import org.smartregister.fhircore.engine.util.extension.showToast
import org.smartregister.fhircore.engine.util.extension.updateLastUpdated
import org.smartregister.fhircore.engine.util.fhirpath.FhirPathDataExtractor
Expand Down Expand Up @@ -544,6 +545,7 @@ constructor(
resourceType = questionnaireConfig.resourceType ?: subjectType,
questionnaireId = questionnaire.logicalId,
encounterId = questionnaireConfig.encounterId,
questionnaireResponseStatus = questionnaireConfig.questionnaireResponseStatus(),
)
?.contained
?.asSequence()
Expand Down Expand Up @@ -672,12 +674,35 @@ constructor(
* This function saves [QuestionnaireResponse] as draft if any of the [QuestionnaireResponse.item]
* has an answer.
*/
fun saveDraftQuestionnaire(questionnaireResponse: QuestionnaireResponse) {
fun saveDraftQuestionnaire(
questionnaireResponse: QuestionnaireResponse,
questionnaireConfig: QuestionnaireConfig,
) {
viewModelScope.launch {
val hasPages = questionnaireResponse.item.any { it.hasItem() }
val questionnaireHasAnswer =
questionnaireResponse.item.any {
it.answer.any { answerComponent -> answerComponent.hasValue() }
if (!hasPages) {
it.answer.any { answerComponent -> answerComponent.hasValue() }
} else {
questionnaireResponse.item.any { page ->
page.item.any { pageItem ->
pageItem.answer.any { answerComponent -> answerComponent.hasValue() }
}
}
}
}
questionnaireResponse.questionnaire =
questionnaireConfig.id.asReference(ResourceType.Questionnaire).reference
if (
!questionnaireConfig.resourceIdentifier.isNullOrBlank() &&
questionnaireConfig.resourceType != null
) {
questionnaireResponse.subject =
questionnaireConfig.resourceIdentifier!!.asReference(
questionnaireConfig.resourceType!!,
)
}
if (questionnaireHasAnswer) {
questionnaireResponse.status = QuestionnaireResponse.QuestionnaireResponseStatus.INPROGRESS
defaultRepository.addOrUpdate(
Expand Down Expand Up @@ -1011,6 +1036,7 @@ constructor(
resourceType: ResourceType,
questionnaireId: String,
encounterId: String?,
questionnaireResponseStatus: String? = null,
): QuestionnaireResponse? {
val search =
Search(ResourceType.QuestionnaireResponse).apply {
Expand All @@ -1031,6 +1057,12 @@ constructor(
},
)
}
if (!questionnaireResponseStatus.isNullOrBlank()) {
filter(
QuestionnaireResponse.STATUS,
{ value = of(questionnaireResponseStatus) },
)
}
}
val questionnaireResponses: List<QuestionnaireResponse> = defaultRepository.search(search)
return questionnaireResponses.maxByOrNull { it.meta.lastUpdated }
Expand Down Expand Up @@ -1100,13 +1132,16 @@ constructor(
if (
resourceType != null &&
!resourceIdentifier.isNullOrEmpty() &&
(questionnaireConfig.isEditable() || questionnaireConfig.isReadOnly())
(questionnaireConfig.isEditable() ||
questionnaireConfig.isReadOnly() ||
questionnaireConfig.saveDraft)
) {
searchQuestionnaireResponse(
resourceId = resourceIdentifier,
resourceType = resourceType,
questionnaireId = questionnaire.logicalId,
encounterId = questionnaireConfig.encounterId,
questionnaireResponseStatus = questionnaireConfig.questionnaireResponseStatus(),
)
?.let {
QuestionnaireResponse().apply {
Expand Down
Loading

0 comments on commit e794590

Please sign in to comment.