Skip to content

Commit c80d877

Browse files
authored
SIMPLEMOB-3 Add Diagnosis button (#5730)
1 parent 2a85061 commit c80d877

19 files changed

Lines changed: 486 additions & 47 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@
99
- Bump sqlcipher to v4.13.0
1010
- Bump dagger to v2.59.1
1111

12+
### Changes
13+
14+
- Add `Sync Medical Records` button on setting page behind feature flag
15+
1216
## 2026.02.02
1317

1418
### Internal

app/src/androidTest/java/org/simple/clinic/settings/SettingsScreenTest.kt

Lines changed: 28 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ class SettingsScreenTest {
1818

1919
private val defaultSettingsModel = SettingsModel.default(
2020
isChangeLanguageFeatureEnabled = true,
21+
showDiagnosisButton = true
2122
)
2223

2324
@Test
@@ -30,7 +31,8 @@ class SettingsScreenTest {
3031
navigationIconClick = { /*no-op*/ },
3132
changeLanguageButtonClick = { /*no-op*/ },
3233
updateButtonClick = { /*no-op*/ },
33-
logoutButtonClick = { /*no-op*/ }
34+
logoutButtonClick = { /*no-op*/ },
35+
syncMedicalRecordClick = { /*no-op*/ }
3436
)
3537
}
3638

@@ -58,7 +60,8 @@ class SettingsScreenTest {
5860
navigationIconClick = { /*no-op*/ },
5961
changeLanguageButtonClick = { /*no-op*/ },
6062
updateButtonClick = { /*no-op*/ },
61-
logoutButtonClick = { /*no-op*/ }
63+
logoutButtonClick = { /*no-op*/ },
64+
syncMedicalRecordClick = { /*no-op*/ }
6265
)
6366
}
6467

@@ -78,7 +81,8 @@ class SettingsScreenTest {
7881
navigationIconClick = { /*no-op*/ },
7982
changeLanguageButtonClick = { /*no-op*/ },
8083
updateButtonClick = { /*no-op*/ },
81-
logoutButtonClick = { /*no-op*/ }
84+
logoutButtonClick = { /*no-op*/ },
85+
syncMedicalRecordClick = { /*no-op*/ }
8286
)
8387
}
8488

@@ -97,7 +101,8 @@ class SettingsScreenTest {
97101
navigationIconClick = { /*no-op*/ },
98102
changeLanguageButtonClick = { /*no-op*/ },
99103
updateButtonClick = { /*no-op*/ },
100-
logoutButtonClick = { /*no-op*/ }
104+
logoutButtonClick = { /*no-op*/ },
105+
syncMedicalRecordClick = { /*no-op*/ }
101106
)
102107
}
103108

@@ -115,7 +120,8 @@ class SettingsScreenTest {
115120
navigationIconClick = { /*no-op*/ },
116121
changeLanguageButtonClick = { /*no-op*/ },
117122
updateButtonClick = { /*no-op*/ },
118-
logoutButtonClick = { /*no-op*/ }
123+
logoutButtonClick = { /*no-op*/ },
124+
syncMedicalRecordClick = { /*no-op*/ }
119125
)
120126
}
121127

@@ -133,7 +139,8 @@ class SettingsScreenTest {
133139
navigationIconClick = { /*no-op*/ },
134140
changeLanguageButtonClick = { /*no-op*/ },
135141
updateButtonClick = { /*no-op*/ },
136-
logoutButtonClick = { /*no-op*/ }
142+
logoutButtonClick = { /*no-op*/ },
143+
syncMedicalRecordClick = { /*no-op*/ }
137144
)
138145
}
139146

@@ -150,7 +157,8 @@ class SettingsScreenTest {
150157
navigationIconClick = { /*no-op*/ },
151158
changeLanguageButtonClick = { /*no-op*/ },
152159
updateButtonClick = { /*no-op*/ },
153-
logoutButtonClick = { /*no-op*/ }
160+
logoutButtonClick = { /*no-op*/ },
161+
syncMedicalRecordClick = { /*no-op*/ }
154162
)
155163
}
156164

