Skip to content

Commit

Permalink
Improvements & bug fixes
Browse files Browse the repository at this point in the history
1. Add patients on-demand
2. Show offline first features based on configs from Binary file
3. Fix & reset cached patient ids
  • Loading branch information
calmwalija committed Nov 27, 2024
1 parent a4e79e3 commit c3bc6c0
Show file tree
Hide file tree
Showing 13 changed files with 578 additions and 29 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
package org.smartregister.fhircore.engine.data.local.syncStrategy

import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
Expand Down Expand Up @@ -62,4 +63,6 @@ abstract class SyncStrategyCacheDao {
abstract suspend fun update(logicalId: String)

@Query("UPDATE syncstrategycacheentity SET shouldSync = 0") abstract suspend fun resetAll()

@Delete abstract suspend fun delete(syncStrategyCacheEntity: SyncStrategyCacheEntity)
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ import android.content.Intent
import androidx.localbroadcastmanager.content.LocalBroadcastManager
import com.google.android.fhir.FhirEngine
import com.google.android.fhir.search.search
import java.util.concurrent.TimeUnit
import org.hl7.fhir.r4.model.ListResource
import org.smartregister.fhircore.engine.configuration.ConfigurationRegistry
import org.smartregister.fhircore.engine.configuration.Item
Expand All @@ -29,7 +28,6 @@ import org.smartregister.fhircore.engine.data.remote.resource.syncStrategy.broad
import org.smartregister.fhircore.engine.data.remote.resource.syncStrategy.broadcast.SyncStatusBroadcastReceiver
import org.smartregister.fhircore.engine.data.remote.resource.syncStrategy.fhir.ParamSyncStatus
import org.smartregister.fhircore.engine.data.remote.resource.syncStrategy.utils.SyncState
import org.smartregister.fhircore.engine.util.SharedPreferenceKey
import org.smartregister.fhircore.engine.util.SharedPreferenceKey.SYNC_STATUS
import org.smartregister.fhircore.engine.util.SharedPreferencesHelper

Expand All @@ -42,31 +40,10 @@ fun hasCompletedInitialSync(sharedPreferencesHelper: SharedPreferencesHelper) =
fun getSyncState(sharedPreferencesHelper: SharedPreferencesHelper) =
sharedPreferencesHelper.read(SYNC_STATUS.name, SyncState.InitialSync.value)

fun Long.before1min(): Boolean {
val currentTimeMillis = System.currentTimeMillis()
val diffMillis = currentTimeMillis - this
return TimeUnit.MILLISECONDS.toMinutes(diffMillis) < 1
}

fun String.splitIdTimestamp() = split("|")

fun toIdTimestamp(sharedPreferencesHelper: SharedPreferencesHelper): IdTimestamp? =
sharedPreferencesHelper
.read(SharedPreferenceKey.SEARCH_PATIENT_ID_TIMESTAMP.name, null)
?.splitIdTimestamp()
?.map { IdTimestamp(it.first().toString(), it.last().toString().toLong()) }
?.firstOrNull()

suspend fun getIdentifier(fhirEngine: FhirEngine) =
fhirEngine
.search<ListResource> { filter(ListResource.TITLE, { value = "Patient Identifier List" }) }
.map { it.resource }
.firstOrNull()

data class IdTimestamp(
val logicalId: String,
val timestamp: Long,
)

fun onPerOrgSyncConfigItem(
configurationRegistry: ConfigurationRegistry,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import org.hl7.fhir.r4.model.ResourceType

class IdentifierSyncParams(
private val identifiers: List<Int>,
private val tagSystem: String,
private val callback: (ParamSyncStatus) -> Unit,
) : DownloadWorkManager {

Expand Down Expand Up @@ -87,7 +88,7 @@ class IdentifierSyncParams(

private fun List<Int>.bundleEntryComponent(): List<Bundle.BundleEntryComponent> {
return flatMap {
listOf("Patient?identifier=$it").map { url ->
listOf("Patient?_tag=$tagSystem&identifier=$it&active=true").map { url ->
Bundle.BundleEntryComponent().apply {
request =
Bundle.BundleEntryRequestComponent().apply {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import org.hl7.fhir.r4.model.ListResource
import org.hl7.fhir.r4.model.ResourceType
import org.smartregister.fhircore.engine.R
import org.smartregister.fhircore.engine.configuration.ConfigurationRegistry
import org.smartregister.fhircore.engine.configuration.preferences.SyncUploadStrategy
import org.smartregister.fhircore.engine.data.local.TingatheDatabase
Expand Down Expand Up @@ -74,13 +75,17 @@ constructor(
private val syncStrategyCacheDao = database.syncStrategyCacheDao
private val broadcaster = LocalBroadcastManager.getInstance(dataStore.context.applicationContext)
private val listResourceTitle = "Patient Identifier List"
private val context = preference.context
private val system = context.getString(R.string.sync_strategy_organization_system)
private val organization = preference.organisationCode()
private val tagSystem = "$system%7C$organization"

private fun downloadWorkManager(): DownloadWorkManager = runBlocking {
syncConfigOfflineFirst(configurationRegistry, preference)
.takeIf { it }
?.let {
getIdentifiers(engine)?.let { item ->
return@runBlocking IdentifierSyncParams(item.data) {
return@runBlocking IdentifierSyncParams(item.data, tagSystem) {
runBlocking {
onSendBroadcast(broadcaster, it)
syncStrategyCacheDao.insert(it.logicalId.toEntity())
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
/*
* Copyright 2021 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.ui.settings

import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material.Card
import androidx.compose.material.CircularProgressIndicator
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog

@Composable
fun FixPatientProgressDialog(
progress: SettingsChannelUiEvent.FixPatientProgress,
onDismissRequest: () -> Unit,
) {
if (progress.totalSize.minus(1) == progress.currentIndex) {
onDismissRequest()
}

Dialog(onDismissRequest = onDismissRequest) {
Card(modifier = Modifier.padding(24.dp)) {
Column(
modifier = Modifier.fillMaxWidth().padding(24.dp),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Box(contentAlignment = Alignment.Center) {
CircularProgressIndicator(
progress = 1f,
modifier = Modifier.size(90.dp),
strokeWidth = 16.dp,
color = Color.LightGray,
)

CircularProgressIndicator(
progress = progress.currentIndex.toFloat().div(progress.totalSize),
modifier = Modifier.size(90.dp),
strokeWidth = 16.dp,
strokeCap = StrokeCap.Round,
)
}

Spacer(modifier = Modifier.height(16.dp))
Text(
text = "${progress.currentIndex} of ${progress.totalSize}",
style = MaterialTheme.typography.h5,
color = Color.Gray,
fontWeight = FontWeight.Bold,
)
Text(text = "This takes a while, please wait ...")
}
}
}
}

@Preview
@Composable
fun FixPatientProgressDialogPreview() {
FixPatientProgressDialog(
progress = SettingsChannelUiEvent.FixPatientProgress(2, 10),
) {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/*
* Copyright 2021 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.ui.settings

sealed interface SettingsChannelUiEvent {

class FixPatientProgress(val currentIndex: Int, val totalSize: Int) : SettingsChannelUiEvent

class FixPatient(val ids: List<String>) : SettingsChannelUiEvent
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,24 +27,37 @@ import androidx.compose.material.MaterialTheme
import androidx.compose.material.ModalBottomSheetLayout
import androidx.compose.material.ModalBottomSheetValue
import androidx.compose.material.Scaffold
import androidx.compose.material.SnackbarDuration
import androidx.compose.material.SnackbarHost
import androidx.compose.material.SnackbarHostState
import androidx.compose.material.SnackbarResult
import androidx.compose.material.Text
import androidx.compose.material.TopAppBar
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.rounded.Logout
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.filled.AutoFixHigh
import androidx.compose.material.icons.filled.ClearAll
import androidx.compose.material.icons.rounded.CleaningServices
import androidx.compose.material.icons.rounded.Report
import androidx.compose.material.icons.rounded.Task
import androidx.compose.material.icons.rounded.Upload
import androidx.compose.material.rememberModalBottomSheetState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.repeatOnLifecycle
import androidx.navigation.NavController
import java.text.SimpleDateFormat
import java.util.Locale
Expand All @@ -64,6 +77,7 @@ import org.smartregister.fhircore.engine.ui.theme.DividerColor
import org.smartregister.fhircore.engine.util.SharedPreferenceKey
import org.smartregister.fhircore.engine.util.SharedPreferenceKey.LAST_PURGE_KEY
import org.smartregister.fhircore.engine.util.SharedPreferencesHelper
import org.smartregister.fhircore.engine.util.extension.showToast

const val SYNC_TIMESTAMP_OUTPUT_FORMAT = "hh:mm aa, MMM d"

Expand All @@ -88,7 +102,59 @@ fun SettingsScreen(
sheetState = devMenuSheetState,
sheetContent = { DevMenu(viewModel = devViewModel) },
) {
val snackbarHostState = remember { SnackbarHostState() }
val coroutineScope = rememberCoroutineScope()

var fixProgress by remember {
mutableStateOf(SettingsChannelUiEvent.FixPatientProgress(0, 0))
}

var showProgressDialog by remember { mutableStateOf(false) }

val lifecycle = LocalLifecycleOwner.current

LaunchedEffect(key1 = settingsViewModel.channelFlow) {
lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
settingsViewModel.channelFlow.collect { event ->
when (event) {
is SettingsChannelUiEvent.FixPatient -> {
if (event.ids.isNotEmpty()) {
coroutineScope.launch {
snackbarHostState
.showSnackbar(
message =
"We found ${event.ids.size} issue(s), would you like to fix that?",
actionLabel = "Proceed",
duration = SnackbarDuration.Indefinite,
)
.also {
when (it) {
SnackbarResult.Dismissed -> Unit
SnackbarResult.ActionPerformed ->
settingsViewModel.fixPatientIssues(event.ids)
}
}
}
} else {
context.showToast("No issue was found")
}
}
is SettingsChannelUiEvent.FixPatientProgress -> {
fixProgress =
SettingsChannelUiEvent.FixPatientProgress(event.currentIndex, event.totalSize)
showProgressDialog = fixProgress.totalSize.minus(1) != event.currentIndex
}
}
}
}
}

if (showProgressDialog) {
FixPatientProgressDialog(fixProgress) { showProgressDialog = false }
}

Scaffold(
snackbarHost = { SnackbarHost(hostState = snackbarHostState) },
topBar = {
TopAppBar(
title = {},
Expand Down Expand Up @@ -201,6 +267,19 @@ fun SettingsScreen(
)
}
}
if (settingsViewModel.isOfflineFirst()) {
item {
UserProfileRow(
icon = Icons.Default.AutoFixHigh,
text = stringResource(id = R.string.fix_patients),
clickListener = {
context.showToast("Working on it, this takes a moment please wait...")
settingsViewModel.findMissingPatientStrategyCache()
},
modifier = modifier,
)
}
}
item {
UserProfileRow(
icon = Icons.AutoMirrored.Rounded.Logout,
Expand Down
Loading

0 comments on commit c3bc6c0

Please sign in to comment.