diff --git a/app/src/main/java/org/dhis2/usescases/teiDashboard/DashboardRepository.kt b/app/src/main/java/org/dhis2/usescases/teiDashboard/DashboardRepository.kt index f889920e6d..8668e16fe9 100644 --- a/app/src/main/java/org/dhis2/usescases/teiDashboard/DashboardRepository.kt +++ b/app/src/main/java/org/dhis2/usescases/teiDashboard/DashboardRepository.kt @@ -74,9 +74,13 @@ interface DashboardRepository { fun saveCatOption(eventUid: String?, catOptionComboUid: String?) - fun deleteTeiIfPossible(): Single + fun checkIfDeleteTeiIsPossible(): Boolean - fun deleteEnrollmentIfPossible(enrollmentUid: String): Single + fun deleteTei(): Single + + fun checkIfDeleteEnrollmentIsPossible(enrollmentUid: String): Boolean + + fun deleteEnrollment(enrollmentUid: String): Single fun getNoteCount(): Single diff --git a/app/src/main/java/org/dhis2/usescases/teiDashboard/DashboardRepositoryImpl.kt b/app/src/main/java/org/dhis2/usescases/teiDashboard/DashboardRepositoryImpl.kt index 30ea10c103..e00e0d579d 100644 --- a/app/src/main/java/org/dhis2/usescases/teiDashboard/DashboardRepositoryImpl.kt +++ b/app/src/main/java/org/dhis2/usescases/teiDashboard/DashboardRepositoryImpl.kt @@ -7,7 +7,6 @@ import io.reactivex.Single import io.reactivex.functions.Function import org.dhis2.commons.data.tuples.Pair import org.dhis2.commons.resources.ResourceManager -import org.dhis2.utils.AuthorityException import org.dhis2.utils.DateUtils import org.dhis2.utils.ValueUtils import org.hisp.dhis.android.core.D2 @@ -386,58 +385,53 @@ class DashboardRepositoryImpl( } } - override fun deleteTeiIfPossible(): Single { - return Single.fromCallable { - val local = d2.trackedEntityModule() - .trackedEntityInstances() - .uid(teiUid) - .blockingGet() - ?.state() == State.TO_POST - val hasAuthority = d2.userModule() - .authorities() - .byName().eq("F_TEI_CASCADE_DELETE") - .one().blockingExists() - local || hasAuthority - }.flatMap { canDelete: Boolean -> - if (canDelete) { - return@flatMap d2.trackedEntityModule() - .trackedEntityInstances() - .uid(teiUid) - .delete() - .andThen(Single.fromCallable { true }) - } else { - return@flatMap Single.fromCallable { false } - } - } + override fun checkIfDeleteTeiIsPossible(): Boolean { + val local = d2.trackedEntityModule() + .trackedEntityInstances() + .uid(teiUid) + .blockingGet() + ?.state() == State.TO_POST + val hasAuthority = d2.userModule() + .authorities() + .byName().eq("F_TEI_CASCADE_DELETE") + .one().blockingExists() + + return local || hasAuthority + } + + override fun deleteTei(): Single { + return d2.trackedEntityModule() + .trackedEntityInstances() + .uid(teiUid) + .delete() + .andThen(Single.fromCallable { true }) } - override fun deleteEnrollmentIfPossible(enrollmentUid: String): Single { + override fun checkIfDeleteEnrollmentIsPossible(enrollmentUid: String): Boolean { + val local = d2.enrollmentModule() + .enrollments() + .uid(enrollmentUid) + .blockingGet()!!.state() == State.TO_POST + val hasAuthority = d2.userModule() + .authorities() + .byName().eq("F_ENROLLMENT_CASCADE_DELETE") + .one().blockingExists() + + return local || hasAuthority + } + + override fun deleteEnrollment(enrollmentUid: String): Single { return Single.fromCallable { - val local = d2.enrollmentModule() - .enrollments() - .uid(enrollmentUid) - .blockingGet()!!.state() == State.TO_POST - val hasAuthority = d2.userModule() - .authorities() - .byName().eq("F_ENROLLMENT_CASCADE_DELETE") - .one().blockingExists() - local || hasAuthority - }.flatMap { canDelete: Boolean -> - if (canDelete) { - return@flatMap Single.fromCallable { - val enrollmentObjectRepository = d2.enrollmentModule() - .enrollments().uid(enrollmentUid) - enrollmentObjectRepository.setStatus( - enrollmentObjectRepository.blockingGet()!!.status()!!, - ) - enrollmentObjectRepository.blockingDelete() - d2.enrollmentModule().enrollments().byTrackedEntityInstance().eq(teiUid) - .byDeleted().isFalse - .byStatus().eq(EnrollmentStatus.ACTIVE).blockingGet().isNotEmpty() - } - } else { - return@flatMap Single.error(AuthorityException(null)) - } + val enrollmentObjectRepository = + d2.enrollmentModule() + .enrollments().uid(enrollmentUid) + enrollmentObjectRepository.setStatus( + enrollmentObjectRepository.blockingGet()!!.status()!!, + ) + enrollmentObjectRepository.blockingDelete() + !d2.enrollmentModule().enrollments().byTrackedEntityInstance().eq(teiUid) + .byDeleted().isFalse + .byStatus().eq(EnrollmentStatus.ACTIVE).blockingGet().isEmpty() } } diff --git a/app/src/main/java/org/dhis2/usescases/teiDashboard/TeiDashboardContracts.java b/app/src/main/java/org/dhis2/usescases/teiDashboard/TeiDashboardContracts.java index fe971a8eb5..2d6fd31544 100644 --- a/app/src/main/java/org/dhis2/usescases/teiDashboard/TeiDashboardContracts.java +++ b/app/src/main/java/org/dhis2/usescases/teiDashboard/TeiDashboardContracts.java @@ -74,5 +74,9 @@ public interface Presenter { void trackDashboardRelationships(); void trackDashboardNotes(); + + Boolean checkIfTEICanBeDeleted(); + + Boolean checkIfEnrollmentCanBeDeleted(String enrollmentUid); } } diff --git a/app/src/main/java/org/dhis2/usescases/teiDashboard/TeiDashboardMobileActivity.kt b/app/src/main/java/org/dhis2/usescases/teiDashboard/TeiDashboardMobileActivity.kt index b6ddd5bcde..a5df046799 100644 --- a/app/src/main/java/org/dhis2/usescases/teiDashboard/TeiDashboardMobileActivity.kt +++ b/app/src/main/java/org/dhis2/usescases/teiDashboard/TeiDashboardMobileActivity.kt @@ -33,6 +33,7 @@ import org.dhis2.commons.sync.OnDismissListener import org.dhis2.commons.sync.SyncContext import org.dhis2.databinding.ActivityDashboardMobileBinding import org.dhis2.ui.ThemeManager +import org.dhis2.ui.dialogs.bottomsheet.DeleteBottomSheetDialog import org.dhis2.usescases.enrollment.EnrollmentActivity import org.dhis2.usescases.enrollment.EnrollmentActivity.Companion.getIntent import org.dhis2.usescases.general.ActivityGlobalAbstract @@ -133,6 +134,7 @@ class TeiDashboardMobileActivity : } } } + override fun onCreate(savedInstanceState: Bundle?) { if (savedInstanceState != null && savedInstanceState.containsKey(Constants.TRACKED_ENTITY_INSTANCE)) { teiUid = savedInstanceState.getString(Constants.TRACKED_ENTITY_INSTANCE) @@ -563,8 +565,18 @@ class TeiDashboardMobileActivity : .menu(this, menu) .onMenuInflated { popupMenu: PopupMenu -> val deleteTeiItem = popupMenu.menu.findItem(R.id.deleteTei) - deleteTeiItem.title = - String.format(deleteTeiItem.title.toString(), presenter.teType) + val showDeleteTeiItem = presenter.checkIfTEICanBeDeleted() + if (showDeleteTeiItem) { + deleteTeiItem.isVisible = true + deleteTeiItem.title = + String.format(deleteTeiItem.title.toString(), presenter.teType) + } else { + deleteTeiItem.isVisible = false + } + + val deleteEnrollmentItem = popupMenu.menu.findItem(R.id.deleteEnrollment) + deleteEnrollmentItem.isVisible = presenter.checkIfEnrollmentCanBeDeleted(enrollmentUid) + if (enrollmentUid != null) { val status = presenter.getEnrollmentStatus(enrollmentUid) if (status == EnrollmentStatus.COMPLETED) { @@ -586,9 +598,10 @@ class TeiDashboardMobileActivity : analyticsHelper.setEvent(SHOW_HELP, CLICK, SHOW_HELP) showTutorial(true) } + R.id.markForFollowUp -> dashboardViewModel.onFollowUp(programModel) - R.id.deleteTei -> presenter.deleteTei() - R.id.deleteEnrollment -> presenter.deleteEnrollment() + R.id.deleteTei -> showDeleteTEIConfirmationDialog() + R.id.deleteEnrollment -> showRemoveEnrollmentConfirmationDialog() R.id.programSelector -> presenter.onEnrollmentSelectorClick() R.id.groupEvents -> groupByStage?.setValue(true) R.id.showTimeline -> groupByStage?.setValue(false) @@ -654,6 +667,35 @@ class TeiDashboardMobileActivity : } } + private fun showDeleteTEIConfirmationDialog() { + DeleteBottomSheetDialog( + title = getString(R.string.delete_tei_dialog_title).format(presenter.teType), + description = getString(R.string.delete_tei_dialog_message).format(presenter.teType), + mainButtonText = getString(R.string.delete), + deleteForever = true, + onMainButtonClick = { + presenter.deleteTei() + }, + ).show( + supportFragmentManager, + DeleteBottomSheetDialog.TAG, + ) + } + + private fun showRemoveEnrollmentConfirmationDialog() { + DeleteBottomSheetDialog( + title = getString(R.string.remove_enrollment_dialog_title).format(programModel.currentProgram.displayName()), + description = getString(R.string.remove_enrollment_dialog_message).format(programModel.currentProgram.displayName()), + mainButtonText = getString(R.string.remove), + onMainButtonClick = { + presenter.deleteEnrollment() + }, + ).show( + supportFragmentManager, + DeleteBottomSheetDialog.TAG, + ) + } + override fun onRelationshipMapLoaded() { binding.toolbarProgress.hide() } diff --git a/app/src/main/java/org/dhis2/usescases/teiDashboard/TeiDashboardPresenter.java b/app/src/main/java/org/dhis2/usescases/teiDashboard/TeiDashboardPresenter.java index 38279d8d09..93abcbf0c5 100644 --- a/app/src/main/java/org/dhis2/usescases/teiDashboard/TeiDashboardPresenter.java +++ b/app/src/main/java/org/dhis2/usescases/teiDashboard/TeiDashboardPresenter.java @@ -1,10 +1,7 @@ package org.dhis2.usescases.teiDashboard; -import static androidx.core.content.ContextCompat.getString; - import com.google.gson.reflect.TypeToken; -import org.dhis2.R; import org.dhis2.commons.prefs.Preference; import org.dhis2.commons.prefs.PreferenceProvider; import org.dhis2.commons.schedulers.SchedulerProvider; @@ -26,13 +23,11 @@ import static org.dhis2.commons.matomo.Actions.OPEN_NOTES; import static org.dhis2.commons.matomo.Actions.OPEN_RELATIONSHIPS; -import static org.dhis2.utils.analytics.AnalyticsConstants.ACTIVE_FOLLOW_UP; import static org.dhis2.utils.analytics.AnalyticsConstants.CLICK; import static org.dhis2.utils.analytics.AnalyticsConstants.DELETE_ENROLL; import static org.dhis2.utils.analytics.AnalyticsConstants.DELETE_TEI; import static org.dhis2.commons.matomo.Actions.OPEN_ANALYTICS; import static org.dhis2.commons.matomo.Categories.DASHBOARD; -import static org.dhis2.utils.analytics.AnalyticsConstants.FOLLOW_UP; public class TeiDashboardPresenter implements TeiDashboardContracts.Presenter { @@ -135,6 +130,16 @@ public void trackDashboardNotes() { matomoAnalyticsController.trackEvent(DASHBOARD, OPEN_NOTES, CLICK); } + @Override + public Boolean checkIfTEICanBeDeleted() { + return dashboardRepository.checkIfDeleteTeiIsPossible(); + } + + @Override + public Boolean checkIfEnrollmentCanBeDeleted(String enrollmentUid) { + return dashboardRepository.checkIfDeleteEnrollmentIsPossible(enrollmentUid); + } + @Override public void onEnrollmentSelectorClick() { view.goToEnrollmentList(); @@ -150,7 +155,7 @@ public void setProgram(Program program) { @Override public void deleteTei() { compositeDisposable.add( - dashboardRepository.deleteTeiIfPossible() + dashboardRepository.deleteTei() .subscribeOn(schedulerProvider.io()) .observeOn(schedulerProvider.ui()) .subscribe( @@ -170,7 +175,7 @@ public void deleteTei() { @Override public void deleteEnrollment() { compositeDisposable.add( - dashboardRepository.deleteEnrollmentIfPossible( + dashboardRepository.deleteEnrollment( dashboardProgramModel.getCurrentEnrollment().uid() ) .subscribeOn(schedulerProvider.io()) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index a6240e2fb7..1fd729cee1 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -975,4 +975,8 @@ Remove Coordinates You don’t have access to data.\nContact your administrator. + Delete this %s? + This %s and all its data across all programs will be deleted. This action cannot be undone. + Remove from %s? + Data from %s will de deleted. This action cannot be undone. diff --git a/app/src/test/java/org/dhis2/usescases/teiDashboard/TeiDashboardPresenterTest.kt b/app/src/test/java/org/dhis2/usescases/teiDashboard/TeiDashboardPresenterTest.kt index 5a2efdb169..4412b7b482 100644 --- a/app/src/test/java/org/dhis2/usescases/teiDashboard/TeiDashboardPresenterTest.kt +++ b/app/src/test/java/org/dhis2/usescases/teiDashboard/TeiDashboardPresenterTest.kt @@ -224,7 +224,7 @@ class TeiDashboardPresenterTest { ) presenter.dashboardProgramModel = dashboardProgramModel whenever( - repository.deleteEnrollmentIfPossible(dashboardProgramModel.currentEnrollment.uid()), + repository.deleteEnrollment(dashboardProgramModel.currentEnrollment.uid()), ) doReturn Single.error(AuthorityException(null)) presenter.deleteEnrollment() @@ -246,7 +246,7 @@ class TeiDashboardPresenterTest { ) presenter.dashboardProgramModel = dashboardProgramModel whenever( - repository.deleteEnrollmentIfPossible(dashboardProgramModel.currentEnrollment.uid()), + repository.deleteEnrollment(dashboardProgramModel.currentEnrollment.uid()), ) doReturn Single.just(true) presenter.deleteEnrollment() @@ -256,7 +256,7 @@ class TeiDashboardPresenterTest { @Test fun `Should not deleteTei if it doesn't has permission`() { - whenever(repository.deleteTeiIfPossible()) doReturn Single.just(false) + whenever(repository.deleteTei()) doReturn Single.just(false) presenter.deleteTei() verify(view).authorityErrorMessage() @@ -264,7 +264,7 @@ class TeiDashboardPresenterTest { @Test fun `Should deleteTei if it has permission`() { - whenever(repository.deleteTeiIfPossible()) doReturn Single.just(true) + whenever(repository.deleteTei()) doReturn Single.just(true) presenter.deleteTei() verify(analyticsHelper).setEvent(DELETE_TEI, CLICK, DELETE_TEI) diff --git a/ui-components/src/main/java/org/dhis2/ui/dialogs/bottomsheet/DeleteBottomSheetDialog.kt b/ui-components/src/main/java/org/dhis2/ui/dialogs/bottomsheet/DeleteBottomSheetDialog.kt new file mode 100644 index 0000000000..06e2cd2d87 --- /dev/null +++ b/ui-components/src/main/java/org/dhis2/ui/dialogs/bottomsheet/DeleteBottomSheetDialog.kt @@ -0,0 +1,102 @@ +package org.dhis2.ui.dialogs.bottomsheet + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Delete +import androidx.compose.material.icons.filled.DeleteForever +import androidx.compose.material.icons.outlined.Info +import androidx.compose.material3.Icon +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.ComposeView +import androidx.compose.ui.platform.ViewCompositionStrategy +import com.google.android.material.bottomsheet.BottomSheetDialogFragment +import org.dhis2.ui.R +import org.hisp.dhis.mobile.ui.designsystem.component.BottomSheetShell +import org.hisp.dhis.mobile.ui.designsystem.component.Button +import org.hisp.dhis.mobile.ui.designsystem.component.ButtonBlock +import org.hisp.dhis.mobile.ui.designsystem.component.ButtonStyle +import org.hisp.dhis.mobile.ui.designsystem.component.ColorStyle +import org.hisp.dhis.mobile.ui.designsystem.theme.SurfaceColor + +class +DeleteBottomSheetDialog( + private val title: String, + private val description: String, + private val mainButtonText: String, + private val deleteForever: Boolean = false, + private val onMainButtonClick: () -> Unit, + +) : BottomSheetDialogFragment() { + + companion object { + const val TAG: String = "DELETE_DIALOG" + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle?, + ): View { + return ComposeView(requireContext()).apply { + setViewCompositionStrategy( + ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed, + ) + setContent { + BottomSheetShell( + title = title, + description = description, + icon = { + Icon( + imageVector = Icons.Outlined.Info, + contentDescription = "Button", + tint = SurfaceColor.Error, + ) + }, + buttonBlock = { + ButtonBlock( + primaryButton = { + Button( + style = ButtonStyle.OUTLINED, + text = getString(R.string.cancel), + onClick = { dismiss() }, + modifier = Modifier.fillMaxWidth(), + ) + }, + secondaryButton = { + Button( + style = ButtonStyle.FILLED, + icon = { + Icon( + imageVector = if (deleteForever) { + Icons.Filled.DeleteForever + } else { + Icons.Filled.Delete + }, + contentDescription = "Button", + ) + }, + text = mainButtonText, + colorStyle = ColorStyle.ERROR, + onClick = onMainButtonClick, + modifier = Modifier + .fillMaxWidth(), + ) + }, + ) + }, + onDismiss = { + dismiss() + }, + content = { + // no-op + }, + showSectionDivider = false, + ) + } + } + } +}