@@ -168,7 +176,8 @@ class SettingsScreenTest {
168176
navigationIconClick = { /*no-op*/ },
169177
changeLanguageButtonClick = { /*no-op*/ },
170178
updateButtonClick = { /*no-op*/ },
171-
logoutButtonClick = { /*no-op*/ }
179+
logoutButtonClick = { /*no-op*/ },
180+
syncMedicalRecordClick = { /*no-op*/ }
172181
)
173182
}
174183

@@ -186,7 +195,8 @@ class SettingsScreenTest {
186195
navigationIconClick = { /*no-op*/ },
187196
changeLanguageButtonClick = { /*no-op*/ },
188197
updateButtonClick = { /*no-op*/ },
189-
logoutButtonClick = { /*no-op*/ }
198+
logoutButtonClick = { /*no-op*/ },
199+
syncMedicalRecordClick = { /*no-op*/ }
190200
)
191201
}
192202

@@ -198,14 +208,16 @@ class SettingsScreenTest {
198208
fun whenChangeLanguageFeatureIsNotEnabledThenDoNotShowChangeLanguageSetting() {
199209
val model = SettingsModel.default(
200210
isChangeLanguageFeatureEnabled = false,
211+
showDiagnosisButton = true
201212
)
202213
composeRule.setContent {
203214
SettingsScreen(
204215
model = model,
205216
navigationIconClick = { /*no-op*/ },
206217
changeLanguageButtonClick = { /*no-op*/ },
207218
updateButtonClick = { /*no-op*/ },
208-
logoutButtonClick = { /*no-op*/ }
219+
logoutButtonClick = { /*no-op*/ },
220+
syncMedicalRecordClick = { /*no-op*/ }
209221
)
210222
}
211223

@@ -217,14 +229,16 @@ class SettingsScreenTest {
217229
fun whenChangeLanguageFeatureIsEnabledThenShowChangeLanguageSetting() {
218230
val model = SettingsModel.default(
219231
isChangeLanguageFeatureEnabled = true,
232+
showDiagnosisButton = true,
220233
)
221234
composeRule.setContent {
222235
SettingsScreen(
223236
model = model,
224237
navigationIconClick = { /*no-op*/ },
225238
changeLanguageButtonClick = { /*no-op*/ },
226239
updateButtonClick = { /*no-op*/ },
227-
logoutButtonClick = { /*no-op*/ }
240+
logoutButtonClick = { /*no-op*/ },
241+
syncMedicalRecordClick = { /*no-op*/ }
228242
)
229243
}
230244

@@ -236,14 +250,16 @@ class SettingsScreenTest {
236250
fun logoutButtonShouldBeVisible() {
237251
val model = SettingsModel.default(
238252
isChangeLanguageFeatureEnabled = true,
253+
showDiagnosisButton = true
239254
)
240255
composeRule.setContent {
241256
SettingsScreen(
242257
model = model,
243258
navigationIconClick = { /*no-op*/ },
244259
changeLanguageButtonClick = { /*no-op*/ },
245260
updateButtonClick = { /*no-op*/ },
246-
logoutButtonClick = { /*no-op*/ }
261+
logoutButtonClick = { /*no-op*/ },
262+
syncMedicalRecordClick = { /*no-op*/ }
247263
)
248264
}
249265

app/src/main/java/org/simple/clinic/feature/Feature.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,5 +26,6 @@ enum class Feature(
2626
PatientStatinNudge(false, "patient_statin_nudge_v0"),
2727
NonLabBasedStatinNudge(false, "non_lab_based_statin_nudge"),
2828
LabBasedStatinNudge(false, "lab_based_statin_nudge"),
29-
Screening(false, "screening_feature_v0")
29+
Screening(false, "screening_feature_v0"),
30+
ShowDiagnosisButton(false, "show_diagnosis_button"),
3031
}

app/src/main/java/org/simple/clinic/patient/PatientRepository.kt

Lines changed: 22 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -743,26 +743,30 @@ class PatientRepository @Inject constructor(
743743

744744
fun fetchCompleteMedicalRecord(): List<CompleteMedicalRecord> {
745745
val patientProfiles = database.patientDao().allPatientProfiles()
746-
return patientProfiles.map { patientProfile ->
747-
val patientUuid = patientProfile.patientUuid
748-
val medicalHistory = database.medicalHistoryDao().historyForPatientImmediate(patientUuid)
749-
val appointments = database.appointmentDao().getAllAppointmentsForPatient(patientUuid)
750-
val bloodPressures = database.bloodPressureDao().allBloodPressuresRecordedSinceImmediate(
751-
patientUuid,
752-
Instant.EPOCH
753-
)
754-
val bloodSugars = database.bloodSugarDao().allBloodSugarsImmediate(patientUuid)
746+
return patientProfiles.mapNotNull { patientProfile ->
747+
try {
748+
val patientUuid = patientProfile.patientUuid
749+
val medicalHistory = database.medicalHistoryDao().historyForPatientImmediate(patientUuid)
750+
val appointments = database.appointmentDao().getAllAppointmentsForPatient(patientUuid)
751+
val bloodPressures = database.bloodPressureDao().allBloodPressuresRecordedSinceImmediate(
752+
patientUuid,
753+
Instant.EPOCH
754+
)
755+
val bloodSugars = database.bloodSugarDao().allBloodSugarsImmediate(patientUuid)
755756

756-
val prescribedDrugs = database.prescriptionDao().forPatientImmediate(patientUuid)
757+
val prescribedDrugs = database.prescriptionDao().forPatientImmediate(patientUuid)
757758

758-
CompleteMedicalRecord(
759-
patient = patientProfile,
760-
medicalHistory = medicalHistory,
761-
appointments = appointments,
762-
bloodPressures = bloodPressures,
763-
bloodSugars = bloodSugars,
764-
prescribedDrugs = prescribedDrugs
765-
)
759+
CompleteMedicalRecord(
760+
patient = patientProfile,
761+
medicalHistory = medicalHistory,
762+
appointments = appointments,
763+
bloodPressures = bloodPressures,
764+
bloodSugars = bloodSugars,
765+
prescribedDrugs = prescribedDrugs
766+
)
767+
} catch (_: Exception) {
768+
null
769+
}
766770
}
767771
}
768772

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package org.simple.clinic.patient.medicalRecords
2+
3+
import com.squareup.moshi.Json
4+
import com.squareup.moshi.JsonClass
5+
import org.simple.clinic.patient.CompleteMedicalRecord
6+
import org.simple.clinic.patient.onlinelookup.api.CompleteMedicalRecordPayload
7+
8+
@JsonClass(generateAdapter = true)
9+
data class CompleteMedicalRecordsPushRequest(
10+
11+
@Json(name = "patients")
12+
val patients: List<CompleteMedicalRecordPayload>
13+
)
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
package org.simple.clinic.patient.medicalRecords
2+
3+
import org.simple.clinic.patient.CompleteMedicalRecord
4+
import org.simple.clinic.patient.onlinelookup.api.CompleteMedicalRecordPayload
5+
import org.simple.clinic.patient.onlinelookup.api.RecordRetention
6+
import org.simple.clinic.patient.onlinelookup.api.RetentionType
7+
import org.simple.clinic.patient.onlinelookup.api.SecondsDuration
8+
import org.simple.clinic.patient.sync.PatientSyncApi
9+
import org.simple.clinic.util.UtcClock
10+
import java.time.Duration
11+
import java.time.Instant
12+
import javax.inject.Inject
13+
14+
class PushMedicalRecordsOnline @Inject constructor(
15+
private val patientSyncApi: PatientSyncApi,
16+
private val clock: UtcClock,
17+
) {
18+
fun pushAllMedicalRecordsOnServer(
19+
medicalRecords: List<CompleteMedicalRecord>
20+
): Result {
21+
22+
if (medicalRecords.isEmpty()) {
23+
return Result.NothingToPush
24+
}
25+
26+
val request = CompleteMedicalRecordsPushRequest(
27+
patients = medicalRecords.map { mapToPayload(it) }
28+
)
29+
30+
return try {
31+
val response = patientSyncApi
32+
.pushAllPatientsData(request)
33+
.execute()
34+
35+
return when (response.code()) {
36+
200 -> Result.Success
37+
else -> Result.ServerError(
38+
code = response.code(),
39+
message = response.errorBody()?.string()
40+
)
41+
}
42+
43+
} catch (e: Exception) {
44+
Result.NetworkError(e)
45+
}
46+
}
47+
48+
49+
fun mapToPayload(
50+
completeMedicalRecord: CompleteMedicalRecord
51+
): CompleteMedicalRecordPayload {
52+
53+
val patientProfile = completeMedicalRecord.patient
54+
val patient = patientProfile.patient
55+
56+
return CompleteMedicalRecordPayload(
57+
id = patient.uuid,
58+
fullName = patient.fullName,
59+
gender = patient.gender,
60+
dateOfBirth = patient.ageDetails.dateOfBirth,
61+
age = patient.ageDetails.ageValue,
62+
ageUpdatedAt = patient.ageDetails.ageUpdatedAt,
63+
status = patient.status,
64+
createdAt = patient.createdAt,
65+
updatedAt = patient.updatedAt,
66+
deletedAt = patient.deletedAt,
67+
68+
address = patientProfile.address.toPayload(),
69+
70+
phoneNumbers = patientProfile.phoneNumbers
71+
.map { it.toPayload() },
72+
73+
businessIds = patientProfile.businessIds
74+
.map { it.toPayload() },
75+
76+
recordedAt = patient.recordedAt,
77+
78+
reminderConsent = patient.reminderConsent,
79+
80+
deletedReason = patient.deletedReason,
81+
82+
registeredFacilityId = patient.registeredFacilityId,
83+
84+
assignedFacilityId = patient.assignedFacilityId,
85+
86+
appointments = completeMedicalRecord.appointments
87+
.map { it.toPayload() },
88+
89+
bloodPressures = completeMedicalRecord.bloodPressures
90+
.map { it.toPayload() },
91+
92+
bloodSugars = completeMedicalRecord.bloodSugars
93+
.map { it.toPayload() },
94+
95+
medicalHistory = completeMedicalRecord.medicalHistory
96+
?.toPayload(),
97+
98+
prescribedDrugs = completeMedicalRecord.prescribedDrugs
99+
.map { it.toPayload() },
100+
101+
retention = patient.retainUntil?.let { retainUntil ->
102+
val duration = Duration.between(
103+
Instant.now(clock),
104+
retainUntil
105+
).coerceAtLeast(Duration.ZERO)
106+
107+
RecordRetention(
108+
type = RetentionType.Temporary,
109+
retainFor = SecondsDuration(duration)
110+
)
111+
} ?: RecordRetention(
112+
type = RetentionType.Permanent,
113+
retainFor = null
114+
)
115+
)
116+
}
117+
118+
sealed class Result {
119+
data object Success : Result()
120+
data object NothingToPush : Result()
121+
data class ServerError(
122+
val code: Int,
123+
val message: String?
124+
) : Result()
125+
126+
data class NetworkError(val throwable: Throwable) : Result()
127+
}
128+
}

app/src/main/java/org/simple/clinic/patient/sync/PatientSyncApi.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package org.simple.clinic.patient.sync
22

33
import org.simple.clinic.di.network.Timeout
4+
import org.simple.clinic.patient.medicalRecords.CompleteMedicalRecordsPushRequest
45
import org.simple.clinic.patient.onlinelookup.api.OnlineLookupResponsePayload
56
import org.simple.clinic.patient.onlinelookup.api.PatientOnlineLookupRequest
67
import org.simple.clinic.sync.DataPushResponse
@@ -31,4 +32,9 @@ interface PatientSyncApi {
3132
fun lookup(
3233
@Body body: PatientOnlineLookupRequest
3334
): Call<OnlineLookupResponsePayload>
35+
36+
@POST("v4/legacy_data_dumps")
37+
fun pushAllPatientsData(
38+
@Body body: CompleteMedicalRecordsPushRequest
39+
): Call<DataPushResponse>
3440
}

app/src/main/java/org/simple/clinic/settings/SettingsEffect.kt

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
package org.simple.clinic.settings
22

3-
import org.simple.clinic.scanid.ScanSimpleIdEffect
3+
import org.simple.clinic.patient.CompleteMedicalRecord
44

55
sealed class SettingsEffect
66

@@ -28,3 +28,7 @@ data object GoBack : SettingsViewEffect()
2828

2929
data object FetchCompleteMedicalRecords : SettingsEffect()
3030

31+
data class PushCompleteMedicalRecordsOnline(
32+
val medicalRecords: List<CompleteMedicalRecord>
33+
) : SettingsEffect()
34+

0 commit comments

Comments
 (0)