diff --git a/.codeclimate.yml b/.codeclimate.yml deleted file mode 100644 index 221ca7a264f..00000000000 --- a/.codeclimate.yml +++ /dev/null @@ -1,49 +0,0 @@ -version: "2" -checks: - argument-count: - enabled: true - config: - threshold: 4 - complex-logic: - enabled: true - config: - threshold: 1 - file-lines: - enabled: false - config: - threshold: 250 - method-complexity: - enabled: true - config: - threshold: 10 - method-count: - enabled: false - config: - threshold: 20 - method-lines: - enabled: true - config: - threshold: 40 - nested-control-flow: - enabled: true - config: - threshold: 1 - return-statements: - enabled: true - config: - threshold: 2 - similar-code: - enabled: false - config: - threshold: # language-specific defaults. an override will affect all languages. - identical-code: - enabled: false - config: - threshold: # language-specific defaults. an override will affect all languages. -exclude_patterns: - - "**/androidTest/" - - "**/test/" - - "**/sharedTest/" - - "**/src/main/java/org/simple/clinic/storage/migrations/Migration_*.kt" - - "**/build/" - - "router/" diff --git a/.qlty/.gitignore b/.qlty/.gitignore new file mode 100644 index 00000000000..30366188def --- /dev/null +++ b/.qlty/.gitignore @@ -0,0 +1,7 @@ +* +!configs +!configs/** +!hooks +!hooks/** +!qlty.toml +!.gitignore diff --git a/.qlty/configs/.shellcheckrc b/.qlty/configs/.shellcheckrc new file mode 100644 index 00000000000..6a38d9281a8 --- /dev/null +++ b/.qlty/configs/.shellcheckrc @@ -0,0 +1 @@ +source-path=SCRIPTDIR \ No newline at end of file diff --git a/.qlty/configs/.yamllint.yaml b/.qlty/configs/.yamllint.yaml new file mode 100644 index 00000000000..d22fa7799f3 --- /dev/null +++ b/.qlty/configs/.yamllint.yaml @@ -0,0 +1,8 @@ +rules: + document-start: disable + quoted-strings: + required: only-when-needed + extra-allowed: ["{|}"] + key-duplicates: {} + octal-values: + forbid-implicit-octal: true diff --git a/.qlty/qlty.toml b/.qlty/qlty.toml new file mode 100644 index 00000000000..e92d486d461 --- /dev/null +++ b/.qlty/qlty.toml @@ -0,0 +1,88 @@ +# This file was automatically generated by `qlty init`. +# You can modify it to suit your needs. +# We recommend you to commit this file to your repository. +# +# This configuration is used by both Qlty CLI and Qlty Cloud. +# +# Qlty CLI -- Code quality toolkit for developers +# Qlty Cloud -- Fully automated Code Health Platform +# +# Try Qlty Cloud: https://qlty.sh +# +# For a guide to configuration, visit https://qlty.sh/d/config +# Or for a full reference, visit https://qlty.sh/d/qlty-toml +config_version = "0" + +exclude_patterns = [ + "*_min.*", + "*-min.*", + "*.min.*", + "**/.yarn/**", + "**/*.d.ts", + "**/assets/**", + "**/bower_components/**", + "**/build/**", + "**/cache/**", + "**/config/**", + "**/db/**", + "**/deps/**", + "**/dist/**", + "**/extern/**", + "**/external/**", + "**/generated/**", + "**/Godeps/**", + "**/gradlew/**", + "**/mvnw/**", + "**/node_modules/**", + "**/protos/**", + "**/seed/**", + "**/target/**", + "**/templates/**", + "**/testdata/**", + "**/vendor/**", "**/androidTest/", "**/test/", "**/sharedTest/", "**/src/main/java/org/simple/clinic/storage/migrations/Migration_*.kt", "**/build/", "router/", +] + +test_patterns = [ + "**/test/**", + "**/spec/**", + "**/*.test.*", + "**/*.spec.*", + "**/*_test.*", + "**/*_spec.*", + "**/test_*.*", + "**/spec_*.*", +] + +[smells] +mode = "block" + +[smells.boolean_logic] +threshold = 1 +enabled = true + +[smells.file_complexity] +threshold = 55 +enabled = false + +[smells.return_statements] +threshold = 2 +enabled = true + +[smells.nested_control_flow] +threshold = 2 +enabled = true + +[smells.function_parameters] +threshold = 6 +enabled = true + +[smells.function_complexity] +threshold = 11 +enabled = true + +[smells.duplication] +enabled = false + +[[source]] +name = "default" +default = true diff --git a/CHANGELOG.md b/CHANGELOG.md index 1f952b9bc47..5d54455ad91 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,11 @@ - Handle window insets when displaying app content in edge-to-edge - Enable edge-to-edge support on all versions of Android - Handle nullable inputs when removing last chip in the `ChipInputAutoCompleteTextView` +- Migrate `PatientSummaryScreen` toolbar to Jetpack Compose +- Add Compose components for Overdue screen +- Migrate medicines summary view to Jetpack Compose +- Migrate codeclimate config to qlty.sh config +- Migrate `OverdueScreen` patient list to Jetpack Compose ## 2025.05.20 diff --git a/app/src/main/java/org/simple/clinic/home/overdue/OverdueScreen.kt b/app/src/main/java/org/simple/clinic/home/overdue/OverdueScreen.kt index 22a2a6a1426..71f916bb29d 100644 --- a/app/src/main/java/org/simple/clinic/home/overdue/OverdueScreen.kt +++ b/app/src/main/java/org/simple/clinic/home/overdue/OverdueScreen.kt @@ -7,7 +7,10 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.appcompat.app.AppCompatActivity -import androidx.recyclerview.widget.LinearLayoutManager +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.compose.ui.platform.ViewCompositionStrategy import com.f2prateek.rx.preferences2.Preference import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.jakewharton.rxbinding3.view.clicks @@ -17,6 +20,7 @@ import io.reactivex.Observable import io.reactivex.disposables.CompositeDisposable import io.reactivex.rxkotlin.cast import io.reactivex.rxkotlin.ofType +import io.reactivex.subjects.PublishSubject import kotlinx.parcelize.Parcelize import org.simple.clinic.R import org.simple.clinic.ReportAnalyticsEvents @@ -24,18 +28,15 @@ import org.simple.clinic.activity.permissions.RequestPermissions import org.simple.clinic.activity.permissions.RuntimePermissions import org.simple.clinic.appconfig.Country import org.simple.clinic.contactpatient.ContactPatientBottomSheet -import org.simple.clinic.databinding.ListItemDividerBinding -import org.simple.clinic.databinding.ListItemNoPendingPatientsBinding -import org.simple.clinic.databinding.ListItemOverdueListSectionHeaderBinding -import org.simple.clinic.databinding.ListItemOverduePatientBinding -import org.simple.clinic.databinding.ListItemOverduePendingListFooterBinding -import org.simple.clinic.databinding.ListItemSearchOverduePatientButtonBinding import org.simple.clinic.databinding.ScreenOverdueBinding import org.simple.clinic.di.injector import org.simple.clinic.feature.Feature.OverdueInstantSearch import org.simple.clinic.feature.Feature.PatientReassignment import org.simple.clinic.feature.Features import org.simple.clinic.home.HomeScreen +import org.simple.clinic.home.overdue.compose.OverdueAppointmentListItem +import org.simple.clinic.home.overdue.compose.OverdueUiModel +import org.simple.clinic.home.overdue.compose.OverdueUiModelMapper import org.simple.clinic.home.overdue.search.OverdueSearchScreen import org.simple.clinic.navigation.v2.Router import org.simple.clinic.navigation.v2.ScreenKey @@ -54,7 +55,6 @@ import org.simple.clinic.util.UserClock import org.simple.clinic.util.UtcClock import org.simple.clinic.util.applyInsetsBottomPadding import org.simple.clinic.util.unsafeLazy -import org.simple.clinic.widgets.ItemAdapter import org.simple.clinic.widgets.UiEvent import java.time.Instant import java.time.LocalDate @@ -115,37 +115,13 @@ class OverdueScreen : BaseScreen< @Inject lateinit var locale: Locale - private val overdueListAdapter = ItemAdapter( - diffCallback = OverdueAppointmentListItem.DiffCallback(), - bindings = mapOf( - R.layout.list_item_overdue_patient to { layoutInflater, parent -> - ListItemOverduePatientBinding.inflate(layoutInflater, parent, false) - }, - R.layout.list_item_overdue_list_section_header to { layoutInflater, parent -> - ListItemOverdueListSectionHeaderBinding.inflate(layoutInflater, parent, false) - }, - R.layout.list_item_overdue_pending_list_footer to { layoutInflater, parent -> - ListItemOverduePendingListFooterBinding.inflate(layoutInflater, parent, false) - }, - R.layout.list_item_no_pending_patients to { layoutInflater, parent -> - ListItemNoPendingPatientsBinding.inflate(layoutInflater, parent, false) - }, - R.layout.list_item_divider to { layoutInflater, parent -> - ListItemDividerBinding.inflate(layoutInflater, parent, false) - }, - R.layout.list_item_search_overdue_patient_button to { layoutInflater, parent -> - ListItemSearchOverduePatientButtonBinding.inflate(layoutInflater, parent, false) - } - ) - ) - private val disposable = CompositeDisposable() private val viewForEmptyList get() = binding.viewForEmptyList - private val overdueRecyclerView - get() = binding.overdueRecyclerView + private val composeView + get() = binding.composeView private val overdueProgressBar get() = binding.overdueProgressBar @@ -172,6 +148,8 @@ class OverdueScreen : BaseScreen< country.isoCountryCode == Country.INDIA } + private var uiModelsState by mutableStateOf>(emptyList()) + override fun defaultModel() = OverdueModel.create() override fun bindView(layoutInflater: LayoutInflater, container: ViewGroup?) = @@ -179,11 +157,13 @@ class OverdueScreen : BaseScreen< override fun uiRenderer() = OverdueUiRenderer(ui = this) + private val composeUiEvents = PublishSubject.create() + override fun events() = Observable.mergeArray( - overdueListAdapter.itemEvents, downloadOverdueListClicks(), shareOverdueListClicks(), - clearSelectedOverdueAppointmentClicks() + clearSelectedOverdueAppointmentClicks(), + composeUiEvents, ) .compose(RequestPermissions(runtimePermissions, screenResults.streamResults().ofType())) .compose(runtimeNetworkStatus::apply) @@ -218,12 +198,38 @@ class OverdueScreen : BaseScreen< buttonsFrame.applyInsetsBottomPadding() - overdueRecyclerView.adapter = overdueListAdapter - overdueRecyclerView.layoutManager = LinearLayoutManager(context) + composeView.apply { + setViewCompositionStrategy( + ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed + ) + setContent { + OverdueAppointmentListItem( + uiModels = uiModelsState, + onCallClicked = { patientId -> + composeUiEvents.onNext(CallPatientClicked(patientId)) + }, + onRowClicked = { patientId -> + composeUiEvents.onNext(OverduePatientClicked(patientId)) + }, + onCheckboxClicked = { appointmentUuid -> + composeUiEvents.onNext(OverdueAppointmentCheckBoxClicked(appointmentUuid)) + }, + onSearch = { + composeUiEvents.onNext(OverdueSearchButtonClicked) + }, + onSectionHeaderClick = { overdueAppointmentSectionTitle -> + composeUiEvents.onNext(ChevronClicked(overdueAppointmentSectionTitle)) + }, + onSectionFooterClick = { + composeUiEvents.onNext(PendingListFooterClicked) + } + ) + } + } } + override fun onDestroyView() { - overdueRecyclerView.adapter = null disposable.clear() super.onDestroyView() } @@ -267,7 +273,7 @@ class OverdueScreen : BaseScreen< selectedOverdueAppointments: Set, overdueListSectionStates: OverdueListSectionStates ) { - overdueListAdapter.submitList(OverdueAppointmentListItem.from( + uiModelsState = OverdueUiModelMapper.from( overdueAppointmentSections = overdueAppointmentSections, clock = userClock, pendingListDefaultStateSize = pendingAppointmentsConfig.pendingListDefaultStateSize, @@ -277,7 +283,8 @@ class OverdueScreen : BaseScreen< selectedOverdueAppointments = selectedOverdueAppointments, isPatientReassignmentFeatureEnabled = features.isEnabled(PatientReassignment), locale = locale, - )) + ) + if (isOverdueListDownloadAndShareEnabled) { buttonsFrame.visibility = View.VISIBLE } @@ -316,11 +323,11 @@ class OverdueScreen : BaseScreen< } override fun showOverdueRecyclerView() { - overdueRecyclerView.visibility = View.VISIBLE + composeView.visibility = View.VISIBLE } override fun hideOverdueRecyclerView() { - overdueRecyclerView.visibility = View.GONE + composeView.visibility = View.GONE } override fun openOverdueSearch() { diff --git a/app/src/main/java/org/simple/clinic/home/overdue/compose/NoPendingPatients.kt b/app/src/main/java/org/simple/clinic/home/overdue/compose/NoPendingPatients.kt new file mode 100644 index 00000000000..c6216aeb757 --- /dev/null +++ b/app/src/main/java/org/simple/clinic/home/overdue/compose/NoPendingPatients.kt @@ -0,0 +1,55 @@ +package org.simple.clinic.home.overdue.compose + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material.Card +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.dimensionResource +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import org.simple.clinic.R +import org.simple.clinic.common.ui.theme.SimpleTheme + +@Composable +fun NoPendingPatients(modifier: Modifier = Modifier) { + Card( + modifier = modifier + .fillMaxWidth() + .padding(8.dp) + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding( + vertical = dimensionResource(R.dimen.spacing_40), + horizontal = dimensionResource(R.dimen.spacing_16) + ), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Image( + painter = painterResource(id = R.drawable.ic_no_pending_patients_illustration), + contentDescription = null + ) + + Text( + modifier = Modifier.padding(top = dimensionResource(R.dimen.spacing_16)), + text = stringResource(id = R.string.overdue_no_pending_patients), + style = SimpleTheme.typography.body0Medium, + color = SimpleTheme.colors.material.secondary + ) + } + } +} + +@Preview +@Composable +private fun NoPendingPatientsPreview(modifier: Modifier = Modifier) { + NoPendingPatients(modifier = modifier) +} diff --git a/app/src/main/java/org/simple/clinic/home/overdue/compose/OverdueAppointmentListItem.kt b/app/src/main/java/org/simple/clinic/home/overdue/compose/OverdueAppointmentListItem.kt new file mode 100644 index 00000000000..bd3e255cfc2 --- /dev/null +++ b/app/src/main/java/org/simple/clinic/home/overdue/compose/OverdueAppointmentListItem.kt @@ -0,0 +1,118 @@ +package org.simple.clinic.home.overdue.compose + +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material.Divider +import androidx.compose.material.Icon +import androidx.compose.material.ProvideTextStyle +import androidx.compose.material.Text +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Search +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.res.dimensionResource +import androidx.compose.ui.res.stringResource +import org.simple.clinic.R +import org.simple.clinic.common.ui.components.ButtonSize +import org.simple.clinic.common.ui.components.OutlinedButton +import org.simple.clinic.common.ui.theme.SimpleTheme +import org.simple.clinic.home.overdue.OverdueAppointmentSectionTitle +import java.util.UUID + +@Composable +fun OverdueAppointmentListItem( + uiModels: List, + onCallClicked: (UUID) -> Unit, + onRowClicked: (UUID) -> Unit, + onCheckboxClicked: (UUID) -> Unit, + onSectionHeaderClick: (OverdueAppointmentSectionTitle) -> Unit, + onSearch: () -> Unit, + onSectionFooterClick: () -> Unit, + + ) { + SimpleTheme { + LazyColumn( + modifier = Modifier.padding( + start = dimensionResource(R.dimen.spacing_8), + end = dimensionResource(R.dimen.spacing_8), + top = dimensionResource(R.dimen.spacing_8), + bottom = dimensionResource(R.dimen.spacing_128) + ) + ) { + items(uiModels) { model -> + when (model) { + is OverdueUiModel.Patient -> { + OverduePatientListItem( + modifier = Modifier.padding(bottom = dimensionResource(R.dimen.spacing_8)), + appointmentUuid = model.appointmentUuid, + patientUuid = model.patientUuid, + name = model.name, + gender = model.gender, + age = model.age, + phoneNumber = model.phoneNumber, + overdueDays = model.overdueDays, + villageName = model.villageName, + isOverdueSelectAndDownloadEnabled = model.isOverdueSelectAndDownloadEnabled, + isAppointmentSelected = model.isAppointmentSelected, + isEligibleForReassignment = model.isEligibleForReassignment, + onCallClicked = onCallClicked, + onRowClicked = onRowClicked, + onCheckboxClicked = onCheckboxClicked + ) + } + + is OverdueUiModel.Header -> { + OverdueSectionHeader( + headerTextRes = model.headerTextRes, + count = model.count, + isExpanded = model.isOverdueSectionHeaderExpanded, + overdueAppointmentSectionTitle = model.overdueAppointmentSectionTitle, + locale = model.locale, + onClick = onSectionHeaderClick + ) + } + + is OverdueUiModel.Footer -> { + OverdueSectionFooter( + pendingListState = model.pendingListState, + onClick = onSectionFooterClick + ) + } + + is OverdueUiModel.Divider -> { + Divider( + color = colorResource(R.color.color_on_surface_11), + modifier = Modifier.padding(dimensionResource(R.dimen.spacing_8)) + ) + } + + is OverdueUiModel.NoPendingPatients -> { + NoPendingPatients() + } + + is OverdueUiModel.SearchButton -> { + OutlinedButton( + modifier = Modifier + .fillMaxWidth(), + buttonSize = ButtonSize.Big, + icon = { + Icon( + imageVector = Icons.Outlined.Search, + contentDescription = "Search Overdue Patient" + ) + }, + onClick = onSearch, + ) { + ProvideTextStyle(value = SimpleTheme.typography.body0) { + Text(text = stringResource(id = R.string.overdue_search_patient_name_or_village)) + } + } + } + } + } + } + } +} diff --git a/app/src/main/java/org/simple/clinic/home/overdue/compose/OverduePatientListItem.kt b/app/src/main/java/org/simple/clinic/home/overdue/compose/OverduePatientListItem.kt new file mode 100644 index 00000000000..2d4ed3fcc05 --- /dev/null +++ b/app/src/main/java/org/simple/clinic/home/overdue/compose/OverduePatientListItem.kt @@ -0,0 +1,235 @@ +package org.simple.clinic.home.overdue.compose + +import androidx.compose.foundation.Image +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material.Card +import androidx.compose.material.Checkbox +import androidx.compose.material.CheckboxDefaults +import androidx.compose.material.Icon +import androidx.compose.material.IconButton +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.res.colorResource +import androidx.compose.ui.res.dimensionResource +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.pluralStringResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import org.simple.clinic.R +import org.simple.clinic.common.ui.theme.SimpleTheme +import org.simple.clinic.patient.Gender +import org.simple.clinic.patient.displayIconRes +import java.util.UUID + +@Composable +fun OverduePatientListItem( + modifier: Modifier = Modifier, + appointmentUuid: UUID, + patientUuid: UUID, + name: String, + gender: Gender, + age: Int, + phoneNumber: String?, + overdueDays: Int, + villageName: String?, + isOverdueSelectAndDownloadEnabled: Boolean, + isAppointmentSelected: Boolean, + isEligibleForReassignment: Boolean, + onCallClicked: (UUID) -> Unit, + onRowClicked: (UUID) -> Unit, + onCheckboxClicked: (UUID) -> Unit +) { + val overdueText = pluralStringResource( + id = R.plurals.overdue_list_item_appointment_overdue_days, count = overdueDays, overdueDays + ) + + Card( + modifier = modifier + .fillMaxWidth() + .clickable { onRowClicked(patientUuid) }, + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(all = dimensionResource(R.dimen.spacing_16)) + ) { + Row( + verticalAlignment = Alignment.Top, + modifier = Modifier + .fillMaxWidth() + ) { + OverduePatientListLeftIcon( + isOverdueSelectAndDownloadEnabled = isOverdueSelectAndDownloadEnabled, + isAppointmentSelected = isAppointmentSelected, + gender = gender, + appointmentUuid = appointmentUuid, + onCheckboxClicked = onCheckboxClicked + ) + + Column( + modifier = Modifier + .weight(1f) + .padding(horizontal = dimensionResource(R.dimen.spacing_12)), + ) { + Text( + text = stringResource(R.string.overdue_list_item_name_age, name, age), + style = SimpleTheme.typography.body0Medium, + color = SimpleTheme.colors.material.primary + ) + + EligibleForReassignmentView(isEligibleForReassignment) + + PatientVillageView(villageName) + + Text( + modifier = Modifier + .padding(top = dimensionResource(R.dimen.spacing_12)), + text = overdueText, + style = SimpleTheme.typography.material.body2, + color = SimpleTheme.colors.material.error, + ) + } + + OverduePatientListItemRightButton( + modifier = Modifier.align(alignment = Alignment.CenterVertically), + patientUuid = patientUuid, + phoneNumber = phoneNumber, + onCallClicked = onCallClicked + ) + } + } + } +} + +@Composable +private fun EligibleForReassignmentView( + isEligibleForReassignment: Boolean +) { + if (isEligibleForReassignment) { + Row( + modifier = Modifier + .padding(top = dimensionResource(R.dimen.spacing_4)), + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + modifier = Modifier.size(dimensionResource(R.dimen.spacing_16)), + painter = painterResource(id = R.drawable.ic_facility_reassignment), + contentDescription = null, + tint = Color.Unspecified, + ) + Text( + modifier = Modifier.padding(start = dimensionResource(R.dimen.spacing_4)), + text = stringResource(R.string.patient_facility_reassignment), + style = SimpleTheme.typography.material.body2, + color = colorResource(id = org.simple.clinic.common.R.color.simple_green_500) + ) + } + } +} + +@Composable +private fun PatientVillageView( + villageName: String? +) { + if (!villageName.isNullOrBlank()) { + Row( + modifier = Modifier + .padding(top = dimensionResource(R.dimen.spacing_12)), + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + modifier = Modifier.padding(end = dimensionResource(R.dimen.spacing_8)), + text = stringResource(R.string.overdue_list_item_village), + style = SimpleTheme.typography.body2Bold, + color = SimpleTheme.colors.material.onSurface.copy(alpha = 0.67f) + ) + Text( + text = villageName, + style = SimpleTheme.typography.material.body2, + color = SimpleTheme.colors.material.onSurface.copy(alpha = 0.67f) + ) + } + } +} + +@Composable +fun OverduePatientListLeftIcon( + isOverdueSelectAndDownloadEnabled: Boolean, + isAppointmentSelected: Boolean, + appointmentUuid: UUID, + gender: Gender, + onCheckboxClicked: (UUID) -> Unit, +) { + if (isOverdueSelectAndDownloadEnabled) { + Checkbox( + modifier = Modifier.size(24.dp), + checked = isAppointmentSelected, + colors = CheckboxDefaults.colors( + checkedColor = SimpleTheme.colors.material.primary, + uncheckedColor = SimpleTheme.colors.material.onSurface.copy(alpha = 0.67f), + ), + onCheckedChange = { onCheckboxClicked(appointmentUuid) }, + ) + } else { + Image( + painter = painterResource(id = gender.displayIconRes), + contentDescription = null, + modifier = Modifier.size(24.dp) + ) + } +} + +@Composable +fun OverduePatientListItemRightButton( + modifier: Modifier, + patientUuid: UUID, + phoneNumber: String?, + onCallClicked: (UUID) -> Unit +) { + IconButton( + onClick = { onCallClicked(patientUuid) }, + modifier = modifier + .size(40.dp) + ) { + Image( + painter = painterResource( + id = if (phoneNumber.isNullOrBlank()) R.drawable.ic_overdue_no_phone_number + else R.drawable.ic_overdue_call + ), + contentDescription = null, + ) + } +} + +@Preview +@Composable +private fun OverduePatientListItemPreview() { + SimpleTheme { + OverduePatientListItem( + appointmentUuid = UUID.fromString("770895ad-0db8-42bf-ba3e-df78bf1b4706"), + patientUuid = UUID.fromString("c6c6b987-86c9-4334-aa30-06ca0f02a70e"), + name = "Ali", + gender = Gender.Male, + age = 44, + phoneNumber = "9876543210", + overdueDays = 43, + villageName = "New Village", + isOverdueSelectAndDownloadEnabled = false, + isAppointmentSelected = false, + isEligibleForReassignment = true, + onCallClicked = {}, + onRowClicked = {}, + onCheckboxClicked = {} + ) + } +} + diff --git a/app/src/main/java/org/simple/clinic/home/overdue/compose/OverdueSectionFooter.kt b/app/src/main/java/org/simple/clinic/home/overdue/compose/OverdueSectionFooter.kt new file mode 100644 index 00000000000..bc3ea5798d6 --- /dev/null +++ b/app/src/main/java/org/simple/clinic/home/overdue/compose/OverdueSectionFooter.kt @@ -0,0 +1,33 @@ +package org.simple.clinic.home.overdue.compose + +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.heightIn +import androidx.compose.material.Text +import androidx.compose.material.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import org.simple.clinic.R +import org.simple.clinic.home.overdue.PendingListState + +@Composable +fun OverdueSectionFooter( + modifier: Modifier = Modifier, + pendingListState: PendingListState, + onClick: () -> Unit, +) { + val buttonTextResource = when (pendingListState) { + PendingListState.SEE_ALL -> R.string.overdue_pending_list_button_see_less + PendingListState.SEE_LESS -> R.string.overdue_pending_list_button_see_all + } + + TextButton( + onClick = onClick, + modifier = modifier + .fillMaxWidth() + .heightIn(min = 48.dp) + ) { + Text(text = stringResource(id = buttonTextResource).uppercase()) + } +} diff --git a/app/src/main/java/org/simple/clinic/home/overdue/compose/OverdueSectionHeader.kt b/app/src/main/java/org/simple/clinic/home/overdue/compose/OverdueSectionHeader.kt new file mode 100644 index 00000000000..77508ad9822 --- /dev/null +++ b/app/src/main/java/org/simple/clinic/home/overdue/compose/OverdueSectionHeader.kt @@ -0,0 +1,85 @@ +package org.simple.clinic.home.overdue.compose + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material.Icon +import androidx.compose.material.Text +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.ChevronRight +import androidx.compose.material.icons.outlined.KeyboardArrowDown +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.dimensionResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import org.simple.clinic.R +import org.simple.clinic.common.ui.theme.SimpleTheme +import org.simple.clinic.home.overdue.OverdueAppointmentSectionTitle +import java.util.Locale + +@Composable +fun OverdueSectionHeader( + modifier: Modifier = Modifier, + headerTextRes: Int, + count: Int, + isExpanded: Boolean, + overdueAppointmentSectionTitle: OverdueAppointmentSectionTitle, + locale: Locale, + onClick: (OverdueAppointmentSectionTitle) -> Unit +) { + val chevronIcon = if (isExpanded) { + Icons.Outlined.KeyboardArrowDown + } else { + Icons.Outlined.ChevronRight + } + + Row( + modifier = modifier + .fillMaxWidth() + .clickable { onClick(overdueAppointmentSectionTitle) } + .padding( + horizontal = dimensionResource(R.dimen.spacing_8), + vertical = dimensionResource(R.dimen.spacing_16) + ), + verticalAlignment = Alignment.CenterVertically + ) { + + Text( + text = stringResource(headerTextRes).uppercase(locale), + modifier = Modifier.weight(1f), + style = SimpleTheme.typography.tag.copy(color = SimpleTheme.colors.onSurface67) + ) + + Text( + modifier = Modifier.padding(horizontal = dimensionResource(R.dimen.spacing_8)), + text = String.format(locale, "%d", count), + style = SimpleTheme.typography.tag.copy(color = SimpleTheme.colors.material.primary) + ) + + Icon( + chevronIcon, + tint = SimpleTheme.colors.material.primary, + contentDescription = null, + ) + } +} + +@Preview +@Composable +private fun OverdueSectionHeaderPreview(modifier: Modifier = Modifier) { + SimpleTheme { + OverdueSectionHeader( + headerTextRes = R.string.overdue_agreed_to_visit_call_header, + count = 40, + isExpanded = false, + overdueAppointmentSectionTitle = OverdueAppointmentSectionTitle.MORE_THAN_A_YEAR_OVERDUE, + locale = Locale.US + ) { + } + } +} + diff --git a/app/src/main/java/org/simple/clinic/home/overdue/compose/OverdueUiModel.kt b/app/src/main/java/org/simple/clinic/home/overdue/compose/OverdueUiModel.kt new file mode 100644 index 00000000000..6ee2e1ab260 --- /dev/null +++ b/app/src/main/java/org/simple/clinic/home/overdue/compose/OverdueUiModel.kt @@ -0,0 +1,42 @@ +package org.simple.clinic.home.overdue.compose + +import androidx.annotation.StringRes +import org.simple.clinic.home.overdue.OverdueAppointmentSectionTitle +import org.simple.clinic.home.overdue.PendingListState +import org.simple.clinic.patient.Gender +import java.util.Locale +import java.util.UUID + +sealed class OverdueUiModel { + data class Patient( + val appointmentUuid: UUID, + val patientUuid: UUID, + val name: String, + val gender: Gender, + val age: Int, + val phoneNumber: String? = null, + val overdueDays: Int, + val villageName: String?, + val isOverdueSelectAndDownloadEnabled: Boolean, + val isAppointmentSelected: Boolean, + val isEligibleForReassignment: Boolean, + ) : OverdueUiModel() + + data class Header( + @StringRes val headerTextRes: Int, + val count: Int, + val isOverdueSectionHeaderExpanded: Boolean, + val overdueAppointmentSectionTitle: OverdueAppointmentSectionTitle, + val locale: Locale, + ) : OverdueUiModel() + + data class Footer( + val pendingListState: PendingListState, + ) : OverdueUiModel() + + data object Divider : OverdueUiModel() + + data object NoPendingPatients : OverdueUiModel() + + data object SearchButton : OverdueUiModel() +} diff --git a/app/src/main/java/org/simple/clinic/home/overdue/OverdueAppointmentListItem.kt b/app/src/main/java/org/simple/clinic/home/overdue/compose/OverdueUiModelMapper.kt similarity index 54% rename from app/src/main/java/org/simple/clinic/home/overdue/OverdueAppointmentListItem.kt rename to app/src/main/java/org/simple/clinic/home/overdue/compose/OverdueUiModelMapper.kt index fb7556835b8..215c3ed96f0 100644 --- a/app/src/main/java/org/simple/clinic/home/overdue/OverdueAppointmentListItem.kt +++ b/app/src/main/java/org/simple/clinic/home/overdue/compose/OverdueUiModelMapper.kt @@ -1,40 +1,24 @@ -package org.simple.clinic.home.overdue - -import android.graphics.Rect -import android.view.TouchDelegate -import android.view.View -import androidx.annotation.StringRes -import androidx.recyclerview.widget.DiffUtil -import io.reactivex.subjects.Subject +package org.simple.clinic.home.overdue.compose + import org.simple.clinic.R -import org.simple.clinic.databinding.ListItemOverdueListSectionHeaderBinding -import org.simple.clinic.databinding.ListItemOverduePatientBinding -import org.simple.clinic.databinding.ListItemOverduePendingListFooterBinding -import org.simple.clinic.databinding.ListItemSearchOverduePatientButtonBinding +import org.simple.clinic.home.overdue.OverdueAppointment import org.simple.clinic.home.overdue.OverdueAppointmentSectionTitle.AGREED_TO_VISIT import org.simple.clinic.home.overdue.OverdueAppointmentSectionTitle.MORE_THAN_A_YEAR_OVERDUE import org.simple.clinic.home.overdue.OverdueAppointmentSectionTitle.PENDING_TO_CALL import org.simple.clinic.home.overdue.OverdueAppointmentSectionTitle.REMIND_TO_CALL import org.simple.clinic.home.overdue.OverdueAppointmentSectionTitle.REMOVED_FROM_OVERDUE +import org.simple.clinic.home.overdue.OverdueAppointmentSections +import org.simple.clinic.home.overdue.OverdueListSectionStates import org.simple.clinic.home.overdue.PendingListState.SEE_ALL import org.simple.clinic.home.overdue.PendingListState.SEE_LESS import org.simple.clinic.patient.Answer -import org.simple.clinic.patient.Gender -import org.simple.clinic.patient.displayIconRes import org.simple.clinic.util.UserClock -import org.simple.clinic.widgets.ItemAdapter -import org.simple.clinic.widgets.UiEvent -import org.simple.clinic.widgets.dp -import org.simple.clinic.widgets.executeOnNextMeasure -import org.simple.clinic.widgets.recyclerview.BindingViewHolder -import org.simple.clinic.widgets.setCompoundDrawableEnd -import org.simple.clinic.widgets.visibleOrGone import java.time.LocalDate import java.time.temporal.ChronoUnit import java.util.Locale import java.util.UUID -sealed class OverdueAppointmentListItem : ItemAdapter.Item { +class OverdueUiModelMapper { companion object { @@ -48,8 +32,10 @@ sealed class OverdueAppointmentListItem : ItemAdapter.Item { selectedOverdueAppointments: Set, isPatientReassignmentFeatureEnabled: Boolean, locale: Locale, - ): List { - val searchOverduePatientsButtonListItem = searchOverduePatientItem(isOverdueInstantSearchEnabled) + ): List { + val searchOverduePatientsButtonListItem = searchOverduePatientItem( + isOverdueInstantSearchEnabled, + ) val pendingToCallListItem = pendingToCallItem( overdueAppointmentSections, @@ -59,8 +45,9 @@ sealed class OverdueAppointmentListItem : ItemAdapter.Item { isOverdueSelectAndDownloadEnabled, selectedOverdueAppointments, isPatientReassignmentFeatureEnabled, - locale + locale, ) + val agreedToVisitListItem = agreedToVisitItem( overdueAppointmentSections, clock, @@ -68,8 +55,9 @@ sealed class OverdueAppointmentListItem : ItemAdapter.Item { isOverdueSelectAndDownloadEnabled, selectedOverdueAppointments, isPatientReassignmentFeatureEnabled, - locale + locale, ) + val remindToCallListItem = remindToCallItem( overdueAppointmentSections, clock, @@ -77,8 +65,9 @@ sealed class OverdueAppointmentListItem : ItemAdapter.Item { isOverdueSelectAndDownloadEnabled, selectedOverdueAppointments, isPatientReassignmentFeatureEnabled, - locale + locale, ) + val removedFromOverdueListItem = removedFromOverdueItem( overdueAppointmentSections, clock, @@ -86,8 +75,9 @@ sealed class OverdueAppointmentListItem : ItemAdapter.Item { isOverdueSelectAndDownloadEnabled, selectedOverdueAppointments, isPatientReassignmentFeatureEnabled, - locale + locale, ) + val moreThanAnOneYearOverdueListItem = moreThanAnOneYearOverdueItem( overdueAppointmentSections, clock, @@ -95,9 +85,10 @@ sealed class OverdueAppointmentListItem : ItemAdapter.Item { isOverdueSelectAndDownloadEnabled, selectedOverdueAppointments, isPatientReassignmentFeatureEnabled, - locale + locale, ) - val dividerListItem = listOf(Divider) + + val dividerListItem = listOf(OverdueUiModel.Divider) return searchOverduePatientsButtonListItem + pendingToCallListItem + dividerListItem + @@ -107,8 +98,15 @@ sealed class OverdueAppointmentListItem : ItemAdapter.Item { moreThanAnOneYearOverdueListItem } - private fun searchOverduePatientItem(isOverdueInstantSearchEnabled: Boolean) = - if (isOverdueInstantSearchEnabled) listOf(SearchOverduePatientsButtonItem) else emptyList() + + private fun searchOverduePatientItem( + isOverdueInstantSearchEnabled: Boolean, + ): List = + if (isOverdueInstantSearchEnabled) { + listOf(OverdueUiModel.SearchButton) + } else { + emptyList() + } private fun moreThanAnOneYearOverdueItem( overdueAppointmentSections: OverdueAppointmentSections, @@ -118,13 +116,14 @@ sealed class OverdueAppointmentListItem : ItemAdapter.Item { selectedOverdueAppointments: Set, isPatientReassignmentFeatureEnabled: Boolean, locale: Locale, - ): List { + ): List { val moreThanAnOneYearOverdueHeader = listOf( - OverdueSectionHeader(R.string.overdue_no_visit_in_one_year_call_header, + OverdueUiModel.Header( + R.string.overdue_no_visit_in_one_year_call_header, overdueAppointmentSections.moreThanAnYearOverdueAppointments.size, overdueListSectionStates.isMoreThanAnOneYearOverdueHeader, MORE_THAN_A_YEAR_OVERDUE, - locale + locale, )) val moreThanAnOneYearOverdueListItems = expandedOverdueAppointmentList( @@ -147,13 +146,14 @@ sealed class OverdueAppointmentListItem : ItemAdapter.Item { selectedOverdueAppointments: Set, isPatientReassignmentFeatureEnabled: Boolean, locale: Locale, - ): List { + ): List { val removedFromOverdueListHeader = listOf( - OverdueSectionHeader(R.string.overdue_removed_from_list_call_header, + OverdueUiModel.Header( + R.string.overdue_removed_from_list_call_header, overdueAppointmentSections.removedFromOverdueAppointments.size, overdueListSectionStates.isRemovedFromOverdueListHeaderExpanded, REMOVED_FROM_OVERDUE, - locale + locale, )) val removedFromOverdueListItems = expandedOverdueAppointmentList( @@ -176,13 +176,14 @@ sealed class OverdueAppointmentListItem : ItemAdapter.Item { selectedOverdueAppointments: Set, isPatientReassignmentFeatureEnabled: Boolean, locale: Locale, - ): List { + ): List { val remindToCallHeader = listOf( - OverdueSectionHeader(R.string.overdue_remind_to_call_header, + OverdueUiModel.Header( + R.string.overdue_remind_to_call_header, overdueAppointmentSections.remindToCallLaterAppointments.size, overdueListSectionStates.isRemindToCallLaterHeaderExpanded, REMIND_TO_CALL, - locale + locale, )) val remindToCallListItems = expandedOverdueAppointmentList( @@ -205,13 +206,14 @@ sealed class OverdueAppointmentListItem : ItemAdapter.Item { selectedOverdueAppointments: Set, isPatientReassignmentFeatureEnabled: Boolean, locale: Locale, - ): List { + ): List { val agreedToVisitHeader = listOf( - OverdueSectionHeader(R.string.overdue_agreed_to_visit_call_header, + OverdueUiModel.Header( + R.string.overdue_agreed_to_visit_call_header, overdueAppointmentSections.agreedToVisitAppointments.size, overdueListSectionStates.isAgreedToVisitHeaderExpanded, AGREED_TO_VISIT, - locale + locale, )) val agreedToVisitListItems = expandedOverdueAppointmentList( @@ -235,15 +237,17 @@ sealed class OverdueAppointmentListItem : ItemAdapter.Item { selectedOverdueAppointments: Set, isPatientReassignmentFeatureEnabled: Boolean, locale: Locale, - ): List { + ): List { val pendingAppointments = overdueAppointmentSections.pendingAppointments val pendingToCallHeader = listOf( - OverdueSectionHeader(R.string.overdue_pending_to_call_header, + OverdueUiModel.Header( + R.string.overdue_pending_to_call_header, overdueAppointmentSections.pendingAppointments.size, overdueListSectionStates.isPendingHeaderExpanded, PENDING_TO_CALL, - locale + locale, )) + val pendingAppointmentsContent = generatePendingAppointmentsContent( overdueAppointmentSections, clock, @@ -255,7 +259,13 @@ sealed class OverdueAppointmentListItem : ItemAdapter.Item { ) val showPendingListFooter = pendingAppointments.size > pendingListDefaultStateSize && overdueListSectionStates.isPendingHeaderExpanded - val pendingListFooterItem = if (showPendingListFooter) listOf(PendingListFooter(overdueListSectionStates.pendingListState)) else emptyList() + val pendingListFooterItem = if (showPendingListFooter) { + listOf(OverdueUiModel.Footer( + pendingListState = overdueListSectionStates.pendingListState, + )) + } else { + emptyList() + } return pendingToCallHeader + pendingAppointmentsContent + pendingListFooterItem } @@ -268,7 +278,7 @@ sealed class OverdueAppointmentListItem : ItemAdapter.Item { isOverdueSelectAndDownloadEnabled: Boolean, selectedOverdueAppointments: Set, isPatientReassignmentFeatureEnabled: Boolean, - ): List { + ): List { val pendingAppointmentsList = when (overdueListSectionStates.pendingListState) { SEE_LESS -> overdueAppointmentSections.pendingAppointments.take(pendingListDefaultStateSize) SEE_ALL -> overdueAppointmentSections.pendingAppointments @@ -280,10 +290,11 @@ sealed class OverdueAppointmentListItem : ItemAdapter.Item { clock, isOverdueSelectAndDownloadEnabled, selectedOverdueAppointments, - isPatientReassignmentFeatureEnabled) + isPatientReassignmentFeatureEnabled, + ) return if (pendingAppointmentsList.isEmpty() && overdueListSectionStates.isPendingHeaderExpanded) { - listOf(NoPendingPatients) + listOf(OverdueUiModel.NoPendingPatients) } else { expandedPendingAppointmentList } @@ -296,7 +307,7 @@ sealed class OverdueAppointmentListItem : ItemAdapter.Item { isOverdueSelectAndDownloadEnabled: Boolean, selectedOverdueAppointments: Set, isPatientReassignmentFeatureEnabled: Boolean, - ): List { + ): List { return if (isListExpanded) { overdueAppointment.map { val isAppointmentSelected = selectedOverdueAppointments.contains(it.appointment.uuid) @@ -319,8 +330,8 @@ sealed class OverdueAppointmentListItem : ItemAdapter.Item { isOverdueSelectAndDownloadEnabled: Boolean, isAppointmentSelected: Boolean, isPatientReassignmentFeatureEnabled: Boolean, - ): OverdueAppointmentListItem { - return OverdueAppointmentRow( + ): OverdueUiModel { + return OverdueUiModel.Patient( appointmentUuid = overdueAppointment.appointment.uuid, patientUuid = overdueAppointment.appointment.patientUuid, name = overdueAppointment.fullName, @@ -331,7 +342,7 @@ sealed class OverdueAppointmentListItem : ItemAdapter.Item { villageName = overdueAppointment.patientAddress.colonyOrVillage, isOverdueSelectAndDownloadEnabled = isOverdueSelectAndDownloadEnabled, isAppointmentSelected = isAppointmentSelected, - isEligibleForReassignment = overdueAppointment.eligibleForReassignment == Answer.Yes && isPatientReassignmentFeatureEnabled, + isEligibleForReassignment = (overdueAppointment.eligibleForReassignment == Answer.Yes) && isPatientReassignmentFeatureEnabled, ) } @@ -342,198 +353,5 @@ sealed class OverdueAppointmentListItem : ItemAdapter.Item { return ChronoUnit.DAYS.between(date, LocalDate.now(clock)).toInt() } } - - data class OverdueAppointmentRow( - val appointmentUuid: UUID, - val patientUuid: UUID, - val name: String, - val gender: Gender, - val age: Int, - val phoneNumber: String? = null, - val overdueDays: Int, - val villageName: String?, - val isOverdueSelectAndDownloadEnabled: Boolean, - val isAppointmentSelected: Boolean, - val isEligibleForReassignment: Boolean, - ) : OverdueAppointmentListItem() { - - override fun layoutResId(): Int = R.layout.list_item_overdue_patient - - override fun render(holder: BindingViewHolder, subject: Subject) { - val binding = holder.binding as ListItemOverduePatientBinding - setupEvents(binding, subject) - bindUi(holder) - } - - private fun setupEvents( - binding: ListItemOverduePatientBinding, - eventSubject: Subject - ) { - binding.callButton.setOnClickListener { - eventSubject.onNext(CallPatientClicked(patientUuid)) - } - - binding.overdueCardView.setOnClickListener { - eventSubject.onNext(OverduePatientClicked(patientUuid)) - } - - binding.checkbox.setOnClickListener { - eventSubject.onNext(OverdueAppointmentCheckBoxClicked(appointmentUuid)) - } - } - - private fun bindUi(holder: BindingViewHolder) { - val binding = holder.binding as ListItemOverduePatientBinding - val context = holder.itemView.context - - binding.patientNameTextView.text = context.getString(R.string.overdue_list_item_name_age, name, age.toString()) - binding.patientGenderIcon.setImageResource(gender.displayIconRes) - binding.villageTextView.text = villageName.orEmpty() - binding.villageTextView.visibleOrGone(isVisible = !villageName.isNullOrBlank()) - - val callButtonDrawable = if (phoneNumber.isNullOrBlank()) { - R.drawable.ic_overdue_no_phone_number - } else { - R.drawable.ic_overdue_call - } - binding.callButton.setImageResource(callButtonDrawable) - increaseCallButtonTapArea(callButton = binding.callButton) - - binding.overdueDaysTextView.text = context.resources.getQuantityString( - R.plurals.overdue_list_item_appointment_overdue_days, - overdueDays, - "$overdueDays" - ) - - binding.checkbox.isChecked = isAppointmentSelected - - binding.checkbox.visibleOrGone(isOverdueSelectAndDownloadEnabled) - binding.patientGenderIcon.visibleOrGone(!isOverdueSelectAndDownloadEnabled) - binding.facilityReassignmentView.root.visibleOrGone(isEligibleForReassignment) - } - - private fun increaseCallButtonTapArea(callButton: View) { - val parent = callButton.parent as View - - parent.executeOnNextMeasure { - val touchableArea = Rect() - callButton.getHitRect(touchableArea) - - val buttonHeight = callButton.height - val parentHeight = parent.height - - val verticalSpace = (parentHeight - buttonHeight) / 2 - val horizontalSpace = 24.dp - - with(touchableArea) { - left -= horizontalSpace - top -= verticalSpace - right += horizontalSpace - bottom += verticalSpace - } - - parent.touchDelegate = TouchDelegate(touchableArea, callButton) - } - } - } - - data class OverdueSectionHeader( - @StringRes val headerText: Int, - val count: Int, - val isOverdueSectionHeaderExpanded: Boolean, - val overdueAppointmentSectionTitle: OverdueAppointmentSectionTitle, - val locale: Locale, - ) : OverdueAppointmentListItem() { - override fun layoutResId(): Int = R.layout.list_item_overdue_list_section_header - - override fun render(holder: BindingViewHolder, subject: Subject) { - val binding = holder.binding as ListItemOverdueListSectionHeaderBinding - - binding.overdueSectionHeaderTextView.setText(headerText) - binding.overdueSectionHeaderIcon.text = String.format(locale = locale, format = "%d", count) - binding.root.setOnClickListener { - subject.onNext(ChevronClicked(overdueAppointmentSectionTitle)) - } - - if (isOverdueSectionHeaderExpanded) { - binding.overdueSectionHeaderIcon.setCompoundDrawableEnd(R.drawable.ic_chevron_down_24) - } else { - binding.overdueSectionHeaderIcon.setCompoundDrawableEnd(R.drawable.ic_chevron_right_24px) - } - } - } - - data class PendingListFooter(val pendingListState: PendingListState) : OverdueAppointmentListItem() { - override fun layoutResId(): Int = R.layout.list_item_overdue_pending_list_footer - - override fun render(holder: BindingViewHolder, subject: Subject) { - val binding = holder.binding as ListItemOverduePendingListFooterBinding - - binding.overduePendingSeeAllOrLessButton.setOnClickListener { - subject.onNext(PendingListFooterClicked) - } - - binding.overduePendingSeeAllOrLessButton.setText(pendingListFooterStringRes()) - } - - private fun pendingListFooterStringRes() = when (pendingListState) { - SEE_ALL -> R.string.overdue_pending_list_button_see_less - SEE_LESS -> R.string.overdue_pending_list_button_see_all - } - } - - data object NoPendingPatients : OverdueAppointmentListItem() { - - override fun layoutResId(): Int = R.layout.list_item_no_pending_patients - - override fun render(holder: BindingViewHolder, subject: Subject) { - /* no-op */ - } - } - - data object SearchOverduePatientsButtonItem : OverdueAppointmentListItem() { - - override fun layoutResId(): Int = R.layout.list_item_search_overdue_patient_button - - override fun render(holder: BindingViewHolder, subject: Subject) { - val binding = holder.binding as ListItemSearchOverduePatientButtonBinding - binding.root.setOnClickListener { - subject.onNext(OverdueSearchButtonClicked) - } - } - } - - data object Divider : OverdueAppointmentListItem() { - - override fun layoutResId(): Int = R.layout.list_item_divider - - override fun render(holder: BindingViewHolder, subject: Subject) { - /* no-op */ - } - } - - class DiffCallback : DiffUtil.ItemCallback() { - - override fun areItemsTheSame( - oldItem: OverdueAppointmentListItem, - newItem: OverdueAppointmentListItem - ): Boolean { - return when { - oldItem is OverdueAppointmentRow && newItem is OverdueAppointmentRow -> oldItem.patientUuid == newItem.patientUuid - oldItem is OverdueSectionHeader && newItem is OverdueSectionHeader -> oldItem.headerText == newItem.headerText - oldItem is PendingListFooter && newItem is PendingListFooter -> oldItem.pendingListState == newItem.pendingListState - oldItem is NoPendingPatients && newItem is NoPendingPatients -> true - oldItem is Divider && newItem is Divider -> true - oldItem is SearchOverduePatientsButtonItem && newItem is SearchOverduePatientsButtonItem -> true - else -> false - } - } - - override fun areContentsTheSame( - oldItem: OverdueAppointmentListItem, - newItem: OverdueAppointmentListItem - ): Boolean { - return oldItem == newItem - } - } } + diff --git a/app/src/main/java/org/simple/clinic/summary/PatientSummaryScreen.kt b/app/src/main/java/org/simple/clinic/summary/PatientSummaryScreen.kt index 32b5d3edae8..a4d99773a7e 100644 --- a/app/src/main/java/org/simple/clinic/summary/PatientSummaryScreen.kt +++ b/app/src/main/java/org/simple/clinic/summary/PatientSummaryScreen.kt @@ -4,8 +4,6 @@ import android.annotation.SuppressLint import android.content.Context import android.os.Bundle import android.os.Parcelable -import android.text.SpannedString -import android.text.style.TextAppearanceSpan import android.view.LayoutInflater import android.view.View import android.view.View.GONE @@ -19,8 +17,6 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.platform.ViewCompositionStrategy import androidx.compose.ui.unit.dp -import androidx.core.text.buildSpannedString -import androidx.core.text.inSpans import androidx.dynamicanimation.animation.DynamicAnimation import androidx.transition.AutoTransition import androidx.transition.Transition @@ -59,12 +55,7 @@ import org.simple.clinic.navigation.v2.HandlesBack import org.simple.clinic.navigation.v2.Router import org.simple.clinic.navigation.v2.Succeeded import org.simple.clinic.navigation.v2.fragments.BaseScreen -import org.simple.clinic.patient.Gender -import org.simple.clinic.patient.PatientAddress -import org.simple.clinic.patient.PatientPhoneNumber -import org.simple.clinic.patient.businessid.BusinessId import org.simple.clinic.patient.businessid.Identifier -import org.simple.clinic.patient.displayLetterRes import org.simple.clinic.patientattribute.entry.BMIEntrySheet import org.simple.clinic.reassignpatient.ReassignPatientSheet import org.simple.clinic.reassignpatient.ReassignPatientSheetOpenedFrom @@ -77,11 +68,11 @@ import org.simple.clinic.summary.compose.StatinNudge import org.simple.clinic.summary.linkId.LinkIdWithPatientSheet.LinkIdWithPatientSheetKey import org.simple.clinic.summary.teleconsultation.contactdoctor.ContactDoctorSheet import org.simple.clinic.summary.teleconsultation.messagebuilder.LongTeleconsultMessageBuilder_Old +import org.simple.clinic.summary.ui.PatientSummaryToolbar import org.simple.clinic.summary.updatephone.UpdatePhoneNumberDialog import org.simple.clinic.teleconsultlog.teleconsultrecord.screen.TeleconsultRecordScreenKey import org.simple.clinic.util.UserClock import org.simple.clinic.util.applyInsetsBottomPadding -import org.simple.clinic.util.applyStatusBarPadding import org.simple.clinic.util.messagesender.WhatsAppMessageSender import org.simple.clinic.util.setFragmentResultListener import org.simple.clinic.util.toLocalDateAtZone @@ -89,7 +80,6 @@ import org.simple.clinic.widgets.UiEvent import org.simple.clinic.widgets.hideKeyboard import org.simple.clinic.widgets.scrollToChild import org.simple.clinic.widgets.spring -import org.simple.clinic.widgets.visibleOrGone import java.time.format.DateTimeFormatter import java.util.UUID import java.util.concurrent.TimeUnit @@ -136,9 +126,6 @@ class PatientSummaryScreen : private val summaryViewsContainer get() = binding.summaryViewsContainer - private val editPatientButton - get() = binding.editPatientButton - private val doneButton get() = binding.doneButton @@ -151,30 +138,12 @@ class PatientSummaryScreen : private val logTeleconsultButtonFrame get() = binding.logTeleconsultButtonFrame - private val backButton - get() = binding.backButton - - private val contactTextView - get() = binding.contactTextView - private val facilityNameAndDateTextView get() = binding.facilityNameAndDateTextView private val labelRegistered get() = binding.labelRegistered - private val addressTextView - get() = binding.addressTextView - - private val fullNameTextView - get() = binding.fullNameTextView - - private val bpPassportTextView - get() = binding.bpPassportTextView - - private val alternateIdTextView - get() = binding.alternateIdTextView - private val doneButtonFrame get() = binding.doneButtonFrame @@ -258,8 +227,6 @@ class PatientSummaryScreen : backClicks(), doneClicks(), bloodPressureSaves(), - editButtonClicks(), - phoneNumberClicks(), contactDoctorClicks(), hotEvents, logTeleconsultClicks(), @@ -323,7 +290,6 @@ class PatientSummaryScreen : // Not sure why but the keyboard stays visible when coming from search. rootLayout.hideKeyboard() - appbar.applyStatusBarPadding() doneButtonFrame.applyInsetsBottomPadding() logTeleconsultButtonFrame.applyInsetsBottomPadding() @@ -411,8 +377,6 @@ class PatientSummaryScreen : } } - private fun editButtonClicks(): Observable = editPatientButton.clicks().map { PatientSummaryEditClicked } - private fun createEditPatientScreenKey( patientSummaryProfile: PatientSummaryProfile ): EditPatientScreen.Key { @@ -435,9 +399,7 @@ class PatientSummaryScreen : private fun logTeleconsultClicks() = logTeleconsultButton.clicks().map { LogTeleconsultClicked } private fun backClicks(): Observable { - return backButton - .clicks() - .mergeWith(hardwareBackClicks) + return hardwareBackClicks .throttleFirst(500, TimeUnit.MILLISECONDS) .map { PatientSummaryBackClicked(screenKey.patientUuid, screenKey.screenCreatedTimestamp) @@ -481,21 +443,44 @@ class PatientSummaryScreen : } } - private fun phoneNumberClicks(): Observable { - return contactTextView.clicks().map { ContactPatientClicked } - } - @SuppressLint("SetTextI18n") override fun populatePatientProfile(patientSummaryProfile: PatientSummaryProfile) { val patient = patientSummaryProfile.patient val ageValue = patient.ageDetails.estimateAge(userClock) - displayNameGenderAge(patient.fullName, patient.gender, ageValue) displayRegistrationFacilityName(patientSummaryProfile) - displayPhoneNumber(patientSummaryProfile.phoneNumber) - displayPatientAddress(patientSummaryProfile.address) - displayBpPassport(patientSummaryProfile.bpPassport) - displayAlternativeId(patientSummaryProfile.alternativeId, patientSummaryProfile.bpPassport != null) + } + + override fun renderPatientSummaryToolbar(patientSummaryProfile: PatientSummaryProfile) { + val patient = patientSummaryProfile.patient + val ageValue = patient.ageDetails.estimateAge(userClock) + + appbar.apply { + setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) + + setContent { + SimpleTheme { + PatientSummaryToolbar( + patientName = patient.fullName, + gender = patient.gender, + age = ageValue, + address = patientSummaryProfile.address.completeAddress, + phoneNumber = patientSummaryProfile.phoneNumber, + bpPassport = patientSummaryProfile.bpPassport, + alternativeId = patientSummaryProfile.alternativeId, + onBack = { + hotEvents.onNext(PatientSummaryBackClicked(screenKey.patientUuid, screenKey.screenCreatedTimestamp)) + }, + onContact = { + hotEvents.onNext(ContactPatientClicked) + }, + onEditPatient = { + hotEvents.onNext(PatientSummaryEditClicked) + } + ) + } + } + } } private fun displayRegistrationFacilityName(patientSummaryProfile: PatientSummaryProfile) { @@ -515,69 +500,6 @@ class PatientSummaryScreen : } } - private fun displayPatientAddress(address: PatientAddress) { - addressTextView.text = address.completeAddress - } - - private fun displayPhoneNumber(phoneNumber: PatientPhoneNumber?) { - if (phoneNumber == null) { - contactTextView.visibility = GONE - - } else { - contactTextView.text = phoneNumber.number - contactTextView.visibility = VISIBLE - } - } - - private fun displayNameGenderAge(name: String, gender: Gender, age: Int) { - val genderLetter = resources.getString(gender.displayLetterRes) - fullNameTextView.text = resources.getString(R.string.patientsummary_toolbar_title, name, genderLetter, age.toString()) - } - - private fun displayBpPassport(bpPassport: BusinessId?) { - bpPassportTextView.visibleOrGone(bpPassport != null) - - bpPassportTextView.text = when (bpPassport) { - null -> "" - else -> { - val identifierNumericSpan = TextAppearanceSpan(requireContext(), R.style.TextAppearance_Simple_Body2_Numeric) - val identifier = bpPassport.identifier - val bpPassportLabel = identifier.displayType(resources) - - buildSpannedString { - append("$bpPassportLabel: ") - - inSpans(identifierNumericSpan) { - append(identifier.displayValue()) - } - } - } - } - } - - private fun displayAlternativeId(alternateId: BusinessId?, isBpPassportVisible: Boolean) { - alternateIdTextView.visibleOrGone(alternateId != null) - - alternateIdTextView.text = when (alternateId) { - null -> "" - else -> generateAlternativeId(alternateId) - } - } - - private fun generateAlternativeId(alternateId: BusinessId): SpannedString { - val alternateIdLabel = alternateId.identifier.displayType(resources) - val identifierNumericSpan = TextAppearanceSpan(requireContext(), R.style.TextAppearance_Simple_Body2_Numeric) - val identifier = alternateId.identifier - - return buildSpannedString { - append("$alternateIdLabel: ") - - inSpans(identifierNumericSpan) { - append(identifier.displayValue()) - } - } - } - override fun showScheduleAppointmentSheet( patientUuid: UUID, sheetOpenedFrom: AppointmentSheetOpenedFrom, @@ -614,10 +536,6 @@ class PatientSummaryScreen : openedFrom = screenKey)) } - override fun showEditButton() { - editPatientButton.visibility = VISIBLE - } - override fun showDiabetesView() { bloodSugarSummaryView.visibility = VISIBLE } @@ -834,7 +752,6 @@ class PatientSummaryScreen : excludeChildren(view, true) excludeTarget(R.id.newBPItemContainer, true) excludeTarget(R.id.bloodSugarItemContainer, true) - excludeTarget(R.id.drugsSummaryContainer, true) // We are doing this to wait for the router transitions to be done before we start this. startDelay = 500 } @@ -884,7 +801,6 @@ class PatientSummaryScreen : excludeChildren(view, true) excludeTarget(R.id.newBPItemContainer, true) excludeTarget(R.id.bloodSugarItemContainer, true) - excludeTarget(R.id.drugsSummaryContainer, true) } TransitionManager.beginDelayedTransition(summaryViewsContainer, transition) diff --git a/app/src/main/java/org/simple/clinic/summary/PatientSummaryScreenUi.kt b/app/src/main/java/org/simple/clinic/summary/PatientSummaryScreenUi.kt index d04f3f155c4..0b7f02eaaf3 100644 --- a/app/src/main/java/org/simple/clinic/summary/PatientSummaryScreenUi.kt +++ b/app/src/main/java/org/simple/clinic/summary/PatientSummaryScreenUi.kt @@ -3,8 +3,8 @@ package org.simple.clinic.summary import org.simple.clinic.cvdrisk.StatinInfo interface PatientSummaryScreenUi { + fun renderPatientSummaryToolbar(patientSummaryProfile: PatientSummaryProfile) fun populatePatientProfile(patientSummaryProfile: PatientSummaryProfile) - fun showEditButton() fun showDiabetesView() fun hideDiabetesView() fun showTeleconsultButton() diff --git a/app/src/main/java/org/simple/clinic/summary/PatientSummaryViewRenderer.kt b/app/src/main/java/org/simple/clinic/summary/PatientSummaryViewRenderer.kt index aa772fbfc9f..f5378bf7669 100644 --- a/app/src/main/java/org/simple/clinic/summary/PatientSummaryViewRenderer.kt +++ b/app/src/main/java/org/simple/clinic/summary/PatientSummaryViewRenderer.kt @@ -23,9 +23,9 @@ class PatientSummaryViewRenderer( with(ui) { if (model.hasLoadedPatientSummaryProfile) { populatePatientProfile(model.patientSummaryProfile!!) - showEditButton() setupUiForAssignedFacility(model) renderPatientDiedStatus(model) + renderPatientSummaryToolbar(model.patientSummaryProfile) } if (model.hasLoadedCurrentFacility) { diff --git a/app/src/main/java/org/simple/clinic/summary/prescribeddrugs/DrugSummaryItemView.kt b/app/src/main/java/org/simple/clinic/summary/prescribeddrugs/DrugSummaryItemView.kt deleted file mode 100644 index d6083623008..00000000000 --- a/app/src/main/java/org/simple/clinic/summary/prescribeddrugs/DrugSummaryItemView.kt +++ /dev/null @@ -1,40 +0,0 @@ -package org.simple.clinic.summary.prescribeddrugs - -import android.content.Context -import android.util.AttributeSet -import android.view.LayoutInflater -import android.widget.FrameLayout -import org.simple.clinic.databinding.PatientsummaryDrugItemContentBinding -import org.simple.clinic.teleconsultlog.medicinefrequency.MedicineFrequency - -class DrugSummaryItemView constructor( - context: Context, - attrs: AttributeSet -) : FrameLayout(context, attrs) { - - private var binding: PatientsummaryDrugItemContentBinding? = null - - private val prescribedDrugName - get() = binding!!.prescribedDrugName - - private val prescribedDrugDate - get() = binding!!.prescribedDrugDate - - init { - val layoutInflater = LayoutInflater.from(context) - binding = PatientsummaryDrugItemContentBinding.inflate(layoutInflater, this, true) - } - - fun render( - drugName: String, - drugDosage: String?, - drugFrequency: MedicineFrequency?, - drugDate: String - ) { - val drugWithDosageAndFrequency = listOfNotNull(drugName, drugDosage, drugFrequency) - .joinToString(separator = " ") - - prescribedDrugName.text = drugWithDosageAndFrequency - prescribedDrugDate.text = drugDate - } -} diff --git a/app/src/main/java/org/simple/clinic/summary/prescribeddrugs/DrugSummaryView.kt b/app/src/main/java/org/simple/clinic/summary/prescribeddrugs/DrugSummaryView.kt index 3e3366db144..0f82db1c6b4 100644 --- a/app/src/main/java/org/simple/clinic/summary/prescribeddrugs/DrugSummaryView.kt +++ b/app/src/main/java/org/simple/clinic/summary/prescribeddrugs/DrugSummaryView.kt @@ -3,14 +3,17 @@ package org.simple.clinic.summary.prescribeddrugs import android.content.Context import android.os.Parcelable import android.util.AttributeSet -import android.view.LayoutInflater import androidx.appcompat.app.AppCompatActivity +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.compose.ui.platform.ComposeView +import androidx.compose.ui.platform.ViewCompositionStrategy import com.google.android.material.card.MaterialCardView import io.reactivex.rxkotlin.ofType import io.reactivex.subjects.PublishSubject -import org.simple.clinic.R import org.simple.clinic.ReportAnalyticsEvents -import org.simple.clinic.databinding.DrugsSummaryViewBinding +import org.simple.clinic.common.ui.theme.SimpleTheme import org.simple.clinic.di.injector import org.simple.clinic.drugs.PrescribedDrug import org.simple.clinic.drugs.selection.PrescribedDrugsScreenKey @@ -24,12 +27,10 @@ import org.simple.clinic.navigation.v2.keyprovider.ScreenKeyProvider import org.simple.clinic.summary.PatientSummaryChildView import org.simple.clinic.summary.PatientSummaryModelUpdateCallback import org.simple.clinic.summary.PatientSummaryScreenKey +import org.simple.clinic.summary.prescribeddrugs.ui.DrugSummary import org.simple.clinic.util.RelativeTimestampGenerator import org.simple.clinic.util.UserClock -import org.simple.clinic.util.toLocalDateAtZone import org.simple.clinic.util.unsafeLazy -import org.simple.clinic.widgets.setPaddingBottom -import org.simple.clinic.widgets.visibleOrGone import java.time.format.DateTimeFormatter import java.util.UUID import javax.inject.Inject @@ -40,17 +41,6 @@ class DrugSummaryView( attributeSet: AttributeSet ) : MaterialCardView(context, attributeSet), DrugSummaryUi, DrugSummaryUiActions, PatientSummaryChildView { - private var binding: DrugsSummaryViewBinding? = null - - private val updateButton - get() = binding!!.updateButton - - private val drugsSummaryContainer - get() = binding!!.drugsSummaryContainer - - private val emptyMedicinesTextView - get() = binding!!.emptyMedicinesTextView - @Inject @Named("full_date") lateinit var fullDateFormatter: DateTimeFormatter @@ -105,10 +95,7 @@ class DrugSummaryView( ) } - init { - val layoutInflater = LayoutInflater.from(context) - binding = DrugsSummaryViewBinding.inflate(layoutInflater, this, true) - } + private var prescribedDrugs by mutableStateOf>(emptyList()) override fun onAttachedToWindow() { super.onAttachedToWindow() @@ -135,15 +122,28 @@ class DrugSummaryView( } context.injector().inject(this) + + addView(ComposeView(context).apply { + setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) + + setContent { + SimpleTheme { + DrugSummary( + prescribedDrugs = prescribedDrugs, + drugDateFormatter = fullDateFormatter, + userClock = userClock, + onEditMedicinesClick = { + internalEvents.onNext(PatientSummaryUpdateDrugsClicked()) + } + ) + } + } + }) } override fun populatePrescribedDrugs(prescribedDrugs: List) { val alphabeticallySortedPrescribedDrugs = prescribedDrugs.sortedBy { it.name } - bind( - prescriptions = alphabeticallySortedPrescribedDrugs, - dateFormatter = fullDateFormatter, - userClock = userClock - ) + this.prescribedDrugs = alphabeticallySortedPrescribedDrugs } override fun showUpdatePrescribedDrugsScreen(patientUuid: UUID, currentFacility: Facility) { @@ -156,59 +156,4 @@ class DrugSummaryView( override fun registerSummaryModelUpdateCallback(callback: PatientSummaryModelUpdateCallback?) { modelUpdateCallback = callback } - - private fun bind( - prescriptions: List, - dateFormatter: DateTimeFormatter, - userClock: UserClock - ) { - updateButton.setOnClickListener { internalEvents.onNext(PatientSummaryUpdateDrugsClicked()) } - - drugsSummaryContainer.visibleOrGone(prescriptions.isNotEmpty()) - emptyMedicinesTextView.visibleOrGone(prescriptions.isEmpty()) - - setButtonText(prescriptions) - setButtonIcon(prescriptions) - - drugsSummaryContainer.removeAllViews() - - if (prescriptions.isNotEmpty()) { - prescriptions - .map { drug -> - val drugItemView = LayoutInflater.from(context).inflate(R.layout.list_patientsummary_prescripton_drug, this, false) as DrugSummaryItemView - drugItemView.render( - drugName = drug.name, - drugDosage = drug.dosage, - drugFrequency = drug.frequency, - dateFormatter.format(drug.updatedAt.toLocalDateAtZone(userClock.zone)) - ) - drugItemView - } - .forEach(drugsSummaryContainer::addView) - } - - val itemContainerBottomPadding = if (prescriptions.size > 1) { - R.dimen.patientsummary_drug_summary_item_container_bottom_padding_8 - } else { - R.dimen.patientsummary_drug_summary_item_container_bottom_padding_24 - } - drugsSummaryContainer.setPaddingBottom(itemContainerBottomPadding) - } - - private fun setButtonText(prescriptions: List) { - updateButton.text = if (prescriptions.isEmpty()) { - context.getString(R.string.patientsummary_prescriptions_add) - } else { - context.getString(R.string.patientsummary_prescriptions_update) - } - } - - private fun setButtonIcon(prescriptions: List) { - val drawableRes = if (prescriptions.isEmpty()) { - R.drawable.ic_add_circle_blue1_24dp - } else { - R.drawable.ic_edit_medicine - } - updateButton.setIconResource(drawableRes) - } } diff --git a/app/src/main/java/org/simple/clinic/summary/prescribeddrugs/ui/DrugSummary.kt b/app/src/main/java/org/simple/clinic/summary/prescribeddrugs/ui/DrugSummary.kt new file mode 100644 index 00000000000..40761c2881a --- /dev/null +++ b/app/src/main/java/org/simple/clinic/summary/prescribeddrugs/ui/DrugSummary.kt @@ -0,0 +1,174 @@ +package org.simple.clinic.summary.prescribeddrugs.ui + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.requiredHeight +import androidx.compose.material.Divider +import androidx.compose.material.Icon +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.dimensionResource +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import org.simple.clinic.R +import org.simple.clinic.common.ui.components.ButtonSize +import org.simple.clinic.common.ui.components.TextButton +import org.simple.clinic.common.ui.theme.SimpleTheme +import org.simple.clinic.drugs.PrescribedDrug +import org.simple.clinic.patient.SyncStatus +import org.simple.clinic.storage.Timestamps +import org.simple.clinic.util.RealUserClock +import org.simple.clinic.util.UserClock +import org.simple.clinic.util.toLocalDateAtZone +import java.time.Instant +import java.time.ZoneId +import java.time.format.DateTimeFormatter +import java.util.UUID + +@Composable +fun DrugSummary( + prescribedDrugs: List, + drugDateFormatter: DateTimeFormatter, + userClock: UserClock, + onEditMedicinesClick: () -> Unit, + modifier: Modifier = Modifier +) { + Column( + modifier = Modifier + .fillMaxWidth() + .background(MaterialTheme.colors.surface) + .then(modifier) + ) { + TextButton( + leadingIcon = { + val icon = if (prescribedDrugs.isNotEmpty()) { + painterResource(id = R.drawable.ic_edit_medicine) + } else { + painterResource(id = R.drawable.ic_add_circle_blue1_24dp) + } + + Icon( + painter = icon, + contentDescription = null + ) + }, + onClick = onEditMedicinesClick, + buttonSize = ButtonSize.Small, + ) { + val label = if (prescribedDrugs.isNotEmpty()) { + stringResource(id = R.string.patientsummary_prescriptions_update) + } else { + stringResource(id = R.string.patientsummary_prescriptions_add) + }.uppercase() + + Text(text = label) + } + + Divider( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = dimensionResource(id = R.dimen.spacing_8)), + thickness = 1.dp, + color = SimpleTheme.colors.onSurface11, + ) + + if (prescribedDrugs.isNotEmpty()) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(top = dimensionResource(id = R.dimen.spacing_4)) + ) { + prescribedDrugs.forEach { prescribedDrug -> + DrugSummaryItem( + drugName = prescribedDrug.name, + drugDosage = prescribedDrug.dosage, + drugFrequency = prescribedDrug.frequency, + drugDate = drugDateFormatter.format(prescribedDrug.updatedAt.toLocalDateAtZone(userClock.zone)) + ) + } + } + } else { + Text( + text = stringResource(id = R.string.drugsummaryview_no_medicines), + modifier = Modifier + .fillMaxWidth() + .padding(vertical = dimensionResource(id = R.dimen.spacing_16)), + textAlign = TextAlign.Center, + style = MaterialTheme.typography.body1, + color = SimpleTheme.colors.onSurface67 + ) + } + + when { + prescribedDrugs.size > 1 -> { + Spacer(Modifier.requiredHeight(dimensionResource(R.dimen.spacing_8))) + } + + prescribedDrugs.isNotEmpty() -> { + Spacer(Modifier.requiredHeight(dimensionResource(R.dimen.spacing_24))) + } + + else -> { + // no-op + } + } + } +} + +@Preview +@Composable +private fun DrugSummaryPreviewEmpty() { + SimpleTheme { + DrugSummary( + prescribedDrugs = emptyList(), + drugDateFormatter = DateTimeFormatter.ISO_DATE, + userClock = RealUserClock(ZoneId.systemDefault()), + onEditMedicinesClick = { + // no-op + }, + ) + } +} + +@Preview +@Composable +private fun DrugSummaryPreviewWithContent() { + SimpleTheme { + DrugSummary( + prescribedDrugs = listOf( + PrescribedDrug( + uuid = UUID.fromString("39173872-ce0b-4b9d-a629-c54cc9b0a2e9"), + name = "Metaforming 500mg BD", + dosage = "500 mg", + rxNormCode = null, + isDeleted = false, + isProtocolDrug = true, + patientUuid = UUID.fromString("519bc844-1120-4265-b5b8-ba4f1340ed04"), + facilityUuid = UUID.fromString("4fe631a2-b860-40f3-be1f-cd5202fc5411"), + syncStatus = SyncStatus.DONE, + timestamps = Timestamps( + createdAt = Instant.parse("2018-01-01T00:00:00Z"), + updatedAt = Instant.parse("2018-01-01T00:00:00Z"), + deletedAt = Instant.parse("2018-01-01T00:00:00Z"), + ), + frequency = null, + durationInDays = null, + teleconsultationId = null, + ) + ), + drugDateFormatter = DateTimeFormatter.ofPattern("dd-MMM-yyyy"), + userClock = RealUserClock(ZoneId.systemDefault()), + onEditMedicinesClick = { + // no-op + }, + ) + } +} diff --git a/app/src/main/java/org/simple/clinic/summary/prescribeddrugs/ui/DrugSummaryItem.kt b/app/src/main/java/org/simple/clinic/summary/prescribeddrugs/ui/DrugSummaryItem.kt new file mode 100644 index 00000000000..2cbd8bf899b --- /dev/null +++ b/app/src/main/java/org/simple/clinic/summary/prescribeddrugs/ui/DrugSummaryItem.kt @@ -0,0 +1,81 @@ +package org.simple.clinic.summary.prescribeddrugs.ui + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +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.res.dimensionResource +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import org.simple.clinic.R +import org.simple.clinic.common.ui.theme.SimpleTheme +import org.simple.clinic.teleconsultlog.medicinefrequency.MedicineFrequency + +@Composable +fun DrugSummaryItem( + drugName: String, + drugDosage: String?, + drugFrequency: MedicineFrequency?, + drugDate: String, + modifier: Modifier = Modifier +) { + Row( + modifier = Modifier + .then(modifier) + .fillMaxWidth() + .padding( + horizontal = dimensionResource(id = R.dimen.spacing_12), + vertical = dimensionResource(id = R.dimen.spacing_4), + ), + verticalAlignment = Alignment.CenterVertically + ) { + Image( + painter = painterResource(id = R.drawable.prescription_drug), + contentDescription = null, + modifier = Modifier.size(dimensionResource(id = R.dimen.spacing_16)) + ) + + Spacer(modifier = Modifier.width(dimensionResource(id = R.dimen.spacing_12))) + + val drugWithDosageAndFrequency = listOfNotNull(drugName, drugDosage, drugFrequency) + .joinToString(separator = " ") + Text( + modifier = Modifier.weight(1f), + text = drugWithDosageAndFrequency, + style = MaterialTheme.typography.body1, + color = MaterialTheme.colors.onBackground, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + + Spacer(modifier = Modifier.width(dimensionResource(id = R.dimen.spacing_8))) + + Text( + text = drugDate, + style = MaterialTheme.typography.body2, + color = SimpleTheme.colors.onSurface67 + ) + } +} + +@Preview +@Composable +private fun DrugSummaryItemPreview() { + SimpleTheme { + DrugSummaryItem( + drugName = "Metformin", + drugDosage = "500 mg", + drugFrequency = MedicineFrequency.BD, + drugDate = "20-Feb-2023" + ) + } +} diff --git a/app/src/main/java/org/simple/clinic/summary/ui/PatientSummaryToolbar.kt b/app/src/main/java/org/simple/clinic/summary/ui/PatientSummaryToolbar.kt new file mode 100644 index 00000000000..dd879c8be1e --- /dev/null +++ b/app/src/main/java/org/simple/clinic/summary/ui/PatientSummaryToolbar.kt @@ -0,0 +1,301 @@ +package org.simple.clinic.summary.ui + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.requiredSize +import androidx.compose.foundation.layout.requiredWidth +import androidx.compose.foundation.layout.statusBarsPadding +import androidx.compose.material.AppBarDefaults +import androidx.compose.material.Icon +import androidx.compose.material.IconButton +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Surface +import androidx.compose.material.Text +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.dimensionResource +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.text.withStyle +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import org.simple.clinic.R +import org.simple.clinic.common.ui.theme.SimpleTheme +import org.simple.clinic.patient.Gender +import org.simple.clinic.patient.PatientPhoneNumber +import org.simple.clinic.patient.PatientPhoneNumberType +import org.simple.clinic.patient.businessid.BusinessId +import org.simple.clinic.patient.businessid.Identifier +import org.simple.clinic.patient.displayLetterRes +import java.time.Instant +import java.util.UUID + +@Composable +fun PatientSummaryToolbar( + patientName: String, + gender: Gender, + age: Int, + address: String, + phoneNumber: PatientPhoneNumber?, + bpPassport: BusinessId?, + alternativeId: BusinessId?, + onBack: () -> Unit, + onContact: () -> Unit, + onEditPatient: () -> Unit, + modifier: Modifier = Modifier +) { + Surface( + color = SimpleTheme.colors.toolbarPrimary, + contentColor = SimpleTheme.colors.onToolbarPrimary, + elevation = AppBarDefaults.TopAppBarElevation, + modifier = modifier, + ) { + Column( + modifier = Modifier.statusBarsPadding() + ) { + // Actions + Row( + verticalAlignment = Alignment.CenterVertically, + ) { + IconButton( + onClick = onBack + ) { + Icon( + imageVector = Icons.AutoMirrored.Filled.ArrowBack, + contentDescription = null, + tint = SimpleTheme.colors.onToolbarPrimary, + ) + } + + Spacer(Modifier.weight(1f)) + + if (phoneNumber != null) { + ToolbarActionTextButton( + label = phoneNumber.number, + icon = painterResource(R.drawable.ic_summary_call_icon), + onClick = onContact, + ) + + Spacer(Modifier.requiredWidth(8.dp)) + } + + ToolbarActionTextButton( + modifier = Modifier.padding(end = 12.dp), + label = stringResource(R.string.patientsummary_edit), + onClick = onEditPatient + ) + } + + // Content + PatientInfoContent( + gender = gender, + patientName = patientName, + age = age, + address = address, + bpPassport = bpPassport, + alternativeId = alternativeId + ) + } + } +} + +@Composable +private fun PatientInfoContent( + gender: Gender, + patientName: String, + age: Int, + address: String, + bpPassport: BusinessId?, + alternativeId: BusinessId? +) { + val context = LocalContext.current + Column( + modifier = Modifier + .fillMaxWidth() + .padding( + start = dimensionResource(id = R.dimen.spacing_16), + end = dimensionResource(id = R.dimen.spacing_16), + bottom = dimensionResource(id = R.dimen.spacing_16) + ) + ) { + val resources = context.resources + val genderLetter = resources.getString(gender.displayLetterRes) + val patientNameGenderAge = resources.getString( + R.string.patientsummary_toolbar_title, + patientName, + genderLetter, + age.toString() + ) + Text( + text = patientNameGenderAge, + style = MaterialTheme.typography.h6, + maxLines = 1, + color = SimpleTheme.colors.onToolbarPrimary, + modifier = Modifier.padding(top = dimensionResource(id = R.dimen.spacing_8)) + ) + + Text( + text = address, + style = MaterialTheme.typography.body2, + color = SimpleTheme.colors.onToolbarPrimary72, + modifier = Modifier.padding(top = dimensionResource(id = R.dimen.spacing_4)) + ) + + Column( + modifier = Modifier.padding(top = dimensionResource(id = R.dimen.spacing_4)) + ) { + if (bpPassport != null) { + Text( + text = buildBusinessIdString(businessId = bpPassport), + style = MaterialTheme.typography.body2, + color = SimpleTheme.colors.onToolbarPrimary72, + modifier = Modifier.padding(end = dimensionResource(id = R.dimen.spacing_8)) + ) + } + + if (alternativeId != null) { + Text( + text = buildBusinessIdString(businessId = alternativeId), + style = MaterialTheme.typography.body2, + color = SimpleTheme.colors.onToolbarPrimary72, + modifier = Modifier.padding(top = if (bpPassport != null) dimensionResource(id = R.dimen.spacing_4) else 0.dp) + ) + } + } + } +} + +@Composable +private fun ToolbarActionTextButton( + label: String, + modifier: Modifier = Modifier, + icon: Painter? = null, + onClick: () -> Unit, +) { + Row( + modifier = Modifier + .then(modifier) + .clip(MaterialTheme.shapes.small) + .clickable { onClick() } + .background(Color.Black.copy(alpha = 0.24f)) + .padding(horizontal = 8.dp, vertical = 4.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + if (icon != null) { + Image( + painter = icon, + contentDescription = null, + modifier = Modifier.requiredSize(16.dp) + ) + + Spacer(Modifier.requiredWidth(4.dp)) + } + + Text( + text = label.uppercase(), + style = MaterialTheme.typography.button, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + color = MaterialTheme.colors.onPrimary + ) + } +} + +@Composable +private fun buildBusinessIdString(businessId: BusinessId): AnnotatedString { + val context = LocalContext.current + val label = businessId.identifier.displayType(context.resources) + val identifier = businessId.identifier + + return buildAnnotatedString { + append("$label: ") + + val body2Numeric = SimpleTheme.typography.body2Numeric + withStyle( + SpanStyle( + fontSize = body2Numeric.fontSize, + letterSpacing = body2Numeric.letterSpacing, + fontFamily = body2Numeric.fontFamily, + ) + ) { + append(identifier.displayValue()) + } + } +} + +@Preview +@Composable +private fun PatientSummaryToolbarPreview() { + SimpleTheme { + PatientSummaryToolbar( + patientName = "SpongeBob", + gender = Gender.Male, + age = 25, + address = "124 Conch Street, Bikini Bottom, Pacific Ocean", + phoneNumber = PatientPhoneNumber( + uuid = UUID.fromString("73f2c465-9833-4922-8fb2-6700094fdb3a"), + patientUuid = UUID.fromString("6878819c-aeb1-4b1f-bcb5-22059697329a"), + number = "83217387122", + phoneType = PatientPhoneNumberType.Mobile, + active = true, + createdAt = Instant.parse("2018-01-01T00:00:00Z"), + updatedAt = Instant.parse("2018-01-01T00:00:00Z"), + deletedAt = null, + ), + bpPassport = BusinessId( + uuid = UUID.fromString("d758d820-b8d4-448d-be6c-ed2e970d193f"), + patientUuid = UUID.fromString("4ea1e739-1d5f-4f23-8e73-d42b1445c58e"), + identifier = Identifier( + value = "9828b60b-b6d1-4f8a-bcfc-b9ddfc8fc1e2", + type = Identifier.IdentifierType.BpPassport + ), + metaDataVersion = BusinessId.MetaDataVersion.BpPassportMetaDataV1, + metaData = "", + createdAt = Instant.parse("2018-01-01T00:00:00Z"), + updatedAt = Instant.parse("2018-01-01T00:00:00Z"), + deletedAt = null, + searchHelp = "123456" + ), + alternativeId = BusinessId( + uuid = UUID.fromString("edfcf367-26a7-40a6-8af7-570c718ab387"), + patientUuid = UUID.fromString("f17756f9-4a45-486e-940d-16cd6c2a2e1b"), + identifier = Identifier( + value = "1276417263891739", + type = Identifier.IdentifierType.BangladeshNationalId + ), + metaDataVersion = BusinessId.MetaDataVersion.BangladeshNationalIdMetaDataV1, + metaData = "", + createdAt = Instant.parse("2018-01-01T00:00:00Z"), + updatedAt = Instant.parse("2018-01-01T00:00:00Z"), + deletedAt = null, + searchHelp = "1276417263891739" + ), + onBack = { + // no-op + }, + onContact = { + // no-op + }, + onEditPatient = { + // no-op + } + ) + } +} diff --git a/app/src/main/res/drawable/ic_chevron_down_24.xml b/app/src/main/res/drawable/ic_chevron_down_24.xml deleted file mode 100644 index 6db40055e80..00000000000 --- a/app/src/main/res/drawable/ic_chevron_down_24.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - diff --git a/app/src/main/res/layout/drugs_summary_view.xml b/app/src/main/res/layout/drugs_summary_view.xml deleted file mode 100644 index ac3b5b72db7..00000000000 --- a/app/src/main/res/layout/drugs_summary_view.xml +++ /dev/null @@ -1,55 +0,0 @@ - - - - - - - - - - - - diff --git a/app/src/main/res/layout/list_item_divider.xml b/app/src/main/res/layout/list_item_divider.xml deleted file mode 100644 index d7da1473ac7..00000000000 --- a/app/src/main/res/layout/list_item_divider.xml +++ /dev/null @@ -1,6 +0,0 @@ - - diff --git a/app/src/main/res/layout/list_item_no_pending_patients.xml b/app/src/main/res/layout/list_item_no_pending_patients.xml deleted file mode 100644 index 26cc857f42f..00000000000 --- a/app/src/main/res/layout/list_item_no_pending_patients.xml +++ /dev/null @@ -1,36 +0,0 @@ - - - - - - - - - - diff --git a/app/src/main/res/layout/list_item_overdue_list_section_header.xml b/app/src/main/res/layout/list_item_overdue_list_section_header.xml deleted file mode 100644 index 4f17cec56b4..00000000000 --- a/app/src/main/res/layout/list_item_overdue_list_section_header.xml +++ /dev/null @@ -1,39 +0,0 @@ - - - - - - - diff --git a/app/src/main/res/layout/list_item_overdue_pending_list_footer.xml b/app/src/main/res/layout/list_item_overdue_pending_list_footer.xml deleted file mode 100644 index bb2ab4d39f9..00000000000 --- a/app/src/main/res/layout/list_item_overdue_pending_list_footer.xml +++ /dev/null @@ -1,9 +0,0 @@ - - diff --git a/app/src/main/res/layout/list_item_search_overdue_patient_button.xml b/app/src/main/res/layout/list_item_search_overdue_patient_button.xml deleted file mode 100644 index b90fcaccdfe..00000000000 --- a/app/src/main/res/layout/list_item_search_overdue_patient_button.xml +++ /dev/null @@ -1,14 +0,0 @@ - - diff --git a/app/src/main/res/layout/list_patientsummary_prescripton_drug.xml b/app/src/main/res/layout/list_patientsummary_prescripton_drug.xml deleted file mode 100644 index 459ca8a0b4c..00000000000 --- a/app/src/main/res/layout/list_patientsummary_prescripton_drug.xml +++ /dev/null @@ -1,4 +0,0 @@ - - diff --git a/app/src/main/res/layout/patientsummary_drug_item_content.xml b/app/src/main/res/layout/patientsummary_drug_item_content.xml deleted file mode 100644 index 5f0a20a6911..00000000000 --- a/app/src/main/res/layout/patientsummary_drug_item_content.xml +++ /dev/null @@ -1,49 +0,0 @@ - - - - - - - - - - diff --git a/app/src/main/res/layout/screen_overdue.xml b/app/src/main/res/layout/screen_overdue.xml index f6aa09b7883..5d7d6442998 100644 --- a/app/src/main/res/layout/screen_overdue.xml +++ b/app/src/main/res/layout/screen_overdue.xml @@ -14,22 +14,11 @@ android:visibility="gone" tools:visibility="visible" /> - - - - - + android:layout_height="match_parent" + android:layout_below="@id/overdueProgressBar" /> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + app:layout_constraintTop_toTopOf="parent" /> Unit ) { val minHeight = when (buttonSize) { - ButtonSize.Big -> 56.dp + ButtonSize.Small -> 40.dp ButtonSize.Default -> 48.dp + ButtonSize.Big -> 56.dp } MaterialOutlinedButton( @@ -56,7 +58,6 @@ fun OutlinedButton( } } - @Composable fun FilledButton( onClick: () -> Unit, @@ -67,8 +68,9 @@ fun FilledButton( content: @Composable RowScope.() -> Unit ) { val minHeight = when (buttonSize) { - ButtonSize.Big -> 56.dp + ButtonSize.Small -> 40.dp ButtonSize.Default -> 48.dp + ButtonSize.Big -> 56.dp } androidx.compose.material.Button( @@ -90,9 +92,48 @@ fun FilledButton( } } +@Composable +fun TextButton( + onClick: () -> Unit, + modifier: Modifier = Modifier, + leadingIcon: (@Composable () -> Unit)? = null, + trailingIcon: (@Composable () -> Unit)? = null, + enabled: Boolean = true, + buttonSize: ButtonSize = ButtonSize.Default, + content: @Composable RowScope.() -> Unit +) { + val minHeight = when (buttonSize) { + ButtonSize.Small -> 40.dp + ButtonSize.Default -> 48.dp + ButtonSize.Big -> 56.dp + } + + MaterialTextButton( + modifier = modifier.defaultMinSize( + minWidth = 64.dp, + minHeight = minHeight + ), + onClick = onClick, + enabled = enabled, + ) { + leadingIcon?.let { + it() + Spacer(modifier = Modifier.requiredWidth(8.dp)) + } + ProvideTextStyle(value = SimpleTheme.typography.buttonBig) { + content() + } + trailingIcon?.let { + Spacer(modifier = Modifier.requiredWidth(8.dp)) + it() + } + } +} + sealed interface ButtonSize { data object Default : ButtonSize data object Big : ButtonSize + data object Small : ButtonSize } @Preview(group = "OutlinedButton") @@ -206,3 +247,89 @@ private fun FilledButtonWithIconPreview() { } } } + +@Preview(group = "TextButton") +@Composable +private fun TextButtonPreview() { + SimpleTheme { + TextButton( + modifier = Modifier.fillMaxWidth(), + onClick = { /*no-op*/ } + ) { + Text(text = "BUTTON") + } + } +} + +@Preview(group = "TextButton") +@Composable +private fun TextButtonSmallPreview() { + SimpleTheme { + TextButton( + modifier = Modifier.fillMaxWidth(), + buttonSize = ButtonSize.Small, + onClick = { /*no-op*/ } + ) { + Text(text = "BUTTON") + } + } +} + +@Preview(group = "TextButton") +@Composable +private fun TextButtonBigPreview() { + SimpleTheme { + TextButton( + modifier = Modifier.fillMaxWidth(), + buttonSize = ButtonSize.Big, + onClick = { /*no-op*/ } + ) { + Text(text = "BUTTON") + } + } +} + +@Preview(group = "TextButton") +@Composable +private fun TextButtonWithDifferentThemePreview() { + SimpleRedTheme { + TextButton( + modifier = Modifier.fillMaxWidth(), + onClick = { /*no-op*/ } + ) { + Text(text = "BUTTON") + } + } +} + +@Preview(group = "TextButton") +@Composable +private fun TextButtonWithLeadingIconPreview() { + SimpleTheme { + TextButton( + modifier = Modifier.fillMaxWidth(), + leadingIcon = { + Icon(imageVector = Icons.Filled.Add, contentDescription = null) + }, + onClick = { /*no-op*/ } + ) { + Text(text = "BUTTON") + } + } +} + +@Preview(group = "TextButton") +@Composable +private fun TextButtonWithTrailingIconPreview() { + SimpleTheme { + TextButton( + modifier = Modifier.fillMaxWidth(), + trailingIcon = { + Icon(imageVector = Icons.Filled.Add, contentDescription = null) + }, + onClick = { /*no-op*/ } + ) { + Text(text = "BUTTON") + } + } +} diff --git a/common-ui/src/main/java/org/simple/clinic/common/ui/theme/SimpleColors.kt b/common-ui/src/main/java/org/simple/clinic/common/ui/theme/SimpleColors.kt index 20877c24954..b154c44e5f9 100644 --- a/common-ui/src/main/java/org/simple/clinic/common/ui/theme/SimpleColors.kt +++ b/common-ui/src/main/java/org/simple/clinic/common/ui/theme/SimpleColors.kt @@ -11,8 +11,10 @@ data class SimpleColors( val toolbarPrimary: Color = Color.Unspecified, val toolbarPrimaryVariant: Color = Color.Unspecified, val onToolbarPrimary: Color = Color.Unspecified, + val onToolbarPrimary72: Color = onToolbarPrimary.copy(alpha = 0.72f), val material: Colors = lightColors(), - val onSurface67: Color = material.onSurface.copy(alpha = 0.67f) + val onSurface67: Color = material.onSurface.copy(alpha = 0.67f), + val onSurface11: Color = material.onSurface.copy(alpha = 0.11f) ) internal val LocalSimpleColors = staticCompositionLocalOf { SimpleColors() } diff --git a/common-ui/src/main/res/values/dimens.xml b/common-ui/src/main/res/values/dimens.xml index 5797e5758b4..9909899518a 100644 --- a/common-ui/src/main/res/values/dimens.xml +++ b/common-ui/src/main/res/values/dimens.xml @@ -36,8 +36,6 @@ 8dp 24dp - 8dp - 24dp 18dp 19dp