diff --git a/app/build.gradle b/app/build.gradle index 0bc2d4019..9000bb85a 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -96,6 +96,7 @@ android { task getApkVersion { println defaultConfig.versionName + "-" + defaultConfig.versionCode } + namespace 'ca.bc.gov.bchealth' } dependencies { @@ -113,11 +114,11 @@ dependencies { implementation composeBom implementation 'androidx.compose.material:material' implementation 'androidx.compose.ui:ui-tooling-preview' + implementation 'androidx.compose.runtime:runtime-livedata' implementation "androidx.constraintlayout:constraintlayout-compose:$versions.constraint_compose" debugImplementation 'androidx.compose.ui:ui-tooling' - //Navigation implementation "androidx.navigation:navigation-fragment-ktx:$versions.navigation" implementation "androidx.navigation:navigation-ui-ktx:$versions.navigation" @@ -125,6 +126,7 @@ dependencies { //JetPack LiveData ViewModel implementation "androidx.lifecycle:lifecycle-viewmodel-compose:$versions.lifecycle" + implementation "androidx.lifecycle:lifecycle-runtime-compose:$versions.lifecycle" implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$versions.lifecycle" implementation "androidx.lifecycle:lifecycle-livedata-ktx:$versions.lifecycle" implementation "androidx.lifecycle:lifecycle-runtime-ktx:$versions.lifecycle" diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index dfea5071b..757b5d433 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,7 +1,6 @@ + xmlns:tools="http://schemas.android.com/tools"> diff --git a/app/src/main/java/ca/bc/gov/bchealth/MainActivity.kt b/app/src/main/java/ca/bc/gov/bchealth/MainActivity.kt index 0008b94a1..6f85ec7a0 100644 --- a/app/src/main/java/ca/bc/gov/bchealth/MainActivity.kt +++ b/app/src/main/java/ca/bc/gov/bchealth/MainActivity.kt @@ -143,7 +143,7 @@ class MainActivity : AppCompatActivity() { false ) if (started) { - binding.navHostFragment.showErrorSnackbar(getString(R.string.notification_title_while_fetching_data)) + binding.navHostFragment.showErrorSnackbar(getString(R.string.notification_title_while_fetching_data), binding.bottomNav) } } @@ -154,7 +154,7 @@ class MainActivity : AppCompatActivity() { WorkInfo.State.SUCCEEDED -> { if (isWorkerStarted) { inAppUpdate.checkForUpdate(AppUpdateType.FLEXIBLE) - binding.navHostFragment.showErrorSnackbar(getString(R.string.notification_title_on_success)) + binding.navHostFragment.showErrorSnackbar(getString(R.string.notification_title_on_success), binding.bottomNav) } } @@ -190,7 +190,7 @@ class MainActivity : AppCompatActivity() { false ) if (isRecordFetchFailed) { - binding.navHostFragment.showErrorSnackbar(getString(R.string.notification_title_on_failed)) + binding.navHostFragment.showErrorSnackbar(getString(R.string.notification_title_on_failed), binding.bottomNav) } } } diff --git a/app/src/main/java/ca/bc/gov/bchealth/SplashViewModel.kt b/app/src/main/java/ca/bc/gov/bchealth/SplashViewModel.kt index c7b6a00e8..9c545a0ce 100644 --- a/app/src/main/java/ca/bc/gov/bchealth/SplashViewModel.kt +++ b/app/src/main/java/ca/bc/gov/bchealth/SplashViewModel.kt @@ -5,7 +5,13 @@ import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import ca.bc.gov.common.BuildConfig.LOCAL_API_VERSION +import ca.bc.gov.common.model.AppFeatureName +import ca.bc.gov.common.model.QuickAccessLinkName +import ca.bc.gov.common.model.settings.AppFeatureDto +import ca.bc.gov.common.model.settings.QuickAccessTileDto import ca.bc.gov.repository.OnBoardingRepository +import ca.bc.gov.repository.settings.AppFeatureRepository +import ca.bc.gov.repository.settings.QuickAccessTileRepository import ca.bc.gov.repository.worker.MobileConfigRepository import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.async @@ -18,7 +24,9 @@ private const val MAX_SPLASH_DELAY = 2000L @HiltViewModel class SplashViewModel @Inject constructor( private val mobileConfigRepository: MobileConfigRepository, - onBoardingRepository: OnBoardingRepository + onBoardingRepository: OnBoardingRepository, + private val appFeatureRepository: AppFeatureRepository, + private val quickAccessTileRepository: QuickAccessTileRepository ) : ViewModel() { private val _updateType: MutableLiveData = MutableLiveData() @@ -27,6 +35,7 @@ class SplashViewModel @Inject constructor( init { onBoardingRepository.checkIfReOnBoardingRequired(BuildConfig.VERSION_CODE) + initializeAppData() } fun checkAppVersion() { @@ -50,6 +59,130 @@ class SplashViewModel @Inject constructor( } } + private fun initializeAppData() = viewModelScope.launch { + val healthRecord = AppFeatureDto( + name = AppFeatureName.HEALTH_RECORDS, + hasManageableQuickAccessLinks = true, + showAsQuickAccess = true + ) + val id = appFeatureRepository.insert(healthRecord) + + if (id > 0) { + + val tiles = timelineQuickLinkTiles(id) + quickAccessTileRepository.insertAll(tiles) + } + + val immunizationSchedule = AppFeatureDto( + name = AppFeatureName.IMMUNIZATION_SCHEDULES, + hasManageableQuickAccessLinks = false, + showAsQuickAccess = true + ) + appFeatureRepository.insert(immunizationSchedule) + + val recommendations = AppFeatureDto( + name = AppFeatureName.RECOMMENDED_IMMUNIZATIONS, + hasManageableQuickAccessLinks = false, + showAsQuickAccess = true + ) + appFeatureRepository.insert(recommendations) + + val healthResources = AppFeatureDto( + name = AppFeatureName.HEALTH_RESOURCES, + hasManageableQuickAccessLinks = false, + showAsQuickAccess = true + ) + appFeatureRepository.insert(healthResources) + + val proofOfVaccine = AppFeatureDto( + name = AppFeatureName.PROOF_OF_VACCINE, + hasManageableQuickAccessLinks = false, + showAsQuickAccess = true + ) + appFeatureRepository.insert(proofOfVaccine) + + val services = AppFeatureDto( + name = AppFeatureName.SERVICES, + hasManageableQuickAccessLinks = true, + showAsQuickAccess = false + ) + + val serviceId = appFeatureRepository.insert(services) + if (serviceId > 0) { + quickAccessTileRepository.insertAll(serviceQuickLinkTilesItem(serviceId)) + } + } + + private fun serviceQuickLinkTilesItem(id: Long): List { + return listOf( + QuickAccessTileDto( + featureId = id, + tileName = QuickAccessLinkName.ORGAN_DONOR, + tilePayload = "Organ Donor", + showAsQuickAccess = false + ) + ) + } + + private fun timelineQuickLinkTiles(id: Long): List { + return listOf( + QuickAccessTileDto( + featureId = id, + tileName = QuickAccessLinkName.IMMUNIZATIONS, + tilePayload = "Immunization", + showAsQuickAccess = false + ), + QuickAccessTileDto( + featureId = id, + tileName = QuickAccessLinkName.MEDICATIONS, + tilePayload = "Medications", + showAsQuickAccess = false + ), + QuickAccessTileDto( + featureId = id, + tileName = QuickAccessLinkName.LAB_RESULTS, + tilePayload = "Laboratory", + showAsQuickAccess = false + ), + QuickAccessTileDto( + featureId = id, + tileName = QuickAccessLinkName.COVID_19_TESTS, + tilePayload = "COVID19Laboratory", + showAsQuickAccess = false + ), + QuickAccessTileDto( + featureId = id, + tileName = QuickAccessLinkName.HEALTH_VISITS, + tilePayload = "HealthVisit", + showAsQuickAccess = false + ), + QuickAccessTileDto( + featureId = id, + tileName = QuickAccessLinkName.SPECIAL_AUTHORITY, + tilePayload = "SpecialAuthority", + showAsQuickAccess = false + ), + QuickAccessTileDto( + featureId = id, + tileName = QuickAccessLinkName.HOSPITAL_VISITS, + tilePayload = "HospitalVisit", + showAsQuickAccess = false + ), + QuickAccessTileDto( + featureId = id, + tileName = QuickAccessLinkName.CLINICAL_DOCUMENTS, + tilePayload = "ClinicalDocument", + showAsQuickAccess = false + ), + QuickAccessTileDto( + featureId = id, + tileName = QuickAccessLinkName.IMAGING_REPORTS, + tilePayload = "ImagingReports", + showAsQuickAccess = false + ) + ) + } + enum class UpdateType { FORCE_UPDATE, CHECK_SOFT_UPDATE } diff --git a/app/src/main/java/ca/bc/gov/bchealth/compose/Styles.kt b/app/src/main/java/ca/bc/gov/bchealth/compose/Styles.kt index 6c57b60f8..4ab501a8a 100644 --- a/app/src/main/java/ca/bc/gov/bchealth/compose/Styles.kt +++ b/app/src/main/java/ca/bc/gov/bchealth/compose/Styles.kt @@ -3,7 +3,6 @@ package ca.bc.gov.bchealth.compose import androidx.compose.material.MaterialTheme import androidx.compose.material.Typography import androidx.compose.runtime.Composable -import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.Font import androidx.compose.ui.text.font.FontFamily @@ -12,6 +11,10 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import ca.bc.gov.bchealth.R +import ca.bc.gov.bchealth.compose.theme.darkText +import ca.bc.gov.bchealth.compose.theme.descriptionGrey +import ca.bc.gov.bchealth.compose.theme.primaryBlue +import ca.bc.gov.bchealth.compose.theme.white // Consider making touch targets at least 48x48dp. https://support.google.com/accessibility/android/answer/7101858?hl=en val minButtonSize = 48.dp @@ -23,20 +26,6 @@ val fonts = FontFamily( Font(R.font.bc_sans_bold_italic, weight = FontWeight.Bold, style = FontStyle.Italic), ) -val darkText = Color(0xFF313132) - -val black = Color(0xFF000000) -val white = Color(0xFFFFFFFF) -val blue = Color(0xFF1A5A96) -val primaryBlue = Color(0xFF003366) -val lightBlue = Color(0xFFB2C1CF) -val statusBlue30 = Color(0x4D38598A) -val descriptionGrey = Color(0xFF6D757D) -val grey = Color(0xFF606060) -val greyBg = Color(0xFFF2F2F2) -val green = Color(0xFF2E8540) -val red = Color(0xFFD8292F) - val Typography.largeButton: TextStyle @Composable get() { @@ -127,16 +116,18 @@ fun TextStyle.bold() = this.copy(fontWeight = FontWeight.Bold) fun TextStyle.italic() = this.copy(fontStyle = FontStyle.Italic) +@Deprecated("Replace with HealthGatewayTheme", replaceWith = ReplaceWith("HealthGatewayTheme"), DeprecationLevel.WARNING) @Composable fun MyHealthTheme(content: @Composable () -> Unit) = MaterialTheme( colors = MaterialTheme.colors.copy( primary = primaryBlue, primaryVariant = white, + onPrimary = white, background = white, surface = white, secondary = primaryBlue, secondaryVariant = primaryBlue, - onBackground = primaryBlue + onBackground = primaryBlue, ), typography = MyHealthTypography, shapes = MaterialTheme.shapes, diff --git a/app/src/main/java/ca/bc/gov/bchealth/compose/component/AnnouncementBannerUI.kt b/app/src/main/java/ca/bc/gov/bchealth/compose/component/AnnouncementBannerUI.kt new file mode 100644 index 000000000..bdc0247fb --- /dev/null +++ b/app/src/main/java/ca/bc/gov/bchealth/compose/component/AnnouncementBannerUI.kt @@ -0,0 +1,204 @@ +package ca.bc.gov.bchealth.compose.component + +import android.text.method.LinkMovementMethod +import android.util.TypedValue +import android.widget.TextView +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.BoxWithConstraints +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.material.Card +import androidx.compose.material.IconButton +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.layoutId +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.viewinterop.AndroidView +import androidx.constraintlayout.compose.ConstraintLayout +import androidx.constraintlayout.compose.ConstraintSet +import androidx.constraintlayout.compose.Dimension +import androidx.constraintlayout.compose.Visibility +import ca.bc.gov.bchealth.R +import ca.bc.gov.bchealth.compose.BasePreview +import ca.bc.gov.bchealth.compose.theme.HealthGatewayTheme +import ca.bc.gov.bchealth.compose.theme.bannerBackgroundBlue +import ca.bc.gov.bchealth.compose.theme.blue +import ca.bc.gov.bchealth.utils.fromHtml + +private const val bannerIconId = "bannerIconId" +private const val bannerTitleId = "bannerTitleId" +private const val bannerArrowId = "bannerArrowId" +private const val bannerBodyId = "bannerBodyId" +private const val buttonLearnModeId = "buttonLearnMoreId" +private const val buttonDismissId = "buttonDismissId" + +@Composable +fun AnnouncementBannerUI( + modifier: Modifier = Modifier, + title: String, + description: String, + showReadMore: Boolean, + onLearnMoreClick: () -> Unit, + onDismissClick: () -> Unit +) { + var expanded by remember { mutableStateOf(true) } + Card( + modifier, + backgroundColor = bannerBackgroundBlue + ) { + BoxWithConstraints( + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight() + .padding(16.dp) + ) { + val constraint = bannerConstraints(expanded, showReadMore) + ConstraintLayout(constraint, modifier = Modifier.fillMaxWidth()) { + Image( + modifier = Modifier.layoutId(bannerIconId), + painter = painterResource(id = R.drawable.ic_banner_icon), + contentDescription = null + ) + + Text( + modifier = Modifier.layoutId(bannerTitleId), + text = title, + style = MaterialTheme.typography.body1, + fontWeight = FontWeight.Bold, + color = blue + ) + val toggleIcon = if (expanded) { + R.drawable.ic_content_short + } else { + R.drawable.ic_content_full + } + + IconButton( + onClick = { expanded = !expanded }, + modifier = Modifier + .layoutId(bannerArrowId) + ) { + Image(painter = painterResource(id = toggleIcon), contentDescription = null) + } + + AndroidView( + modifier = Modifier.layoutId(bannerBodyId), + factory = { context -> + TextView(context).apply { + setTextAppearance(R.style.HealthGateway_TextAppearance_MaterialComponents_Headline4) + setTextSize(TypedValue.COMPLEX_UNIT_SP, 13f) + } + }, + update = { + it.text = description.fromHtml().trimEnd().take(120) + it.movementMethod = LinkMovementMethod.getInstance() + } + ) + + HGTextButton( + onClick = { onDismissClick() }, + text = stringResource(id = R.string.dismiss), + modifier = Modifier.layoutId(buttonDismissId), + defaultHeight = HGButtonDefaults.SmallButtonHeight, + leadingIcon = painterResource(id = R.drawable.ic_dismiss) + ) + + HGTextButton( + onClick = { onLearnMoreClick() }, + text = stringResource(id = R.string.learn_more), + modifier = Modifier.layoutId(buttonLearnModeId), + defaultHeight = HGButtonDefaults.SmallButtonHeight, + leadingIcon = painterResource(id = R.drawable.ic_external_link) + ) + } + } + } +} + +private fun bannerConstraints(expanded: Boolean, showReadMore: Boolean): ConstraintSet { + return ConstraintSet { + val bannerIcon = createRefFor(bannerIconId) + val bannerTitle = createRefFor(bannerTitleId) + val bannerArrow = createRefFor(bannerArrowId) + val contentBody = createRefFor(bannerBodyId) + val buttonLearnMore = createRefFor(buttonLearnModeId) + val buttonDismiss = createRefFor(buttonDismissId) + + constrain(bannerIcon) { + top.linkTo(parent.top) + start.linkTo(parent.start) + } + + constrain(bannerTitle) { + start.linkTo(bannerIcon.end, 16.dp) + top.linkTo(bannerIcon.top) + end.linkTo(bannerArrow.start) + bottom.linkTo(bannerIcon.bottom) + width = Dimension.fillToConstraints + } + + constrain(bannerArrow) { + top.linkTo(bannerTitle.top) + end.linkTo(parent.end) + bottom.linkTo(bannerTitle.bottom) + } + + constrain(contentBody) { + start.linkTo(bannerTitle.start) + top.linkTo(bannerTitle.bottom, 16.dp) + end.linkTo(parent.end) + width = Dimension.fillToConstraints + visibility = if (expanded) { + Visibility.Visible + } else { + Visibility.Gone + } + } + + constrain(buttonDismiss) { + top.linkTo(contentBody.bottom, 16.dp) + end.linkTo(parent.end) + bottom.linkTo(parent.bottom) + visibility = if (expanded) { + Visibility.Visible + } else { + Visibility.Gone + } + } + + constrain(buttonLearnMore) { + top.linkTo(buttonDismiss.top) + end.linkTo(buttonDismiss.start) + bottom.linkTo(buttonDismiss.bottom) + visibility = if (expanded && showReadMore) { + Visibility.Visible + } else { + Visibility.Gone + } + } + } +} + +@Composable +@BasePreview +private fun AnnouncementBannerUIPreview() { + HealthGatewayTheme { + AnnouncementBannerUI( + title = stringResource(id = R.string.news_feed), + description = stringResource(id = R.string.news_feed), + showReadMore = false, + onDismissClick = {}, + onLearnMoreClick = {} + ) + } +} diff --git a/app/src/main/java/ca/bc/gov/bchealth/compose/component/Button.kt b/app/src/main/java/ca/bc/gov/bchealth/compose/component/Button.kt new file mode 100644 index 000000000..308c7fdae --- /dev/null +++ b/app/src/main/java/ca/bc/gov/bchealth/compose/component/Button.kt @@ -0,0 +1,313 @@ +package ca.bc.gov.bchealth.compose.component + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.defaultMinSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.sizeIn +import androidx.compose.material.Button +import androidx.compose.material.ButtonDefaults +import androidx.compose.material.Icon +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.material.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import ca.bc.gov.bchealth.R +import ca.bc.gov.bchealth.compose.BasePreview +import ca.bc.gov.bchealth.compose.theme.HealthGatewayTheme + +@Composable +fun HGButton( + onClick: () -> Unit, + modifier: Modifier = Modifier, + enabled: Boolean = true, + defaultHeight: Dp = HGButtonDefaults.LargeButtonHeight, + contentPadding: PaddingValues = ButtonDefaults.ContentPadding, + content: @Composable RowScope.() -> Unit +) { + Button( + onClick = onClick, + modifier = modifier.defaultMinSize(minHeight = defaultHeight), + enabled = enabled, + colors = ButtonDefaults.buttonColors( + disabledBackgroundColor = MaterialTheme.colors.primary.copy(alpha = 0.20f), + disabledContentColor = MaterialTheme.colors.onPrimary.copy(alpha = 0.20f) + ), + contentPadding = contentPadding, + content = content + ) +} + +@Composable +fun HGButton( + onClick: () -> Unit, + text: String, + modifier: Modifier = Modifier, + enabled: Boolean = true, + defaultHeight: Dp = HGButtonDefaults.LargeButtonHeight, + leadingIcon: @Composable (() -> Unit)? = null +) { + HGButton( + onClick, + modifier, + enabled, + defaultHeight, + contentPadding = if (leadingIcon != null) { + ButtonDefaults.ButtonWithIconContentPadding + } else { + ButtonDefaults.ContentPadding + } + ) { + HGButtonContent( + { + Text( + text = text, + style = if (defaultHeight == HGButtonDefaults.SmallButtonHeight) { + MaterialTheme.typography.body2 + } else { + MaterialTheme.typography.subtitle2 + }, + fontWeight = FontWeight.Bold + ) + }, + leadingIcon + ) + } +} + +@Composable +fun HGButton( + onClick: () -> Unit, + modifier: Modifier = Modifier, + enabled: Boolean = true, + defaultHeight: Dp = HGButtonDefaults.LargeButtonHeight, + content: @Composable () -> Unit, + leadingIcon: @Composable (() -> Unit)? = null +) { + HGButton( + onClick, + modifier, + enabled, + defaultHeight, + contentPadding = if (leadingIcon != null) { + ButtonDefaults.ButtonWithIconContentPadding + } else { + ButtonDefaults.ContentPadding + } + ) { + HGButtonContent( + content, + leadingIcon + ) + } +} + +@Composable +fun HGTextButton( + onClick: () -> Unit, + text: String, + modifier: Modifier = Modifier, + enabled: Boolean = true, + defaultHeight: Dp = HGButtonDefaults.LargeButtonHeight, + leadingIcon: Painter +) { + + HGTextButton( + onClick, + text, + modifier, + enabled, + defaultHeight + ) { + Icon( + painter = leadingIcon, + contentDescription = text + ) + } +} + +@Composable +fun HGTextButton( + onClick: () -> Unit, + text: String, + modifier: Modifier = Modifier, + enabled: Boolean = true, + defaultHeight: Dp = HGButtonDefaults.LargeButtonHeight, + leadingIcon: @Composable (() -> Unit)? = null +) { + HGTextButton( + onClick, + modifier, + enabled, + defaultHeight, + contentPadding = if (leadingIcon != null) { + ButtonDefaults.ButtonWithIconContentPadding + } else { + ButtonDefaults.TextButtonContentPadding + } + ) { + HGButtonContent( + { + Text( + text = text, + style = if (defaultHeight == HGButtonDefaults.SmallButtonHeight) { + MaterialTheme.typography.body2 + } else { + MaterialTheme.typography.subtitle2 + }, + fontWeight = FontWeight.Bold + ) + }, + leadingIcon + ) + } +} + +@Composable +fun HGTextButton( + onClick: () -> Unit, + modifier: Modifier = Modifier, + enabled: Boolean = true, + defaultHeight: Dp = HGButtonDefaults.LargeButtonHeight, + content: @Composable () -> Unit, + leadingIcon: @Composable (() -> Unit)? = null +) { + HGTextButton( + onClick, + modifier, + enabled, + defaultHeight, + contentPadding = if (leadingIcon != null) { + ButtonDefaults.ButtonWithIconContentPadding + } else { + ButtonDefaults.TextButtonContentPadding + } + ) { + HGButtonContent( + content, + leadingIcon + ) + } +} + +@Composable +fun HGTextButton( + onClick: () -> Unit, + modifier: Modifier = Modifier, + enabled: Boolean = true, + defaultHeight: Dp = HGButtonDefaults.LargeButtonHeight, + contentPadding: PaddingValues = ButtonDefaults.TextButtonContentPadding, + content: @Composable RowScope.() -> Unit +) { + + TextButton( + onClick = onClick, + modifier = modifier.defaultMinSize(minHeight = defaultHeight), + enabled = enabled, + colors = ButtonDefaults.textButtonColors( + disabledContentColor = MaterialTheme.colors.onPrimary.copy(alpha = 0.20f) + ), + contentPadding = contentPadding, + content = content + ) +} + +@Composable +private fun HGButtonContent( + content: @Composable () -> Unit, + leadingIcon: @Composable (() -> Unit)? = null +) { + if (leadingIcon != null) { + Box(Modifier.sizeIn(maxHeight = ButtonDefaults.IconSize)) { + leadingIcon() + } + } + Box( + Modifier + .padding( + start = if (leadingIcon != null) { + ButtonDefaults.IconSpacing + } else { + 0.dp + }, + ), + ) { + content() + } +} + +val ButtonDefaults.ButtonWithIconContentPadding: PaddingValues + get() = PaddingValues( + start = 16.dp, + top = 8.dp, + end = 8.dp, + bottom = 8.dp + ) + +object HGButtonDefaults { + + val SmallButtonHeight = 42.dp + + val LargeButtonHeight = 54.dp +} + +@Composable +@BasePreview +private fun HGButtonLargePreview() { + HealthGatewayTheme { + HGButton(onClick = { /*TODO*/ }) { + Text(text = "Hello") + } + } +} + +@Composable +@BasePreview +private fun HGButtonLargeDisabledPreview() { + HealthGatewayTheme { + HGButton(onClick = { /*TODO*/ }, enabled = false) { + Text(text = "Hello") + } + } +} + +@Composable +@BasePreview +private fun HGButtonSmallPreview() { + HealthGatewayTheme { + HGButton(onClick = { /*TODO*/ }, defaultHeight = HGButtonDefaults.SmallButtonHeight) { + Text(text = "Hello") + } + } +} + +@Composable +@BasePreview +private fun HGButtonLargeSmallPreview() { + HealthGatewayTheme { + HGButton( + onClick = { /*TODO*/ }, + enabled = false, + defaultHeight = HGButtonDefaults.SmallButtonHeight + ) { + Text(text = "Hello") + } + } +} + +@Composable +@BasePreview +private fun HGTextButtonWithIconPreview() { + HealthGatewayTheme { + HGTextButton(onClick = { /*TODO*/ }, text = "TextButton") { + Icon(painter = painterResource(id = R.drawable.ic_dismiss), contentDescription = "") + } + } +} diff --git a/app/src/main/java/ca/bc/gov/bchealth/compose/component/HGProgressIndicator.kt b/app/src/main/java/ca/bc/gov/bchealth/compose/component/HGProgressIndicator.kt new file mode 100644 index 000000000..f4277d1d3 --- /dev/null +++ b/app/src/main/java/ca/bc/gov/bchealth/compose/component/HGProgressIndicator.kt @@ -0,0 +1,52 @@ +package ca.bc.gov.bchealth.compose.component + +import androidx.compose.animation.core.RepeatMode +import androidx.compose.animation.core.animateFloat +import androidx.compose.animation.core.infiniteRepeatable +import androidx.compose.animation.core.rememberInfiniteTransition +import androidx.compose.animation.core.tween +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material.Icon +import androidx.compose.material.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.scale +import androidx.compose.ui.res.painterResource +import ca.bc.gov.bchealth.R +import ca.bc.gov.bchealth.compose.BasePreview +import ca.bc.gov.bchealth.compose.theme.HealthGatewayTheme + +@Composable +fun HGProgressIndicator(modifier: Modifier = Modifier) { + + Box(modifier = modifier.fillMaxSize()) { + val infiniteTransition = rememberInfiniteTransition() + val heartbeatAnimation by infiniteTransition.animateFloat( + initialValue = 1f, + targetValue = 1.4f, + animationSpec = infiniteRepeatable( + animation = tween(1000), + repeatMode = RepeatMode.Reverse + ) + ) + + Icon( + modifier = Modifier.align(Alignment.Center) + .scale(heartbeatAnimation), + painter = painterResource(id = R.drawable.ic_heart), + contentDescription = null, + tint = MaterialTheme.colors.primary + ) + } +} + +@Composable +@BasePreview +private fun HGProgressIndicatorPreview() { + HealthGatewayTheme { + HGProgressIndicator() + } +} diff --git a/app/src/main/java/ca/bc/gov/bchealth/compose/component/LoginInfoCardUI.kt b/app/src/main/java/ca/bc/gov/bchealth/compose/component/LoginInfoCardUI.kt new file mode 100644 index 000000000..426037c60 --- /dev/null +++ b/app/src/main/java/ca/bc/gov/bchealth/compose/component/LoginInfoCardUI.kt @@ -0,0 +1,87 @@ +package ca.bc.gov.bchealth.compose.component + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material.Card +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import ca.bc.gov.bchealth.R +import ca.bc.gov.bchealth.compose.BasePreview +import ca.bc.gov.bchealth.compose.theme.HealthGatewayTheme +import ca.bc.gov.bchealth.compose.theme.blue +import ca.bc.gov.bchealth.compose.theme.greyBg + +@Composable +fun LoginInfoCardUI( + onClick: () -> Unit, + modifier: Modifier = Modifier, + title: String, + description: String, + buttonText: String, + image: Painter? = null +) { + Card( + modifier = modifier, + backgroundColor = greyBg + ) { + Box(modifier = modifier) { + image?.let { + Image( + modifier = Modifier.align(Alignment.BottomEnd), + painter = it, + contentDescription = null + ) + } + + Column( + modifier = modifier + .padding(start = 16.dp, top = 24.dp, end = 16.dp, bottom = 24.dp) + .fillMaxWidth() + ) { + + Text( + text = title, + style = MaterialTheme.typography.subtitle2, + fontWeight = FontWeight.Bold, + color = blue + ) + Spacer(modifier = Modifier.height(4.dp)) + Text( + text = description, + style = MaterialTheme.typography.body2 + ) + Spacer(modifier = Modifier.height(16.dp)) + HGButton( + onClick = onClick, + text = buttonText, + defaultHeight = HGButtonDefaults.SmallButtonHeight + ) + } + } + } +} + +@Composable +@BasePreview +private fun LoginInfoCardUIPreview() { + HealthGatewayTheme { + LoginInfoCardUI( + onClick = { /*TODO*/ }, + title = stringResource(id = R.string.log_in_with_bc_services_card), + description = stringResource(id = R.string.login_to_view_hidden_records_msg), + buttonText = stringResource(id = R.string.get_started) + ) + } +} diff --git a/app/src/main/java/ca/bc/gov/bchealth/compose/component/QuickAccessTileItemUI.kt b/app/src/main/java/ca/bc/gov/bchealth/compose/component/QuickAccessTileItemUI.kt new file mode 100644 index 000000000..8ae70713f --- /dev/null +++ b/app/src/main/java/ca/bc/gov/bchealth/compose/component/QuickAccessTileItemUI.kt @@ -0,0 +1,155 @@ +package ca.bc.gov.bchealth.compose.component + +import androidx.compose.foundation.Image +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.BoxWithConstraints +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.material.Card +import androidx.compose.material.Icon +import androidx.compose.material.IconButton +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.MoreVert +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.layout.layoutId +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.constraintlayout.compose.ConstraintLayout +import androidx.constraintlayout.compose.ConstraintSet +import androidx.constraintlayout.compose.Dimension +import androidx.constraintlayout.compose.Visibility +import ca.bc.gov.bchealth.R +import ca.bc.gov.bchealth.compose.BasePreview +import ca.bc.gov.bchealth.compose.theme.HealthGatewayTheme + +private const val tileIconId = "tileIconId" +private const val tileTitleId = "tileTitleId" +private const val tileArrowId = "tileArrowId" +private const val moreActionId = "moreActionId" + +@Composable +fun QuickAccessTileItemUI( + onClick: () -> Unit, + onMoreActionClick: () -> Unit, + modifier: Modifier = Modifier, + icon: Painter, + title: String, + hasMoreOptions: Boolean = false +) { + Card( + modifier = modifier + .height(120.dp) + .clickable { onClick() }, + elevation = 15.dp, + backgroundColor = MaterialTheme.colors.background + ) { + + BoxWithConstraints( + modifier = modifier + .fillMaxSize() + ) { + ConstraintLayout( + tileConstraints(hasMoreOptions), + modifier = modifier + .fillMaxSize() + ) { + + Image( + modifier = Modifier + .size(32.dp) + .layoutId(tileIconId), + painter = icon, + contentDescription = null + ) + + Text( + modifier = Modifier + .wrapContentHeight(Alignment.Bottom) + .layoutId(tileTitleId), + text = title, + softWrap = true, + style = MaterialTheme.typography.body2, + color = MaterialTheme.colors.primary, + maxLines = 2 + ) + + Image( + modifier = Modifier.layoutId(tileArrowId), + painter = painterResource(id = R.drawable.ic_right_arrow), + contentDescription = null + ) + + IconButton( + onClick = { + if (hasMoreOptions) { + onMoreActionClick() + } + }, + modifier = Modifier.layoutId(moreActionId) + ) { + Icon(imageVector = Icons.Filled.MoreVert, contentDescription = null) + } + } + } + } +} + +private fun tileConstraints(showMoreAction: Boolean = false): ConstraintSet { + + return ConstraintSet { + val tileIcon = createRefFor(tileIconId) + val tileTitle = createRefFor(tileTitleId) + val tileArrow = createRefFor(tileArrowId) + val moreAction = createRefFor(moreActionId) + + constrain(tileIcon) { + start.linkTo(parent.start, 16.dp) + top.linkTo(parent.top, 16.dp) + } + + constrain(tileTitle) { + start.linkTo(parent.start, 16.dp) + bottom.linkTo(parent.bottom, 16.dp) + end.linkTo(tileArrow.start, 16.dp) + width = Dimension.fillToConstraints + } + + constrain(tileArrow) { + bottom.linkTo(tileTitle.bottom) + end.linkTo(parent.end, 16.dp) + } + + constrain(moreAction) { + top.linkTo(parent.top) + end.linkTo(parent.end) + visibility = if (showMoreAction) { + Visibility.Visible + } else { + Visibility.Gone + } + } + } +} + +@Composable +@BasePreview +private fun QuickAccessTileItemUIPreview() { + + HealthGatewayTheme { + QuickAccessTileItemUI( + onClick = { /*TODO*/ }, + icon = painterResource(id = R.drawable.ic_tile_healt_resources), + title = stringResource(id = R.string.health_resources), + hasMoreOptions = true, + onMoreActionClick = {} + ) + } +} diff --git a/app/src/main/java/ca/bc/gov/bchealth/compose/component/TopAppBar.kt b/app/src/main/java/ca/bc/gov/bchealth/compose/component/TopAppBar.kt new file mode 100644 index 000000000..1f6ca8a4e --- /dev/null +++ b/app/src/main/java/ca/bc/gov/bchealth/compose/component/TopAppBar.kt @@ -0,0 +1,116 @@ +package ca.bc.gov.bchealth.compose.component + +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.material.Icon +import androidx.compose.material.IconButton +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.material.TopAppBar +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import ca.bc.gov.bchealth.R +import ca.bc.gov.bchealth.compose.BasePreview +import ca.bc.gov.bchealth.compose.component.menu.ActionMenu +import ca.bc.gov.bchealth.compose.component.menu.TopAppBarActionItem +import ca.bc.gov.bchealth.compose.theme.HealthGatewayTheme +import ca.bc.gov.bchealth.compose.theme.primaryBlue + +@Composable +fun HGTopAppBar( + modifier: Modifier = Modifier, + title: String, + actionItems: List = emptyList() +) { + var menuOpen by remember { + mutableStateOf(false) + } + + TopAppBar( + modifier = modifier, + title = { + Text( + title, + style = MaterialTheme.typography.subtitle2, + fontWeight = FontWeight.Bold, + ) + }, + actions = { + ActionMenu( + items = actionItems, + isOpen = menuOpen, + onToggleOverflow = { menuOpen = !menuOpen }, + maxVisibleItems = 3 + ) + }, + backgroundColor = MaterialTheme.colors.surface, + contentColor = MaterialTheme.colors.primary + ) +} + +@Composable +fun HGCenterAlignedTopAppBar( + modifier: Modifier = Modifier, + onNavigationAction: () -> Unit, + title: String, + actionItems: List = emptyList() +) { + var menuOpen by remember { + mutableStateOf(false) + } + TopAppBar( + modifier = modifier, + navigationIcon = { + IconButton(onClick = { onNavigationAction() }) { + Icon( + painter = painterResource(id = R.drawable.ic_toolbar_back), + contentDescription = stringResource(id = R.string.back), + tint = primaryBlue + ) + } + }, + title = { + Text( + modifier = Modifier + .fillMaxWidth(), + text = title, + style = MaterialTheme.typography.subtitle2, + fontWeight = FontWeight.Bold, + textAlign = TextAlign.Center + ) + }, + actions = { + ActionMenu( + items = actionItems, + isOpen = menuOpen, + onToggleOverflow = { menuOpen = !menuOpen }, + maxVisibleItems = 3 + ) + }, + backgroundColor = MaterialTheme.colors.surface, + contentColor = MaterialTheme.colors.primary + ) +} + +@Composable +@BasePreview +private fun HGTopAppBarPreview() { + HealthGatewayTheme { + HGTopAppBar(title = stringResource(id = R.string.home)) + } +} + +@Composable +@BasePreview +private fun HGCenterAlignedTopAppBarPreview() { + HealthGatewayTheme { + HGCenterAlignedTopAppBar(onNavigationAction = {}, title = stringResource(id = R.string.home)) + } +} diff --git a/app/src/main/java/ca/bc/gov/bchealth/compose/component/menu/ActionMenu.kt b/app/src/main/java/ca/bc/gov/bchealth/compose/component/menu/ActionMenu.kt new file mode 100644 index 000000000..d4c61302b --- /dev/null +++ b/app/src/main/java/ca/bc/gov/bchealth/compose/component/menu/ActionMenu.kt @@ -0,0 +1,94 @@ +package ca.bc.gov.bchealth.compose.component.menu + +import androidx.compose.material.DropdownMenu +import androidx.compose.material.DropdownMenuItem +import androidx.compose.material.Icon +import androidx.compose.material.IconButton +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.MoreVert +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import ca.bc.gov.bchealth.R + +@Composable +fun ActionMenu( + items: List, + isOpen: Boolean, + onToggleOverflow: () -> Unit, + maxVisibleItems: Int, +) { + val menuItems = remember( + key1 = items, + key2 = maxVisibleItems, + ) { + splitMenuItems(items, maxVisibleItems) + } + + menuItems.alwaysShownItems.forEach { item -> + IconButton(onClick = item.onClick) { + Icon( + painter = painterResource(id = item.icon), + contentDescription = item.contentDescription, + tint = MaterialTheme.colors.primary + ) + } + } + + if (menuItems.overflowItems.isNotEmpty()) { + IconButton(onClick = onToggleOverflow) { + Icon( + imageVector = Icons.Filled.MoreVert, + contentDescription = stringResource(id = R.string.more_options), + tint = MaterialTheme.colors.primary + ) + } + DropdownMenu( + expanded = isOpen, + onDismissRequest = onToggleOverflow, + ) { + menuItems.overflowItems.forEach { item -> + DropdownMenuItem( + content = { + Text(item.title) + }, + onClick = item.onClick + ) + } + } + } +} + +private data class MenuItems( + val alwaysShownItems: List, + val overflowItems: List, +) + +private fun splitMenuItems( + items: List, + maxVisibleItems: Int, +): MenuItems { + val alwaysShownItems: MutableList = + items.filterIsInstance().toMutableList() + val ifRoomItems: MutableList = + items.filterIsInstance().toMutableList() + val overflowItems = items.filterIsInstance() + + val hasOverflow = overflowItems.isNotEmpty() || + (alwaysShownItems.size + ifRoomItems.size - 1) > maxVisibleItems + val usedSlots = alwaysShownItems.size + (if (hasOverflow) 1 else 0) + val availableSlots = maxVisibleItems - usedSlots + if (availableSlots > 0 && ifRoomItems.isNotEmpty()) { + val visible = ifRoomItems.subList(0, availableSlots.coerceAtMost(ifRoomItems.size)) + alwaysShownItems.addAll(visible) + ifRoomItems.removeAll(visible) + } + + return MenuItems( + alwaysShownItems = alwaysShownItems, + overflowItems = ifRoomItems + overflowItems, + ) +} diff --git a/app/src/main/java/ca/bc/gov/bchealth/compose/component/menu/TopAppBarActionItem.kt b/app/src/main/java/ca/bc/gov/bchealth/compose/component/menu/TopAppBarActionItem.kt new file mode 100644 index 000000000..0086589ea --- /dev/null +++ b/app/src/main/java/ca/bc/gov/bchealth/compose/component/menu/TopAppBarActionItem.kt @@ -0,0 +1,31 @@ +package ca.bc.gov.bchealth.compose.component.menu + +import androidx.annotation.DrawableRes + +sealed interface TopAppBarActionItem { + val title: String + val onClick: () -> Unit + + sealed interface IconActionItem : TopAppBarActionItem { + @get:DrawableRes val icon: Int + val contentDescription: String + + data class AlwaysShown( + override val onClick: () -> Unit, + override val title: String, + override val icon: Int, + override val contentDescription: String + ) : IconActionItem + + data class ShowIfRoom( + override val onClick: () -> Unit, + override val title: String, + override val icon: Int, + override val contentDescription: String + ) : IconActionItem + } + data class NeverShown( + override val title: String, + override val onClick: () -> Unit, + ) : TopAppBarActionItem +} diff --git a/app/src/main/java/ca/bc/gov/bchealth/compose/theme/Colors.kt b/app/src/main/java/ca/bc/gov/bchealth/compose/theme/Colors.kt new file mode 100644 index 000000000..76ee2d048 --- /dev/null +++ b/app/src/main/java/ca/bc/gov/bchealth/compose/theme/Colors.kt @@ -0,0 +1,24 @@ +package ca.bc.gov.bchealth.compose.theme + +import androidx.compose.ui.graphics.Color + +val darkText = Color(0xFF313132) +val black = Color(0xFF000000) + +val white = Color(0xFFFFFFFF) + +val blue = Color(0xFF1A5A96) +val primaryBlue = Color(0xFF003366) +val lightBlue = Color(0xFFB2C1CF) +val statusBlue30 = Color(0x4D38598A) +val statusBlue = Color(0xFF38598A) +val bannerBackgroundBlue = Color(0xFFD9EAF7) + +val descriptionGrey = Color(0xFF6D757D) +val grey = Color(0xFF606060) +val greyBg = Color(0xFFF2F2F2) +val dividerGrey = Color(0x14212121) +val disableBackground = Color(0xFFCFCFCF) + +val green = Color(0xFF2E8540) +val red = Color(0xFFD8292F) diff --git a/app/src/main/java/ca/bc/gov/bchealth/compose/theme/FontType.kt b/app/src/main/java/ca/bc/gov/bchealth/compose/theme/FontType.kt new file mode 100644 index 000000000..261b38f13 --- /dev/null +++ b/app/src/main/java/ca/bc/gov/bchealth/compose/theme/FontType.kt @@ -0,0 +1,67 @@ +package ca.bc.gov.bchealth.compose.theme + +import androidx.compose.material.Typography +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.Font +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.sp +import ca.bc.gov.bchealth.R + +val hgFonts = FontFamily( + Font(R.font.bc_sans_regular, weight = FontWeight.Normal, style = FontStyle.Normal), + Font(R.font.bc_sans_bold, weight = FontWeight.Bold, style = FontStyle.Normal), + Font(R.font.bc_sans_italic, weight = FontWeight.Normal, style = FontStyle.Italic), + Font(R.font.bc_sans_bold_italic, weight = FontWeight.Bold, style = FontStyle.Italic), +) + +internal val HealthGatewayTypography = Typography( + defaultFontFamily = hgFonts, + h1 = TextStyle( + fontSize = 60.sp, + lineHeight = 72.sp, + letterSpacing = 0.sp, + ), + h2 = TextStyle( + fontSize = 50.sp, + lineHeight = 64.sp, + letterSpacing = 0.sp, + ), + h3 = TextStyle( + fontSize = 40.sp, + lineHeight = 56.sp, + letterSpacing = 0.sp, + ), + h4 = TextStyle( + fontSize = 33.sp, + lineHeight = 40.sp, + letterSpacing = 0.sp, + ), + h5 = TextStyle( + fontSize = 24.sp, + lineHeight = 32.sp, + letterSpacing = 0.sp, + ), + subtitle1 = TextStyle( + fontSize = 20.sp, + lineHeight = 38.sp, + letterSpacing = 0.sp, + ), + subtitle2 = TextStyle( + fontSize = 17.sp, + lineHeight = 34.sp, + letterSpacing = 0.sp, + ), + body1 = TextStyle( + fontSize = 15.sp, + lineHeight = 24.sp, + letterSpacing = 0.sp, + ), + body2 = TextStyle( + fontSize = 13.sp, + lineHeight = 22.sp, + fontStyle = FontStyle.Normal, + letterSpacing = 0.sp, + ) +) diff --git a/app/src/main/java/ca/bc/gov/bchealth/compose/theme/Theme.kt b/app/src/main/java/ca/bc/gov/bchealth/compose/theme/Theme.kt new file mode 100644 index 000000000..9ed32ed5d --- /dev/null +++ b/app/src/main/java/ca/bc/gov/bchealth/compose/theme/Theme.kt @@ -0,0 +1,16 @@ +package ca.bc.gov.bchealth.compose.theme + +import androidx.compose.material.MaterialTheme +import androidx.compose.material.lightColors +import androidx.compose.runtime.Composable + +private val LightColors = lightColors( + primary = primaryBlue, + onPrimary = white, + background = white, + surface = white +) + +@Composable +fun HealthGatewayTheme(content: @Composable () -> Unit) = + MaterialTheme(colors = LightColors, typography = HealthGatewayTypography, content = content) diff --git a/app/src/main/java/ca/bc/gov/bchealth/ui/BaseFragment.kt b/app/src/main/java/ca/bc/gov/bchealth/ui/BaseFragment.kt index e5eeea7eb..a679f496c 100644 --- a/app/src/main/java/ca/bc/gov/bchealth/ui/BaseFragment.kt +++ b/app/src/main/java/ca/bc/gov/bchealth/ui/BaseFragment.kt @@ -15,8 +15,6 @@ import ca.bc.gov.bchealth.R import ca.bc.gov.bchealth.compose.MyHealthTheme import ca.bc.gov.bchealth.ui.custom.MyHealthToolbar import ca.bc.gov.bchealth.utils.AlertDialogHelper -import ca.bc.gov.bchealth.utils.HEALTH_GATEWAY_EMAIL_ADDRESS -import ca.bc.gov.bchealth.utils.composeEmail import ca.bc.gov.bchealth.utils.launchOnStart import ca.bc.gov.bchealth.utils.showNoInternetConnectionMessage import ca.bc.gov.bchealth.utils.showServiceDownMessage @@ -63,6 +61,12 @@ abstract class BaseFragment(@LayoutRes private val contentLayoutId: Int?) : Frag open fun setToolBar(appBarConfiguration: AppBarConfiguration) {} + @Deprecated( + "Should replace with extension mentioned in the FragmentExtensions" + + "as in compose we need to get rid of all the fragment", + replaceWith = ReplaceWith("launchAndRepeatWithLifecycle"), + level = DeprecationLevel.WARNING + ) fun StateFlow.collectOnStart(action: ((T) -> Unit)) { launchOnStart { this@collectOnStart.collect { state -> @@ -71,10 +75,6 @@ abstract class BaseFragment(@LayoutRes private val contentLayoutId: Int?) : Frag } } - fun composeEmail(address: String = HEALTH_GATEWAY_EMAIL_ADDRESS, subject: String = "") { - requireActivity().composeEmail(address, subject) - } - fun showGenericError() { AlertDialogHelper.showAlertDialog( context = requireContext(), @@ -84,17 +84,13 @@ abstract class BaseFragment(@LayoutRes private val contentLayoutId: Int?) : Frag ) } - fun popNavigation() { - findNavController().popBackStack() - } - fun setupComposeToolbar(composeView: ComposeView, title: String? = null) { composeView.apply { setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) setContent { MyHealthTheme { MyHealthToolbar(title) { - popNavigation() + findNavController().popBackStack() } } } @@ -103,13 +99,13 @@ abstract class BaseFragment(@LayoutRes private val contentLayoutId: Int?) : Frag private fun resetBaseUiState() = getBaseViewModel()?.resetBaseUiState() - private fun showServiceDownMessage() { + fun showServiceDownMessage() { view?.let { it.showServiceDownMessage(it.context) } } - private fun showNoInternetConnectionMessage() { + fun showNoInternetConnectionMessage() { view?.let { it.showNoInternetConnectionMessage(it.context) } diff --git a/app/src/main/java/ca/bc/gov/bchealth/ui/auth/BCServicesCardSessionScreen.kt b/app/src/main/java/ca/bc/gov/bchealth/ui/auth/BCServicesCardSessionScreen.kt index b82cac823..45a2d8074 100644 --- a/app/src/main/java/ca/bc/gov/bchealth/ui/auth/BCServicesCardSessionScreen.kt +++ b/app/src/main/java/ca/bc/gov/bchealth/ui/auth/BCServicesCardSessionScreen.kt @@ -19,7 +19,7 @@ import ca.bc.gov.bchealth.R import ca.bc.gov.bchealth.compose.BasePreview import ca.bc.gov.bchealth.compose.MyHealthTheme import ca.bc.gov.bchealth.compose.MyHealthTypography -import ca.bc.gov.bchealth.compose.greyBg +import ca.bc.gov.bchealth.compose.theme.greyBg @Composable fun BCServicesCardSessionScreen( diff --git a/app/src/main/java/ca/bc/gov/bchealth/ui/comment/CommentsFragment.kt b/app/src/main/java/ca/bc/gov/bchealth/ui/comment/CommentsFragment.kt index 5f9d76eba..047054b69 100644 --- a/app/src/main/java/ca/bc/gov/bchealth/ui/comment/CommentsFragment.kt +++ b/app/src/main/java/ca/bc/gov/bchealth/ui/comment/CommentsFragment.kt @@ -34,7 +34,7 @@ class CommentsFragment : BaseFragment(null) { MyHealthScaffold( title = stringResource(id = R.string.comments), isLoading = uiState.onLoading, - navigationAction = ::popNavigation, + navigationAction = { findNavController().popBackStack() }, ) { CommentsScreen( uiState, @@ -103,7 +103,7 @@ class CommentsFragment : BaseFragment(null) { showError() viewModel.resetUiState() } - state.commentsList != null && state.commentsList.isEmpty() -> popNavigation() + state.commentsList != null && state.commentsList.isEmpty() -> findNavController().popBackStack() } } } diff --git a/app/src/main/java/ca/bc/gov/bchealth/ui/comment/CommentsScreen.kt b/app/src/main/java/ca/bc/gov/bchealth/ui/comment/CommentsScreen.kt index ed74af7d7..74dd30621 100644 --- a/app/src/main/java/ca/bc/gov/bchealth/ui/comment/CommentsScreen.kt +++ b/app/src/main/java/ca/bc/gov/bchealth/ui/comment/CommentsScreen.kt @@ -44,11 +44,11 @@ import androidx.compose.ui.unit.dp import ca.bc.gov.bchealth.R import ca.bc.gov.bchealth.compose.BasePreview import ca.bc.gov.bchealth.compose.MyHealthTypography -import ca.bc.gov.bchealth.compose.grey -import ca.bc.gov.bchealth.compose.greyBg import ca.bc.gov.bchealth.compose.minButtonSize -import ca.bc.gov.bchealth.compose.primaryBlue -import ca.bc.gov.bchealth.compose.red +import ca.bc.gov.bchealth.compose.theme.grey +import ca.bc.gov.bchealth.compose.theme.greyBg +import ca.bc.gov.bchealth.compose.theme.primaryBlue +import ca.bc.gov.bchealth.compose.theme.red import ca.bc.gov.bchealth.widget.CommentInputUI import ca.bc.gov.bchealth.widget.EditableCommentInputUI import ca.bc.gov.common.model.SyncStatus diff --git a/app/src/main/java/ca/bc/gov/bchealth/ui/comment/CommentsSummaryUI.kt b/app/src/main/java/ca/bc/gov/bchealth/ui/comment/CommentsSummaryUI.kt index 01a7ca4e1..2f8ed4124 100644 --- a/app/src/main/java/ca/bc/gov/bchealth/ui/comment/CommentsSummaryUI.kt +++ b/app/src/main/java/ca/bc/gov/bchealth/ui/comment/CommentsSummaryUI.kt @@ -18,9 +18,9 @@ import ca.bc.gov.bchealth.R import ca.bc.gov.bchealth.compose.BasePreview import ca.bc.gov.bchealth.compose.MyHealthTheme import ca.bc.gov.bchealth.compose.MyHealthTypography -import ca.bc.gov.bchealth.compose.blue -import ca.bc.gov.bchealth.compose.grey -import ca.bc.gov.bchealth.compose.greyBg +import ca.bc.gov.bchealth.compose.theme.blue +import ca.bc.gov.bchealth.compose.theme.grey +import ca.bc.gov.bchealth.compose.theme.greyBg import ca.bc.gov.common.model.SyncStatus import ca.bc.gov.common.utils.toDateTimeString import java.time.Instant diff --git a/app/src/main/java/ca/bc/gov/bchealth/ui/custom/MyHealthClickableText.kt b/app/src/main/java/ca/bc/gov/bchealth/ui/custom/MyHealthClickableText.kt index 7aa1c9178..2c5b78812 100644 --- a/app/src/main/java/ca/bc/gov/bchealth/ui/custom/MyHealthClickableText.kt +++ b/app/src/main/java/ca/bc/gov/bchealth/ui/custom/MyHealthClickableText.kt @@ -9,7 +9,7 @@ import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.text.withStyle import androidx.compose.ui.tooling.preview.Preview -import ca.bc.gov.bchealth.compose.primaryBlue +import ca.bc.gov.bchealth.compose.theme.primaryBlue private const val TEXT_TAG = "TEXT_TAG" diff --git a/app/src/main/java/ca/bc/gov/bchealth/ui/custom/MyHealthToolbar.kt b/app/src/main/java/ca/bc/gov/bchealth/ui/custom/MyHealthToolbar.kt index 8fecb0f8b..ac381eeb2 100644 --- a/app/src/main/java/ca/bc/gov/bchealth/ui/custom/MyHealthToolbar.kt +++ b/app/src/main/java/ca/bc/gov/bchealth/ui/custom/MyHealthToolbar.kt @@ -22,8 +22,8 @@ import androidx.compose.ui.unit.dp import ca.bc.gov.bchealth.R import ca.bc.gov.bchealth.compose.MyHealthTheme import ca.bc.gov.bchealth.compose.MyHealthTypography -import ca.bc.gov.bchealth.compose.primaryBlue -import ca.bc.gov.bchealth.compose.white +import ca.bc.gov.bchealth.compose.theme.primaryBlue +import ca.bc.gov.bchealth.compose.theme.white @Composable fun MyHealthToolbar( diff --git a/app/src/main/java/ca/bc/gov/bchealth/ui/dependents/profile/DependentProfileFragment.kt b/app/src/main/java/ca/bc/gov/bchealth/ui/dependents/profile/DependentProfileFragment.kt index ba34a9356..1d318501c 100644 --- a/app/src/main/java/ca/bc/gov/bchealth/ui/dependents/profile/DependentProfileFragment.kt +++ b/app/src/main/java/ca/bc/gov/bchealth/ui/dependents/profile/DependentProfileFragment.kt @@ -19,7 +19,7 @@ class DependentProfileFragment : BaseDependentFragment(null) { @Composable override fun GetComposableLayout() { - DependentProfileUI(viewModel, ::popNavigation) { dto -> + DependentProfileUI(viewModel, { findNavController().popBackStack() }) { dto -> dto?.let { confirmDeletion(dto.patientId, dto.firstname) } diff --git a/app/src/main/java/ca/bc/gov/bchealth/ui/dependents/profile/DependentProfileUI.kt b/app/src/main/java/ca/bc/gov/bchealth/ui/dependents/profile/DependentProfileUI.kt index 74139a62c..0b640e8b3 100644 --- a/app/src/main/java/ca/bc/gov/bchealth/ui/dependents/profile/DependentProfileUI.kt +++ b/app/src/main/java/ca/bc/gov/bchealth/ui/dependents/profile/DependentProfileUI.kt @@ -23,9 +23,9 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import ca.bc.gov.bchealth.R import ca.bc.gov.bchealth.compose.MyHealthTypography -import ca.bc.gov.bchealth.compose.primaryBlue -import ca.bc.gov.bchealth.compose.statusBlue30 -import ca.bc.gov.bchealth.compose.white +import ca.bc.gov.bchealth.compose.theme.primaryBlue +import ca.bc.gov.bchealth.compose.theme.statusBlue30 +import ca.bc.gov.bchealth.compose.theme.white import ca.bc.gov.bchealth.ui.custom.DecorativeImage import ca.bc.gov.bchealth.ui.custom.MyHealthScaffold import ca.bc.gov.common.BuildConfig diff --git a/app/src/main/java/ca/bc/gov/bchealth/ui/dependents/registration/AddDependentFragment.kt b/app/src/main/java/ca/bc/gov/bchealth/ui/dependents/registration/AddDependentFragment.kt index 1227ffc67..667b46f31 100644 --- a/app/src/main/java/ca/bc/gov/bchealth/ui/dependents/registration/AddDependentFragment.kt +++ b/app/src/main/java/ca/bc/gov/bchealth/ui/dependents/registration/AddDependentFragment.kt @@ -12,6 +12,7 @@ import ca.bc.gov.bchealth.ui.BaseFragment import ca.bc.gov.bchealth.utils.AlertDialogHelper import ca.bc.gov.bchealth.utils.DatePickerHelper import ca.bc.gov.bchealth.utils.PhnHelper +import ca.bc.gov.bchealth.utils.composeEmail import ca.bc.gov.bchealth.utils.hideKeyboard import ca.bc.gov.bchealth.utils.launchOnStart import ca.bc.gov.bchealth.utils.showNoInternetConnectionMessage diff --git a/app/src/main/java/ca/bc/gov/bchealth/ui/feeback/FeedbackFragment.kt b/app/src/main/java/ca/bc/gov/bchealth/ui/feeback/FeedbackFragment.kt index 5efc4b32e..0e01323f8 100644 --- a/app/src/main/java/ca/bc/gov/bchealth/ui/feeback/FeedbackFragment.kt +++ b/app/src/main/java/ca/bc/gov/bchealth/ui/feeback/FeedbackFragment.kt @@ -2,6 +2,7 @@ package ca.bc.gov.bchealth.ui.feeback import androidx.compose.runtime.Composable import androidx.fragment.app.viewModels +import androidx.navigation.fragment.findNavController import ca.bc.gov.bchealth.R import ca.bc.gov.bchealth.ui.BaseFragment import ca.bc.gov.bchealth.utils.showErrorSnackbar @@ -18,7 +19,7 @@ class FeedbackFragment : BaseFragment(null) { override fun GetComposableLayout() { FeedbackUI( uiStateFlow = feedbackViewModel.uiState, - navigationAction = ::popNavigation, + navigationAction = { findNavController().popBackStack() }, sendAction = ::onClickSend, onMessageSent = ::onMessageSent, onError = ::displayError, diff --git a/app/src/main/java/ca/bc/gov/bchealth/ui/feeback/FeedbackUI.kt b/app/src/main/java/ca/bc/gov/bchealth/ui/feeback/FeedbackUI.kt index 3abefc3bd..16181c007 100644 --- a/app/src/main/java/ca/bc/gov/bchealth/ui/feeback/FeedbackUI.kt +++ b/app/src/main/java/ca/bc/gov/bchealth/ui/feeback/FeedbackUI.kt @@ -31,11 +31,11 @@ import ca.bc.gov.bchealth.R import ca.bc.gov.bchealth.compose.BasePreview import ca.bc.gov.bchealth.compose.MyHealthTypography import ca.bc.gov.bchealth.compose.SmallDevicePreview -import ca.bc.gov.bchealth.compose.black -import ca.bc.gov.bchealth.compose.grey -import ca.bc.gov.bchealth.compose.lightBlue import ca.bc.gov.bchealth.compose.minButtonSize -import ca.bc.gov.bchealth.compose.red +import ca.bc.gov.bchealth.compose.theme.black +import ca.bc.gov.bchealth.compose.theme.grey +import ca.bc.gov.bchealth.compose.theme.lightBlue +import ca.bc.gov.bchealth.compose.theme.red import ca.bc.gov.bchealth.model.validation.BaseTextValidation import ca.bc.gov.bchealth.model.validation.BaseTextValidation.BLANK import ca.bc.gov.bchealth.model.validation.BaseTextValidation.EXCEEDS_LENGTH diff --git a/app/src/main/java/ca/bc/gov/bchealth/ui/healthrecord/HealthRecordListItem.kt b/app/src/main/java/ca/bc/gov/bchealth/ui/healthrecord/HealthRecordListItem.kt index fcfd5044f..caa2a4646 100644 --- a/app/src/main/java/ca/bc/gov/bchealth/ui/healthrecord/HealthRecordListItem.kt +++ b/app/src/main/java/ca/bc/gov/bchealth/ui/healthrecord/HealthRecordListItem.kt @@ -10,8 +10,8 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import ca.bc.gov.bchealth.compose.MyHealthTypography import ca.bc.gov.bchealth.compose.bold -import ca.bc.gov.bchealth.compose.descriptionGrey import ca.bc.gov.bchealth.compose.italic +import ca.bc.gov.bchealth.compose.theme.descriptionGrey @Composable fun HealthRecordListItem(modifier: Modifier = Modifier, label: String, value: String, footer: String? = null) { diff --git a/app/src/main/java/ca/bc/gov/bchealth/ui/healthrecord/clinicaldocument/ClinicalDocumentDetailFragment.kt b/app/src/main/java/ca/bc/gov/bchealth/ui/healthrecord/clinicaldocument/ClinicalDocumentDetailFragment.kt index ddb783c9b..869fb1fc5 100644 --- a/app/src/main/java/ca/bc/gov/bchealth/ui/healthrecord/clinicaldocument/ClinicalDocumentDetailFragment.kt +++ b/app/src/main/java/ca/bc/gov/bchealth/ui/healthrecord/clinicaldocument/ClinicalDocumentDetailFragment.kt @@ -29,7 +29,7 @@ class ClinicalDocumentDetailFragment : BaseFragment(null) { @Composable override fun GetComposableLayout() { - ClinicalDocumentDetailUI(viewModel, ::popNavigation) + ClinicalDocumentDetailUI(viewModel, { findNavController().popBackStack() }) } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { diff --git a/app/src/main/java/ca/bc/gov/bchealth/ui/healthrecord/healthvisits/HealthVisitDetailFragment.kt b/app/src/main/java/ca/bc/gov/bchealth/ui/healthrecord/healthvisits/HealthVisitDetailFragment.kt index 9bb109fa3..5af2b5d72 100644 --- a/app/src/main/java/ca/bc/gov/bchealth/ui/healthrecord/healthvisits/HealthVisitDetailFragment.kt +++ b/app/src/main/java/ca/bc/gov/bchealth/ui/healthrecord/healthvisits/HealthVisitDetailFragment.kt @@ -41,7 +41,7 @@ class HealthVisitDetailFragment : BaseFragment(null) { MyHealthScaffold( title = uiState.title, isLoading = uiState.onLoading, - navigationAction = ::popNavigation + navigationAction = { findNavController().popBackStack() } ) { HealthVisitDetailScreen( uiState = uiState, @@ -56,7 +56,7 @@ class HealthVisitDetailFragment : BaseFragment(null) { showGenericError() viewModel.resetUiState() } - if (commentState?.isBcscSessionActive == false) popNavigation() + if (commentState?.isBcscSessionActive == false) findNavController().popBackStack() } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { diff --git a/app/src/main/java/ca/bc/gov/bchealth/ui/healthrecord/hospitalvisits/HospitalVisitDetailFragment.kt b/app/src/main/java/ca/bc/gov/bchealth/ui/healthrecord/hospitalvisits/HospitalVisitDetailFragment.kt index 817035e38..85a0731f5 100644 --- a/app/src/main/java/ca/bc/gov/bchealth/ui/healthrecord/hospitalvisits/HospitalVisitDetailFragment.kt +++ b/app/src/main/java/ca/bc/gov/bchealth/ui/healthrecord/hospitalvisits/HospitalVisitDetailFragment.kt @@ -4,6 +4,7 @@ import android.os.Bundle import android.view.View import androidx.compose.runtime.Composable import androidx.fragment.app.viewModels +import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.navArgs import ca.bc.gov.bchealth.ui.BaseFragment import dagger.hilt.android.AndroidEntryPoint @@ -15,7 +16,7 @@ class HospitalVisitDetailFragment : BaseFragment(null) { @Composable override fun GetComposableLayout() { - HospitalVisitDetailUI(viewModel, ::popNavigation) + HospitalVisitDetailUI(viewModel, { findNavController().popBackStack() }) } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { diff --git a/app/src/main/java/ca/bc/gov/bchealth/ui/healthrecord/imaging/DiagnosticImagingDetailScreen.kt b/app/src/main/java/ca/bc/gov/bchealth/ui/healthrecord/imaging/DiagnosticImagingDetailScreen.kt index e1a0e2e98..b43d9b0b6 100644 --- a/app/src/main/java/ca/bc/gov/bchealth/ui/healthrecord/imaging/DiagnosticImagingDetailScreen.kt +++ b/app/src/main/java/ca/bc/gov/bchealth/ui/healthrecord/imaging/DiagnosticImagingDetailScreen.kt @@ -20,7 +20,7 @@ import ca.bc.gov.bchealth.R import ca.bc.gov.bchealth.compose.BasePreview import ca.bc.gov.bchealth.compose.MyHealthTheme import ca.bc.gov.bchealth.compose.MyHealthTypography -import ca.bc.gov.bchealth.compose.descriptionGrey +import ca.bc.gov.bchealth.compose.theme.descriptionGrey import ca.bc.gov.bchealth.ui.component.HGLargeOutlinedButton import ca.bc.gov.bchealth.ui.healthrecord.HealthRecordDetailItem import ca.bc.gov.bchealth.ui.healthrecord.HealthRecordListItem diff --git a/app/src/main/java/ca/bc/gov/bchealth/ui/healthrecord/labtest/LabTestDetailAdapter.kt b/app/src/main/java/ca/bc/gov/bchealth/ui/healthrecord/labtest/LabTestDetailAdapter.kt deleted file mode 100644 index 3439d5941..000000000 --- a/app/src/main/java/ca/bc/gov/bchealth/ui/healthrecord/labtest/LabTestDetailAdapter.kt +++ /dev/null @@ -1,213 +0,0 @@ -package ca.bc.gov.bchealth.ui.healthrecord.labtest - -import android.content.Context -import android.graphics.Typeface -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.core.content.res.ResourcesCompat -import androidx.recyclerview.widget.DiffUtil -import androidx.recyclerview.widget.ListAdapter -import androidx.recyclerview.widget.RecyclerView -import ca.bc.gov.bchealth.R -import ca.bc.gov.bchealth.databinding.ItemLabTestDetailBannerBinding -import ca.bc.gov.bchealth.databinding.ItemLabTestDetailBinding -import ca.bc.gov.bchealth.databinding.ItemViewPdfBinding -import ca.bc.gov.bchealth.utils.makeLinks -import ca.bc.gov.bchealth.utils.redirect -import ca.bc.gov.bchealth.utils.showIfNullOrBlank - -/** - * @author: Created by Rashmi Bambhania on 02,March,2022 - */ -class LabTestDetailAdapter(private val viewPdfClickListener: ViewPdfClickListener) : - ListAdapter(LabTestRecordsDiffCallBacks()) { - - class LabTestBannerViewHolder(val binding: ItemLabTestDetailBannerBinding) : - RecyclerView.ViewHolder(binding.root) - - class LabOrderViewHolder(val binding: ItemLabTestDetailBinding) : - RecyclerView.ViewHolder(binding.root) - - class LabTestViewHolder(val binding: ItemLabTestDetailBinding) : - RecyclerView.ViewHolder(binding.root) - - class LabTestViewPdfViewHolder(val binding: ItemViewPdfBinding) : - RecyclerView.ViewHolder(binding.root) - - fun interface ViewPdfClickListener { - fun onClick() - } - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { - return when (viewType) { - LabTestDetailViewModel.ITEM_VIEW_TYPE_LAB_TEST_BANNER -> { - val binding = ItemLabTestDetailBannerBinding.inflate( - LayoutInflater.from(parent.context), parent, false - ) - LabTestBannerViewHolder(binding) - } - LabTestDetailViewModel.ITEM_VIEW_TYPE_LAB_ORDER -> { - val binding = ItemLabTestDetailBinding.inflate( - LayoutInflater.from(parent.context), parent, false - ) - LabOrderViewHolder(binding) - } - LabTestDetailViewModel.ITEM_VIEW_TYPE_LAB_TEST -> { - val binding = ItemLabTestDetailBinding.inflate( - LayoutInflater.from(parent.context), parent, false - ) - LabTestViewHolder(binding) - } - LabTestDetailViewModel.ITEM_VIEW_PDF -> { - val binding = ItemViewPdfBinding.inflate( - LayoutInflater.from(parent.context), parent, false - ) - LabTestViewPdfViewHolder(binding) - } - else -> throw ClassCastException("Unknown viewType $viewType") - } - } - - override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { - val labTestDetail = getItem(position) - when (holder) { - is LabTestBannerViewHolder -> displayBanner(holder, labTestDetail) - is LabOrderViewHolder -> displayLabOrder(holder, labTestDetail) - is LabTestViewHolder -> displayLabTest(holder, labTestDetail) - is LabTestViewPdfViewHolder -> { - holder.binding.btnViewPdf.setOnClickListener { viewPdfClickListener.onClick() } - } - } - } - - private fun displayBanner(holder: LabTestBannerViewHolder, labTestDetail: LabTestDetail) { - holder.binding.apply { - holder.itemView.context.apply { - tvTitle.text = labTestDetail.bannerHeader?.let { getString(it) } - tvInfo.text = labTestDetail.bannerText?.let { getString(it) } - labTestDetail.bannerClickableText?.let { - tvInfo.makeLinks( - getString(it) to View.OnClickListener { - redirect(getString(R.string.faq_link)) - }, - ) - } - } - } - } - - private fun displayLabOrder(holder: LabOrderViewHolder, labTestDetail: LabTestDetail) { - holder.binding.apply { - tvHeader.visibility = View.GONE - tvSummary.visibility = View.GONE - tvDesc1.text = - labTestDetail.timelineDateTime.showIfNullOrBlank(holder.itemView.context) - tvDesc2.text = - labTestDetail.orderingProvider.showIfNullOrBlank(holder.itemView.context) - tvDesc3.text = - labTestDetail.reportingSource.showIfNullOrBlank(holder.itemView.context) - holder.itemView.context.apply { - tvTitle1.text = labTestDetail.title1?.let { getString(it) } - tvTitle2.text = labTestDetail.title2?.let { getString(it) } - tvTitle3.text = labTestDetail.title3?.let { getString(it) } - } - } - } - - private fun displayLabTest(holder: LabTestViewHolder, labTestDetail: LabTestDetail) { - holder.binding.apply { - labTestDetail.header?.let { - tvHeader.text = holder.itemView.context.getString(it) - tvHeader.visibility = View.VISIBLE - } ?: run { - tvHeader.visibility = View.GONE - } - labTestDetail.summary?.let { - holder.itemView.context.apply { - tvSummary.text = getString(it) - tvSummary.makeLinks( - Pair( - getString(R.string.learn_more), - View.OnClickListener { - redirect(getString(R.string.faq_link)) - } - ) - ) - } - tvSummary.visibility = View.VISIBLE - } ?: run { - tvSummary.visibility = View.GONE - } - holder.itemView.context.apply { - tvTitle1.text = labTestDetail.title1?.let { getString(it) } - tvTitle2.text = labTestDetail.title2?.let { getString(it) } - tvTitle3.text = labTestDetail.title3?.let { getString(it) } - } - - tvDesc1.text = labTestDetail.testName.showIfNullOrBlank(holder.itemView.context) - - val pair = getTestResult( - labTestDetail.isOutOfRange, - labTestDetail.testStatus, - holder.itemView.context - ) - tvDesc2.text = pair.first.showIfNullOrBlank(holder.itemView.context) - tvDesc2.setTextColor(pair.second) - val typeface = - ResourcesCompat.getFont(holder.itemView.context, R.font.bc_sans_bold) - tvDesc2.setTypeface(typeface, Typeface.BOLD) - - tvDesc3.text = labTestDetail.testStatus?.let { - holder.itemView.context.getString( - it - ) - } - } - } - - private fun getTestResult( - outOfRange: Boolean?, - testStatus: Int?, - context: Context - ): Pair { - outOfRange?.let { - return if (outOfRange) { - Pair( - context.resources.getString(R.string.out_of_range), - context.resources.getColor( - R.color.error, null - ) - ) - } else { - Pair( - context.resources.getString(R.string.in_range), - context.resources.getColor( - R.color.status_green, null - ) - ) - } - } ?: run { - return Pair( - testStatus?.let { context.resources.getString(it) } ?: run { "" }, - context.resources.getColor( - R.color.text_black, null - ) - ) - } - } - - override fun getItemViewType(position: Int): Int { - return getItem(position).viewType - } -} - -class LabTestRecordsDiffCallBacks : DiffUtil.ItemCallback() { - override fun areItemsTheSame(oldItem: LabTestDetail, newItem: LabTestDetail): Boolean { - return oldItem.id == newItem.id - } - - override fun areContentsTheSame(oldItem: LabTestDetail, newItem: LabTestDetail): Boolean { - return oldItem == newItem - } -} diff --git a/app/src/main/java/ca/bc/gov/bchealth/ui/healthrecord/labtest/LabTestDetailFragment.kt b/app/src/main/java/ca/bc/gov/bchealth/ui/healthrecord/labtest/LabTestDetailFragment.kt index 70c061f45..1be94d0ce 100644 --- a/app/src/main/java/ca/bc/gov/bchealth/ui/healthrecord/labtest/LabTestDetailFragment.kt +++ b/app/src/main/java/ca/bc/gov/bchealth/ui/healthrecord/labtest/LabTestDetailFragment.kt @@ -1,40 +1,27 @@ package ca.bc.gov.bchealth.ui.healthrecord.labtest -import android.os.Bundle -import android.view.View import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.runtime.Composable import androidx.core.os.bundleOf -import androidx.core.view.isVisible import androidx.fragment.app.viewModels -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.lifecycleScope -import androidx.lifecycle.repeatOnLifecycle import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.navArgs -import androidx.recyclerview.widget.ConcatAdapter import ca.bc.gov.bchealth.R -import ca.bc.gov.bchealth.databinding.FragmentLabTestDetailBinding -import ca.bc.gov.bchealth.ui.comment.CommentEntryTypeCode -import ca.bc.gov.bchealth.ui.healthrecord.BaseRecordDetailFragment -import ca.bc.gov.bchealth.utils.AlertDialogHelper +import ca.bc.gov.bchealth.ui.BaseFragment +import ca.bc.gov.bchealth.ui.comment.CommentsSummary +import ca.bc.gov.bchealth.ui.comment.CommentsViewModel import ca.bc.gov.bchealth.utils.PdfHelper -import ca.bc.gov.bchealth.utils.showNoInternetConnectionMessage -import ca.bc.gov.bchealth.utils.showServiceDownMessage -import ca.bc.gov.bchealth.utils.viewBindings +import ca.bc.gov.bchealth.utils.redirect +import ca.bc.gov.bchealth.viewmodel.PdfDecoderUiState import ca.bc.gov.bchealth.viewmodel.PdfDecoderViewModel -import ca.bc.gov.bchealth.widget.AddCommentLayout import dagger.hilt.android.AndroidEntryPoint -import kotlinx.coroutines.launch import java.io.File @AndroidEntryPoint -class LabTestDetailFragment : BaseRecordDetailFragment(R.layout.fragment_lab_test_detail) { - - private val binding by viewBindings(FragmentLabTestDetailBinding::bind) +class LabTestDetailFragment : BaseFragment(null) { private val viewModel: LabTestDetailViewModel by viewModels() + private val commentsViewModel: CommentsViewModel by viewModels() - private lateinit var labTestDetailAdapter: LabTestDetailAdapter - private lateinit var concatAdapter: ConcatAdapter private val args: LabTestDetailFragmentArgs by navArgs() private val pdfDecoderViewModel: PdfDecoderViewModel by viewModels() private var fileInMemory: File? = null @@ -42,109 +29,47 @@ class LabTestDetailFragment : BaseRecordDetailFragment(R.layout.fragment_lab_tes ActivityResultContracts.StartActivityForResult() ) { fileInMemory?.delete() } - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - setupComposeToolbar(binding.composeToolbar.root) - setUpRecyclerView(args.hdid) - viewModel.getLabTestDetails(args.labOrderId) - observeUiState() - observePdfData() - initComments() - } - - override fun getCommentView(): AddCommentLayout = binding.comment - - override fun getScrollableView() = binding.rvLabTestDetailList - - override fun getCommentEntryTypeCode() = CommentEntryTypeCode.LAB_RESULTS - - override fun getParentEntryId(): String? = viewModel.uiState.value.parentEntryId - - override fun getProgressBar(): View = binding.progressBar - - private fun setUpRecyclerView(hdid: String?) { - labTestDetailAdapter = LabTestDetailAdapter( - viewPdfClickListener = { viewModel.getLabTestPdf(hdid) } + @Composable + override fun GetComposableLayout() { + LabTestScreen( + hdid = args.hdid, + labOrderId = args.labOrderId, + viewModel = viewModel, + commentsViewModel = commentsViewModel, + pdfDecoderViewModel = pdfDecoderViewModel, + onClickFaq = { context?.redirect(getString(R.string.faq_link)) }, + onPopNavigation = findNavController()::popBackStack, + showServiceDownMessage = ::showServiceDownMessage, + showNoInternetConnectionMessage = ::showNoInternetConnectionMessage, + onPdfStateChanged = ::onPdfStateChanged, + navigateToComments = ::navigateToComments ) - - concatAdapter = ConcatAdapter(labTestDetailAdapter, getRecordCommentsAdapter()) - - binding.rvLabTestDetailList.adapter = concatAdapter - } - - private fun observeUiState() { - viewModel.uiState.collectOnStart { state -> - binding.progressBar.isVisible = state.onLoading - - handledServiceDown(state) - - if (state.labTestDetails?.isNotEmpty() == true) { - labTestDetailAdapter.submitList(state.labTestDetails) - setupComposeToolbar(binding.composeToolbar.root, state.toolbarTitle) - } - - if (state.onError) { - showError() - viewModel.resetUiState() - } - - handlePdfDownload(state) - - handleNoInternetConnection(state) - getComments(state.parentEntryId) - } - } - - private fun handledServiceDown(state: LabTestDetailUiState) { - if (!state.isHgServicesUp) { - binding.root.showServiceDownMessage(requireContext()) - viewModel.resetUiState() - } - } - - private fun handleNoInternetConnection(uiState: LabTestDetailUiState) { - if (!uiState.isConnected) { - binding.root.showNoInternetConnectionMessage(requireContext()) - viewModel.resetUiState() - } - } - - private fun handlePdfDownload(state: LabTestDetailUiState) { - if (state.pdfData?.isNotEmpty() == true) { - pdfDecoderViewModel.base64ToPDFFile(state.pdfData) - viewModel.resetUiState() - } } - private fun showError() { - AlertDialogHelper.showAlertDialog( - context = requireContext(), - title = getString(R.string.error), - msg = getString(R.string.error_message), - positiveBtnMsg = getString(R.string.dialog_button_ok) + private fun navigateToComments(commentsSummary: CommentsSummary) { + findNavController().navigate( + R.id.commentsFragment, + bundleOf( + "parentEntryId" to commentsSummary.parentEntryId, + "recordType" to commentsSummary.entryTypeCode, + ) ) } - private fun observePdfData() { - viewLifecycleOwner.lifecycleScope.launch { - viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { - pdfDecoderViewModel.uiState.collect { uiState -> - uiState.pdf?.let { - val (base64Pdf, file) = it - if (file != null) { - try { - fileInMemory = file - PdfHelper().showPDF(file, requireActivity(), resultListener) - } catch (e: Exception) { - fallBackToPdfRenderer(base64Pdf) - } - } else { - fallBackToPdfRenderer(base64Pdf) - } - pdfDecoderViewModel.resetUiState() - } + private fun onPdfStateChanged(uiState: PdfDecoderUiState) { + uiState.pdf?.let { + val (base64Pdf, file) = it + if (file != null) { + try { + fileInMemory = file + PdfHelper().showPDF(file, requireActivity(), resultListener) + } catch (e: Exception) { + fallBackToPdfRenderer(base64Pdf) } + } else { + fallBackToPdfRenderer(base64Pdf) } + pdfDecoderViewModel.resetUiState() } } diff --git a/app/src/main/java/ca/bc/gov/bchealth/ui/healthrecord/labtest/LabTestDetailViewModel.kt b/app/src/main/java/ca/bc/gov/bchealth/ui/healthrecord/labtest/LabTestDetailViewModel.kt index 22fd99385..3e3c85eba 100644 --- a/app/src/main/java/ca/bc/gov/bchealth/ui/healthrecord/labtest/LabTestDetailViewModel.kt +++ b/app/src/main/java/ca/bc/gov/bchealth/ui/healthrecord/labtest/LabTestDetailViewModel.kt @@ -1,9 +1,13 @@ package ca.bc.gov.bchealth.ui.healthrecord.labtest import androidx.annotation.StringRes +import androidx.compose.ui.graphics.Color import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import ca.bc.gov.bchealth.R +import ca.bc.gov.bchealth.compose.theme.darkText +import ca.bc.gov.bchealth.compose.theme.green +import ca.bc.gov.bchealth.compose.theme.red import ca.bc.gov.common.exceptions.NetworkConnectionException import ca.bc.gov.common.exceptions.ServiceDownException import ca.bc.gov.common.model.labtest.LabOrderWithLabTestDto @@ -90,6 +94,7 @@ class LabTestDetailViewModel @Inject constructor( testStatus = R.string.corrected result = labTest.outOfRange } + else -> { testStatus = R.string.completed result = labTest.outOfRange @@ -211,6 +216,15 @@ class LabTestDetailViewModel @Inject constructor( ) } } + + fun resetPdfUiState() { + _uiState.update { + it.copy( + onLoading = false, + pdfData = null, + ) + } + } } data class LabTestDetailUiState( @@ -243,4 +257,16 @@ data class LabTestDetail( val bannerText: Int? = null, val bannerClickableText: Int? = null, val viewType: Int = LabTestDetailViewModel.ITEM_VIEW_TYPE_LAB_ORDER -) +) { + fun getResultColor(): Color = when (isOutOfRange) { + null -> darkText + true -> red + false -> green + } + + fun getResultText(): Int? = when (isOutOfRange) { + null -> testStatus + true -> R.string.out_of_range + false -> R.string.in_range + } +} diff --git a/app/src/main/java/ca/bc/gov/bchealth/ui/healthrecord/labtest/LabTestScreen.kt b/app/src/main/java/ca/bc/gov/bchealth/ui/healthrecord/labtest/LabTestScreen.kt new file mode 100644 index 000000000..4333aa477 --- /dev/null +++ b/app/src/main/java/ca/bc/gov/bchealth/ui/healthrecord/labtest/LabTestScreen.kt @@ -0,0 +1,312 @@ +package ca.bc.gov.bchealth.ui.healthrecord.labtest + +import androidx.compose.foundation.gestures.detectTapGestures +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +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.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.livedata.observeAsState +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.Modifier +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalSoftwareKeyboardController +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.work.WorkInfo +import androidx.work.WorkManager +import ca.bc.gov.bchealth.R +import ca.bc.gov.bchealth.compose.BasePreview +import ca.bc.gov.bchealth.compose.theme.HealthGatewayTheme +import ca.bc.gov.bchealth.ui.comment.CommentEntryTypeCode +import ca.bc.gov.bchealth.ui.comment.CommentsSummary +import ca.bc.gov.bchealth.ui.comment.CommentsSummaryUI +import ca.bc.gov.bchealth.ui.comment.CommentsUiState +import ca.bc.gov.bchealth.ui.comment.CommentsViewModel +import ca.bc.gov.bchealth.ui.custom.MyHealthScaffold +import ca.bc.gov.bchealth.ui.healthrecord.labtest.LabTestDetailViewModel.Companion.ITEM_VIEW_PDF +import ca.bc.gov.bchealth.ui.healthrecord.labtest.LabTestDetailViewModel.Companion.ITEM_VIEW_TYPE_LAB_ORDER +import ca.bc.gov.bchealth.ui.healthrecord.labtest.LabTestDetailViewModel.Companion.ITEM_VIEW_TYPE_LAB_TEST +import ca.bc.gov.bchealth.ui.healthrecord.labtest.LabTestDetailViewModel.Companion.ITEM_VIEW_TYPE_LAB_TEST_BANNER +import ca.bc.gov.bchealth.utils.AlertDialogHelper +import ca.bc.gov.bchealth.viewmodel.PdfDecoderUiState +import ca.bc.gov.bchealth.viewmodel.PdfDecoderViewModel +import ca.bc.gov.bchealth.widget.CommentInputUI +import ca.bc.gov.common.BuildConfig +import ca.bc.gov.repository.SYNC_COMMENTS + +@Composable +fun LabTestScreen( + onPdfStateChanged: (PdfDecoderUiState) -> Unit, + onClickFaq: () -> Unit, + onPopNavigation: () -> Unit, + showServiceDownMessage: () -> Unit, + showNoInternetConnectionMessage: () -> Unit, + navigateToComments: (CommentsSummary) -> Unit, + hdid: String?, + labOrderId: Long, + viewModel: LabTestDetailViewModel, + commentsViewModel: CommentsViewModel, + pdfDecoderViewModel: PdfDecoderViewModel, +) { + val uiState = viewModel.uiState + .collectAsStateWithLifecycle(minActiveState = Lifecycle.State.RESUMED).value + + var commentState: CommentsUiState? = null + + if (BuildConfig.FLAG_ADD_COMMENTS) { + uiState.parentEntryId?.let { commentsViewModel.getComments(it) } + commentState = commentsViewModel.uiState + .collectAsStateWithLifecycle(minActiveState = Lifecycle.State.RESUMED).value + } + + ObserveCommentsWorker(viewModel, commentsViewModel) + + val pdfUiState = pdfDecoderViewModel.uiState + .collectAsStateWithLifecycle(minActiveState = Lifecycle.State.RESUMED).value + + LaunchedEffect(Unit) { + viewModel.getLabTestDetails(labOrderId) + } + + MyHealthScaffold( + title = uiState.toolbarTitle, + isLoading = uiState.onLoading, + navigationAction = onPopNavigation + ) { + LabTestContent( + uiState = uiState, + onClickViewPdf = { viewModel.getLabTestPdf(hdid) }, + onClickFaq = onClickFaq, + onClickComments = navigateToComments, + commentsSummary = commentState?.commentsSummary, + onSubmitComment = { onSubmitComment(viewModel, commentsViewModel, it) } + ) + } + + onPdfStateChanged(pdfUiState) + + handledServiceDown(uiState, viewModel, showServiceDownMessage) + + if (uiState.onError) { + ErrorDialog() + viewModel.resetUiState() + } + + handlePdfDownload(uiState, viewModel, pdfDecoderViewModel) + + handleNoInternetConnection(uiState, viewModel, showNoInternetConnectionMessage) + + if (commentState?.isBcscSessionActive == false) onPopNavigation.invoke() +} + +private fun onSubmitComment( + viewModel: LabTestDetailViewModel, + commentsViewModel: CommentsViewModel, + content: String +) { + viewModel.uiState.value.parentEntryId?.let { parentEntryId -> + commentsViewModel.addComment( + parentEntryId, + content, + CommentEntryTypeCode.LAB_RESULTS.value, + ) + } +} + +@Composable +private fun ObserveCommentsWorker( + viewModel: LabTestDetailViewModel, + commentsViewModel: CommentsViewModel +) { + if (BuildConfig.FLAG_ADD_COMMENTS.not()) return + + val workRequest = WorkManager + .getInstance(LocalContext.current) + .getWorkInfosForUniqueWorkLiveData(SYNC_COMMENTS) + .observeAsState() + val workState = workRequest.value?.firstOrNull()?.state + if (workState == WorkInfo.State.SUCCEEDED && workState.isFinished) { + LaunchedEffect(key1 = Unit) { + viewModel.uiState.value.parentEntryId?.let { parentId -> + commentsViewModel.getComments(parentId) + } + } + } +} + +private fun handledServiceDown( + state: LabTestDetailUiState, + viewModel: LabTestDetailViewModel, + showServiceDownMessage: () -> Unit, +) { + if (!state.isHgServicesUp) { + showServiceDownMessage() + viewModel.resetUiState() + } +} + +private fun handleNoInternetConnection( + uiState: LabTestDetailUiState, + viewModel: LabTestDetailViewModel, + showNoInternetConnectionMessage: () -> Unit, +) { + if (!uiState.isConnected) { + showNoInternetConnectionMessage() + viewModel.resetUiState() + } +} + +private fun handlePdfDownload( + state: LabTestDetailUiState, + viewModel: LabTestDetailViewModel, + pdfDecoderViewModel: PdfDecoderViewModel, +) { + if (state.pdfData?.isNotEmpty() == true) { + pdfDecoderViewModel.base64ToPDFFile(state.pdfData) + viewModel.resetPdfUiState() + } +} + +@Composable +private fun ErrorDialog() { + AlertDialogHelper.showAlertDialog( + context = LocalContext.current, + title = stringResource(R.string.error), + msg = stringResource(R.string.error_message), + positiveBtnMsg = stringResource(R.string.dialog_button_ok) + ) +} + +@OptIn(ExperimentalComposeUiApi::class) +@Composable +private fun LabTestContent( + uiState: LabTestDetailUiState, + onClickViewPdf: () -> Unit, + onClickFaq: () -> Unit, + onClickComments: (CommentsSummary) -> Unit, + commentsSummary: CommentsSummary?, + onSubmitComment: (String) -> Unit, +) { + val keyboardController = LocalSoftwareKeyboardController.current + + Column( + modifier = Modifier + .fillMaxSize() + .pointerInput(Unit) { + detectTapGestures(onPress = { keyboardController?.hide() }) + }, + ) { + LabTestContent(uiState, onClickViewPdf, onClickFaq, onClickComments, commentsSummary) + + if (BuildConfig.FLAG_ADD_COMMENTS) { + CommentInputUI(onSubmitComment = onSubmitComment) + } + } +} + +@Composable +private fun ColumnScope.LabTestContent( + uiState: LabTestDetailUiState, + onClickViewPdf: () -> Unit, + onClickFaq: () -> Unit, + onClickComments: (CommentsSummary) -> Unit, + commentsSummary: CommentsSummary?, +) { + if (uiState.labTestDetails.isNullOrEmpty()) return + + LazyColumn( + Modifier + .fillMaxWidth() + .padding(horizontal = 32.dp) + .weight(1f) + ) { + items(uiState.labTestDetails) { sample -> + when (sample.viewType) { + ITEM_VIEW_TYPE_LAB_ORDER -> LabOrderUi(sample) + ITEM_VIEW_TYPE_LAB_TEST -> LabTestUi(sample, onClickFaq) + ITEM_VIEW_TYPE_LAB_TEST_BANNER -> LabTestBannerUi(sample, onClickFaq) + ITEM_VIEW_PDF -> LabTestPdfButton(onClickViewPdf) + } + } + + if (BuildConfig.FLAG_ADD_COMMENTS) { + item { Spacer(modifier = Modifier.weight(1f)) } + item { + CommentsSummaryUI( + commentsSummary = commentsSummary, + onClickComments = onClickComments + ) + } + } + } +} + +@Composable +@BasePreview +fun LabTestContentPreview() { + val sample = listOf( + LabTestDetail( + bannerHeader = R.string.lab_test_banner_pending_title, + bannerText = R.string.lab_test_banner_pending_message_1, + bannerClickableText = R.string.lab_test_banner_pending_clickable_text, + viewType = ITEM_VIEW_TYPE_LAB_TEST_BANNER + ), + + LabTestDetail( + viewType = ITEM_VIEW_PDF + ), + + LabTestDetail( + title1 = R.string.collection_date, + collectionDateTime = "08/11/2022", + timelineDateTime = "09/11/2022", + title2 = R.string.ordering_provider, + orderingProvider = "provider", + title3 = R.string.reporting_lab, + reportingSource = "source" + ), + + LabTestDetail( + header = R.string.test_summary, + summary = R.string.summary_desc, + title1 = R.string.test_name, + testName = "the test name", + title2 = R.string.result, + isOutOfRange = false, + title3 = R.string.lab_test_status, + testStatus = R.string.corrected, + viewType = ITEM_VIEW_TYPE_LAB_TEST + ), + + LabTestDetail( + header = R.string.test_summary, + summary = R.string.summary_desc, + title1 = R.string.test_name, + testName = "the test name", + title2 = R.string.result, + isOutOfRange = false, + title3 = R.string.lab_test_status, + testStatus = R.string.corrected, + viewType = ITEM_VIEW_TYPE_LAB_TEST + ), + ) + HealthGatewayTheme { + LabTestContent( + LabTestDetailUiState(labTestDetails = sample, toolbarTitle = "Lab Results"), + {}, + {}, + {}, + null, + {} + ) + } +} diff --git a/app/src/main/java/ca/bc/gov/bchealth/ui/healthrecord/labtest/LabTestUI.kt b/app/src/main/java/ca/bc/gov/bchealth/ui/healthrecord/labtest/LabTestUI.kt new file mode 100644 index 000000000..5976feec1 --- /dev/null +++ b/app/src/main/java/ca/bc/gov/bchealth/ui/healthrecord/labtest/LabTestUI.kt @@ -0,0 +1,284 @@ +package ca.bc.gov.bchealth.ui.healthrecord.labtest + +import androidx.compose.foundation.background +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.size +import androidx.compose.material.Divider +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextDecoration +import androidx.compose.ui.unit.dp +import ca.bc.gov.bchealth.R +import ca.bc.gov.bchealth.compose.BasePreview +import ca.bc.gov.bchealth.compose.MyHealthTypography +import ca.bc.gov.bchealth.compose.bold +import ca.bc.gov.bchealth.compose.theme.HealthGatewayTheme +import ca.bc.gov.bchealth.compose.theme.bannerBackgroundBlue +import ca.bc.gov.bchealth.compose.theme.blue +import ca.bc.gov.bchealth.compose.theme.dividerGrey +import ca.bc.gov.bchealth.compose.theme.primaryBlue +import ca.bc.gov.bchealth.ui.component.HGSmallOutlinedButton +import ca.bc.gov.bchealth.ui.custom.DecorativeImage +import ca.bc.gov.bchealth.ui.custom.MyHealthClickableText +import ca.bc.gov.bchealth.utils.orPlaceholderIfNullOrBlank + +@Composable +fun LabTestPdfButton(onClickViewPdf: () -> Unit) { + HGSmallOutlinedButton( + modifier = Modifier + .fillMaxWidth() + .padding(top = 8.dp), + onClick = onClickViewPdf, + text = "View PDF" + ) + + LabTestDivider() +} + +@Composable +fun LabTestBannerUi(labTestDetail: LabTestDetail, onClickFaq: () -> Unit) { + + val title = labTestDetail.bannerHeader?.let { stringResource(it) } ?: return + val body = labTestDetail.bannerText?.let { stringResource(it) } ?: return + val clickableText = labTestDetail.bannerClickableText?.let { stringResource(it) } + + Row( + Modifier + .fillMaxWidth() + .padding(vertical = 16.dp) + .background(color = bannerBackgroundBlue) + .padding(16.dp) + ) { + DecorativeImage(resourceId = R.drawable.ic_info) + Spacer(modifier = Modifier.size(8.dp)) + Column { + Text( + text = title, + color = blue, + style = MyHealthTypography.body2.bold() + ) + + clickableText?.let { + MyHealthClickableText( + style = MyHealthTypography.body2.copy(color = blue), + fullText = body, + clickableText = it, + action = onClickFaq, + clickableStyle = SpanStyle( + color = primaryBlue, + textDecoration = TextDecoration.Underline, + fontWeight = FontWeight.Bold + ) + ) + } ?: run { + Text( + style = MyHealthTypography.body2.copy(color = blue), + text = body, + ) + } + } + } +} + +@Composable +fun LabTestUi(labTestDetail: LabTestDetail, onClickFaq: () -> Unit) { + Column { + LabTestHeaderUi( + labTestDetail.header, + labTestDetail.summary?.let { stringResource(id = it) }, + onClickFaq + ) + + LabTestItemUi( + labTestDetail.title1, + labTestDetail.testName.orPlaceholderIfNullOrBlank(), + ) + + LabTestResultItemUi(labTestDetail) + + LabTestItemUi( + labTestDetail.title3, + labTestDetail.testStatus?.let { stringResource(id = it) }, + ) + + LabTestDivider() + } +} + +@Composable +fun LabOrderUi(labTestDetail: LabTestDetail) { + Column { + LabTestItemUi( + labTestDetail.title1, + labTestDetail.timelineDateTime.orPlaceholderIfNullOrBlank(), + ) + + LabTestItemUi( + labTestDetail.title2, + labTestDetail.orderingProvider.orPlaceholderIfNullOrBlank(), + ) + + LabTestItemUi( + labTestDetail.title3, + labTestDetail.reportingSource.orPlaceholderIfNullOrBlank(), + ) + + LabTestDivider() + } +} + +@Composable +private fun LabTestHeaderUi(title: Int?, body: String?, onClickFaq: () -> Unit) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(top = 16.dp) + ) { + title?.let { + Text( + text = stringResource(id = it), + style = MyHealthTypography.body2.bold() + ) + } + + body?.let { + MyHealthClickableText( + style = MyHealthTypography.body2, + fullText = it, + clickableText = stringResource(R.string.learn_more), + action = onClickFaq, + clickableStyle = SpanStyle( + color = primaryBlue, + textDecoration = TextDecoration.Underline, + fontWeight = FontWeight.Bold + ) + ) + } + } +} + +@Composable +private fun LabTestItemUi(title: Int?, body: String?) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(top = 16.dp) + ) { + title?.let { + Text( + text = stringResource(id = it), + style = MyHealthTypography.body2.bold() + ) + } + + body?.let { + Text( + text = it, + style = MyHealthTypography.body2 + ) + } + } +} + +@Composable +private fun LabTestResultItemUi(labTestDetail: LabTestDetail) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(top = 16.dp) + ) { + labTestDetail.title2?.let { + Text( + text = stringResource(id = it), + style = MyHealthTypography.body2.bold() + ) + } + + Text( + text = labTestDetail.getResultText()?.let { stringResource(id = it) } + .orPlaceholderIfNullOrBlank(), + color = labTestDetail.getResultColor(), + style = MyHealthTypography.body2.bold() + ) + } +} + +@Composable +private fun LabTestDivider() { + Divider( + modifier = Modifier + .fillMaxWidth() + .padding(top = 16.dp), + color = dividerGrey, + thickness = 1.dp + ) +} + +@BasePreview +@Composable +private fun LabTestPdfButtonPreview() { + HealthGatewayTheme { + LabTestPdfButton {} + } +} + +@BasePreview +@Composable +private fun LabTestBannerUiPreview() { + HealthGatewayTheme { + LabTestBannerUi( + LabTestDetail( + bannerHeader = R.string.lab_test_banner_pending_title, + bannerText = R.string.lab_test_banner_pending_message_1, + bannerClickableText = R.string.lab_test_banner_pending_clickable_text, + viewType = LabTestDetailViewModel.ITEM_VIEW_TYPE_LAB_TEST_BANNER + ), + ) {} + } +} + +@BasePreview +@Composable +private fun LabTestUiPreview() { + HealthGatewayTheme { + LabTestUi( + LabTestDetail( + header = R.string.test_summary, + summary = R.string.summary_desc, + title1 = R.string.test_name, + testName = "the test name", + title2 = R.string.result, + isOutOfRange = false, + title3 = R.string.lab_test_status, + testStatus = R.string.corrected, + viewType = LabTestDetailViewModel.ITEM_VIEW_TYPE_LAB_TEST + ), + ) {} + } +} + +@BasePreview +@Composable +private fun LabOrderUiPreview() { + HealthGatewayTheme { + LabOrderUi( + LabTestDetail( + title1 = R.string.collection_date, + collectionDateTime = "08/11/2022", + timelineDateTime = "09/11/2022", + title2 = R.string.ordering_provider, + orderingProvider = "provider", + title3 = R.string.reporting_lab, + reportingSource = "source" + ), + ) + } +} diff --git a/app/src/main/java/ca/bc/gov/bchealth/ui/home/HomeBannerUI.kt b/app/src/main/java/ca/bc/gov/bchealth/ui/home/HomeBannerUI.kt index ed56648b4..e69de29bb 100644 --- a/app/src/main/java/ca/bc/gov/bchealth/ui/home/HomeBannerUI.kt +++ b/app/src/main/java/ca/bc/gov/bchealth/ui/home/HomeBannerUI.kt @@ -1,248 +0,0 @@ -package ca.bc.gov.bchealth.ui.home - -import android.text.method.LinkMovementMethod -import android.util.TypedValue -import android.widget.TextView -import androidx.compose.foundation.Image -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.Text -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.draw.shadow -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import androidx.compose.ui.viewinterop.AndroidView -import androidx.constraintlayout.compose.ConstraintLayout -import androidx.constraintlayout.compose.Dimension -import ca.bc.gov.bchealth.R -import ca.bc.gov.bchealth.compose.BasePreview -import ca.bc.gov.bchealth.compose.MyHealthTypography -import ca.bc.gov.bchealth.compose.blue -import ca.bc.gov.bchealth.compose.bold -import ca.bc.gov.bchealth.compose.minButtonSize -import ca.bc.gov.bchealth.compose.primaryBlue -import ca.bc.gov.bchealth.ui.custom.DecorativeImage -import ca.bc.gov.bchealth.utils.fromHtml - -@Composable -fun BannerUI( - uiState: BannerItem, - onClickToggle: () -> Unit, - onClickLearnMore: (BannerItem) -> Unit, - onClickDismiss: () -> Unit, -) { - - if (uiState.isHidden) return - - ConstraintLayout( - Modifier - .fillMaxWidth() - .padding(top = 16.dp, bottom = 8.dp, start = 32.dp, end = 32.dp) - .shadow(elevation = 8.dp, shape = RoundedCornerShape(10.dp)) - .clip(RoundedCornerShape(10.dp)) - .background(Color(0xFFD9EAF7)) - - ) { - - val (imgIcon, txtTitle, btToggle, txtBody, btLearnMore, btDismiss, spacer) = createRefs() - - DecorativeImage( - resourceId = R.drawable.ic_banner_icon, - modifier = Modifier - .constrainAs(imgIcon) { - start.linkTo(parent.start, margin = 16.dp) - linkTo(txtTitle.top, txtTitle.bottom, bias = 0.52f) - } - ) - - Text( - text = uiState.title, - modifier = Modifier - .constrainAs(txtTitle) { - start.linkTo(imgIcon.end, margin = 8.dp) - end.linkTo(btToggle.start) - top.linkTo(parent.top, margin = 16.dp) - width = Dimension.fillToConstraints - }, - color = blue, - style = MyHealthTypography.h3, - maxLines = 1, - overflow = TextOverflow.Ellipsis - ) - - val toggleIcon = if (uiState.expanded) { - R.drawable.ic_content_short - } else { - R.drawable.ic_content_full - } - - Image( - painter = painterResource(id = toggleIcon), - contentScale = ContentScale.Inside, - modifier = Modifier - .constrainAs(btToggle) { - end.linkTo(parent.end) - top.linkTo(txtTitle.top) - bottom.linkTo(txtTitle.bottom) - } - .width(minButtonSize) - .height(minButtonSize) - .clickable { onClickToggle.invoke() }, - contentDescription = stringResource(id = R.string.expand_content) - ) - - if (uiState.expanded) { - Box( - modifier = Modifier - .constrainAs(txtBody) { - top.linkTo(txtTitle.bottom, margin = 8.dp) - start.linkTo(txtTitle.start) - end.linkTo(parent.end, margin = 24.dp) - width = Dimension.fillToConstraints - } - ) { - AndroidView( - modifier = Modifier, - factory = { context -> - TextView(context).apply { - setTextAppearance(R.style.HealthGateway_TextAppearance_MaterialComponents_Headline4) - setTextSize(TypedValue.COMPLEX_UNIT_SP, 13f) - } - }, - update = { - it.text = uiState.body.fromHtml().trimEnd().take(120) - it.movementMethod = LinkMovementMethod.getInstance() - } - ) - } - - if (uiState.displayReadMore) { - Row( - modifier = Modifier - .clickable { onClickLearnMore.invoke(uiState) } - .constrainAs(btLearnMore) { - top.linkTo(btDismiss.top) - end.linkTo(btDismiss.start, margin = 24.dp) - }, - verticalAlignment = Alignment.CenterVertically - ) { - DecorativeImage(resourceId = R.drawable.ic_external_link) - Text( - modifier = Modifier.padding(start = 4.dp, top = 16.dp, bottom = 16.dp), - text = stringResource(id = R.string.learn_more).uppercase(), - style = MyHealthTypography.h4.bold(), - color = primaryBlue, - fontSize = 13.sp - ) - } - } - - Row( - modifier = Modifier - .clickable { onClickDismiss.invoke() } - .constrainAs(btDismiss) { - top.linkTo(txtBody.bottom) - end.linkTo(parent.end, margin = 24.dp) - }, - verticalAlignment = Alignment.CenterVertically - ) { - DecorativeImage(resourceId = R.drawable.ic_dismiss) - Text( - modifier = Modifier.padding(start = 4.dp, top = 16.dp, bottom = 16.dp), - text = stringResource(id = R.string.dismiss).uppercase(), - style = MyHealthTypography.h4.bold(), - color = primaryBlue, - fontSize = 13.sp - ) - } - } - Spacer( - modifier = Modifier - .constrainAs(spacer) { - top.linkTo(txtTitle.bottom, margin = 16.dp) - } - ) - } -} - -@BasePreview -@Composable -private fun PreviewBannerUI() { - BannerUI( - uiState = BannerItem( - title = "Great news! Really Big Announcement", - body = "View and manage all your available health records, including dispensed medications, health visits, COVID-19 test results, immunizations and more.", - date = "", - displayReadMore = true, - ), - onClickToggle = {}, - onClickLearnMore = {}, - onClickDismiss = {} - ) -} - -@BasePreview -@Composable -private fun PreviewBannerUICollapsed() { - BannerUI( - uiState = BannerItem( - title = "Great news! Really Big Announcement", - body = "View and manage all your available health records, including dispensed medications, health visits, COVID-19 test results, immunizations and more.", - date = "", - displayReadMore = true, - expanded = false - ), - onClickToggle = {}, - onClickLearnMore = {}, - onClickDismiss = {} - ) -} - -@BasePreview -@Composable -private fun PreviewBannerUIWithoutReadMore() { - BannerUI( - uiState = BannerItem( - title = "Great news! Really Big Announcement", - body = "View and manage all your available health records, including dispensed medications, health visits, COVID-19 test results, immunizations and more.", - date = "", - displayReadMore = false, - ), - onClickToggle = {}, - onClickLearnMore = {}, - onClickDismiss = {} - ) -} - -@BasePreview -@Composable -private fun PreviewBannerUIHidden() { - BannerUI( - uiState = BannerItem( - title = "Great news! Really Big Announcement", - body = "View and manage all your available health records, including dispensed medications, health visits, COVID-19 test results, immunizations and more.", - date = "", - displayReadMore = true, - isHidden = true, - ), - onClickToggle = {}, - onClickLearnMore = {}, - onClickDismiss = {} - ) -} diff --git a/app/src/main/java/ca/bc/gov/bchealth/ui/home/HomeCardUI.kt b/app/src/main/java/ca/bc/gov/bchealth/ui/home/HomeCardUI.kt index a8090f3be..e69de29bb 100644 --- a/app/src/main/java/ca/bc/gov/bchealth/ui/home/HomeCardUI.kt +++ b/app/src/main/java/ca/bc/gov/bchealth/ui/home/HomeCardUI.kt @@ -1,137 +0,0 @@ -package ca.bc.gov.bchealth.ui.home - -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.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.Button -import androidx.compose.material.Text -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.draw.shadow -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import ca.bc.gov.bchealth.R -import ca.bc.gov.bchealth.compose.BasePreview -import ca.bc.gov.bchealth.compose.MyHealthTheme -import ca.bc.gov.bchealth.compose.MyHealthTypography -import ca.bc.gov.bchealth.compose.bold -import ca.bc.gov.bchealth.compose.primaryBlue -import ca.bc.gov.bchealth.compose.white -import ca.bc.gov.bchealth.ui.custom.DecorativeImage - -@Composable -fun HomeCardUI(uiItem: HomeRecordItem, onClickItem: (HomeRecordItem) -> Unit) { - Column( - modifier = Modifier - .fillMaxWidth() - .padding(top = 16.dp, bottom = 8.dp, start = 32.dp, end = 32.dp) - .shadow(elevation = 8.dp, shape = RoundedCornerShape(4.dp)) - .clip(RoundedCornerShape(4.dp)) - .clickable { onClickItem.invoke(uiItem) } - .background(white) - .padding(bottom = 32.dp) - ) { - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.padding(start = 32.dp, top = 32.dp) - ) { - DecorativeImage(resourceId = uiItem.iconTitle) - - Text( - modifier = Modifier.padding(start = 8.dp, end = 32.dp), - text = stringResource(id = uiItem.title), - style = MyHealthTypography.h3 - ) - } - Text( - modifier = Modifier.padding(top = 8.dp, start = 32.dp, end = 32.dp), - text = stringResource(id = uiItem.description), - style = MyHealthTypography.h4, - fontSize = 13.sp, - ) - - CardButtonUI(uiItem, onClickItem) - } -} - -@Composable -private fun CardButtonUI(uiItem: HomeRecordItem, onClickItem: (HomeRecordItem) -> Unit) { - when (uiItem.recordType) { - HomeNavigationType.HEALTH_RECORD -> { - Button( - onClick = { onClickItem.invoke(uiItem) }, - modifier = Modifier.padding(top = 8.dp, start = 32.dp, end = 32.dp) - ) { - Text( - text = stringResource(id = uiItem.btnTitle), - style = MyHealthTypography.button.bold() - ) - } - } - - else -> { - Row( - modifier = Modifier.padding(top = 16.dp, start = 32.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Text( - text = stringResource(id = uiItem.btnTitle), - style = MyHealthTypography.h4, - fontSize = 15.sp, - color = primaryBlue - ) - DecorativeImage( - resourceId = uiItem.icon, - modifier = Modifier.padding(start = 16.dp) - ) - } - } - } -} - -@BasePreview -@Composable -fun PreviewHomeCardUIHealthRecord() { - MyHealthTheme { - HomeCardUI( - uiItem = - HomeRecordItem( - iconTitle = R.drawable.ic_login_info, - title = R.string.health_records, - description = R.string.home_recommendations_body, - icon = R.drawable.ic_right_arrow, - btnTitle = R.string.get_started, - recordType = HomeNavigationType.HEALTH_RECORD - - ), - onClickItem = {}, - ) - } -} - -@BasePreview -@Composable -fun PreviewHomeCardUI() { - MyHealthTheme { - HomeCardUI( - uiItem = - HomeRecordItem( - iconTitle = R.drawable.ic_login_info, - title = R.string.recommendations_home_title, - description = R.string.home_recommendations_body, - icon = R.drawable.ic_right_arrow, - btnTitle = R.string.get_started, - recordType = HomeNavigationType.RECOMMENDATIONS - - ), - onClickItem = {}, - ) - } -} diff --git a/app/src/main/java/ca/bc/gov/bchealth/ui/home/HomeFragment.kt b/app/src/main/java/ca/bc/gov/bchealth/ui/home/HomeFragment.kt index 00e09831c..c8ab183a4 100644 --- a/app/src/main/java/ca/bc/gov/bchealth/ui/home/HomeFragment.kt +++ b/app/src/main/java/ca/bc/gov/bchealth/ui/home/HomeFragment.kt @@ -1,85 +1,58 @@ package ca.bc.gov.bchealth.ui.home -import android.app.Activity import android.os.Bundle import android.view.View -import androidx.activity.result.contract.ActivityResultContracts -import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.statusBarsPadding -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll -import androidx.compose.material.Icon -import androidx.compose.material.IconButton -import androidx.compose.material.MaterialTheme +import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.Scaffold -import androidx.compose.material.contentColorFor import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState import androidx.compose.ui.Modifier -import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.core.os.bundleOf import androidx.fragment.app.activityViewModels import androidx.fragment.app.viewModels import androidx.lifecycle.Lifecycle -import androidx.lifecycle.lifecycleScope -import androidx.lifecycle.repeatOnLifecycle +import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.fragment.findNavController +import ca.bc.gov.bchealth.HomeDirections import ca.bc.gov.bchealth.R -import ca.bc.gov.bchealth.compose.BasePreview -import ca.bc.gov.bchealth.compose.MyHealthTheme +import ca.bc.gov.bchealth.compose.component.HGTopAppBar +import ca.bc.gov.bchealth.compose.component.menu.TopAppBarActionItem +import ca.bc.gov.bchealth.compose.theme.HealthGatewayTheme import ca.bc.gov.bchealth.ui.BaseSecureFragment import ca.bc.gov.bchealth.ui.BcscAuthState +import ca.bc.gov.bchealth.ui.NavigationAction import ca.bc.gov.bchealth.ui.auth.BioMetricState import ca.bc.gov.bchealth.ui.auth.BiometricsAuthenticationFragment -import ca.bc.gov.bchealth.ui.custom.MyHealthToolBar +import ca.bc.gov.bchealth.ui.filter.TimelineTypeFilter +import ca.bc.gov.bchealth.ui.healthrecord.filter.PatientFilterViewModel import ca.bc.gov.bchealth.ui.login.BcscAuthViewModel -import ca.bc.gov.bchealth.ui.login.LoginStatus -import ca.bc.gov.bchealth.utils.AlertDialogHelper -import ca.bc.gov.bchealth.utils.launchOnStart import ca.bc.gov.bchealth.utils.observeCurrentBackStackForAction -import ca.bc.gov.bchealth.utils.showServiceDownMessage import ca.bc.gov.bchealth.viewmodel.SharedViewModel +import ca.bc.gov.common.model.UserAuthenticationStatus import dagger.hilt.android.AndroidEntryPoint -import kotlinx.coroutines.launch +@OptIn(ExperimentalMaterialApi::class) @AndroidEntryPoint class HomeFragment : BaseSecureFragment(null) { - private val viewModel: HomeViewModel by activityViewModels() + private val viewModel: HomeViewModel by viewModels() + private val authViewModel: BcscAuthViewModel by viewModels() private val sharedViewModel: SharedViewModel by activityViewModels() - private val bcscAuthViewModel: BcscAuthViewModel by viewModels() - - private var logoutResultLauncher = registerForActivityResult( - ActivityResultContracts.StartActivityForResult() - ) { activityResult -> - if (activityResult.resultCode == Activity.RESULT_OK) { - bcscAuthViewModel.processLogoutResponse(requireContext()).invokeOnCompletion { - updateHomeRecordsList() - } - } else { - AlertDialogHelper.showAlertDialog( - context = requireContext(), - title = getString(R.string.error), - msg = getString(R.string.error_message), - positiveBtnMsg = getString(R.string.dialog_button_ok) - ) - } - } + private val filterSharedViewModel: PatientFilterViewModel by activityViewModels() override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) observeCurrentBackStackForAction(BiometricsAuthenticationFragment.BIOMETRIC_STATE) { + viewModel.onBiometricAuthenticationCompleted() when (it) { BioMetricState.SUCCESS -> { + sharedViewModel.shouldFetchBanner = true findNavController().currentBackStackEntry?.savedStateHandle?.remove( BiometricsAuthenticationFragment.BIOMETRIC_STATE ) - viewModel.onAuthenticationRequired(false) - viewModel.launchCheck() viewModel.executeOneTimeDataFetch() } @@ -88,198 +61,135 @@ class HomeFragment : BaseSecureFragment(null) { } } } - - viewLifecycleOwner.lifecycleScope.launch { - viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { - launch { - onBoardingFlow() - } - } - } - - updateHomeRecordsList() - observeAuthStatus() - - bcscAuthViewModel.checkSession() - - viewModel.launchCheck() - viewModel.getAuthenticatedPatientName() } @Composable override fun GetComposableLayout() { - val homeUiState = viewModel.uiState.collectAsState().value - val bannerUiState = viewModel.bannerState.collectAsState().value - val homeItems = viewModel.homeList.collectAsState().value.orEmpty() - val authState = bcscAuthViewModel.authStatus.collectAsState().value + val userAuthState = + authViewModel.userAuthenticationState.collectAsStateWithLifecycle(minActiveState = Lifecycle.State.RESUMED).value + + val authState = authViewModel.authStatus.collectAsStateWithLifecycle(minActiveState = Lifecycle.State.RESUMED).value + val menuItems = mutableListOf( + TopAppBarActionItem.IconActionItem.AlwaysShown( + title = getString(R.string.settings), + onClick = { findNavController().navigate(R.id.settingsFragment) }, + icon = R.drawable.ic_menu_settings, + contentDescription = getString(R.string.settings), + ) + ) + + if (userAuthState == UserAuthenticationStatus.AUTHENTICATED) { + menuItems.add( + 0, + TopAppBarActionItem.IconActionItem.AlwaysShown( + title = getString(R.string.notifications), + onClick = { findNavController().navigate(R.id.notificationFragment) }, + icon = R.drawable.ic_notification, + contentDescription = getString(R.string.notifications), + ) + ) + } - MyHealthTheme { + HealthGatewayTheme { Scaffold( - topBar = { HomeToolbar(authState.loginStatus != LoginStatus.NOT_AUTHENTICATED) }, + topBar = { + HGTopAppBar( + title = authState.userName ?: stringResource(id = R.string.home), + actionItems = menuItems + ) + }, content = { - Column( - modifier = Modifier + HomeScreen( + Modifier .statusBarsPadding() .navigationBarsPadding() - .padding(it) - .verticalScroll(rememberScrollState()), - ) { - HomeScreen( - homeUiState.patientFirstName, - bannerUiState, - viewModel::toggleBanner, - viewModel::dismissBanner, - ::onClickLearnMore, - homeItems, - ::navigateToDestination - ) - } - }, - contentColor = contentColorFor(backgroundColor = MaterialTheme.colors.background) + .padding(it), + authViewModel = authViewModel, + viewModel = viewModel, + sharedViewModel = sharedViewModel, + onLoginClick = ::onLoginClick, + onManageClick = ::onManageClicked, + onOnBoardingRequired = ::onOnBoardingRequired, + onBiometricAuthenticationRequired = ::onBiometricAuthenticationRequired, + onQuickAccessTileClicked = ::onQuickAccessTileClicked, + onMoreActionClick = ::onMoreActionClicked + ) + } ) } } - @Composable - private fun HomeToolbar(isAuthenticated: Boolean) { - MyHealthToolBar( - title = "", - actions = { - - if (isAuthenticated) { - IconButton( - onClick = { findNavController().navigate(R.id.notificationFragment) } - ) { - Icon( - painter = painterResource(id = R.drawable.ic_notification), - contentDescription = stringResource(id = R.string.notifications), - tint = MaterialTheme.colors.primary - ) - } - } - IconButton( - onClick = { findNavController().navigate(R.id.settingsFragment) } - ) { - Icon( - painter = painterResource(id = R.drawable.ic_settings), - contentDescription = stringResource( - id = R.string.settings - ), - tint = MaterialTheme.colors.primary - ) + override fun handleBCSCAuthState(bcscAuthState: BcscAuthState) { + viewModel.resetUIState() + when (bcscAuthState) { + BcscAuthState.SUCCESS -> { + if (sharedViewModel.destinationId > 0) { + findNavController().navigate(sharedViewModel.destinationId) } } - ) - } - override fun handleBCSCAuthState(bcscAuthState: BcscAuthState) { - if (bcscAuthState == BcscAuthState.SUCCESS) { - if (sharedViewModel.destinationId > 0) { - findNavController().navigate(sharedViewModel.destinationId) + BcscAuthState.NO_ACTION, + BcscAuthState.NOT_NOW -> { } } } - private fun observeAuthStatus() { - viewLifecycleOwner.lifecycleScope.launch { - viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { - bcscAuthViewModel.authStatus.collect { - - if (it.isError) { - bcscAuthViewModel.resetAuthStatus() - } + override fun handleNavigationAction(navigationAction: NavigationAction) { + when (navigationAction) { + NavigationAction.ACTION_BACK -> { + findNavController().popBackStack() + } - if (it.endSessionIntent != null) { - logoutResultLauncher.launch(it.endSessionIntent) - bcscAuthViewModel.resetAuthStatus() - } - } + NavigationAction.ACTION_RE_CHECK -> { + authViewModel.checkSession() } } } - private fun updateHomeRecordsList() { - launchOnStart { viewModel.getHomeRecordsList() } + private fun onLoginClick() { + sharedViewModel.destinationId = 0 + findNavController().navigate(R.id.bcscAuthInfoFragment) } - private fun navigateToDestination(destinationType: HomeNavigationType) { - val destination = when (destinationType) { - HomeNavigationType.HEALTH_RECORD -> { - if (bcscAuthViewModel.authStatus.value.loginStatus == LoginStatus.ACTIVE) { - R.id.action_homeFragment_to_health_records - } else { - sharedViewModel.destinationId = R.id.health_records - R.id.bcscAuthInfoFragment - } - } - - HomeNavigationType.VACCINE_PROOF -> R.id.action_homeFragment_to_health_pass - - HomeNavigationType.RESOURCES -> R.id.action_homeFragment_to_resources - - HomeNavigationType.RECOMMENDATIONS -> R.id.action_homeFragment_to_recommendations + private fun onBiometricAuthenticationRequired() { + if (!sharedViewModel.isBiometricAuthShown) { + findNavController().navigate(R.id.biometricsAuthenticationFragment) + sharedViewModel.isBiometricAuthShown = true } - findNavController().navigate(destination) + viewModel.onBiometricAuthenticationCompleted() } - private suspend fun onBoardingFlow() { - viewModel.uiState.collect { uiState -> - if (uiState.isOnBoardingRequired || uiState.isReOnBoardingRequired) { - - findNavController().navigate( - R.id.onBoardingSliderFragment, - bundleOf("reOnBoardingRequired" to uiState.isReOnBoardingRequired) - ) - viewModel.onBoardingShown() - } - - if (uiState.isAuthenticationRequired && !sharedViewModel.isBiometricAuthShown) { - findNavController().navigate(R.id.biometricsAuthenticationFragment) - sharedViewModel.isBiometricAuthShown = true - viewModel.onAuthenticationRequired(false) - } - - if (uiState.isBcscLoginRequiredPostBiometrics) { - sharedViewModel.destinationId = 0 - findNavController().navigate(R.id.bcscAuthInfoFragment) - sharedViewModel.isBCSCAuthShown = true - viewModel.onBcscLoginRequired(false) - } + private fun onOnBoardingRequired(isReOnBoarding: Boolean) { + findNavController().navigate( + R.id.onBoardingSliderFragment, + bundleOf("reOnBoardingRequired" to isReOnBoarding) + ) + } - if (uiState.isForceLogout) { - bcscAuthViewModel.getEndSessionIntent() - viewModel.onForceLogout(false) + private fun onQuickAccessTileClicked(quickAccessTileItem: QuickAccessTileItem) { + when (quickAccessTileItem) { + is QuickAccessTileItem.QuickLinkTileItem -> { + quickAccessTileItem.payload?.let { + filterSharedViewModel.updateFilter( + listOf(TimelineTypeFilter.findByFilterValue(it).name) + ) + } } - if (uiState.displayServiceDownMessage) { - view?.showServiceDownMessage(requireContext()) - viewModel.resetUiState() + else -> { + filterSharedViewModel.clearFilter() } } + findNavController().navigate(quickAccessTileItem.destinationId) } - private fun onClickLearnMore(banner: BannerItem) { - val action = HomeFragmentDirections.actionHomeFragmentToBannerDetail( - title = banner.title, - date = banner.date, - body = banner.body, - ) - findNavController().navigate(action) - } - - @BasePreview - @Composable - private fun PreviewHomeToolbar() { - MyHealthTheme { - HomeToolbar(true) - } + private fun onManageClicked() { + findNavController().navigate(R.id.quickAccessManagementFragment) } - @BasePreview - @Composable - private fun PreviewHomeToolbarNonAuthenticated() { - MyHealthTheme { - HomeToolbar(false) - } + private fun onMoreActionClicked(id: Long, name: String) { + val action = + HomeDirections.actionGlobalRemoveQuickAccessTileBottomSheetFragment(id, name) + findNavController().navigate(action) } } diff --git a/app/src/main/java/ca/bc/gov/bchealth/ui/home/HomeScreen.kt b/app/src/main/java/ca/bc/gov/bchealth/ui/home/HomeScreen.kt index 7a7a28deb..f78b468c5 100644 --- a/app/src/main/java/ca/bc/gov/bchealth/ui/home/HomeScreen.kt +++ b/app/src/main/java/ca/bc/gov/bchealth/ui/home/HomeScreen.kt @@ -1,109 +1,348 @@ package ca.bc.gov.bchealth.ui.home -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.height +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.BoxWithConstraints +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.GridItemSpan +import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.foundation.lazy.grid.items import androidx.compose.material.MaterialTheme import androidx.compose.material.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.layoutId +import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextDecoration +import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp +import androidx.compose.ui.window.Popup +import androidx.constraintlayout.compose.ConstraintLayout +import androidx.constraintlayout.compose.ConstraintSet +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.compose.collectAsStateWithLifecycle import ca.bc.gov.bchealth.R import ca.bc.gov.bchealth.compose.BasePreview -import ca.bc.gov.bchealth.compose.MyHealthTypography -import ca.bc.gov.bchealth.compose.blue +import ca.bc.gov.bchealth.compose.component.AnnouncementBannerUI +import ca.bc.gov.bchealth.compose.component.HGProgressIndicator +import ca.bc.gov.bchealth.compose.component.HGTextButton +import ca.bc.gov.bchealth.compose.component.LoginInfoCardUI +import ca.bc.gov.bchealth.compose.component.QuickAccessTileItemUI +import ca.bc.gov.bchealth.compose.theme.HealthGatewayTheme +import ca.bc.gov.bchealth.compose.theme.blue +import ca.bc.gov.bchealth.compose.theme.statusBlue +import ca.bc.gov.bchealth.compose.theme.white +import ca.bc.gov.bchealth.ui.login.BcscAuthViewModel +import ca.bc.gov.bchealth.ui.login.LoginStatus +import ca.bc.gov.bchealth.viewmodel.SharedViewModel @Composable fun HomeScreen( - patientFirstName: String?, - bannerUiState: BannerItem?, - onClickToggle: () -> Unit, - onClickDismiss: () -> Unit, - onClickLearnMore: (BannerItem) -> Unit, - homeItems: List, - onClickHomeCard: (HomeNavigationType) -> Unit, + modifier: Modifier = Modifier, + authViewModel: BcscAuthViewModel, + viewModel: HomeViewModel, + sharedViewModel: SharedViewModel, + onLoginClick: () -> Unit, + onManageClick: () -> Unit, + onOnBoardingRequired: (isReOnBoarding: Boolean) -> Unit, + onBiometricAuthenticationRequired: () -> Unit, + onQuickAccessTileClicked: (QuickAccessTileItem) -> Unit, + onMoreActionClick: (id: Long, name: String) -> Unit ) { - val greeting = if (!patientFirstName.isNullOrBlank()) { - stringResource(R.string.hi) - .plus(" ") - .plus(patientFirstName) - .plus(",") + val uiState = viewModel.uiState.collectAsStateWithLifecycle( + minActiveState = Lifecycle.State.RESUMED + ).value + + val authState = authViewModel.authStatus.collectAsStateWithLifecycle( + minActiveState = Lifecycle.State.RESUMED + ).value + + LaunchedEffect(key1 = Unit) { + viewModel.launchCheck() + } + + if (uiState.isLoading) { + HGProgressIndicator(modifier) } else { - stringResource(R.string.hello).plus(",") + uiState.launchCheckStatus?.let { + when (it) { + LaunchCheckStatus.REQUIRE_ON_BOARDING -> { + LaunchedEffect(key1 = Unit) { + onOnBoardingRequired(false) + viewModel.resetUIState() + } + } + + LaunchCheckStatus.REQUIRE_RE_ON_BOARDING -> { + LaunchedEffect(key1 = Unit) { + onOnBoardingRequired(true) + viewModel.resetUIState() + } + } + + LaunchCheckStatus.REQUIRE_BIOMETRIC_AUTHENTICATION -> { + LaunchedEffect(key1 = Unit) { + onBiometricAuthenticationRequired() + } + } + + LaunchCheckStatus.SUCCESS -> { + LaunchedEffect(key1 = Unit) { + authViewModel.checkSession() + } + } + } + } } - Text( - modifier = Modifier.padding(horizontal = 32.dp), - text = greeting, - style = MyHealthTypography.h2, - fontSize = 28.sp, - color = MaterialTheme.colors.primary - ) - Spacer(modifier = Modifier.height(8.dp)) - - Text( - modifier = Modifier.padding(horizontal = 32.dp), - text = stringResource(id = R.string.home_subtitle), - style = MyHealthTypography.h2, - fontSize = 20.sp, - color = blue - ) - Spacer(modifier = Modifier.height(16.dp)) - - bannerUiState?.let { - BannerUI(it, onClickToggle, onClickLearnMore, onClickDismiss) + authState.loginStatus?.let { + LaunchedEffect(key1 = it) { + viewModel.loadQuickAccessTiles(it) + } + + if (sharedViewModel.shouldFetchBanner) { + LaunchedEffect(key1 = Unit) { + viewModel.fetchBanner() + } + } + + HomeScreenContent( + modifier, + onLoginClick, + onQuickAccessTileClicked, + onManageClick, + onDismissClick = { + sharedViewModel.shouldFetchBanner = false + viewModel.dismissBanner() + }, + onDismissTutorialClicked = { viewModel.tutorialDismissed() }, + onMoreActionClick = onMoreActionClick, + it, + viewModel.getLoginInfoCardData(it), + uiState.bannerItem, + uiState.quickAccessTileItems, + uiState.isQuickAccessTileTutorialRequired + ) } +} - homeItems.forEach { - HomeCardUI(uiItem = it, onClickItem = { onClickHomeCard.invoke(it.recordType) }) +@Composable +private fun HomeScreenContent( + modifier: Modifier = Modifier, + onLoginClick: () -> Unit, + onQuickAccessTileClicked: (QuickAccessTileItem) -> Unit, + onManageClick: () -> Unit, + onDismissClick: () -> Unit, + onDismissTutorialClicked: () -> Unit, + onMoreActionClick: (id: Long, name: String) -> Unit, + loginStatus: LoginStatus, + loginInfoCardData: LoginInfoCardData?, + bannerItem: HomeBannerItem?, + quickAccessTileItems: List, + isQuickAccessTileTutorialRequired: Boolean +) { + LazyVerticalGrid( + columns = GridCells.Fixed(2), + modifier = modifier.fillMaxSize(), + contentPadding = PaddingValues(32.dp), + verticalArrangement = Arrangement.spacedBy(16.dp), + horizontalArrangement = Arrangement.spacedBy(20.dp) + ) { + + bannerItem?.let { banner -> + if (!banner.isDismissed) { + item(span = { GridItemSpan(maxLineSpan) }) { + AnnouncementBannerUI( + title = banner.title, + description = banner.body, + showReadMore = banner.showReadMore(), + onLearnMoreClick = { /*TODO*/ }, + onDismissClick = { onDismissClick() } + ) + } + } + } + + loginInfoCardData?.let { data -> + item(span = { GridItemSpan(maxLineSpan) }) { + LoginInfoCardUI( + onClick = { onLoginClick() }, + title = stringResource(id = data.title), + description = stringResource(id = data.description), + buttonText = stringResource(id = data.buttonText), + image = if (data.image > 0) { + painterResource(id = data.image) + } else { + null + } + ) + } + } + + item(span = { GridItemSpan(maxLineSpan) }) { + QuickAccessHeaderUI(onManageClick, onDismissTutorialClicked, loginStatus, isQuickAccessTileTutorialRequired) + } + + items(quickAccessTileItems) { + QuickAccessTileItemUI( + onClick = { onQuickAccessTileClicked(it) }, + icon = painterResource(id = it.icon), + title = it.name, + hasMoreOptions = it is QuickAccessTileItem.QuickLinkTileItem, + onMoreActionClick = { + if (it is QuickAccessTileItem.QuickLinkTileItem) { + onMoreActionClick(it.id, it.name) + } + } + ) + } } } +@Composable +private fun QuickAccessHeaderUI(onManageClick: () -> Unit, onDismissTutorialClicked: () -> Unit, loginStatus: LoginStatus, isQuickAccessTileTutorialRequired: Boolean) { + + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text( + text = stringResource(id = R.string.quick_access), + style = MaterialTheme.typography.subtitle2, + fontWeight = FontWeight.Bold, + color = MaterialTheme.colors.primary + ) + + AnimatedVisibility(visible = (LoginStatus.ACTIVE == loginStatus)) { + HGTextButton(onClick = onManageClick) { + Text( + text = stringResource(id = R.string.manage), + style = MaterialTheme.typography.body2, + fontWeight = FontWeight.Bold, + color = blue, + textDecoration = TextDecoration.Underline + ) + } + if (isQuickAccessTileTutorialRequired) { + QuickAccessManagementTutorialUI(onDismissTutorialClicked) + } + } + } +} + +private const val anchorIconId = "anchorIconId" +private const val bannerBodyId = "bannerBodyId" + +private fun quickAccessManagementTutorialConstraint(): ConstraintSet { + return ConstraintSet { + val anchorIcon = createRefFor(anchorIconId) + val body = createRefFor(bannerBodyId) + + constrain(anchorIcon) { + top.linkTo(parent.top, 16.dp) + end.linkTo(parent.end, 16.dp) + } + + constrain(body) { + start.linkTo(parent.start) + top.linkTo(anchorIcon.bottom) + end.linkTo(parent.end) + bottom.linkTo(parent.bottom) + } + } +} + +@Composable +private fun QuickAccessManagementTutorialUI(onDismissTutorialClicked: () -> Unit) { + Popup( + alignment = Alignment.TopEnd, + offset = IntOffset(0, 100) + ) { + BoxWithConstraints(modifier = Modifier.wrapContentSize()) { + + ConstraintLayout(constraintSet = quickAccessManagementTutorialConstraint()) { + Image( + modifier = Modifier.layoutId(anchorIconId), + painter = painterResource(id = R.drawable.ic_anchor), + contentDescription = null + ) + Column( + modifier = Modifier + .layoutId(bannerBodyId) + .background(statusBlue) + .padding(start = 8.dp, top = 8.dp, end = 8.dp) + ) { + Text( + modifier = Modifier.padding(start = 8.dp, end = 8.dp), + text = stringResource(id = R.string.manage_hint), + style = MaterialTheme.typography.body2, + color = white + ) + HGTextButton(onClick = { onDismissTutorialClicked() }) { + Text( + text = "Got it", + style = MaterialTheme.typography.body2, + fontWeight = FontWeight.Bold, + color = white, + textDecoration = TextDecoration.Underline + ) + } + } + } + } + } +} + +@Composable @BasePreview +private fun HomeScreenNonAuthenticatedPreview() { + HealthGatewayTheme { + HomeScreenContent( + onLoginClick = {}, + onManageClick = {}, + onQuickAccessTileClicked = {}, + onDismissClick = {}, + onDismissTutorialClicked = {}, + loginStatus = LoginStatus.NOT_AUTHENTICATED, + loginInfoCardData = null, + bannerItem = null, + quickAccessTileItems = emptyList(), + isQuickAccessTileTutorialRequired = false, + onMoreActionClick = { id, name -> } + ) + } +} + @Composable -private fun PreviewHomeScreen() { - HomeScreen( - "Bruno", - bannerUiState = BannerItem( - title = "Great news! Really Big Announcement", - body = "View and manage all your available health records, including dispensed medications, health visits, COVID-19 test results, immunizations and more.", - date = "", - displayReadMore = true, - isHidden = false, - ), - onClickToggle = {}, - onClickLearnMore = {}, - onClickDismiss = {}, - homeItems = listOf( - HomeRecordItem( - iconTitle = R.drawable.ic_login_info, - title = R.string.recommendations_home_title, - description = R.string.home_recommendations_body, - icon = R.drawable.ic_right_arrow, - btnTitle = R.string.get_started, - recordType = HomeNavigationType.RECOMMENDATIONS - - ), - HomeRecordItem( - iconTitle = R.drawable.ic_login_info, - title = R.string.recommendations_home_title, - description = R.string.home_recommendations_body, - icon = R.drawable.ic_right_arrow, - btnTitle = R.string.get_started, - recordType = HomeNavigationType.RECOMMENDATIONS - - ), - HomeRecordItem( - iconTitle = R.drawable.ic_login_info, - title = R.string.recommendations_home_title, - description = R.string.home_recommendations_body, - icon = R.drawable.ic_right_arrow, - btnTitle = R.string.get_started, - recordType = HomeNavigationType.RECOMMENDATIONS - ) - ), - onClickHomeCard = {} - ) +@BasePreview +private fun HomeScreenAuthenticatedPreview() { + HealthGatewayTheme { + HomeScreenContent( + onLoginClick = {}, + onManageClick = {}, + onQuickAccessTileClicked = {}, + onDismissClick = {}, + onDismissTutorialClicked = {}, + loginStatus = LoginStatus.ACTIVE, + loginInfoCardData = null, + bannerItem = null, + quickAccessTileItems = emptyList(), + isQuickAccessTileTutorialRequired = false, + onMoreActionClick = { id, name -> } + ) + } } diff --git a/app/src/main/java/ca/bc/gov/bchealth/ui/home/HomeViewModel.kt b/app/src/main/java/ca/bc/gov/bchealth/ui/home/HomeViewModel.kt index f25faf3c9..e05acbf16 100644 --- a/app/src/main/java/ca/bc/gov/bchealth/ui/home/HomeViewModel.kt +++ b/app/src/main/java/ca/bc/gov/bchealth/ui/home/HomeViewModel.kt @@ -1,304 +1,357 @@ package ca.bc.gov.bchealth.ui.home import androidx.annotation.DrawableRes +import androidx.annotation.IdRes import androidx.annotation.StringRes import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import ca.bc.gov.bchealth.R +import ca.bc.gov.bchealth.ui.login.LoginStatus import ca.bc.gov.bchealth.utils.COMMUNICATION_BANNER_MAX_LENGTH -import ca.bc.gov.bchealth.utils.INDEX_NOT_FOUND import ca.bc.gov.bchealth.utils.fromHtml import ca.bc.gov.bchealth.workers.WorkerInvoker -import ca.bc.gov.common.exceptions.ServiceDownException -import ca.bc.gov.common.model.AuthenticationStatus -import ca.bc.gov.common.model.banner.BannerDto -import ca.bc.gov.common.utils.toDate -import ca.bc.gov.common.utils.yyyy_MM_dd +import ca.bc.gov.common.model.AppFeatureName +import ca.bc.gov.common.model.QuickAccessLinkName +import ca.bc.gov.common.model.settings.AppFeatureDto +import ca.bc.gov.common.model.settings.QuickAccessTileDto import ca.bc.gov.repository.BannerRepository import ca.bc.gov.repository.OnBoardingRepository -import ca.bc.gov.repository.bcsc.BcscAuthRepo -import ca.bc.gov.repository.bcsc.PostLoginCheck -import ca.bc.gov.repository.immunization.ImmunizationRecommendationRepository -import ca.bc.gov.repository.patient.PatientRepository +import ca.bc.gov.repository.settings.AppFeatureWithQuickAccessTilesRepository import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import javax.inject.Inject @HiltViewModel class HomeViewModel @Inject constructor( + private val appFeatureWithQuickAccessTilesRepository: AppFeatureWithQuickAccessTilesRepository, private val onBoardingRepository: OnBoardingRepository, - private val patientRepository: PatientRepository, - private val bcscAuthRepo: BcscAuthRepo, - private val workerInvoker: WorkerInvoker, - recommendationRepository: ImmunizationRecommendationRepository, private val bannerRepository: BannerRepository, + private val workerInvoker: WorkerInvoker, ) : ViewModel() { + private val _uiState = + MutableStateFlow(HomeComposeUiState(isQuickAccessTileTutorialRequired = appFeatureWithQuickAccessTilesRepository.isQuickAccessTileTutorialRequired)) + val uiState: StateFlow = _uiState.asStateFlow() + + private var isBiometricAuthenticationRequired: Boolean = true + + fun loadQuickAccessTiles(loginStatus: LoginStatus) = viewModelScope.launch { + val quickAccessTileItems = mutableListOf() + val data = appFeatureWithQuickAccessTilesRepository.getAppFeaturesWithQuickAccessTiles() + + val appFeatures = + appFeatureWithQuickAccessTilesRepository.getAppFeaturesWithQuickAccessTiles() + .filter { it.appFeatureDto.showAsQuickAccess } + .map { + QuickAccessTileItem.FeatureTileItem.from(it.appFeatureDto) + } + + quickAccessTileItems.addAll(appFeatures) - private var bannerRequested = false - - private val _bannerState = MutableStateFlow(null) - val bannerState: StateFlow = _bannerState.asStateFlow() - - private val _uiState = MutableStateFlow(HomeUiState()) - val uiState: StateFlow = _uiState.asStateFlow() - var isAuthenticationRequired: Boolean = true - var isForceLogout: Boolean = false - - private val _homeList = MutableStateFlow?>(null) - val homeList: StateFlow?> = _homeList.asStateFlow() - - private val recommendationItem = HomeRecordItem( - R.drawable.ic_recommendation, - R.string.home_recommendations_title, - R.string.home_recommendations_body, - R.drawable.ic_right_arrow, - R.string.learn_more, - HomeNavigationType.RECOMMENDATIONS - ) - - private val displayRecommendations = - recommendationRepository.getAllRecommendations().map { list -> - val isLoggedIn: Boolean = try { - bcscAuthRepo.checkSession() - } catch (e: Exception) { - false + if (loginStatus == LoginStatus.ACTIVE) { + data.filter { it.appFeatureDto.hasManageableQuickAccessLinks }.forEach { + val quickLink = it.quickAccessTiles.filter { tile -> tile.showAsQuickAccess } + .map { tile -> QuickAccessTileItem.QuickLinkTileItem.from(tile) } + quickAccessTileItems.addAll(quickLink) } - list.isNotEmpty() && isLoggedIn + + quickAccessTileItems.removeIf { tile -> + (tile.name == AppFeatureName.IMMUNIZATION_SCHEDULES.value) + } + quickAccessTileItems.find { it.name == AppFeatureName.RECOMMENDED_IMMUNIZATIONS.value } + ?.let { + val index = quickAccessTileItems.indexOf(it) + if (index != 1) { + quickAccessTileItems.removeAt(index) + quickAccessTileItems.add(1, it) + } + } } - fun launchCheck() = viewModelScope.launch { - if (bcscAuthRepo.checkSession()) { - onBoardingRepository.onBCSCLoginRequiredPostBiometric = false + quickAccessTileItems.removeIf { + (it.name == AppFeatureName.RECOMMENDED_IMMUNIZATIONS.value && (loginStatus != LoginStatus.ACTIVE)) } + + _uiState.update { it.copy(isLoading = false, quickAccessTileItems = quickAccessTileItems) } + } + + fun launchCheck() = viewModelScope.launch { when { onBoardingRepository.onBoardingRequired -> { _uiState.update { state -> - state.copy(isLoading = false, isOnBoardingRequired = true) + state.copy( + isLoading = false, + launchCheckStatus = LaunchCheckStatus.REQUIRE_ON_BOARDING + ) } } onBoardingRepository.isReOnBoardingRequired -> { - _uiState.update { state -> - state.copy(isLoading = false, isReOnBoardingRequired = true) + _uiState.update { + it.copy( + isLoading = false, + launchCheckStatus = LaunchCheckStatus.REQUIRE_RE_ON_BOARDING + ) } } - isAuthenticationRequired -> { - _uiState.update { state -> state.copy(isAuthenticationRequired = true) } - } - - onBoardingRepository.onBCSCLoginRequiredPostBiometric -> { - _uiState.update { state -> state.copy(isBcscLoginRequiredPostBiometrics = true) } + isBiometricAuthenticationRequired -> { + _uiState.update { + it.copy( + isLoading = false, + launchCheckStatus = LaunchCheckStatus.REQUIRE_BIOMETRIC_AUTHENTICATION + ) + } } - bcscAuthRepo.getPostLoginCheck() == PostLoginCheck.IN_PROGRESS.name -> { - _uiState.update { state -> state.copy(isForceLogout = true) } + else -> { + _uiState.update { + it.copy(isLoading = false, launchCheckStatus = LaunchCheckStatus.SUCCESS) + } } } } - fun onBoardingShown() { + fun onBiometricAuthenticationCompleted() { + isBiometricAuthenticationRequired = false + resetUIState() + launchCheck() + } + + fun resetUIState() { _uiState.update { - it.copy(isOnBoardingRequired = false, isReOnBoardingRequired = false) + it.copy(launchCheckStatus = null) } } - fun onAuthenticationRequired(isRequired: Boolean) { - isAuthenticationRequired = isRequired - _uiState.update { state -> state.copy(isAuthenticationRequired = isRequired) } + fun fetchBanner() = viewModelScope.launch { + try { + bannerRepository.getBanner()?.let { banner -> + _uiState.update { + it.copy( + isLoading = false, + bannerItem = HomeBannerItem( + banner.title, + body = banner.body + ) + ) + } + } + } catch (e: Exception) { + } } - fun onBcscLoginRequired(isRequired: Boolean) { - onBoardingRepository.onBCSCLoginRequiredPostBiometric = isRequired - _uiState.update { state -> state.copy(isBcscLoginRequiredPostBiometrics = isRequired) } + fun dismissBanner() { + _uiState.update { + it.copy(isLoading = false, bannerItem = it.bannerItem?.copy(isDismissed = true)) + } } - fun onForceLogout(isRequired: Boolean) { - isForceLogout = isRequired - _uiState.update { state -> state.copy(isForceLogout = isRequired) } - } + fun getLoginInfoCardData(loginStatus: LoginStatus): LoginInfoCardData? { - fun getAuthenticatedPatientName() = viewModelScope.launch { - try { - val patient = - patientRepository.findPatientByAuthStatus(AuthenticationStatus.AUTHENTICATED) - val names = patient.fullName.split(" ") - val firstName = if (names.isNotEmpty()) names.first() else "" - _uiState.update { - it.copy(patientFirstName = firstName) + return when (loginStatus) { + + LoginStatus.ACTIVE -> { + null } - } catch (e: Exception) { - _uiState.update { - it.copy(patientFirstName = "") + + LoginStatus.EXPIRED -> { + LoginInfoCardData( + title = R.string.session_time_out, + description = R.string.login_to_view_hidden_records_msg, + buttonText = R.string.log_in + ) } - } - } - suspend fun getHomeRecordsList() { - val isLoggedIn: Boolean = try { - bcscAuthRepo.checkSession() - } catch (e: Exception) { - false + LoginStatus.NOT_AUTHENTICATED -> { + LoginInfoCardData( + title = R.string.log_in_with_bc_services_card, + description = R.string.log_in_description, + buttonText = R.string.get_started, + image = R.drawable.img_un_authenticated_home_screen + ) + } } + } - val list = mutableListOf( - HomeRecordItem( - R.drawable.ic_login_info, - R.string.health_records, - R.string.health_records_desc, - 0, - if (isLoggedIn) R.string.view_records else R.string.get_started, - HomeNavigationType.HEALTH_RECORD - ), - HomeRecordItem( - R.drawable.ic_resources, - R.string.health_resources, - R.string.resources_desc, - R.drawable.ic_right_arrow, - R.string.learn_more, - HomeNavigationType.RESOURCES - ), - HomeRecordItem( - R.drawable.ic_green_tick, - R.string.health_passes, - R.string.proof_of_vaccination_desc, - R.drawable.ic_right_arrow, - R.string.add_proofs, - HomeNavigationType.VACCINE_PROOF - ), - ) - - _homeList.update { list } - displayRecommendations.collect { - manageRecommendationCard(it) + fun tutorialDismissed() { + appFeatureWithQuickAccessTilesRepository.isQuickAccessTileTutorialRequired = false + _uiState.update { + it.copy(isQuickAccessTileTutorialRequired = false) } } - private fun manageRecommendationCard(displayCard: Boolean) { - _homeList.value?.let { list -> - val cardIndex = list.indexOfFirst { - it.recordType == HomeNavigationType.RECOMMENDATIONS - } + fun executeOneTimeDataFetch() = workerInvoker.executeOneTimeDataFetch() +} + +data class HomeComposeUiState( + val isLoading: Boolean = false, + val launchCheckStatus: LaunchCheckStatus? = null, + val bannerItem: HomeBannerItem? = null, + val loginInfoCardData: LoginInfoCardData? = null, + val quickAccessTileItems: List = emptyList(), + val isQuickAccessTileTutorialRequired: Boolean = false +) + +enum class LaunchCheckStatus { + REQUIRE_ON_BOARDING, + REQUIRE_RE_ON_BOARDING, + REQUIRE_BIOMETRIC_AUTHENTICATION, + SUCCESS +} - if (displayCard) { - if (cardIndex == INDEX_NOT_FOUND) { - _homeList.update { - list.toMutableList().apply { add(1, recommendationItem) } +data class LoginInfoCardData( + @StringRes val title: Int, + @StringRes val description: Int, + @StringRes val buttonText: Int, + @DrawableRes val image: Int = 0 +) + +sealed class QuickAccessTileItem( + @DrawableRes open val icon: Int, + open val name: String, + open val payload: String? = null, + @IdRes open val destinationId: Int, + open val isEditable: Boolean = false +) { + data class FeatureTileItem( + val id: Long, + override val icon: Int, + override val name: String, + override val payload: String?, + override val destinationId: Int, + override val isEditable: Boolean + ) : QuickAccessTileItem( + icon, name, payload, destinationId, isEditable + ) { + companion object { + fun from(appFeatureDto: AppFeatureDto): FeatureTileItem { + val (tileIcon, endDestinationId) = when (appFeatureDto.name) { + AppFeatureName.HEALTH_RECORDS -> { + Pair(R.drawable.icon_tile_health_record, R.id.health_records) } - } - } else { - if (cardIndex > INDEX_NOT_FOUND) { - _homeList.update { - list.toMutableList().apply { removeAt(cardIndex) } + + AppFeatureName.IMMUNIZATION_SCHEDULES -> { + + Pair( + R.drawable.ic_tile_immunization_schedules, + R.id.immunizationSchedulesFragment + ) + } + + AppFeatureName.HEALTH_RESOURCES -> { + Pair( + R.drawable.ic_tile_healt_resources, + R.id.action_homeFragment_to_resources + ) + } + + AppFeatureName.PROOF_OF_VACCINE -> { + Pair( + R.drawable.ic_tile_proof_of_vaccine, + R.id.action_homeFragment_to_health_pass + ) + } + + AppFeatureName.SERVICES -> { + Pair(R.drawable.ic_organ_donor, R.id.services) + } + + AppFeatureName.RECOMMENDED_IMMUNIZATIONS -> { + Pair(R.drawable.ic_recommendation_immunization, R.id.recommendations) } } + return FeatureTileItem( + id = appFeatureDto.id, + name = appFeatureDto.name.value, + icon = tileIcon, + destinationId = endDestinationId, + payload = null, + isEditable = false + ) } } } - fun executeOneTimeDataFetch() { - fetchBanner() - workerInvoker.executeOneTimeDataFetch() - } + data class QuickLinkTileItem( + val id: Long, + val featureId: Long, + override val icon: Int, + override val name: String, + override val payload: String?, + override val destinationId: Int, + override val isEditable: Boolean + ) : QuickAccessTileItem( + icon, name, payload, destinationId, isEditable + ) { + companion object { + fun from(quickAccessTileDto: QuickAccessTileDto): QuickLinkTileItem { + val (tileIcon, endDestination) = when (quickAccessTileDto.tileName) { + QuickAccessLinkName.IMMUNIZATIONS -> { + Pair(R.drawable.ic_health_record_vaccine, R.id.health_records) + } - private fun fetchBanner() { - if (bannerRequested.not()) { - viewModelScope.launch { + QuickAccessLinkName.MEDICATIONS -> { + Pair(R.drawable.ic_health_record_medication, R.id.health_records) + } - try { - callBannerRepository() - } catch (e: Exception) { - when (e) { - is ServiceDownException -> displayServiceDownMessage() - else -> e.printStackTrace() + QuickAccessLinkName.LAB_RESULTS -> { + Pair(R.drawable.ic_lab_test, R.id.health_records) } - } - } - bannerRequested = true - } - } + QuickAccessLinkName.COVID_19_TESTS -> { + Pair(R.drawable.ic_health_record_covid_test, R.id.health_records) + } - private fun displayServiceDownMessage() { - _uiState.update { state -> - state.copy(displayServiceDownMessage = true) - } - } + QuickAccessLinkName.HEALTH_VISITS -> { + Pair(R.drawable.ic_health_record_health_visit, R.id.health_records) + } - private suspend fun callBannerRepository() { - bannerRepository.getBanner()?.apply { - if (validateBannerDates(this)) { - _bannerState.update { - BannerItem( - title = title, - date = startDate.toDate(yyyy_MM_dd), - body = body, - displayReadMore = shouldDisplayReadMore(body), - ) - } - } - } - } + QuickAccessLinkName.MY_NOTES -> { + Pair(R.drawable.ic_health_record_vaccine, R.id.health_records) + } - private fun validateBannerDates(bannerDto: BannerDto): Boolean { - val currentTime = System.currentTimeMillis() - return bannerDto.startDate.toEpochMilli() <= currentTime && - currentTime < bannerDto.endDate.toEpochMilli() - } + QuickAccessLinkName.SPECIAL_AUTHORITY -> { + Pair(R.drawable.ic_health_record_special_authority, R.id.health_records) + } - fun toggleBanner() { - _bannerState.update { it?.copy(expanded = it.expanded.not()) } - } + QuickAccessLinkName.CLINICAL_DOCUMENTS -> { + Pair(R.drawable.ic_health_record_clinical_document, R.id.health_records) + } - fun dismissBanner() { - _bannerState.update { it?.copy(isHidden = true) } - } + QuickAccessLinkName.HOSPITAL_VISITS -> { + Pair(R.drawable.ic_health_record_hospital_visit, R.id.health_records) + } - private fun shouldDisplayReadMore(body: String): Boolean = - body.fromHtml().length > COMMUNICATION_BANNER_MAX_LENGTH + QuickAccessLinkName.IMAGING_REPORTS -> { + Pair(R.drawable.ic_health_record_diagnostic_imaging, R.id.health_records) + } - fun resetUiState() { - _uiState.tryEmit(HomeUiState()) + QuickAccessLinkName.ORGAN_DONOR -> { + Pair(R.drawable.ic_organ_donor, R.id.services) + } + } + return QuickLinkTileItem( + id = quickAccessTileDto.id, + featureId = quickAccessTileDto.featureId, + name = quickAccessTileDto.tileName.value, + payload = quickAccessTileDto.tilePayload, + icon = tileIcon, + destinationId = endDestination, + isEditable = true + ) + } + } } } -data class BannerItem( +data class HomeBannerItem( val title: String, - val date: String, val body: String, - val displayReadMore: Boolean, - var expanded: Boolean = true, - var isHidden: Boolean = false -) - -data class HomeUiState( - val isLoading: Boolean = false, - val isOnBoardingRequired: Boolean = false, - val isReOnBoardingRequired: Boolean = false, - val isAuthenticationRequired: Boolean = false, - val isBcscLoginRequiredPostBiometrics: Boolean = false, - val patientFirstName: String? = null, - val isForceLogout: Boolean = false, - val displayServiceDownMessage: Boolean = false -) - -data class HomeRecordItem( - @DrawableRes val iconTitle: Int, - @StringRes val title: Int, - @StringRes val description: Int, - @DrawableRes val icon: Int, - @StringRes val btnTitle: Int, - val recordType: HomeNavigationType -) - -enum class HomeNavigationType { - HEALTH_RECORD, - RECOMMENDATIONS, - RESOURCES, - VACCINE_PROOF, + var isDismissed: Boolean = false +) { + fun showReadMore() = body.fromHtml().length > COMMUNICATION_BANNER_MAX_LENGTH } diff --git a/app/src/main/java/ca/bc/gov/bchealth/ui/home/RemoveQuickAccessTileBottomSheetFragment.kt b/app/src/main/java/ca/bc/gov/bchealth/ui/home/RemoveQuickAccessTileBottomSheetFragment.kt new file mode 100644 index 000000000..3c73ad307 --- /dev/null +++ b/app/src/main/java/ca/bc/gov/bchealth/ui/home/RemoveQuickAccessTileBottomSheetFragment.kt @@ -0,0 +1,51 @@ +package ca.bc.gov.bchealth.ui.home + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.compose.ui.platform.ComposeView +import androidx.compose.ui.platform.ViewCompositionStrategy +import androidx.fragment.app.viewModels +import androidx.navigation.fragment.findNavController +import androidx.navigation.fragment.navArgs +import ca.bc.gov.bchealth.R +import ca.bc.gov.bchealth.compose.theme.HealthGatewayTheme +import com.google.android.material.bottomsheet.BottomSheetDialogFragment +import dagger.hilt.android.AndroidEntryPoint + +@AndroidEntryPoint +class RemoveQuickAccessTileBottomSheetFragment : BottomSheetDialogFragment() { + + private val removeQuickAccessTileViewModel: RemoveQuickAccessTileViewModel by viewModels() + private val args: RemoveQuickAccessTileBottomSheetFragmentArgs by navArgs() + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + return ComposeView(requireContext()).apply { + setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) + setContent { + HealthGatewayTheme { + RemoveQuickAccessTileBottomSheetScreen( + viewModel = removeQuickAccessTileViewModel, + id = args.id, + name = args.name, + onRemoveClicked = ::onRemoveClicked, + ondDismissClicked = ::ondDismissClicked + ) + } + } + } + } + + private fun onRemoveClicked() { + dismiss() + findNavController().navigate(R.id.action_home_self) + } + private fun ondDismissClicked() { + dismiss() + } +} diff --git a/app/src/main/java/ca/bc/gov/bchealth/ui/home/RemoveQuickAccessTileBottomSheetScreen.kt b/app/src/main/java/ca/bc/gov/bchealth/ui/home/RemoveQuickAccessTileBottomSheetScreen.kt new file mode 100644 index 000000000..64c064bdb --- /dev/null +++ b/app/src/main/java/ca/bc/gov/bchealth/ui/home/RemoveQuickAccessTileBottomSheetScreen.kt @@ -0,0 +1,111 @@ +package ca.bc.gov.bchealth.ui.home + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material.Divider +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.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextDecoration +import androidx.compose.ui.unit.dp +import ca.bc.gov.bchealth.R +import ca.bc.gov.bchealth.compose.BasePreview +import ca.bc.gov.bchealth.compose.component.HGButton +import ca.bc.gov.bchealth.compose.component.HGButtonDefaults +import ca.bc.gov.bchealth.compose.component.HGTextButton +import ca.bc.gov.bchealth.compose.theme.HealthGatewayTheme +import ca.bc.gov.bchealth.compose.theme.blue +import ca.bc.gov.bchealth.compose.theme.descriptionGrey +import ca.bc.gov.bchealth.compose.theme.dividerGrey + +@Composable +fun RemoveQuickAccessTileBottomSheetScreen( + modifier: Modifier = Modifier, + viewModel: RemoveQuickAccessTileViewModel, + id: Long = 0, + name: String, + onRemoveClicked: () -> Unit, + ondDismissClicked: () -> Unit +) { + RemoveQuickAccessTileBottomSheetContent( + modifier = modifier, + name = name, + onRemoveClicked = { + viewModel.updateTile(id) + onRemoveClicked() + }, + ondDismissClicked + ) +} + +@Composable +private fun RemoveQuickAccessTileBottomSheetContent( + modifier: Modifier = Modifier, + name: String, + onRemoveClicked: () -> Unit, + ondDismissClicked: () -> Unit +) { + + Column( + modifier = modifier + .padding(16.dp) + .fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + + Text( + text = stringResource(id = R.string.remove_tile_confirmation_title, name), + style = MaterialTheme.typography.subtitle2, + fontWeight = FontWeight.Bold + ) + Text( + text = stringResource(id = R.string.remove_tile_confirmation_description), + style = MaterialTheme.typography.body2, + color = descriptionGrey + ) + Spacer(modifier = Modifier.height(16.dp)) + Divider( + modifier = Modifier.fillMaxWidth(), + color = dividerGrey, + thickness = 1.dp + ) + Spacer(modifier = Modifier.height(16.dp)) + HGButton( + modifier = Modifier.fillMaxWidth(), + onClick = onRemoveClicked, + text = stringResource(id = R.string.remove), + defaultHeight = HGButtonDefaults.SmallButtonHeight + ) + Spacer(modifier = Modifier.height(16.dp)) + HGTextButton(onClick = ondDismissClicked) { + Text( + text = stringResource(id = R.string.dismiss), + style = MaterialTheme.typography.body2, + fontWeight = FontWeight.Bold, + color = blue, + textDecoration = TextDecoration.Underline + ) + } + } +} + +@Composable +@BasePreview +private fun RemoveQuickAccessTileBottomSheetScreenPreview() { + HealthGatewayTheme { + RemoveQuickAccessTileBottomSheetContent( + name = "Test", + onRemoveClicked = {}, + ondDismissClicked = {} + ) + } +} diff --git a/app/src/main/java/ca/bc/gov/bchealth/ui/home/RemoveQuickAccessTileViewModel.kt b/app/src/main/java/ca/bc/gov/bchealth/ui/home/RemoveQuickAccessTileViewModel.kt new file mode 100644 index 000000000..d54dd75a1 --- /dev/null +++ b/app/src/main/java/ca/bc/gov/bchealth/ui/home/RemoveQuickAccessTileViewModel.kt @@ -0,0 +1,19 @@ +package ca.bc.gov.bchealth.ui.home + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import ca.bc.gov.common.model.QuickAccessTileShowAsQuickLinkDto +import ca.bc.gov.repository.settings.QuickAccessTileRepository +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class RemoveQuickAccessTileViewModel @Inject constructor( + private val quickAccessTileRepository: QuickAccessTileRepository +) : ViewModel() { + + fun updateTile(id: Long) = viewModelScope.launch { + quickAccessTileRepository.update(QuickAccessTileShowAsQuickLinkDto(id, showAsQuickAccess = false)) + } +} diff --git a/app/src/main/java/ca/bc/gov/bchealth/ui/home/immunizationschedules/ImmunizationSchedulesFragment.kt b/app/src/main/java/ca/bc/gov/bchealth/ui/home/immunizationschedules/ImmunizationSchedulesFragment.kt new file mode 100644 index 000000000..f12af0566 --- /dev/null +++ b/app/src/main/java/ca/bc/gov/bchealth/ui/home/immunizationschedules/ImmunizationSchedulesFragment.kt @@ -0,0 +1,49 @@ +package ca.bc.gov.bchealth.ui.home.immunizationschedules + +import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.statusBarsPadding +import androidx.compose.material.Scaffold +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.fragment.app.viewModels +import androidx.navigation.fragment.findNavController +import ca.bc.gov.bchealth.R +import ca.bc.gov.bchealth.compose.MyHealthTheme +import ca.bc.gov.bchealth.ui.BaseFragment +import ca.bc.gov.bchealth.ui.custom.MyHealthToolbar +import ca.bc.gov.bchealth.utils.redirect + +class ImmunizationSchedulesFragment : BaseFragment(null) { + + private val viewModel: ImmunizationSchedulesViewModel by viewModels() + + @Composable + override fun GetComposableLayout() { + MyHealthTheme { + Scaffold( + topBar = { + MyHealthToolbar( + title = stringResource(id = R.string.immnz_schedules_title), + navigationAction = { findNavController().popBackStack() } + ) + }, + content = { + ImmunizationSchedulesScreen( + viewModel = viewModel, + onClickItem = ::onClickUrl, + modifier = Modifier + .statusBarsPadding() + .navigationBarsPadding() + .padding(it) + ) + }, + ) + } + } + + private fun onClickUrl(url: String) { + context?.redirect(url) + } +} diff --git a/app/src/main/java/ca/bc/gov/bchealth/ui/home/immunizationschedules/ImmunizationSchedulesScreen.kt b/app/src/main/java/ca/bc/gov/bchealth/ui/home/immunizationschedules/ImmunizationSchedulesScreen.kt new file mode 100644 index 000000000..fd62c57d1 --- /dev/null +++ b/app/src/main/java/ca/bc/gov/bchealth/ui/home/immunizationschedules/ImmunizationSchedulesScreen.kt @@ -0,0 +1,129 @@ +package ca.bc.gov.bchealth.ui.home.immunizationschedules + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.PaddingValues +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.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Card +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import ca.bc.gov.bchealth.R +import ca.bc.gov.bchealth.compose.BasePreview +import ca.bc.gov.bchealth.compose.MyHealthTypography +import ca.bc.gov.bchealth.compose.bold +import ca.bc.gov.bchealth.compose.theme.bannerBackgroundBlue +import ca.bc.gov.bchealth.compose.theme.white +import ca.bc.gov.bchealth.ui.custom.DecorativeImage +import ca.bc.gov.bchealth.ui.home.immunizationschedules.ImmunizationSchedulesViewModel.ImmunizationSchedulesItem + +@Composable +fun ImmunizationSchedulesScreen( + viewModel: ImmunizationSchedulesViewModel, + onClickItem: (String) -> Unit, + modifier: Modifier = Modifier +) { + val uiState = viewModel.uiState.collectAsState().value + + LaunchedEffect(Unit) { + viewModel.loadUiList() + } + + ImmunizationSchedulesContent(uiState.uiList, onClickItem, modifier) +} + +@Composable +private fun ImmunizationSchedulesContent( + uiList: List, + onClickItem: (String) -> Unit, + modifier: Modifier = Modifier +) { + LazyColumn( + modifier = modifier, + verticalArrangement = Arrangement.spacedBy(12.dp), + contentPadding = PaddingValues(horizontal = 32.dp, vertical = 22.dp), + content = { + item { + Text( + modifier = Modifier.padding(bottom = 4.dp), + text = stringResource(id = R.string.immnz_schedules_body), + style = MyHealthTypography.caption + ) + } + items(uiList) { + ImmunizationScheduleUI(it, onClickItem) + } + } + ) +} + +@Composable +private fun ImmunizationScheduleUI(item: ImmunizationSchedulesItem, onClickItem: (String) -> Unit) { + val url = stringResource(id = item.url) + + Card( + modifier = Modifier.clickable { onClickItem.invoke(url) }, + shape = RoundedCornerShape(4.dp), + backgroundColor = white, + elevation = 8.dp, + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(start = 10.dp, end = 22.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Box( + modifier = Modifier + .padding(top = 16.dp, bottom = 10.dp) + .size(48.dp) + .clip(RoundedCornerShape(4.dp)) + .background(bannerBackgroundBlue), + contentAlignment = Alignment.Center + ) { + + DecorativeImage(resourceId = item.icon) + } + + Text( + modifier = Modifier + .padding(start = 22.dp) + .fillMaxWidth() + .weight(1f), + style = MyHealthTypography.body2.bold(), + text = stringResource(id = item.title) + ) + DecorativeImage( + resourceId = R.drawable.ic_arrow_right_24 + ) + } + } +} + +@BasePreview +@Composable +private fun PreviewImmunizationSchedulesScreen() { + val item = ImmunizationSchedulesItem( + R.drawable.ic_immnz_schedules_infant, + R.string.immnz_schedules_infant, + R.string.url_immnz_schedules_infant + ) + + ImmunizationSchedulesContent( + listOf(item, item, item), {} + ) +} diff --git a/app/src/main/java/ca/bc/gov/bchealth/ui/home/immunizationschedules/ImmunizationSchedulesViewModel.kt b/app/src/main/java/ca/bc/gov/bchealth/ui/home/immunizationschedules/ImmunizationSchedulesViewModel.kt new file mode 100644 index 000000000..a3cc5e02f --- /dev/null +++ b/app/src/main/java/ca/bc/gov/bchealth/ui/home/immunizationschedules/ImmunizationSchedulesViewModel.kt @@ -0,0 +1,52 @@ +package ca.bc.gov.bchealth.ui.home.immunizationschedules + +import androidx.annotation.DrawableRes +import androidx.annotation.StringRes +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import ca.bc.gov.bchealth.R +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch + +class ImmunizationSchedulesViewModel : ViewModel() { + + private val _uiState = MutableStateFlow(ImmunizationSchedulesUiState()) + val uiState: StateFlow = _uiState.asStateFlow() + + fun loadUiList() = viewModelScope.launch { + _uiState.update { + ImmunizationSchedulesUiState( + uiList = listOf( + ImmunizationSchedulesItem( + R.drawable.ic_immnz_schedules_infant, + R.string.immnz_schedules_infant, + R.string.url_immnz_schedules_infant, + ), + ImmunizationSchedulesItem( + R.drawable.ic_immnz_schedules_school_age, + R.string.immnz_schedules_school_age, + R.string.url_immnz_schedules_school_age, + ), + ImmunizationSchedulesItem( + R.drawable.ic_immnz_schedules_adult_seniors, + R.string.immnz_schedules_adult_seniors, + R.string.url_immnz_schedules_adult_seniors, + ) + ) + ) + } + } + + data class ImmunizationSchedulesUiState( + val uiList: List = listOf() + ) + + data class ImmunizationSchedulesItem( + @DrawableRes val icon: Int, + @StringRes val title: Int, + @StringRes val url: Int, + ) +} diff --git a/app/src/main/java/ca/bc/gov/bchealth/ui/home/manage/QuickAccessManagementFragment.kt b/app/src/main/java/ca/bc/gov/bchealth/ui/home/manage/QuickAccessManagementFragment.kt new file mode 100644 index 000000000..c2b51a009 --- /dev/null +++ b/app/src/main/java/ca/bc/gov/bchealth/ui/home/manage/QuickAccessManagementFragment.kt @@ -0,0 +1,66 @@ +package ca.bc.gov.bchealth.ui.home.manage + +import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.statusBarsPadding +import androidx.compose.material.Icon +import androidx.compose.material.IconButton +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Scaffold +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.fragment.app.viewModels +import androidx.navigation.fragment.findNavController +import ca.bc.gov.bchealth.R +import ca.bc.gov.bchealth.compose.MyHealthTheme +import ca.bc.gov.bchealth.ui.BaseFragment +import ca.bc.gov.bchealth.ui.custom.AppBarDefaults +import ca.bc.gov.bchealth.ui.custom.MyHealthBackButton +import ca.bc.gov.bchealth.ui.custom.MyHealthToolBar +import dagger.hilt.android.AndroidEntryPoint + +@AndroidEntryPoint +class QuickAccessManagementFragment : BaseFragment(null) { + private val viewModel: QuickAccessManagementViewModel by viewModels() + + @Composable + override fun GetComposableLayout() { + MyHealthTheme { + Scaffold( + topBar = { + MyHealthToolBar( + title = stringResource(id = R.string.quick_access_management_title), + navigationIcon = { MyHealthBackButton({ findNavController().popBackStack() }) }, + actions = { + IconButton(onClick = viewModel::saveSelection) { + Icon( + painter = painterResource(id = R.drawable.ic_check), + contentDescription = stringResource(id = R.string.quick_access_management_save), + tint = MaterialTheme.colors.primary + ) + } + }, + elevation = AppBarDefaults.TopAppBarElevation + ) + }, + content = { + QuickAccessManagementScreen( + viewModel = viewModel, + onClickItem = ::onClickItem, + onUpdateCompleted = { findNavController().popBackStack() }, + modifier = Modifier + .statusBarsPadding() + .navigationBarsPadding() + .padding(it) + ) + }, + ) + } + } + + private fun onClickItem(item: QuickAccessManagementViewModel.QuickAccessItem) { + viewModel.toggleItem(item) + } +} diff --git a/app/src/main/java/ca/bc/gov/bchealth/ui/home/manage/QuickAccessManagementScreen.kt b/app/src/main/java/ca/bc/gov/bchealth/ui/home/manage/QuickAccessManagementScreen.kt new file mode 100644 index 000000000..e7241c7cd --- /dev/null +++ b/app/src/main/java/ca/bc/gov/bchealth/ui/home/manage/QuickAccessManagementScreen.kt @@ -0,0 +1,157 @@ +package ca.bc.gov.bchealth.ui.home.manage + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.RowScope +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.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Card +import androidx.compose.material.Checkbox +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import ca.bc.gov.bchealth.R +import ca.bc.gov.bchealth.compose.BasePreview +import ca.bc.gov.bchealth.compose.MyHealthTheme +import ca.bc.gov.bchealth.compose.MyHealthTypography +import ca.bc.gov.bchealth.compose.bold +import ca.bc.gov.bchealth.compose.component.HGProgressIndicator +import ca.bc.gov.bchealth.compose.theme.primaryBlue +import ca.bc.gov.bchealth.compose.theme.statusBlue +import ca.bc.gov.bchealth.compose.theme.white + +@Composable +fun QuickAccessManagementScreen( + viewModel: QuickAccessManagementViewModel, + onClickItem: (QuickAccessManagementViewModel.QuickAccessItem) -> Unit, + onUpdateCompleted: () -> Unit, + modifier: Modifier = Modifier +) { + val uiState = viewModel.uiState.collectAsState().value + + LaunchedEffect(Unit) { + viewModel.loadQuickAccessTileData() + } + + if (uiState.isLoading) { + HGProgressIndicator(modifier) + } else { + QuickAccessManagementContent(uiState.featureWithQuickAccessItems, onClickItem = onClickItem) + } + + if (uiState.isUpdateCompleted) { + onUpdateCompleted() + } +} + +@Composable +private fun QuickAccessManagementContent( + featureWithQuickAccessItems: List, + onClickItem: (QuickAccessManagementViewModel.QuickAccessItem) -> Unit, + modifier: Modifier = Modifier, +) { + LazyColumn( + modifier = modifier, + contentPadding = PaddingValues(start = 32.dp, end = 32.dp, top = 20.dp, bottom = 64.dp), + content = { + item { + Text( + text = stringResource(id = R.string.quick_access_management_body), + style = MyHealthTypography.body1, + color = primaryBlue + ) + } + + item { Spacer(modifier = Modifier.size(16.dp)) } + + featureWithQuickAccessItems.forEach { + item { + Text( + text = it.name, + style = MyHealthTypography.body1.bold(), + color = statusBlue + ) + } + item { Spacer(modifier = Modifier.size(12.dp)) } + items(it.quickAccessItems) { tile -> + TileItemUi(tile, onClickItem) + Spacer(modifier = Modifier.size(10.dp)) + } + item { Spacer(modifier = Modifier.size(6.dp)) } + } + } + ) +} + +@Composable +private fun TileItemUi( + item: QuickAccessManagementViewModel.QuickAccessItem, + onClickItem: (QuickAccessManagementViewModel.QuickAccessItem) -> Unit, +) { + val checkedState = remember { mutableStateOf(item.isEnabled) } + + Card( + modifier = Modifier.clickable { + checkedState.value = checkedState.value.not() + onClickItem.invoke(item) + }, + shape = RoundedCornerShape(4.dp), + backgroundColor = white, + elevation = 5.dp, + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(start = 15.dp, end = 20.dp, top = 13.dp, bottom = 18.dp), + verticalAlignment = Alignment.CenterVertically + ) { + + TileName(item) + + Checkbox( + modifier = Modifier.size(24.dp), + checked = checkedState.value, + onCheckedChange = { + checkedState.value = it + onClickItem.invoke(item) + } + ) + } + } +} + +@Composable +private fun RowScope.TileName(item: QuickAccessManagementViewModel.QuickAccessItem) { + Text( + modifier = Modifier + .fillMaxWidth() + .weight(1f), + text = item.name, + style = MyHealthTypography.body1.bold(), + ) +} + +@BasePreview +@Composable +private fun PreviewQuickAccessManagementContent() { + + MyHealthTheme { + QuickAccessManagementContent( + emptyList(), + {} + ) + } +} diff --git a/app/src/main/java/ca/bc/gov/bchealth/ui/home/manage/QuickAccessManagementViewModel.kt b/app/src/main/java/ca/bc/gov/bchealth/ui/home/manage/QuickAccessManagementViewModel.kt new file mode 100644 index 000000000..dbfd22a12 --- /dev/null +++ b/app/src/main/java/ca/bc/gov/bchealth/ui/home/manage/QuickAccessManagementViewModel.kt @@ -0,0 +1,86 @@ +package ca.bc.gov.bchealth.ui.home.manage + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import ca.bc.gov.common.model.QuickAccessTileShowAsQuickLinkDto +import ca.bc.gov.repository.settings.AppFeatureWithQuickAccessTilesRepository +import ca.bc.gov.repository.settings.QuickAccessTileRepository +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class QuickAccessManagementViewModel @Inject constructor( + private val appFeatureRepository: AppFeatureWithQuickAccessTilesRepository, + private val quickAccessTileRepository: QuickAccessTileRepository +) : ViewModel() { + + private val _uiState = MutableStateFlow(QuickAccessManagementUiState()) + val uiState: StateFlow = _uiState.asStateFlow() + + fun loadQuickAccessTileData() = viewModelScope.launch { + _uiState.update { it.copy(isLoading = true) } + + val featureWithQuickAccessItems = appFeatureRepository.getAppFeaturesWithQuickAccessTiles().filter { it.appFeatureDto.hasManageableQuickAccessLinks } + .map { + FeatureWithQuickAccessItems( + id = it.appFeatureDto.id, + name = it.appFeatureDto.name.value, + quickAccessItems = it.quickAccessTiles.map { tile -> + QuickAccessItem( + tile.id, + tile.tileName.value, + tile.showAsQuickAccess + ) + } + ) + } + + _uiState.update { it.copy(featureWithQuickAccessItems = featureWithQuickAccessItems, isLoading = false) } + } + + fun toggleItem(item: QuickAccessItem) { + item.isEnabled = item.isEnabled.not() + } + + fun saveSelection() { + viewModelScope.launch { + _uiState.update { it.copy(isLoading = true) } + + _uiState.value.featureWithQuickAccessItems.forEach { + quickAccessTileRepository.updateAll( + it.quickAccessItems.map { tile -> + QuickAccessTileShowAsQuickLinkDto(tile.id, tile.isEnabled) + } + ) + } + + delay(300L) + + _uiState.update { it.copy(isLoading = false, isUpdateCompleted = true) } + } + } + + data class QuickAccessManagementUiState( + val isLoading: Boolean = false, + val isUpdateCompleted: Boolean = false, + val featureWithQuickAccessItems: List = emptyList() + ) + + data class FeatureWithQuickAccessItems( + val id: Long = 0, + val name: String, + val quickAccessItems: List = emptyList() + ) + + data class QuickAccessItem( + val id: Long = 0, + val name: String, + var isEnabled: Boolean = false + ) +} diff --git a/app/src/main/java/ca/bc/gov/bchealth/ui/login/BcscAuthViewModel.kt b/app/src/main/java/ca/bc/gov/bchealth/ui/login/BcscAuthViewModel.kt index d1b89fea1..ab51f2d84 100644 --- a/app/src/main/java/ca/bc/gov/bchealth/ui/login/BcscAuthViewModel.kt +++ b/app/src/main/java/ca/bc/gov/bchealth/ui/login/BcscAuthViewModel.kt @@ -12,6 +12,7 @@ import ca.bc.gov.common.exceptions.NetworkConnectionException import ca.bc.gov.common.exceptions.ServiceDownException import ca.bc.gov.common.model.AuthParametersDto import ca.bc.gov.common.model.AuthenticationStatus +import ca.bc.gov.common.model.UserAuthenticationStatus import ca.bc.gov.common.model.patient.PatientDto import ca.bc.gov.common.utils.toUniquePatientName import ca.bc.gov.repository.CacheRepository @@ -22,11 +23,15 @@ import ca.bc.gov.repository.bcsc.BACKGROUND_AUTH_RECORD_FETCH_WORK_NAME import ca.bc.gov.repository.bcsc.BcscAuthRepo import ca.bc.gov.repository.bcsc.PostLoginCheck import ca.bc.gov.repository.patient.PatientRepository +import ca.bc.gov.repository.settings.QuickAccessTileRepository import ca.bc.gov.repository.worker.MobileConfigRepository import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import javax.inject.Inject @@ -43,12 +48,20 @@ class BcscAuthViewModel @Inject constructor( private val patientRepository: PatientRepository, private val patientWithBCSCLoginRepository: PatientWithBCSCLoginRepository, private val mobileConfigRepository: MobileConfigRepository, - private val cacheRepository: CacheRepository + private val cacheRepository: CacheRepository, + private val quickAccessTileRepository: QuickAccessTileRepository ) : ViewModel() { private val _authStatus = MutableStateFlow(AuthStatus()) val authStatus: StateFlow = _authStatus.asStateFlow() + val userAuthenticationState = + bcscAuthRepo.userAuthenticationStatus.catch { excepton -> emit(UserAuthenticationStatus.UN_AUTHENTICATED) }.stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(500), + initialValue = UserAuthenticationStatus.UN_AUTHENTICATED + ) + /* * Throttle calls to BCSC login * */ @@ -73,12 +86,14 @@ class BcscAuthViewModel @Inject constructor( ) } } + is ServiceDownException -> _authStatus.update { it.copy( showLoading = true, canInitiateBcscLogin = false ) } + else -> { _authStatus.update { it.copy( @@ -227,8 +242,9 @@ class BcscAuthViewModel @Inject constructor( val isLoggedSuccess = bcscAuthRepo.checkSession() var userName: String? = null try { - userName = - patientRepository.findPatientByAuthStatus(AuthenticationStatus.AUTHENTICATED).fullName + val patient = + patientRepository.findPatientByAuthStatus(AuthenticationStatus.AUTHENTICATED) + userName = patient.fullName val loginSessionStatus = if (isLoggedSuccess) { LoginStatus.ACTIVE } else { @@ -238,7 +254,8 @@ class BcscAuthViewModel @Inject constructor( it.copy( showLoading = false, userName = userName, - loginStatus = loginSessionStatus + loginStatus = loginSessionStatus, + patient = patient ) } } catch (e: Exception) { @@ -263,7 +280,7 @@ class BcscAuthViewModel @Inject constructor( isError = false, userName = null, queItTokenUpdated = false, - loginStatus = LoginStatus.NOT_AUTHENTICATED, + loginStatus = null, ageLimitCheck = null, canInitiateBcscLogin = null, tosStatus = null, @@ -302,6 +319,7 @@ class BcscAuthViewModel @Inject constructor( ) } } + else -> { _authStatus.update { it.copy( @@ -351,6 +369,7 @@ class BcscAuthViewModel @Inject constructor( ) } } + else -> { _authStatus.update { it.copy( @@ -384,6 +403,7 @@ class BcscAuthViewModel @Inject constructor( ) { patientRepository.deleteByPatientId(patientFromLocalSource.id) patientRepository.insertAuthenticatedPatient(patientFromRemoteSource) + quickAccessTileRepository.update(showAsQuickAccess = false) } } catch (e: java.lang.Exception) { /* @@ -391,6 +411,7 @@ class BcscAuthViewModel @Inject constructor( * to show patient name soon after login * */ patientFromRemoteSource?.let { patientRepository.insertAuthenticatedPatient(it) } + quickAccessTileRepository.update(showAsQuickAccess = false) } } @@ -430,6 +451,7 @@ class BcscAuthViewModel @Inject constructor( ) } } + else -> { _authStatus.update { it.copy( @@ -457,11 +479,12 @@ data class AuthStatus( val userName: String? = null, val queItTokenUpdated: Boolean = false, val queItUrl: String? = null, - val loginStatus: LoginStatus = LoginStatus.NOT_AUTHENTICATED, + val loginStatus: LoginStatus? = null, val ageLimitCheck: AgeLimitCheck? = null, val canInitiateBcscLogin: Boolean? = null, val tosStatus: TOSStatus? = null, - val isConnected: Boolean = true + val isConnected: Boolean = true, + val patient: PatientDto? = null ) enum class LoginStatus { diff --git a/app/src/main/java/ca/bc/gov/bchealth/ui/login/error/BcscAuthErrorFragment.kt b/app/src/main/java/ca/bc/gov/bchealth/ui/login/error/BcscAuthErrorFragment.kt index 9a6a5103b..faf2064bf 100644 --- a/app/src/main/java/ca/bc/gov/bchealth/ui/login/error/BcscAuthErrorFragment.kt +++ b/app/src/main/java/ca/bc/gov/bchealth/ui/login/error/BcscAuthErrorFragment.kt @@ -1,8 +1,10 @@ package ca.bc.gov.bchealth.ui.login.error import androidx.compose.runtime.Composable +import androidx.navigation.fragment.findNavController import ca.bc.gov.bchealth.R import ca.bc.gov.bchealth.ui.BaseFragment +import ca.bc.gov.bchealth.utils.composeEmail import dagger.hilt.android.AndroidEntryPoint @AndroidEntryPoint @@ -10,7 +12,7 @@ class BcscAuthErrorFragment : BaseFragment(null) { @Composable override fun GetComposableLayout() { - BcscAuthErrorUI(::popNavigation, ::onClickEmail) + BcscAuthErrorUI({ findNavController().popBackStack() }, ::onClickEmail) } private fun onClickEmail() { diff --git a/app/src/main/java/ca/bc/gov/bchealth/ui/notification/NotificationFragment.kt b/app/src/main/java/ca/bc/gov/bchealth/ui/notification/NotificationFragment.kt index b36eeafe6..98dc22548 100644 --- a/app/src/main/java/ca/bc/gov/bchealth/ui/notification/NotificationFragment.kt +++ b/app/src/main/java/ca/bc/gov/bchealth/ui/notification/NotificationFragment.kt @@ -22,7 +22,7 @@ import androidx.navigation.fragment.findNavController import ca.bc.gov.bchealth.R import ca.bc.gov.bchealth.compose.BasePreview import ca.bc.gov.bchealth.compose.MyHealthTheme -import ca.bc.gov.bchealth.compose.primaryBlue +import ca.bc.gov.bchealth.compose.theme.primaryBlue import ca.bc.gov.bchealth.ui.BaseFragment import ca.bc.gov.bchealth.ui.BaseViewModel import ca.bc.gov.bchealth.ui.auth.BCServicesCardSessionContent @@ -105,7 +105,7 @@ class NotificationFragment : BaseFragment(null) { private fun NotificationToolbar(uiState: NotificationViewModel.NotificationsUIState) { MyHealthToolBar( title = if (uiState.sessionExpired) "" else stringResource(id = R.string.notifications), - navigationIcon = { MyHealthBackButton(::popNavigation) }, + navigationIcon = { MyHealthBackButton({ findNavController().popBackStack() }) }, actions = { IconButton(onClick = { if (isDeleteIconEnabled(uiState)) { diff --git a/app/src/main/java/ca/bc/gov/bchealth/ui/notification/NotificationScreen.kt b/app/src/main/java/ca/bc/gov/bchealth/ui/notification/NotificationScreen.kt index c5e171586..2f87e3d98 100644 --- a/app/src/main/java/ca/bc/gov/bchealth/ui/notification/NotificationScreen.kt +++ b/app/src/main/java/ca/bc/gov/bchealth/ui/notification/NotificationScreen.kt @@ -37,10 +37,10 @@ import ca.bc.gov.bchealth.R import ca.bc.gov.bchealth.compose.BasePreview import ca.bc.gov.bchealth.compose.MyHealthTheme import ca.bc.gov.bchealth.compose.MyHealthTypography -import ca.bc.gov.bchealth.compose.grey -import ca.bc.gov.bchealth.compose.greyBg import ca.bc.gov.bchealth.compose.minButtonSize -import ca.bc.gov.bchealth.compose.primaryBlue +import ca.bc.gov.bchealth.compose.theme.grey +import ca.bc.gov.bchealth.compose.theme.greyBg +import ca.bc.gov.bchealth.compose.theme.primaryBlue import ca.bc.gov.bchealth.ui.component.HGLargeButton import ca.bc.gov.bchealth.ui.custom.DecorativeImage import ca.bc.gov.common.model.notification.NotificationActionTypeDto diff --git a/app/src/main/java/ca/bc/gov/bchealth/ui/profile/CommunicationPrefsUI.kt b/app/src/main/java/ca/bc/gov/bchealth/ui/profile/CommunicationPrefsUI.kt index 239822701..7cdc7da12 100644 --- a/app/src/main/java/ca/bc/gov/bchealth/ui/profile/CommunicationPrefsUI.kt +++ b/app/src/main/java/ca/bc/gov/bchealth/ui/profile/CommunicationPrefsUI.kt @@ -16,8 +16,8 @@ import androidx.compose.ui.unit.sp import ca.bc.gov.bchealth.R import ca.bc.gov.bchealth.compose.BasePreview import ca.bc.gov.bchealth.compose.MyHealthTypography -import ca.bc.gov.bchealth.compose.green -import ca.bc.gov.bchealth.compose.red +import ca.bc.gov.bchealth.compose.theme.green +import ca.bc.gov.bchealth.compose.theme.red import ca.bc.gov.bchealth.ui.custom.DecorativeImage import ca.bc.gov.bchealth.ui.custom.MyHealthClickableText diff --git a/app/src/main/java/ca/bc/gov/bchealth/ui/profile/ProfileFragment.kt b/app/src/main/java/ca/bc/gov/bchealth/ui/profile/ProfileFragment.kt index 0e282682e..9839d42cf 100644 --- a/app/src/main/java/ca/bc/gov/bchealth/ui/profile/ProfileFragment.kt +++ b/app/src/main/java/ca/bc/gov/bchealth/ui/profile/ProfileFragment.kt @@ -4,6 +4,7 @@ import android.os.Bundle import android.view.View import androidx.compose.runtime.Composable import androidx.fragment.app.viewModels +import androidx.navigation.fragment.findNavController import ca.bc.gov.bchealth.ui.BaseFragment import ca.bc.gov.bchealth.utils.URL_ADDRESS_CHANGE import ca.bc.gov.bchealth.utils.URL_COMMUNICATION_PREFS @@ -19,7 +20,7 @@ class ProfileFragment : BaseFragment(null) { override fun GetComposableLayout() { ProfileUI( viewModel = viewModel, - navigationAction = ::popNavigation, + navigationAction = { findNavController().popBackStack() }, onClickAddress = ::onClickAddressChange, onClickPrefs = ::onClickCommunicationPrefs, ) @@ -28,7 +29,7 @@ class ProfileFragment : BaseFragment(null) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) viewModel.uiState.collectOnStart { - if (it.error != null) popNavigation() + if (it.error != null) findNavController().popBackStack() } viewModel.load() } diff --git a/app/src/main/java/ca/bc/gov/bchealth/ui/profile/ProfileUI.kt b/app/src/main/java/ca/bc/gov/bchealth/ui/profile/ProfileUI.kt index 040470a34..cda631c00 100644 --- a/app/src/main/java/ca/bc/gov/bchealth/ui/profile/ProfileUI.kt +++ b/app/src/main/java/ca/bc/gov/bchealth/ui/profile/ProfileUI.kt @@ -21,8 +21,8 @@ import androidx.compose.ui.unit.dp import ca.bc.gov.bchealth.R import ca.bc.gov.bchealth.compose.BasePreview import ca.bc.gov.bchealth.compose.MyHealthTypography -import ca.bc.gov.bchealth.compose.darkText -import ca.bc.gov.bchealth.compose.primaryBlue +import ca.bc.gov.bchealth.compose.theme.darkText +import ca.bc.gov.bchealth.compose.theme.primaryBlue import ca.bc.gov.bchealth.ui.custom.DecorativeImage import ca.bc.gov.bchealth.ui.custom.MyHealthClickableText import ca.bc.gov.bchealth.ui.custom.MyHealthScaffold diff --git a/app/src/main/java/ca/bc/gov/bchealth/ui/recommendations/RecommendationAdapter.kt b/app/src/main/java/ca/bc/gov/bchealth/ui/recommendations/RecommendationAdapter.kt deleted file mode 100644 index ad1c4955d..000000000 --- a/app/src/main/java/ca/bc/gov/bchealth/ui/recommendations/RecommendationAdapter.kt +++ /dev/null @@ -1,83 +0,0 @@ -package ca.bc.gov.bchealth.ui.recommendations - -import android.view.LayoutInflater -import android.view.ViewGroup -import androidx.appcompat.content.res.AppCompatResources -import androidx.recyclerview.widget.DiffUtil -import androidx.recyclerview.widget.ListAdapter -import androidx.recyclerview.widget.RecyclerView -import ca.bc.gov.bchealth.R -import ca.bc.gov.bchealth.databinding.ItemRecommendationBinding -import ca.bc.gov.bchealth.utils.orPlaceholder -import ca.bc.gov.bchealth.utils.setColorSpannable -import ca.bc.gov.bchealth.utils.toggleVisibility -import ca.bc.gov.common.model.immunization.ForecastStatus - -class RecommendationAdapter : ListAdapter( - RecommendationDiffCallBacks() -) { - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ViewHolder( - ItemRecommendationBinding.inflate( - LayoutInflater.from(parent.context), parent, false - ) - ) - - override fun onBindViewHolder(holder: ViewHolder, position: Int) = with(holder.binding) { - val recommendation = getItem(position) - - tvTitle.text = recommendation.title - - val icon = if (recommendation.status is ForecastStatus.Completed) { - R.drawable.ic_recommendation_checked - } else { - R.drawable.ic_recommendation - } - ivIcon.setImageDrawable( - AppCompatResources.getDrawable(this.root.context, icon) - ) - - val fullStatus: String - val statusOrPlaceholder: String = recommendation.status?.text.orPlaceholder() - val date: String - this.root.context.apply { - fullStatus = getString(R.string.immnz_forecast_status, statusOrPlaceholder) - date = getString(R.string.immnz_forecast_due_date, recommendation.date) - } - - val colorId: Int = when (recommendation.status) { - is ForecastStatus.Eligible -> R.color.status_green - is ForecastStatus.Overdue -> R.color.status_red - else -> R.color.status_grey - } - - tvStatus.setColorSpannable( - fullStatus, - statusOrPlaceholder, - this.root.context.getColor(colorId), - true - ) - tvDueDate.text = date - - renderContentState(recommendation.fullContent) - - holder.itemView.setOnClickListener { - recommendation.fullContent = recommendation.fullContent.not() - renderContentState(recommendation.fullContent) - } - } - - private fun ItemRecommendationBinding.renderContentState(displayFullContent: Boolean) { - groupFullContent.toggleVisibility(displayFullContent) - ivContentState.isSelected = displayFullContent - } - - class ViewHolder(val binding: ItemRecommendationBinding) : RecyclerView.ViewHolder(binding.root) -} - -class RecommendationDiffCallBacks : DiffUtil.ItemCallback() { - override fun areItemsTheSame(oldItem: RecommendationDetailItem, newItem: RecommendationDetailItem) = - oldItem == newItem - - override fun areContentsTheSame(oldItem: RecommendationDetailItem, newItem: RecommendationDetailItem) = - oldItem.title == newItem.title -} diff --git a/app/src/main/java/ca/bc/gov/bchealth/ui/recommendations/RecommendationDetailItemUI.kt b/app/src/main/java/ca/bc/gov/bchealth/ui/recommendations/RecommendationDetailItemUI.kt new file mode 100644 index 000000000..2ac8e97db --- /dev/null +++ b/app/src/main/java/ca/bc/gov/bchealth/ui/recommendations/RecommendationDetailItemUI.kt @@ -0,0 +1,101 @@ +package ca.bc.gov.bchealth.ui.recommendations + +import androidx.compose.foundation.layout.BoxWithConstraints +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.layoutId +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.constraintlayout.compose.ConstraintLayout +import androidx.constraintlayout.compose.ConstraintSet +import androidx.constraintlayout.compose.Dimension +import ca.bc.gov.bchealth.compose.BasePreview +import ca.bc.gov.bchealth.compose.theme.HealthGatewayTheme +import ca.bc.gov.bchealth.compose.theme.descriptionGrey +import ca.bc.gov.bchealth.compose.theme.statusBlue +import ca.bc.gov.common.model.immunization.ForecastStatus + +private const val ITEM_VACCINE_NAME_ID = "item_vaccine_name_id" +private const val ITEM_STATUS_ID = "item_status_id" +private const val ITEM_DATE_ID = "item_date_id" +private const val ITEM_ICON_ID = "item_icon_id" + +@Composable +fun RecommendationDetailItem( + modifier: Modifier = Modifier, + recommendationDetailItem: RecommendationDetailItem +) { + BoxWithConstraints(modifier) { + ConstraintLayout( + modifier = Modifier.fillMaxWidth(), + constraintSet = recommendationDetailItemConstraint() + ) { + Text( + modifier = Modifier + .layoutId(ITEM_VACCINE_NAME_ID), + text = recommendationDetailItem.title, + style = MaterialTheme.typography.body1, + fontWeight = FontWeight.Bold, + maxLines = 2, + overflow = TextOverflow.Ellipsis, + color = statusBlue + ) + + Text( + modifier = Modifier.layoutId(ITEM_STATUS_ID), + text = recommendationDetailItem.status?.text ?: "", + style = MaterialTheme.typography.body2, + color = descriptionGrey + ) + Text( + modifier = Modifier.layoutId(ITEM_DATE_ID), + text = recommendationDetailItem.date, + style = MaterialTheme.typography.body2, + color = descriptionGrey + ) + } + } +} + +private fun recommendationDetailItemConstraint() = ConstraintSet { + val itemVaccineNameId = createRefFor(ITEM_VACCINE_NAME_ID) + val itemStatusId = createRefFor(ITEM_STATUS_ID) + val itemDateId = createRefFor(ITEM_DATE_ID) + val itemIconId = createRefFor(ITEM_ICON_ID) + + constrain(itemVaccineNameId) { + start.linkTo(parent.start, 16.dp) + end.linkTo(parent.end, 16.dp) + top.linkTo(parent.top, 16.dp) + width = Dimension.fillToConstraints + } + + constrain(itemStatusId) { + start.linkTo(itemVaccineNameId.start) + top.linkTo(itemVaccineNameId.bottom) + } + + constrain(itemDateId) { + start.linkTo(itemStatusId.start) + top.linkTo(itemStatusId.bottom) + bottom.linkTo(parent.bottom, 16.dp) + } +} + +@Composable +@BasePreview +private fun RecommendationDetailItemPreview() { + HealthGatewayTheme { + RecommendationDetailItem( + recommendationDetailItem = RecommendationDetailItem( + title = "test", + date = "date", + status = ForecastStatus.getByText("Test") + ) + ) + } +} diff --git a/app/src/main/java/ca/bc/gov/bchealth/ui/recommendations/RecommendationItemUI.kt b/app/src/main/java/ca/bc/gov/bchealth/ui/recommendations/RecommendationItemUI.kt new file mode 100644 index 000000000..5943a78c6 --- /dev/null +++ b/app/src/main/java/ca/bc/gov/bchealth/ui/recommendations/RecommendationItemUI.kt @@ -0,0 +1,210 @@ +package ca.bc.gov.bchealth.ui.recommendations + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.BoxWithConstraints +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Card +import androidx.compose.material.Divider +import androidx.compose.material.IconButton +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.layout.layoutId +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.constraintlayout.compose.ConstraintLayout +import androidx.constraintlayout.compose.ConstraintSet +import androidx.constraintlayout.compose.Dimension +import androidx.constraintlayout.compose.Visibility +import ca.bc.gov.bchealth.R +import ca.bc.gov.bchealth.compose.theme.bannerBackgroundBlue +import ca.bc.gov.bchealth.compose.theme.black +import ca.bc.gov.bchealth.compose.theme.descriptionGrey +import ca.bc.gov.bchealth.compose.theme.disableBackground +import ca.bc.gov.bchealth.compose.theme.grey + +private const val ITEM_ICON_ID = "item_icon_id" +private const val ITEM_ARROW_ID = "item_arrow_id" +private const val ITEM_DETAIL_ID = "item_detail_id" +private const val ITEM_PATIENT_NAME_ID = "item_patient_name_id" +private const val ITEM_RECORD_COUNT_ID = "item_record_count_id" + +@Composable +fun RecommendationItem( + modifier: Modifier = Modifier, + patientWithRecommendations: PatientWithRecommendations, + expanded: Boolean, + onArrowClick: () -> Unit +) { + val hasRecommendations = patientWithRecommendations.recommendations.isNotEmpty() + + Card( + modifier = modifier.fillMaxWidth(), + elevation = 15.dp, + backgroundColor = MaterialTheme.colors.background + ) { + BoxWithConstraints( + modifier = Modifier.fillMaxWidth() + ) { + ConstraintLayout( + recommendationItemConstraints(hasRecommendations), + modifier = Modifier.fillMaxWidth() + ) { + + Image( + modifier = Modifier + .layoutId(ITEM_ICON_ID) + .size(48.dp) + .clip(RoundedCornerShape(8.dp)) + .background( + if (hasRecommendations) { + bannerBackgroundBlue + } else { + disableBackground + } + ), + painter = painterResource( + id = if (patientWithRecommendations.isDependent) { + R.drawable.ic_manage_dependent + } else { + R.drawable.ic_profile + } + ), + contentScale = ContentScale.None, + colorFilter = ColorFilter.tint( + if (hasRecommendations) { + MaterialTheme.colors.primary + } else { + grey + } + ), + contentDescription = null + ) + + Text( + modifier = Modifier.layoutId(ITEM_PATIENT_NAME_ID).clickable { + if (hasRecommendations) { + onArrowClick() + } + }, + text = patientWithRecommendations.name ?: "", + style = MaterialTheme.typography.body1, + fontWeight = FontWeight.Bold, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + color = if (!hasRecommendations) { + descriptionGrey + } else { + black + } + ) + + Text( + modifier = Modifier.layoutId(ITEM_RECORD_COUNT_ID), + text = "${patientWithRecommendations.recommendations.size}", + style = MaterialTheme.typography.body1, + fontWeight = FontWeight.Bold, + color = if (!hasRecommendations) { + descriptionGrey + } else { + MaterialTheme.colors.primary + } + ) + + val toggleIcon = if (expanded) { + R.drawable.ic_content_short + } else { + R.drawable.ic_content_full + } + + IconButton( + onClick = { + if (hasRecommendations) { + onArrowClick() + } + }, + modifier = Modifier + .layoutId(ITEM_ARROW_ID) + ) { + Image( + painter = painterResource(id = toggleIcon), + contentDescription = null + ) + } + + AnimatedVisibility( + modifier = Modifier.layoutId(ITEM_DETAIL_ID), + visible = expanded + ) { + + LazyColumn( + modifier = Modifier.heightIn(max = (patientWithRecommendations.recommendations.size * 136).dp), + userScrollEnabled = false + ) { + item { + Divider() + } + items(patientWithRecommendations.recommendations) { + RecommendationDetailItem(recommendationDetailItem = it) + } + } + } + } + } + } +} + +private fun recommendationItemConstraints(hasRecommendations: Boolean) = ConstraintSet { + val itemIconId = createRefFor(ITEM_ICON_ID) + val itemArrow = createRefFor(ITEM_ARROW_ID) + val itemDetailId = createRefFor(ITEM_DETAIL_ID) + val itemPatientNameId = createRefFor(ITEM_PATIENT_NAME_ID) + val itemRecordCountId = createRefFor(ITEM_RECORD_COUNT_ID) + + constrain(itemIconId) { + start.linkTo(parent.start, 16.dp) + top.linkTo(parent.top, 16.dp) + } + + constrain(itemPatientNameId) { + start.linkTo(itemIconId.end, 16.dp) + top.linkTo(itemIconId.top) + bottom.linkTo(itemIconId.bottom) + end.linkTo(itemRecordCountId.start) + width = Dimension.fillToConstraints + } + + constrain(itemRecordCountId) { + top.linkTo(itemArrow.top) + end.linkTo(itemArrow.start) + bottom.linkTo(itemArrow.bottom) + } + + constrain(itemArrow) { + top.linkTo(parent.top, 16.dp) + end.linkTo(parent.end, 16.dp) + visibility = if (hasRecommendations) { + Visibility.Visible + } else { + Visibility.Invisible + } + } + + constrain(itemDetailId) { + top.linkTo(itemIconId.bottom, 16.dp) + } +} diff --git a/app/src/main/java/ca/bc/gov/bchealth/ui/recommendations/RecommendationScreen.kt b/app/src/main/java/ca/bc/gov/bchealth/ui/recommendations/RecommendationScreen.kt new file mode 100644 index 000000000..dad0746da --- /dev/null +++ b/app/src/main/java/ca/bc/gov/bchealth/ui/recommendations/RecommendationScreen.kt @@ -0,0 +1,126 @@ +package ca.bc.gov.bchealth.ui.recommendations + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.livedata.observeAsState +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.work.WorkManager +import ca.bc.gov.bchealth.R +import ca.bc.gov.bchealth.compose.BasePreview +import ca.bc.gov.bchealth.compose.component.HGProgressIndicator +import ca.bc.gov.bchealth.compose.theme.HealthGatewayTheme +import ca.bc.gov.bchealth.compose.theme.grey +import ca.bc.gov.bchealth.ui.custom.MyHealthClickableText +import ca.bc.gov.repository.bcsc.BACKGROUND_AUTH_RECORD_FETCH_WORK_NAME + +@Composable +fun RecommendationScreen( + modifier: Modifier = Modifier, + viewModel: RecommendationsViewModel, + onLinkClicked: () -> Unit +) { + + val uiState = + viewModel.uiState.collectAsStateWithLifecycle(minActiveState = Lifecycle.State.RESUMED).value + val context = LocalContext.current + val workRequest = WorkManager.getInstance(context) + .getWorkInfosForUniqueWorkLiveData(BACKGROUND_AUTH_RECORD_FETCH_WORK_NAME) + .observeAsState() + val workState = workRequest.value?.firstOrNull()?.state + if (workState != null && workState.isFinished) { + LaunchedEffect(key1 = Unit) { + viewModel.loadRecommendations() + } + } else { + LaunchedEffect(Unit) { + viewModel.showProgress() + } + } + + if (uiState.isLoading) { + HGProgressIndicator(modifier) + } else { + RecommendationScreenContent( + modifier, + uiState.patientWithRecommendations, + expandedIds = uiState.expandedCardIds, + onArrowClick = { + viewModel.expandedCard(it) + }, + onLinkClicked = onLinkClicked + ) + } +} + +@Composable +private fun RecommendationScreenContent( + modifier: Modifier = Modifier, + recommendations: List, + expandedIds: Set, + onArrowClick: (Long) -> Unit, + onLinkClicked: () -> Unit +) { + LazyColumn( + modifier = modifier.fillMaxSize(), + contentPadding = PaddingValues(32.dp), + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + + item { + Text( + modifier = Modifier.fillMaxWidth(), + text = stringResource(id = R.string.recommendations_para1), + style = MaterialTheme.typography.body2, + color = grey + ) + } + item { + + MyHealthClickableText( + style = MaterialTheme.typography.body2.copy(color = grey), + modifier = Modifier.fillMaxWidth(), + fullText = stringResource(id = R.string.recommendations_para2), + clickableText = stringResource(id = R.string.immunizeBC), + action = onLinkClicked + ) + } + + items( + recommendations, + key = { item -> item.patientId } + ) { + RecommendationItem( + patientWithRecommendations = it, + expanded = expandedIds.contains(it.patientId), + onArrowClick = { onArrowClick(it.patientId) } + ) + } + } +} + +@BasePreview +@Composable +private fun RecommendationScreenPreview() { + + HealthGatewayTheme { + RecommendationScreenContent( + recommendations = emptyList(), + expandedIds = emptySet(), + onArrowClick = {}, + onLinkClicked = {} + ) + } +} diff --git a/app/src/main/java/ca/bc/gov/bchealth/ui/recommendations/RecommendationsFragment.kt b/app/src/main/java/ca/bc/gov/bchealth/ui/recommendations/RecommendationsFragment.kt index b750572be..576f40a6d 100644 --- a/app/src/main/java/ca/bc/gov/bchealth/ui/recommendations/RecommendationsFragment.kt +++ b/app/src/main/java/ca/bc/gov/bchealth/ui/recommendations/RecommendationsFragment.kt @@ -1,71 +1,50 @@ package ca.bc.gov.bchealth.ui.recommendations -import android.os.Bundle -import android.view.View +import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.statusBarsPadding +import androidx.compose.material.Scaffold +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource import androidx.fragment.app.viewModels -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.lifecycleScope -import androidx.lifecycle.repeatOnLifecycle import androidx.navigation.fragment.findNavController -import androidx.navigation.ui.AppBarConfiguration import ca.bc.gov.bchealth.R -import ca.bc.gov.bchealth.databinding.FragmentRecommendationsBinding +import ca.bc.gov.bchealth.compose.theme.HealthGatewayTheme import ca.bc.gov.bchealth.ui.BaseFragment -import ca.bc.gov.bchealth.utils.makeLinks +import ca.bc.gov.bchealth.ui.custom.MyHealthToolbar import ca.bc.gov.bchealth.utils.redirect -import ca.bc.gov.bchealth.utils.viewBindings import dagger.hilt.android.AndroidEntryPoint -import kotlinx.coroutines.launch @AndroidEntryPoint -class RecommendationsFragment : BaseFragment(R.layout.fragment_recommendations) { - private val binding by viewBindings(FragmentRecommendationsBinding::bind) - +class RecommendationsFragment : BaseFragment(null) { private val viewModel: RecommendationsViewModel by viewModels() - override fun setToolBar(appBarConfiguration: AppBarConfiguration) { - with(binding.layoutToolbar.appbar) { - stateListAnimator = null - elevation = 0f - } - with(binding.layoutToolbar.topAppBar) { - setNavigationIcon(R.drawable.ic_toolbar_back) - setNavigationOnClickListener { - findNavController().popBackStack() - } - title = getString(R.string.recommendations_home_title) - inflateMenu(R.menu.settings_menu) - setOnMenuItemClickListener { menu -> - when (menu.itemId) { - R.id.menu_settings -> { - findNavController().navigate(R.id.settingsFragment) - } + @Composable + override fun GetComposableLayout() { + HealthGatewayTheme { + Scaffold( + topBar = { + MyHealthToolbar( + navigationAction = { findNavController().popBackStack() }, + title = stringResource(id = R.string.recommendations_home_title) + ) + }, + content = { + RecommendationScreen( + Modifier + .statusBarsPadding() + .navigationBarsPadding() + .padding(it), + viewModel = viewModel, + onLinkClicked = ::onLinkClicked + ) } - return@setOnMenuItemClickListener true - } + ) } } - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - with(binding) { - super.onViewCreated(view, savedInstanceState) - tvDescription.makeLinks( - getString(R.string.recommendations_description_clickable) to - View.OnClickListener { - it.context.redirect(getString(R.string.url_immunize_bc)) - } - ) - val adapter = RecommendationAdapter() - rvRecommendations.adapter = adapter - - viewLifecycleOwner.lifecycleScope.launch { - viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { - - viewModel.recommendationList.collect { list -> - adapter.submitList(list) - } - } - } - } + private fun onLinkClicked() { + requireContext().redirect("https://immunizebc.ca/") } } diff --git a/app/src/main/java/ca/bc/gov/bchealth/ui/recommendations/RecommendationsViewModel.kt b/app/src/main/java/ca/bc/gov/bchealth/ui/recommendations/RecommendationsViewModel.kt index 2e37cb528..c08e677c7 100644 --- a/app/src/main/java/ca/bc/gov/bchealth/ui/recommendations/RecommendationsViewModel.kt +++ b/app/src/main/java/ca/bc/gov/bchealth/ui/recommendations/RecommendationsViewModel.kt @@ -1,31 +1,99 @@ package ca.bc.gov.bchealth.ui.recommendations import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope import ca.bc.gov.bchealth.model.mapper.toUiModel +import ca.bc.gov.common.model.AuthenticationStatus import ca.bc.gov.common.model.immunization.ForecastStatus -import ca.bc.gov.repository.immunization.ImmunizationRecommendationRepository +import ca.bc.gov.repository.DependentsRepository +import ca.bc.gov.repository.patient.PatientRepository import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch import javax.inject.Inject @HiltViewModel class RecommendationsViewModel @Inject constructor( - recommendationRepository: ImmunizationRecommendationRepository, + private val patientRepository: PatientRepository, + private val dependentsRepository: DependentsRepository ) : ViewModel() { - val recommendationList = recommendationRepository.getAllRecommendations().map { list -> - list.mapIndexed { index, dto -> - dto.toUiModel().apply { - expandFirstItem(index) + private val _uiState = + MutableStateFlow(RecommendationUiState()) + val uiState: StateFlow = _uiState.asStateFlow() + + fun showProgress() { + _uiState.update { it.copy(isLoading = true) } + } + + fun loadRecommendations() = viewModelScope.launch { + _uiState.update { it.copy(isLoading = true) } + try { + val patient = + patientRepository.findPatientByAuthStatus(AuthenticationStatus.AUTHENTICATED) + val patientData = + patientRepository.getPatientWithImmunizationRecommendations(patient.id) + + val records = mutableListOf() + records.add( + PatientWithRecommendations( + patient.id, + patient.fullName, + patientData.recommendations.map { recommendation -> recommendation.toUiModel() } + ) + ) + + val patientWithDependents = patientRepository.getPatientWithDependents(patient.id) + patientWithDependents.dependents.forEach { + dependentsRepository.requestRecordsIfNeeded(it.patientId, it.hdid) + val dependentData = + dependentsRepository.getPatientWithImmunizationRecommendations(it.patientId) + + records.add( + PatientWithRecommendations( + it.patientId, it.firstname, + dependentData.recommendations.map { recommendation -> recommendation.toUiModel() }, + isDependent = true + ) + ) } + + _uiState.update { it.copy(isLoading = false, patientWithRecommendations = records) } + } catch (e: Exception) { + _uiState.update { it.copy(isLoading = false) } } } - private fun RecommendationDetailItem.expandFirstItem(index: Int) { - if (index == 0) this.fullContent = true + fun expandedCard(id: Long) { + if (_uiState.value.expandedCardIds.contains(id)) { + val ids = _uiState.value.expandedCardIds.toMutableSet() + ids.remove(id) + _uiState.update { it.copy(expandedCardIds = ids) } + } else { + val ids = _uiState.value.expandedCardIds.toMutableSet() + ids.add(id) + _uiState.update { it.copy(expandedCardIds = ids) } + } } } +data class RecommendationUiState( + val isLoading: Boolean = true, + val patientWithRecommendations: List = emptyList(), + val expandedCardIds: Set = emptySet() +) + +data class PatientWithRecommendations( + val patientId: Long = 0, + val name: String? = null, + val recommendations: List, + var expanded: Boolean = false, + var isDependent: Boolean = false +) + data class RecommendationDetailItem( val title: String, val status: ForecastStatus?, diff --git a/app/src/main/java/ca/bc/gov/bchealth/ui/resources/ResourcesFragment.kt b/app/src/main/java/ca/bc/gov/bchealth/ui/resources/ResourcesFragment.kt index 5b9117a4a..8a6188a0f 100644 --- a/app/src/main/java/ca/bc/gov/bchealth/ui/resources/ResourcesFragment.kt +++ b/app/src/main/java/ca/bc/gov/bchealth/ui/resources/ResourcesFragment.kt @@ -2,6 +2,7 @@ package ca.bc.gov.bchealth.ui.resources import androidx.compose.runtime.Composable import androidx.fragment.app.viewModels +import androidx.navigation.fragment.findNavController import ca.bc.gov.bchealth.ui.BaseFragment import ca.bc.gov.bchealth.utils.redirect import ca.bc.gov.bchealth.viewmodel.AnalyticsFeatureViewModel @@ -18,7 +19,7 @@ class ResourcesFragment : BaseFragment(null) { override fun GetComposableLayout() { ResourcesUI( uiList = resourcesViewModel.getResourcesList(), - navigationAction = ::popNavigation, + navigationAction = { findNavController().popBackStack() }, onClickResource = ::onClickResource ) } diff --git a/app/src/main/java/ca/bc/gov/bchealth/ui/resources/ResourcesUI.kt b/app/src/main/java/ca/bc/gov/bchealth/ui/resources/ResourcesUI.kt index 55a0cd172..d03f2b3b6 100644 --- a/app/src/main/java/ca/bc/gov/bchealth/ui/resources/ResourcesUI.kt +++ b/app/src/main/java/ca/bc/gov/bchealth/ui/resources/ResourcesUI.kt @@ -19,8 +19,8 @@ import androidx.compose.ui.unit.dp import ca.bc.gov.bchealth.R import ca.bc.gov.bchealth.compose.BasePreview import ca.bc.gov.bchealth.compose.MyHealthTypography -import ca.bc.gov.bchealth.compose.blue -import ca.bc.gov.bchealth.compose.greyBg +import ca.bc.gov.bchealth.compose.theme.blue +import ca.bc.gov.bchealth.compose.theme.greyBg import ca.bc.gov.bchealth.ui.custom.DecorativeImage import ca.bc.gov.bchealth.ui.custom.MyHealthScaffold diff --git a/app/src/main/java/ca/bc/gov/bchealth/ui/services/ServicesFragment.kt b/app/src/main/java/ca/bc/gov/bchealth/ui/services/ServicesFragment.kt index 7ab3ac4fa..55c210fff 100644 --- a/app/src/main/java/ca/bc/gov/bchealth/ui/services/ServicesFragment.kt +++ b/app/src/main/java/ca/bc/gov/bchealth/ui/services/ServicesFragment.kt @@ -70,17 +70,19 @@ class ServicesFragment : BaseSecureFragment(null) { if (it.showLoading) { servicesViewModel.showProgressBar() } else { - when (it.loginStatus) { - LoginStatus.ACTIVE -> { - observeHealthRecordsSyncCompletion() - } + it.loginStatus?.let { status -> + when (status) { + LoginStatus.ACTIVE -> { + observeHealthRecordsSyncCompletion() + } - LoginStatus.EXPIRED -> { - findNavController().navigate(R.id.bcServiceCardSessionFragment) - } + LoginStatus.EXPIRED -> { + findNavController().navigate(R.id.bcServiceCardSessionFragment) + } - LoginStatus.NOT_AUTHENTICATED -> { - findNavController().navigate(R.id.bcServicesCardLoginFragment) + LoginStatus.NOT_AUTHENTICATED -> { + findNavController().navigate(R.id.bcServicesCardLoginFragment) + } } } } diff --git a/app/src/main/java/ca/bc/gov/bchealth/ui/services/ServicesScreen.kt b/app/src/main/java/ca/bc/gov/bchealth/ui/services/ServicesScreen.kt index 119f87353..4de5692e3 100644 --- a/app/src/main/java/ca/bc/gov/bchealth/ui/services/ServicesScreen.kt +++ b/app/src/main/java/ca/bc/gov/bchealth/ui/services/ServicesScreen.kt @@ -2,7 +2,6 @@ package ca.bc.gov.bchealth.ui.services import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.Image -import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer @@ -36,6 +35,7 @@ import ca.bc.gov.bchealth.R import ca.bc.gov.bchealth.compose.BasePreview import ca.bc.gov.bchealth.compose.MyHealthTheme import ca.bc.gov.bchealth.compose.MyHealthTypography +import ca.bc.gov.bchealth.compose.component.HGProgressIndicator import ca.bc.gov.common.model.services.OrganDonorStatusDto @Composable @@ -107,14 +107,7 @@ private fun ServiceScreenContent( Spacer(modifier = Modifier.height(16.dp)) if (onLoading || organDonorRegistrationDetail == null) { - Box( - modifier = modifier - .fillMaxSize() - ) { - CircularProgressIndicator( - modifier = Modifier.align(Alignment.Center), - ) - } + HGProgressIndicator(modifier) } else { OrganDonor(organDonorRegistrationDetail, organDonorFileStatus, onRegisterOnUpdateDecisionClicked = { url -> onRegisterOnUpdateDecisionClicked(url) diff --git a/app/src/main/java/ca/bc/gov/bchealth/utils/Extensions.kt b/app/src/main/java/ca/bc/gov/bchealth/utils/Extensions.kt index f6678174e..54ab84373 100644 --- a/app/src/main/java/ca/bc/gov/bchealth/utils/Extensions.kt +++ b/app/src/main/java/ca/bc/gov/bchealth/utils/Extensions.kt @@ -223,7 +223,7 @@ fun View.showNoInternetConnectionMessage(context: Context) { showErrorSnackbar(context.getString(R.string.no_internet_connection)) } -fun View?.showErrorSnackbar(message: String) { +fun View?.showErrorSnackbar(message: String, anchor: View? = null) { this ?: return val snackBar = Snackbar.make( @@ -232,6 +232,9 @@ fun View?.showErrorSnackbar(message: String) { snackBar.setAction(context.getString(R.string.dismiss)) { performClick() } + if (anchor != null) { + snackBar.anchorView = anchor + } snackBar.view.findViewById(com.google.android.material.R.id.snackbar_text).maxLines = 10 snackBar.show() diff --git a/app/src/main/java/ca/bc/gov/bchealth/utils/FragmentExtensions.kt b/app/src/main/java/ca/bc/gov/bchealth/utils/FragmentExtensions.kt index 0a0a36dab..21011c25c 100644 --- a/app/src/main/java/ca/bc/gov/bchealth/utils/FragmentExtensions.kt +++ b/app/src/main/java/ca/bc/gov/bchealth/utils/FragmentExtensions.kt @@ -66,3 +66,7 @@ inline fun NavController.removeActionFromPreviousBackStackEntry(key: inline fun NavController.setActionToPreviousBackStackEntry(key: String, value: T) { previousBackStackEntry?.savedStateHandle?.set(key, value) } + +fun Fragment.composeEmail(address: String = HEALTH_GATEWAY_EMAIL_ADDRESS, subject: String = "") { + requireActivity().composeEmail(address, subject) +} diff --git a/app/src/main/java/ca/bc/gov/bchealth/utils/StringExtensions.kt b/app/src/main/java/ca/bc/gov/bchealth/utils/StringExtensions.kt index a3d794d7d..4c1cd514f 100644 --- a/app/src/main/java/ca/bc/gov/bchealth/utils/StringExtensions.kt +++ b/app/src/main/java/ca/bc/gov/bchealth/utils/StringExtensions.kt @@ -7,5 +7,8 @@ import androidx.core.text.HtmlCompat fun String?.orPlaceholder(placeholder: String = "--"): String = this ?: placeholder +fun String?.orPlaceholderIfNullOrBlank(placeholder: String = "--"): String = + if (this.isNullOrBlank()) placeholder else this + fun String.fromHtml(): Spanned = Html.fromHtml(this, HtmlCompat.FROM_HTML_MODE_COMPACT) diff --git a/app/src/main/java/ca/bc/gov/bchealth/viewmodel/SharedViewModel.kt b/app/src/main/java/ca/bc/gov/bchealth/viewmodel/SharedViewModel.kt index 764655e87..11e7822c2 100644 --- a/app/src/main/java/ca/bc/gov/bchealth/viewmodel/SharedViewModel.kt +++ b/app/src/main/java/ca/bc/gov/bchealth/viewmodel/SharedViewModel.kt @@ -25,4 +25,5 @@ class SharedViewModel @Inject constructor() : ViewModel() { var destinationId: Int = 0 var isBCSCAuthShown = false var isBiometricAuthShown = false + var shouldFetchBanner = true } diff --git a/app/src/main/java/ca/bc/gov/bchealth/widget/CommentInputUI.kt b/app/src/main/java/ca/bc/gov/bchealth/widget/CommentInputUI.kt index 4d909b947..39456deb3 100644 --- a/app/src/main/java/ca/bc/gov/bchealth/widget/CommentInputUI.kt +++ b/app/src/main/java/ca/bc/gov/bchealth/widget/CommentInputUI.kt @@ -41,14 +41,14 @@ import ca.bc.gov.bchealth.R import ca.bc.gov.bchealth.compose.BasePreview import ca.bc.gov.bchealth.compose.MyHealthTheme import ca.bc.gov.bchealth.compose.MyHealthTypography -import ca.bc.gov.bchealth.compose.blue -import ca.bc.gov.bchealth.compose.darkText -import ca.bc.gov.bchealth.compose.grey -import ca.bc.gov.bchealth.compose.greyBg import ca.bc.gov.bchealth.compose.minButtonSize -import ca.bc.gov.bchealth.compose.primaryBlue -import ca.bc.gov.bchealth.compose.red -import ca.bc.gov.bchealth.compose.white +import ca.bc.gov.bchealth.compose.theme.blue +import ca.bc.gov.bchealth.compose.theme.darkText +import ca.bc.gov.bchealth.compose.theme.grey +import ca.bc.gov.bchealth.compose.theme.greyBg +import ca.bc.gov.bchealth.compose.theme.primaryBlue +import ca.bc.gov.bchealth.compose.theme.red +import ca.bc.gov.bchealth.compose.theme.white import ca.bc.gov.bchealth.ui.comment.Comment import ca.bc.gov.common.model.SyncStatus import java.time.Instant diff --git a/app/src/main/res/drawable/bottom_nav_selector.xml b/app/src/main/res/drawable/bottom_nav_selector.xml index 4c9e79216..8d7cdd158 100644 --- a/app/src/main/res/drawable/bottom_nav_selector.xml +++ b/app/src/main/res/drawable/bottom_nav_selector.xml @@ -1,5 +1,5 @@ - + \ No newline at end of file diff --git a/app/src/main/res/drawable/home_selector.xml b/app/src/main/res/drawable/home_selector.xml new file mode 100644 index 000000000..14d44c159 --- /dev/null +++ b/app/src/main/res/drawable/home_selector.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_anchor.xml b/app/src/main/res/drawable/ic_anchor.xml new file mode 100644 index 000000000..932720169 --- /dev/null +++ b/app/src/main/res/drawable/ic_anchor.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_arrow_right_24.xml b/app/src/main/res/drawable/ic_arrow_right_24.xml new file mode 100644 index 000000000..54425b6b9 --- /dev/null +++ b/app/src/main/res/drawable/ic_arrow_right_24.xml @@ -0,0 +1,13 @@ + + + + + + diff --git a/app/src/main/res/drawable/ic_dependents.xml b/app/src/main/res/drawable/ic_dependents.xml index 09fbd2ebe..725eb6e4b 100644 --- a/app/src/main/res/drawable/ic_dependents.xml +++ b/app/src/main/res/drawable/ic_dependents.xml @@ -3,8 +3,7 @@ android:height="24dp" android:viewportWidth="24" android:viewportHeight="24"> - - + diff --git a/app/src/main/res/drawable/ic_heart.xml b/app/src/main/res/drawable/ic_heart.xml new file mode 100644 index 000000000..26c374787 --- /dev/null +++ b/app/src/main/res/drawable/ic_heart.xml @@ -0,0 +1,15 @@ + + + + + + diff --git a/app/src/main/res/drawable/ic_home.xml b/app/src/main/res/drawable/ic_home.xml index 8aa89af38..7f2d4b311 100644 --- a/app/src/main/res/drawable/ic_home.xml +++ b/app/src/main/res/drawable/ic_home.xml @@ -1,9 +1,9 @@ + android:width="24dp" + android:height="24dp" + android:viewportWidth="24" + android:viewportHeight="24"> + android:pathData="M10.198,19.655V14.655H14.198V19.655C14.198,20.205 14.648,20.655 15.198,20.655H18.198C18.748,20.655 19.198,20.205 19.198,19.655V12.655H20.898C21.358,12.655 21.578,12.085 21.228,11.785L12.868,4.255C12.488,3.915 11.908,3.915 11.528,4.255L3.168,11.785C2.828,12.085 3.038,12.655 3.498,12.655H5.198V19.655C5.198,20.205 5.648,20.655 6.198,20.655H9.198C9.748,20.655 10.198,20.205 10.198,19.655Z" + android:fillColor="#38598A"/> diff --git a/app/src/main/res/drawable/ic_home_un_selected.xml b/app/src/main/res/drawable/ic_home_un_selected.xml new file mode 100644 index 000000000..52df99ec3 --- /dev/null +++ b/app/src/main/res/drawable/ic_home_un_selected.xml @@ -0,0 +1,13 @@ + + + + + + diff --git a/app/src/main/res/drawable/ic_immnz_schedules_adult_seniors.xml b/app/src/main/res/drawable/ic_immnz_schedules_adult_seniors.xml new file mode 100644 index 000000000..28f8dd5ca --- /dev/null +++ b/app/src/main/res/drawable/ic_immnz_schedules_adult_seniors.xml @@ -0,0 +1,13 @@ + + + + + + diff --git a/app/src/main/res/drawable/ic_immnz_schedules_infant.xml b/app/src/main/res/drawable/ic_immnz_schedules_infant.xml new file mode 100644 index 000000000..7793164d8 --- /dev/null +++ b/app/src/main/res/drawable/ic_immnz_schedules_infant.xml @@ -0,0 +1,13 @@ + + + + + + diff --git a/app/src/main/res/drawable/ic_immnz_schedules_school_age.xml b/app/src/main/res/drawable/ic_immnz_schedules_school_age.xml new file mode 100644 index 000000000..40ca6419c --- /dev/null +++ b/app/src/main/res/drawable/ic_immnz_schedules_school_age.xml @@ -0,0 +1,13 @@ + + + + + + diff --git a/app/src/main/res/drawable/ic_recommendation_immunization.xml b/app/src/main/res/drawable/ic_recommendation_immunization.xml new file mode 100644 index 000000000..902e91898 --- /dev/null +++ b/app/src/main/res/drawable/ic_recommendation_immunization.xml @@ -0,0 +1,16 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_services.xml b/app/src/main/res/drawable/ic_services.xml index 306d08805..ddcf7b98d 100644 --- a/app/src/main/res/drawable/ic_services.xml +++ b/app/src/main/res/drawable/ic_services.xml @@ -6,7 +6,6 @@ - - - - + + + + diff --git a/app/src/main/res/drawable/ic_tile_healt_resources.xml b/app/src/main/res/drawable/ic_tile_healt_resources.xml new file mode 100644 index 000000000..9731723ad --- /dev/null +++ b/app/src/main/res/drawable/ic_tile_healt_resources.xml @@ -0,0 +1,13 @@ + + + + + + diff --git a/app/src/main/res/drawable/ic_tile_immunization_schedules.xml b/app/src/main/res/drawable/ic_tile_immunization_schedules.xml new file mode 100644 index 000000000..c812d2fec --- /dev/null +++ b/app/src/main/res/drawable/ic_tile_immunization_schedules.xml @@ -0,0 +1,18 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_tile_proof_of_vaccine.xml b/app/src/main/res/drawable/ic_tile_proof_of_vaccine.xml new file mode 100644 index 000000000..f5b718541 --- /dev/null +++ b/app/src/main/res/drawable/ic_tile_proof_of_vaccine.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/drawable/icon_tile_health_record.xml b/app/src/main/res/drawable/icon_tile_health_record.xml new file mode 100644 index 000000000..3f92b8ed4 --- /dev/null +++ b/app/src/main/res/drawable/icon_tile_health_record.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/img_un_authenticated_home_screen.xml b/app/src/main/res/drawable/img_un_authenticated_home_screen.xml new file mode 100644 index 000000000..b2ba52dec --- /dev/null +++ b/app/src/main/res/drawable/img_un_authenticated_home_screen.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 31a75d1ca..38cabc3ef 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -29,6 +29,8 @@ android:filterTouchesWhenObscured="true" app:itemIconTint="@drawable/bottom_nav_selector" app:itemTextColor="@drawable/bottom_nav_selector" + app:itemTextAppearanceActive="@style/HealthGateway.BottomNavigationView.TextAppearanceActive" + app:itemTextAppearanceInactive="@style/HealthGateway.BottomNavigationView.TextAppearanceInactive" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" /> diff --git a/app/src/main/res/layout/fragment_lab_test_detail.xml b/app/src/main/res/layout/fragment_lab_test_detail.xml deleted file mode 100644 index 963bb63da..000000000 --- a/app/src/main/res/layout/fragment_lab_test_detail.xml +++ /dev/null @@ -1,47 +0,0 @@ - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/item_lab_test_detail.xml b/app/src/main/res/layout/item_lab_test_detail.xml deleted file mode 100644 index c48f3cefe..000000000 --- a/app/src/main/res/layout/item_lab_test_detail.xml +++ /dev/null @@ -1,108 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/item_lab_test_detail_banner.xml b/app/src/main/res/layout/item_lab_test_detail_banner.xml deleted file mode 100644 index 784728cbe..000000000 --- a/app/src/main/res/layout/item_lab_test_detail_banner.xml +++ /dev/null @@ -1,52 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/item_recommendation.xml b/app/src/main/res/layout/item_recommendation.xml deleted file mode 100644 index 45e528daf..000000000 --- a/app/src/main/res/layout/item_recommendation.xml +++ /dev/null @@ -1,84 +0,0 @@ - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/item_view_pdf.xml b/app/src/main/res/layout/item_view_pdf.xml deleted file mode 100644 index ee16b4f8a..000000000 --- a/app/src/main/res/layout/item_view_pdf.xml +++ /dev/null @@ -1,23 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/menu/bottom_nav_services_menu.xml b/app/src/main/res/menu/bottom_nav_services_menu.xml index 9ef8c8ce1..424cefc24 100644 --- a/app/src/main/res/menu/bottom_nav_services_menu.xml +++ b/app/src/main/res/menu/bottom_nav_services_menu.xml @@ -4,7 +4,7 @@ + android:id="@+id/home" + app:startDestination="@id/homeFragment"> + + + + + @@ -50,4 +60,25 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/configs.xml b/app/src/main/res/values/configs.xml index e986277c2..2c7127316 100644 --- a/app/src/main/res/values/configs.xml +++ b/app/src/main/res/values/configs.xml @@ -6,6 +6,11 @@ https://news.gov.bc.ca/news-subscribe/covid-19/feed https://immunizebc.ca/ + + https://www.healthlinkbc.ca/bc-immunization-schedules#child + https://www.healthlinkbc.ca/bc-immunization-schedules#school + https://www.healthlinkbc.ca/bc-immunization-schedules#adult + https://immunizationrecord.gov.bc.ca/ https://www.healthlinkbc.ca/ diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 5f95df2e5..03e31aa01 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -12,6 +12,21 @@ Expand content Collapse content + + Health record + Service + Dependents’ records + %s’s record + + My Notes + Immunization + Medications + Lab Results + Special authority + Health visit + Clinic documents + Organ donor + Privacy Policy Allow camera access @@ -33,6 +48,9 @@ Recommended immunizations For more information on vaccines recommendation and eligibility, please visit immunizeBC or speak to your health care provider. immunizeBC + Recommended immunization are suggestion for your health journey. This page will contains all of future immunization for you and your dependents. To add dependents to application, click on Dependent in the menu bar at the bottom. + For more information on vaccines recommendation and eligibility, please visit immunizeBC or speak to your health care provider. + immunizeBC Dependent @@ -484,6 +502,24 @@ Learn more What do you want to focus on today? New updates + Quick access + Manage + Immunization schedules + Log in + Gain full access to health records for you and your family. + Set a folder to display \non your home page \nfor quick access. + + + Immunization Schedules + Click on the tabs below to expand and view individual immunization schedules for different age groups and individuals at high risk. + Infant and children + School age children + Adult, seniors and individual with high risk + + + Manage + Set a folder to display on your home page for quick and easy access. + Save Terms of service @@ -587,4 +623,11 @@ Health Authority: Facility: + + More Options + + + Remove %s + You can easily restore it by managing the settings. + Remove \ No newline at end of file diff --git a/app/src/main/res/values/typography.xml b/app/src/main/res/values/typography.xml index 9a716d9f5..576d0ee01 100644 --- a/app/src/main/res/values/typography.xml +++ b/app/src/main/res/values/typography.xml @@ -77,4 +77,19 @@ @font/bc_sans_font_family normal + + + + + \ No newline at end of file diff --git a/appauth/src/main/java/net/openid/appauth/extension/AppAuthExtensions.kt b/appauth/src/main/java/net/openid/appauth/extension/AppAuthExtensions.kt index 2e4302db5..ed7143f4a 100644 --- a/appauth/src/main/java/net/openid/appauth/extension/AppAuthExtensions.kt +++ b/appauth/src/main/java/net/openid/appauth/extension/AppAuthExtensions.kt @@ -72,6 +72,7 @@ suspend fun getBCSCAuthData(applicationContext: Context, authState: AuthState): val accessToken = awaitPerformActionWithFreshTokens(applicationContext, authState) val json = decodeAccessToken(accessToken) val hdId = json.get(HDID).toString() + println("USERNAME = ${json.get("name")} PREFEREDNAME = ${json.get("preferred_username")}") if (hdId.isEmpty()) throw MyHealthAuthException("Invalid access token!") else diff --git a/common/build.gradle b/common/build.gradle index 4abede1d1..d3cc79950 100644 --- a/common/build.gradle +++ b/common/build.gradle @@ -102,6 +102,7 @@ android { kotlinOptions { jvmTarget = '17' } + namespace 'ca.bc.gov.common' } dependencies { diff --git a/common/src/main/AndroidManifest.xml b/common/src/main/AndroidManifest.xml index 39a61523b..a5918e68a 100644 --- a/common/src/main/AndroidManifest.xml +++ b/common/src/main/AndroidManifest.xml @@ -1,5 +1,4 @@ - + \ No newline at end of file diff --git a/common/src/main/java/ca/bc/gov/common/model/AppFeatureName.kt b/common/src/main/java/ca/bc/gov/common/model/AppFeatureName.kt new file mode 100644 index 000000000..948fe1e24 --- /dev/null +++ b/common/src/main/java/ca/bc/gov/common/model/AppFeatureName.kt @@ -0,0 +1,15 @@ +package ca.bc.gov.common.model + +enum class AppFeatureName(val value: String) { + HEALTH_RECORDS("Health records"), + IMMUNIZATION_SCHEDULES("Immunization schedules"), + RECOMMENDED_IMMUNIZATIONS("Recommended immunizations"), + HEALTH_RESOURCES("Health resources"), + PROOF_OF_VACCINE("Proof of vaccination"), + SERVICES("Services"); + + companion object { + private val map = AppFeatureName.values().associateBy(AppFeatureName::value) + operator fun get(value: String) = map[value] + } +} diff --git a/common/src/main/java/ca/bc/gov/common/model/QuickAccessLinkName.kt b/common/src/main/java/ca/bc/gov/common/model/QuickAccessLinkName.kt new file mode 100644 index 000000000..0cd65cb21 --- /dev/null +++ b/common/src/main/java/ca/bc/gov/common/model/QuickAccessLinkName.kt @@ -0,0 +1,20 @@ +package ca.bc.gov.common.model + +enum class QuickAccessLinkName(val value: String) { + IMMUNIZATIONS("Immunizations"), + MEDICATIONS("Medications"), + LAB_RESULTS("Lab Results"), + COVID_19_TESTS("COVID‑19 Tests"), + HEALTH_VISITS("Health Visits"), + MY_NOTES("My Notes"), + SPECIAL_AUTHORITY("Special Authority"), + CLINICAL_DOCUMENTS("Clinical Documents"), + HOSPITAL_VISITS("Hospital Visits"), + IMAGING_REPORTS("Imaging Reports"), + ORGAN_DONOR("Organ Donor"); + + companion object { + private val map = QuickAccessLinkName.values().associateBy(QuickAccessLinkName::value) + operator fun get(value: String) = map[value] + } +} diff --git a/common/src/main/java/ca/bc/gov/common/model/QuickAccessTileShowAsQuickLinkDto.kt b/common/src/main/java/ca/bc/gov/common/model/QuickAccessTileShowAsQuickLinkDto.kt new file mode 100644 index 000000000..941604e41 --- /dev/null +++ b/common/src/main/java/ca/bc/gov/common/model/QuickAccessTileShowAsQuickLinkDto.kt @@ -0,0 +1,6 @@ +package ca.bc.gov.common.model + +data class QuickAccessTileShowAsQuickLinkDto( + val id: Long = 0, + val showAsQuickAccess: Boolean = false +) diff --git a/common/src/main/java/ca/bc/gov/common/model/UserAuthenticationStatus.kt b/common/src/main/java/ca/bc/gov/common/model/UserAuthenticationStatus.kt new file mode 100644 index 000000000..d26f0c650 --- /dev/null +++ b/common/src/main/java/ca/bc/gov/common/model/UserAuthenticationStatus.kt @@ -0,0 +1,7 @@ +package ca.bc.gov.common.model + +enum class UserAuthenticationStatus { + AUTHENTICATED, + SESSION_TIME_OUT, + UN_AUTHENTICATED +} diff --git a/common/src/main/java/ca/bc/gov/common/model/patient/PatientWithDependentAndListOrderDto.kt b/common/src/main/java/ca/bc/gov/common/model/patient/PatientWithDependentAndListOrderDto.kt new file mode 100644 index 000000000..689d90187 --- /dev/null +++ b/common/src/main/java/ca/bc/gov/common/model/patient/PatientWithDependentAndListOrderDto.kt @@ -0,0 +1,8 @@ +package ca.bc.gov.common.model.patient + +import ca.bc.gov.common.model.dependents.DependentDto + +data class PatientWithDependentAndListOrderDto( + val patient: PatientDto, + val dependents: List +) diff --git a/common/src/main/java/ca/bc/gov/common/model/patient/PatientWithImmunizationRecommendationsDto.kt b/common/src/main/java/ca/bc/gov/common/model/patient/PatientWithImmunizationRecommendationsDto.kt new file mode 100644 index 000000000..021f74c85 --- /dev/null +++ b/common/src/main/java/ca/bc/gov/common/model/patient/PatientWithImmunizationRecommendationsDto.kt @@ -0,0 +1,11 @@ +package ca.bc.gov.common.model.patient + +import ca.bc.gov.common.model.immunization.ImmunizationRecommendationsDto + +/** + * @author Pinakin Kansara + */ +data class PatientWithImmunizationRecommendationsDto( + val patient: PatientDto, + val recommendations: List +) diff --git a/common/src/main/java/ca/bc/gov/common/model/settings/AppFeatureDto.kt b/common/src/main/java/ca/bc/gov/common/model/settings/AppFeatureDto.kt new file mode 100644 index 000000000..18233edcb --- /dev/null +++ b/common/src/main/java/ca/bc/gov/common/model/settings/AppFeatureDto.kt @@ -0,0 +1,10 @@ +package ca.bc.gov.common.model.settings + +import ca.bc.gov.common.model.AppFeatureName + +data class AppFeatureDto( + val id: Long = 0, + val name: AppFeatureName, + val hasManageableQuickAccessLinks: Boolean = false, + val showAsQuickAccess: Boolean = false +) diff --git a/common/src/main/java/ca/bc/gov/common/model/settings/AppFeatureWithQuickAccessTilesDto.kt b/common/src/main/java/ca/bc/gov/common/model/settings/AppFeatureWithQuickAccessTilesDto.kt new file mode 100644 index 000000000..8fddc3b36 --- /dev/null +++ b/common/src/main/java/ca/bc/gov/common/model/settings/AppFeatureWithQuickAccessTilesDto.kt @@ -0,0 +1,6 @@ +package ca.bc.gov.common.model.settings + +data class AppFeatureWithQuickAccessTilesDto( + val appFeatureDto: AppFeatureDto, + val quickAccessTiles: List = emptyList() +) diff --git a/common/src/main/java/ca/bc/gov/common/model/settings/QuickAccessTileDto.kt b/common/src/main/java/ca/bc/gov/common/model/settings/QuickAccessTileDto.kt new file mode 100644 index 000000000..a7d84b9ef --- /dev/null +++ b/common/src/main/java/ca/bc/gov/common/model/settings/QuickAccessTileDto.kt @@ -0,0 +1,11 @@ +package ca.bc.gov.common.model.settings + +import ca.bc.gov.common.model.QuickAccessLinkName + +data class QuickAccessTileDto( + val id: Long = 0, + val featureId: Long = 0, + val tileName: QuickAccessLinkName, + val tilePayload: String? = null, + val showAsQuickAccess: Boolean = false +) diff --git a/data/build.gradle b/data/build.gradle index 333de1533..02f3920e6 100644 --- a/data/build.gradle +++ b/data/build.gradle @@ -63,6 +63,7 @@ android { // Adds exported schema location as test app assets. androidTest.assets.srcDirs += files("$projectDir/schemas".toString()) } + namespace 'ca.bc.gov.data' } dependencies { diff --git a/data/schemas/ca.bc.gov.data.datasource.local.MyHealthDataBase/14.json b/data/schemas/ca.bc.gov.data.datasource.local.MyHealthDataBase/14.json index 499f8f603..c18c9065d 100644 --- a/data/schemas/ca.bc.gov.data.datasource.local.MyHealthDataBase/14.json +++ b/data/schemas/ca.bc.gov.data.datasource.local.MyHealthDataBase/14.json @@ -2,7 +2,7 @@ "formatVersion": 1, "database": { "version": 14, - "identityHash": "cff70079a468f7fab21b2c8957aa411e", + "identityHash": "276a9e926008aa206fc634d8899c0b77", "entities": [ { "tableName": "patient", @@ -1997,12 +1997,82 @@ }, "indices": [], "foreignKeys": [] + }, + { + "tableName": "app_feature", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `feature_name` TEXT, `feature_name_id` INTEGER, `category_name_id` INTEGER NOT NULL, `feature_icon_id` INTEGER NOT NULL, `destination_id` INTEGER NOT NULL, `destination_param` TEXT, `is_management_enabled` INTEGER NOT NULL DEFAULT false, `quick_access_enabled` INTEGER NOT NULL DEFAULT false)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "featureName", + "columnName": "feature_name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "featureNameId", + "columnName": "feature_name_id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "categoryNameId", + "columnName": "category_name_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "featureIconId", + "columnName": "feature_icon_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "destinationId", + "columnName": "destination_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "destinationParam", + "columnName": "destination_param", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isManagementEnabled", + "columnName": "is_management_enabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "false" + }, + { + "fieldPath": "isQuickAccessEnabled", + "columnName": "quick_access_enabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "false" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] } ], "views": [], "setupQueries": [ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", - "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'cff70079a468f7fab21b2c8957aa411e')" + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '276a9e926008aa206fc634d8899c0b77')" ] } } \ No newline at end of file diff --git a/data/schemas/ca.bc.gov.data.datasource.local.MyHealthDataBase/15.json b/data/schemas/ca.bc.gov.data.datasource.local.MyHealthDataBase/15.json index 807ef0b05..796a6db76 100644 --- a/data/schemas/ca.bc.gov.data.datasource.local.MyHealthDataBase/15.json +++ b/data/schemas/ca.bc.gov.data.datasource.local.MyHealthDataBase/15.json @@ -2,7 +2,7 @@ "formatVersion": 1, "database": { "version": 15, - "identityHash": "1cd6080273feb77af674dc6bd9751e38", + "identityHash": "bfa234b2e6680ceb5454934b27b3bba8", "entities": [ { "tableName": "patient", @@ -2004,12 +2004,130 @@ }, "indices": [], "foreignKeys": [] + }, + { + "tableName": "app_feature", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `feature_name` TEXT NOT NULL, `has_manageable_quick_access_links` INTEGER NOT NULL DEFAULT false, `show_as_quick_access` INTEGER NOT NULL DEFAULT false)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "feature_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "hasManageableQuickAccessLinks", + "columnName": "has_manageable_quick_access_links", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "false" + }, + { + "fieldPath": "showAsQuickAccess", + "columnName": "show_as_quick_access", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "false" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_app_feature_feature_name", + "unique": true, + "columnNames": [ + "feature_name" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_app_feature_feature_name` ON `${TABLE_NAME}` (`feature_name`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "quick_access_tile", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `feature_id` INTEGER NOT NULL, `tile_name` TEXT NOT NULL, `tile_payload` TEXT DEFAULT null, `show_as_quick_access` INTEGER NOT NULL DEFAULT false, FOREIGN KEY(`feature_id`) REFERENCES `app_feature`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "featureId", + "columnName": "feature_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "tileName", + "columnName": "tile_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "tilePayload", + "columnName": "tile_payload", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "null" + }, + { + "fieldPath": "showAsQuickAccess", + "columnName": "show_as_quick_access", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "false" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_quick_access_tile_tile_name", + "unique": true, + "columnNames": [ + "tile_name" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_quick_access_tile_tile_name` ON `${TABLE_NAME}` (`tile_name`)" + } + ], + "foreignKeys": [ + { + "table": "app_feature", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "feature_id" + ], + "referencedColumns": [ + "id" + ] + } + ] } ], "views": [], "setupQueries": [ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", - "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '1cd6080273feb77af674dc6bd9751e38')" + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'bfa234b2e6680ceb5454934b27b3bba8')" ] } } \ No newline at end of file diff --git a/data/src/main/AndroidManifest.xml b/data/src/main/AndroidManifest.xml index 2761d1372..8c4c98268 100644 --- a/data/src/main/AndroidManifest.xml +++ b/data/src/main/AndroidManifest.xml @@ -1,6 +1,5 @@ - + \ No newline at end of file diff --git a/data/src/main/java/ca/bc/gov/data/datasource/local/AppFeatureLocalDataSource.kt b/data/src/main/java/ca/bc/gov/data/datasource/local/AppFeatureLocalDataSource.kt new file mode 100644 index 000000000..b791475f3 --- /dev/null +++ b/data/src/main/java/ca/bc/gov/data/datasource/local/AppFeatureLocalDataSource.kt @@ -0,0 +1,21 @@ +package ca.bc.gov.data.datasource.local + +import ca.bc.gov.common.model.settings.AppFeatureDto +import ca.bc.gov.common.model.settings.AppFeatureWithQuickAccessTilesDto +import ca.bc.gov.data.datasource.local.dao.AppFeatureDao +import ca.bc.gov.data.model.mapper.toDto +import ca.bc.gov.data.model.mapper.toEntity +import javax.inject.Inject + +class AppFeatureLocalDataSource @Inject constructor( + private val appFeatureDao: AppFeatureDao +) { + + suspend fun insert(appFeatureDto: AppFeatureDto): Long { + return appFeatureDao.insert(appFeatureDto.toEntity()) + } + + suspend fun getAppFeaturesWithQuickAccessTiles(): List { + return appFeatureDao.getAllFeatureWithQuickAccessTiles().map { it.toDto() } + } +} diff --git a/data/src/main/java/ca/bc/gov/data/datasource/local/ImmunizationRecommendationLocalDataSource.kt b/data/src/main/java/ca/bc/gov/data/datasource/local/ImmunizationRecommendationLocalDataSource.kt index 7b05a1a75..13e5d52c9 100644 --- a/data/src/main/java/ca/bc/gov/data/datasource/local/ImmunizationRecommendationLocalDataSource.kt +++ b/data/src/main/java/ca/bc/gov/data/datasource/local/ImmunizationRecommendationLocalDataSource.kt @@ -17,6 +17,8 @@ class ImmunizationRecommendationLocalDataSource @Inject constructor( list.map { it.toDto() } } + suspend fun hasRecommendations() = dao.hasRecommendations() + suspend fun insert(immunizationRecommendation: ImmunizationRecommendationsDto): Long = dao.insert(immunizationRecommendation.toEntity()) diff --git a/data/src/main/java/ca/bc/gov/data/datasource/local/MyHealthDataBase.kt b/data/src/main/java/ca/bc/gov/data/datasource/local/MyHealthDataBase.kt index 271141d43..58d94eee2 100644 --- a/data/src/main/java/ca/bc/gov/data/datasource/local/MyHealthDataBase.kt +++ b/data/src/main/java/ca/bc/gov/data/datasource/local/MyHealthDataBase.kt @@ -2,13 +2,19 @@ package ca.bc.gov.data.datasource.local import androidx.room.AutoMigration import androidx.room.Database +import androidx.room.DeleteColumn +import androidx.room.RenameColumn import androidx.room.RoomDatabase import androidx.room.TypeConverters +import androidx.room.migration.AutoMigrationSpec import ca.bc.gov.data.datasource.local.converter.AddressConverter +import ca.bc.gov.data.datasource.local.converter.AppFeatureNameConverter import ca.bc.gov.data.datasource.local.converter.AuthenticationStatusTypeConverter import ca.bc.gov.data.datasource.local.converter.DateTimeConverter import ca.bc.gov.data.datasource.local.converter.PatientNameConverter +import ca.bc.gov.data.datasource.local.converter.QuickAccessLinkNameConverter import ca.bc.gov.data.datasource.local.converter.SyncStatusConverter +import ca.bc.gov.data.datasource.local.dao.AppFeatureDao import ca.bc.gov.data.datasource.local.dao.ClinicalDocumentDao import ca.bc.gov.data.datasource.local.dao.CommentDao import ca.bc.gov.data.datasource.local.dao.CovidOrderDao @@ -29,6 +35,7 @@ import ca.bc.gov.data.datasource.local.dao.MedicationSummaryDao import ca.bc.gov.data.datasource.local.dao.NotificationDao import ca.bc.gov.data.datasource.local.dao.OrganDonorDao import ca.bc.gov.data.datasource.local.dao.PatientDao +import ca.bc.gov.data.datasource.local.dao.QuickAccessTileDao import ca.bc.gov.data.datasource.local.dao.SpecialAuthorityDao import ca.bc.gov.data.datasource.local.dao.UserProfileDao import ca.bc.gov.data.datasource.local.dao.VaccineRecordDao @@ -54,6 +61,8 @@ import ca.bc.gov.data.datasource.local.entity.medication.MedicationSummaryEntity import ca.bc.gov.data.datasource.local.entity.notification.NotificationEntity import ca.bc.gov.data.datasource.local.entity.services.DiagnosticImagingDataEntity import ca.bc.gov.data.datasource.local.entity.services.OrganDonorEntity +import ca.bc.gov.data.datasource.local.entity.settings.AppFeatureEntity +import ca.bc.gov.data.datasource.local.entity.settings.QuickAccessTileEntity import ca.bc.gov.data.datasource.local.entity.specialauthority.SpecialAuthorityEntity import ca.bc.gov.data.datasource.local.entity.userprofile.UserProfileEntity @@ -87,6 +96,8 @@ import ca.bc.gov.data.datasource.local.entity.userprofile.UserProfileEntity OrganDonorEntity::class, DiagnosticImagingDataEntity::class, NotificationEntity::class, + AppFeatureEntity::class, + QuickAccessTileEntity::class ], autoMigrations = [ AutoMigration(from = 8, to = 9), @@ -95,7 +106,7 @@ import ca.bc.gov.data.datasource.local.entity.userprofile.UserProfileEntity AutoMigration(from = 11, to = 12), AutoMigration(from = 12, to = 13), AutoMigration(from = 13, to = 14), - AutoMigration(from = 14, to = 15) + AutoMigration(from = 14, to = 15, spec = MyHealthDataBase.AppFeatureMigration::class) ], exportSchema = true ) @@ -104,10 +115,33 @@ import ca.bc.gov.data.datasource.local.entity.userprofile.UserProfileEntity AuthenticationStatusTypeConverter::class, AddressConverter::class, SyncStatusConverter::class, - PatientNameConverter::class + PatientNameConverter::class, + AppFeatureNameConverter::class, + QuickAccessLinkNameConverter::class ) abstract class MyHealthDataBase : RoomDatabase() { + @DeleteColumn.Entries( + DeleteColumn(tableName = "app_feature", columnName = "feature_name_id"), + DeleteColumn(tableName = "app_feature", columnName = "feature_icon_id"), + DeleteColumn(tableName = "app_feature", columnName = "destination_id"), + DeleteColumn(tableName = "app_feature", columnName = "category_name_id"), + DeleteColumn(tableName = "app_feature", columnName = "destination_param") + ) + @RenameColumn.Entries( + RenameColumn( + tableName = "app_feature", + fromColumnName = "quick_access_enabled", + toColumnName = "show_as_quick_access" + ), + RenameColumn( + tableName = "app_feature", + fromColumnName = "is_management_enabled", + toColumnName = "has_manageable_quick_access_links" + ) + ) + class AppFeatureMigration : AutoMigrationSpec + abstract fun getPatientDao(): PatientDao abstract fun getVaccineRecordDao(): VaccineRecordDao @@ -153,4 +187,8 @@ abstract class MyHealthDataBase : RoomDatabase() { abstract fun getDiagnosticImagingDataDao(): DiagnosticImagingDataDao abstract fun getNotificationDao(): NotificationDao + + abstract fun getAppFeatureDao(): AppFeatureDao + + abstract fun getQuickAccessTileDao(): QuickAccessTileDao } diff --git a/data/src/main/java/ca/bc/gov/data/datasource/local/PatientLocalDataSource.kt b/data/src/main/java/ca/bc/gov/data/datasource/local/PatientLocalDataSource.kt index c2c8b66e1..9fe9c7130 100644 --- a/data/src/main/java/ca/bc/gov/data/datasource/local/PatientLocalDataSource.kt +++ b/data/src/main/java/ca/bc/gov/data/datasource/local/PatientLocalDataSource.kt @@ -131,6 +131,12 @@ class PatientLocalDataSource @Inject constructor( suspend fun getPatientWithData(patientId: Long): PatientWithDataDto? = patientDao.getPatientWithData(patientId)?.toDto() + suspend fun getPatientWithImmunizationRecommendations(patientId: Long) = + patientDao.getPatientWithImmunizationRecommendations(patientId)?.toDto() + + suspend fun getPatientWithDependents(patientId: Long) = + patientDao.getPatientWithDependents(patientId)?.toDto() + suspend fun getAuthenticatedPatientId() = patientDao.getAuthenticatedPatientId() ?: -1 suspend fun deleteDependentPatients() { diff --git a/data/src/main/java/ca/bc/gov/data/datasource/local/QuickActionTileLocalDataSource.kt b/data/src/main/java/ca/bc/gov/data/datasource/local/QuickActionTileLocalDataSource.kt new file mode 100644 index 000000000..bd5a3a42b --- /dev/null +++ b/data/src/main/java/ca/bc/gov/data/datasource/local/QuickActionTileLocalDataSource.kt @@ -0,0 +1,31 @@ +package ca.bc.gov.data.datasource.local + +import ca.bc.gov.common.model.QuickAccessTileShowAsQuickLinkDto +import ca.bc.gov.common.model.settings.QuickAccessTileDto +import ca.bc.gov.data.datasource.local.dao.QuickAccessTileDao +import ca.bc.gov.data.model.mapper.toEntity +import javax.inject.Inject + +class QuickActionTileLocalDataSource @Inject constructor( + private val quickAccessTileDao: QuickAccessTileDao +) { + + suspend fun insert(quickAccessTileDto: QuickAccessTileDto): Long { + return quickAccessTileDao.insert(quickAccessTileDto.toEntity()) + } + + suspend fun insertAll(tiles: List): List { + return quickAccessTileDao.insert(tiles.map { it.toEntity() }) + } + + suspend fun updateAll(tiles: List): Int { + return quickAccessTileDao.updateAll(tiles.map { it.toEntity() }) + } + suspend fun update(tile: QuickAccessTileShowAsQuickLinkDto) { + quickAccessTileDao.update(tile.toEntity()) + } + + suspend fun update(showAsQuickAction: Boolean) { + quickAccessTileDao.update(showAsQuickAction) + } +} diff --git a/data/src/main/java/ca/bc/gov/data/datasource/local/converter/AppFeatureNameConverter.kt b/data/src/main/java/ca/bc/gov/data/datasource/local/converter/AppFeatureNameConverter.kt new file mode 100644 index 000000000..acb94a522 --- /dev/null +++ b/data/src/main/java/ca/bc/gov/data/datasource/local/converter/AppFeatureNameConverter.kt @@ -0,0 +1,13 @@ +package ca.bc.gov.data.datasource.local.converter + +import androidx.room.TypeConverter +import ca.bc.gov.common.model.AppFeatureName + +class AppFeatureNameConverter { + + @TypeConverter + fun stringToEnum(name: String) = AppFeatureName[name] + + @TypeConverter + fun enumToString(appFeatureName: AppFeatureName) = appFeatureName.value +} diff --git a/data/src/main/java/ca/bc/gov/data/datasource/local/converter/QuickAccessLinkNameConverter.kt b/data/src/main/java/ca/bc/gov/data/datasource/local/converter/QuickAccessLinkNameConverter.kt new file mode 100644 index 000000000..9851a7996 --- /dev/null +++ b/data/src/main/java/ca/bc/gov/data/datasource/local/converter/QuickAccessLinkNameConverter.kt @@ -0,0 +1,13 @@ +package ca.bc.gov.data.datasource.local.converter + +import androidx.room.TypeConverter +import ca.bc.gov.common.model.QuickAccessLinkName + +class QuickAccessLinkNameConverter { + + @TypeConverter + fun stringToEnum(value: String) = QuickAccessLinkName[value] + + @TypeConverter + fun enumToString(linkName: QuickAccessLinkName) = linkName.value +} diff --git a/data/src/main/java/ca/bc/gov/data/datasource/local/dao/AppFeatureDao.kt b/data/src/main/java/ca/bc/gov/data/datasource/local/dao/AppFeatureDao.kt new file mode 100644 index 000000000..800ff3ee2 --- /dev/null +++ b/data/src/main/java/ca/bc/gov/data/datasource/local/dao/AppFeatureDao.kt @@ -0,0 +1,20 @@ +package ca.bc.gov.data.datasource.local.dao + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import ca.bc.gov.data.datasource.local.entity.relations.AppFeatureWithQuickAccessTiles +import ca.bc.gov.data.datasource.local.entity.settings.AppFeatureEntity + +@Dao +interface AppFeatureDao { + @Insert(onConflict = OnConflictStrategy.IGNORE) + suspend fun insert(appFeatureEntity: AppFeatureEntity): Long + + @Query("DELETE FROM app_feature") + suspend fun deleteAll() + + @Query("SELECT * FROM app_feature") + suspend fun getAllFeatureWithQuickAccessTiles(): List +} diff --git a/data/src/main/java/ca/bc/gov/data/datasource/local/dao/ImmunizationRecommendationDao.kt b/data/src/main/java/ca/bc/gov/data/datasource/local/dao/ImmunizationRecommendationDao.kt index ea6e58677..fa84d2f1e 100644 --- a/data/src/main/java/ca/bc/gov/data/datasource/local/dao/ImmunizationRecommendationDao.kt +++ b/data/src/main/java/ca/bc/gov/data/datasource/local/dao/ImmunizationRecommendationDao.kt @@ -15,6 +15,9 @@ interface ImmunizationRecommendationDao : BaseDao> + @Query("SELECT EXISTS(SELECT * ,(SELECT id FROM patient WHERE authentication_status = 'AUTHENTICATED') AS patientId FROM immunization_recommendation)") + fun hasRecommendations(): Boolean + @Query("DELETE FROM immunization_recommendation WHERE recommendation_set_id = :id") suspend fun delete(id: Long): Int } diff --git a/data/src/main/java/ca/bc/gov/data/datasource/local/dao/PatientDao.kt b/data/src/main/java/ca/bc/gov/data/datasource/local/dao/PatientDao.kt index 274b76264..7a4ede314 100644 --- a/data/src/main/java/ca/bc/gov/data/datasource/local/dao/PatientDao.kt +++ b/data/src/main/java/ca/bc/gov/data/datasource/local/dao/PatientDao.kt @@ -12,8 +12,10 @@ import ca.bc.gov.data.datasource.local.entity.PatientOrderUpdate import ca.bc.gov.data.datasource.local.entity.relations.PatientWithClinicalDocuments import ca.bc.gov.data.datasource.local.entity.relations.PatientWithCovidOrderAndCovidTest import ca.bc.gov.data.datasource.local.entity.relations.PatientWithData +import ca.bc.gov.data.datasource.local.entity.relations.PatientWithDependentAndListOder import ca.bc.gov.data.datasource.local.entity.relations.PatientWithHealthVisits import ca.bc.gov.data.datasource.local.entity.relations.PatientWithHospitalVisits +import ca.bc.gov.data.datasource.local.entity.relations.PatientWithImmunizationRecommendations import ca.bc.gov.data.datasource.local.entity.relations.PatientWithImmunizationRecordAndForecast import ca.bc.gov.data.datasource.local.entity.relations.PatientWithLabOrdersAndLabTests import ca.bc.gov.data.datasource.local.entity.relations.PatientWithMedicationRecords @@ -98,6 +100,14 @@ interface PatientDao { @Query("SELECT * FROM patient WHERE id = :patientId") suspend fun getPatientWithData(patientId: Long): PatientWithData? + @Transaction + @Query("SELECT * FROM patient WHERE id = :patientId") + suspend fun getPatientWithImmunizationRecommendations(patientId: Long): PatientWithImmunizationRecommendations? + + @Transaction + @Query("SELECT * FROM patient WHERE id = :patientId") + suspend fun getPatientWithDependents(patientId: Long): PatientWithDependentAndListOder? + @Query("SELECT id FROM patient WHERE authentication_status = 'AUTHENTICATED'") suspend fun getAuthenticatedPatientId(): Long? diff --git a/data/src/main/java/ca/bc/gov/data/datasource/local/dao/QuickAccessTileDao.kt b/data/src/main/java/ca/bc/gov/data/datasource/local/dao/QuickAccessTileDao.kt new file mode 100644 index 000000000..1113b51aa --- /dev/null +++ b/data/src/main/java/ca/bc/gov/data/datasource/local/dao/QuickAccessTileDao.kt @@ -0,0 +1,20 @@ +package ca.bc.gov.data.datasource.local.dao + +import androidx.room.Dao +import androidx.room.Query +import androidx.room.Update +import ca.bc.gov.data.datasource.local.entity.settings.QuickAccessTileEntity +import ca.bc.gov.data.datasource.local.entity.settings.QuickAccessTileShowAsQuickLinkEntity + +@Dao +interface QuickAccessTileDao : BaseDao { + + @Update(entity = QuickAccessTileEntity::class) + suspend fun updateAll(tiles: List): Int + + @Update(entity = QuickAccessTileEntity::class) + suspend fun update(tile: QuickAccessTileShowAsQuickLinkEntity) + + @Query("UPDATE quick_access_tile SET show_as_quick_access = :showAsQuickAccess") + suspend fun update(showAsQuickAccess: Boolean) +} diff --git a/data/src/main/java/ca/bc/gov/data/datasource/local/entity/relations/AppFeatureWithQuickAccessTiles.kt b/data/src/main/java/ca/bc/gov/data/datasource/local/entity/relations/AppFeatureWithQuickAccessTiles.kt new file mode 100644 index 000000000..b27a645ac --- /dev/null +++ b/data/src/main/java/ca/bc/gov/data/datasource/local/entity/relations/AppFeatureWithQuickAccessTiles.kt @@ -0,0 +1,17 @@ +package ca.bc.gov.data.datasource.local.entity.relations + +import androidx.room.Embedded +import androidx.room.Relation +import ca.bc.gov.data.datasource.local.entity.settings.AppFeatureEntity +import ca.bc.gov.data.datasource.local.entity.settings.QuickAccessTileEntity + +data class AppFeatureWithQuickAccessTiles( + @Embedded + val appFeature: AppFeatureEntity, + + @Relation( + parentColumn = "id", + entityColumn = "feature_id" + ) + val quickAccessTiles: List = emptyList() +) diff --git a/data/src/main/java/ca/bc/gov/data/datasource/local/entity/relations/PatientWithDependentAndListOder.kt b/data/src/main/java/ca/bc/gov/data/datasource/local/entity/relations/PatientWithDependentAndListOder.kt new file mode 100644 index 000000000..4a8d07c1c --- /dev/null +++ b/data/src/main/java/ca/bc/gov/data/datasource/local/entity/relations/PatientWithDependentAndListOder.kt @@ -0,0 +1,17 @@ +package ca.bc.gov.data.datasource.local.entity.relations + +import androidx.room.Embedded +import androidx.room.Relation +import ca.bc.gov.data.datasource.local.entity.PatientEntity +import ca.bc.gov.data.datasource.local.entity.dependent.DependentEntity + +data class PatientWithDependentAndListOder( + @Embedded + val patient: PatientEntity, + + @Relation( + parentColumn = "id", + entityColumn = "guardian_id" + ) + val dependentAndListOrder: List = emptyList() +) diff --git a/data/src/main/java/ca/bc/gov/data/datasource/local/entity/relations/PatientWithImmunizationRecommendations.kt b/data/src/main/java/ca/bc/gov/data/datasource/local/entity/relations/PatientWithImmunizationRecommendations.kt new file mode 100644 index 000000000..e95f0de0e --- /dev/null +++ b/data/src/main/java/ca/bc/gov/data/datasource/local/entity/relations/PatientWithImmunizationRecommendations.kt @@ -0,0 +1,17 @@ +package ca.bc.gov.data.datasource.local.entity.relations + +import androidx.room.Embedded +import androidx.room.Relation +import ca.bc.gov.data.datasource.local.entity.PatientEntity +import ca.bc.gov.data.datasource.local.entity.immunization.ImmunizationRecommendationEntity + +data class PatientWithImmunizationRecommendations( + @Embedded + val patient: PatientEntity, + + @Relation( + parentColumn = "id", + entityColumn = "patient_id" + ) + val recommendations: List = emptyList() +) diff --git a/data/src/main/java/ca/bc/gov/data/datasource/local/entity/settings/AppFeatureEntity.kt b/data/src/main/java/ca/bc/gov/data/datasource/local/entity/settings/AppFeatureEntity.kt new file mode 100644 index 000000000..038f97622 --- /dev/null +++ b/data/src/main/java/ca/bc/gov/data/datasource/local/entity/settings/AppFeatureEntity.kt @@ -0,0 +1,22 @@ +package ca.bc.gov.data.datasource.local.entity.settings + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.Index +import androidx.room.PrimaryKey +import ca.bc.gov.common.model.AppFeatureName + +@Entity( + tableName = "app_feature", + indices = [Index(value = ["feature_name"], unique = true)] +) +data class AppFeatureEntity( + @PrimaryKey(autoGenerate = true) + val id: Long = 0, + @ColumnInfo(name = "feature_name") + val name: AppFeatureName, + @ColumnInfo(name = "has_manageable_quick_access_links", defaultValue = "false") + val hasManageableQuickAccessLinks: Boolean = false, + @ColumnInfo(name = "show_as_quick_access", defaultValue = "false") + val showAsQuickAccess: Boolean = false +) diff --git a/data/src/main/java/ca/bc/gov/data/datasource/local/entity/settings/QuickAccessTileEntity.kt b/data/src/main/java/ca/bc/gov/data/datasource/local/entity/settings/QuickAccessTileEntity.kt new file mode 100644 index 000000000..14708894e --- /dev/null +++ b/data/src/main/java/ca/bc/gov/data/datasource/local/entity/settings/QuickAccessTileEntity.kt @@ -0,0 +1,33 @@ +package ca.bc.gov.data.datasource.local.entity.settings + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.Index +import androidx.room.PrimaryKey +import ca.bc.gov.common.model.QuickAccessLinkName + +@Entity( + tableName = "quick_access_tile", + foreignKeys = [ + androidx.room.ForeignKey( + entity = AppFeatureEntity::class, + parentColumns = ["id"], + childColumns = ["feature_id"], + onDelete = androidx.room.ForeignKey.CASCADE, + onUpdate = androidx.room.ForeignKey.CASCADE + ) + ], + indices = [Index(value = ["tile_name"], unique = true)] +) +data class QuickAccessTileEntity( + @PrimaryKey(autoGenerate = true) + val id: Long = 0, + @ColumnInfo(name = "feature_id") + val featureId: Long, + @ColumnInfo(name = "tile_name") + val tileName: QuickAccessLinkName, + @ColumnInfo(name = "tile_payload", defaultValue = "null") + val tilePayload: String? = null, + @ColumnInfo(name = "show_as_quick_access", defaultValue = "false") + val showAsQuickAccess: Boolean = false +) diff --git a/data/src/main/java/ca/bc/gov/data/datasource/local/entity/settings/QuickAccessTileShowAsQuickLinkEntity.kt b/data/src/main/java/ca/bc/gov/data/datasource/local/entity/settings/QuickAccessTileShowAsQuickLinkEntity.kt new file mode 100644 index 000000000..ad293d889 --- /dev/null +++ b/data/src/main/java/ca/bc/gov/data/datasource/local/entity/settings/QuickAccessTileShowAsQuickLinkEntity.kt @@ -0,0 +1,9 @@ +package ca.bc.gov.data.datasource.local.entity.settings + +import androidx.room.ColumnInfo + +data class QuickAccessTileShowAsQuickLinkEntity( + val id: Long = 0, + @ColumnInfo(name = "show_as_quick_access", defaultValue = "false") + val showAsQuickAccess: Boolean = false +) diff --git a/data/src/main/java/ca/bc/gov/data/di/LocalDataSourceModule.kt b/data/src/main/java/ca/bc/gov/data/di/LocalDataSourceModule.kt index 14093968f..47d7ce943 100644 --- a/data/src/main/java/ca/bc/gov/data/di/LocalDataSourceModule.kt +++ b/data/src/main/java/ca/bc/gov/data/di/LocalDataSourceModule.kt @@ -1,5 +1,6 @@ package ca.bc.gov.data.di +import ca.bc.gov.data.datasource.local.AppFeatureLocalDataSource import ca.bc.gov.data.datasource.local.ClinicalDocumentLocalDataSource import ca.bc.gov.data.datasource.local.CommentLocalDataSource import ca.bc.gov.data.datasource.local.CovidOrderLocalDataSource @@ -19,6 +20,7 @@ import ca.bc.gov.data.datasource.local.MyHealthDataBase import ca.bc.gov.data.datasource.local.NotificationLocalDataSource import ca.bc.gov.data.datasource.local.OrganDonorLocalDataSource import ca.bc.gov.data.datasource.local.PatientLocalDataSource +import ca.bc.gov.data.datasource.local.QuickActionTileLocalDataSource import ca.bc.gov.data.datasource.local.SpecialAuthorityLocalDataSource import ca.bc.gov.data.datasource.local.UserProfileLocalDataSource import ca.bc.gov.data.datasource.local.VaccineRecordLocalDataSource @@ -153,4 +155,14 @@ class LocalDataSourceModule { @Singleton fun providesDiagnosticImagingLocalDataSource(db: MyHealthDataBase): DiagnosticImagingLocalDataSource = DiagnosticImagingLocalDataSource(db.getDiagnosticImagingDataDao()) + + @Provides + @Singleton + fun provideAppFeatureLocalDataSource(db: MyHealthDataBase): AppFeatureLocalDataSource = + AppFeatureLocalDataSource(db.getAppFeatureDao()) + + @Provides + @Singleton + fun provideQuickAccessLocalDataSource(db: MyHealthDataBase): QuickActionTileLocalDataSource = + QuickActionTileLocalDataSource(db.getQuickAccessTileDao()) } diff --git a/data/src/main/java/ca/bc/gov/data/model/mapper/DtoToEntityMapper.kt b/data/src/main/java/ca/bc/gov/data/model/mapper/DtoToEntityMapper.kt index 5d272bcb1..7ce119eac 100644 --- a/data/src/main/java/ca/bc/gov/data/model/mapper/DtoToEntityMapper.kt +++ b/data/src/main/java/ca/bc/gov/data/model/mapper/DtoToEntityMapper.kt @@ -5,6 +5,7 @@ import ca.bc.gov.common.model.DispensingPharmacyDto import ca.bc.gov.common.model.MedicationRecordDto import ca.bc.gov.common.model.MedicationSummaryDto import ca.bc.gov.common.model.PatientAddressDto +import ca.bc.gov.common.model.QuickAccessTileShowAsQuickLinkDto import ca.bc.gov.common.model.VaccineDoseDto import ca.bc.gov.common.model.VaccineRecordDto import ca.bc.gov.common.model.clinicaldocument.ClinicalDocumentDto @@ -22,6 +23,8 @@ import ca.bc.gov.common.model.patient.PatientDto import ca.bc.gov.common.model.patient.PatientNameDto import ca.bc.gov.common.model.services.DiagnosticImagingDataDto import ca.bc.gov.common.model.services.OrganDonorDto +import ca.bc.gov.common.model.settings.AppFeatureDto +import ca.bc.gov.common.model.settings.QuickAccessTileDto import ca.bc.gov.common.model.specialauthority.SpecialAuthorityDto import ca.bc.gov.common.model.test.CovidOrderDto import ca.bc.gov.common.model.test.CovidTestDto @@ -50,6 +53,9 @@ import ca.bc.gov.data.datasource.local.entity.medication.MedicationSummaryEntity import ca.bc.gov.data.datasource.local.entity.notification.NotificationEntity import ca.bc.gov.data.datasource.local.entity.services.DiagnosticImagingDataEntity import ca.bc.gov.data.datasource.local.entity.services.OrganDonorEntity +import ca.bc.gov.data.datasource.local.entity.settings.AppFeatureEntity +import ca.bc.gov.data.datasource.local.entity.settings.QuickAccessTileEntity +import ca.bc.gov.data.datasource.local.entity.settings.QuickAccessTileShowAsQuickLinkEntity import ca.bc.gov.data.datasource.local.entity.specialauthority.SpecialAuthorityEntity import ca.bc.gov.data.datasource.local.entity.userprofile.UserProfileEntity @@ -356,3 +362,22 @@ fun NotificationDto.toEntity() = NotificationEntity( actionType = actionType.value, date = date, ) + +fun AppFeatureDto.toEntity() = AppFeatureEntity( + id, + name, + hasManageableQuickAccessLinks, + showAsQuickAccess +) + +fun QuickAccessTileDto.toEntity() = QuickAccessTileEntity( + id, + featureId, + tileName, + tilePayload, + showAsQuickAccess +) + +fun QuickAccessTileShowAsQuickLinkDto.toEntity() = QuickAccessTileShowAsQuickLinkEntity( + id, showAsQuickAccess +) diff --git a/data/src/main/java/ca/bc/gov/data/model/mapper/EntityToDtoMapper.kt b/data/src/main/java/ca/bc/gov/data/model/mapper/EntityToDtoMapper.kt index 56618001e..3da2c497f 100644 --- a/data/src/main/java/ca/bc/gov/data/model/mapper/EntityToDtoMapper.kt +++ b/data/src/main/java/ca/bc/gov/data/model/mapper/EntityToDtoMapper.kt @@ -30,8 +30,10 @@ import ca.bc.gov.common.model.patient.PatientNameDto import ca.bc.gov.common.model.patient.PatientWithClinicalDocumentsDto import ca.bc.gov.common.model.patient.PatientWithCovidOrderAndTestDto import ca.bc.gov.common.model.patient.PatientWithDataDto +import ca.bc.gov.common.model.patient.PatientWithDependentAndListOrderDto import ca.bc.gov.common.model.patient.PatientWithHealthVisitsDto import ca.bc.gov.common.model.patient.PatientWithHospitalVisitsDto +import ca.bc.gov.common.model.patient.PatientWithImmunizationRecommendationsDto import ca.bc.gov.common.model.patient.PatientWithImmunizationRecordAndForecastDto import ca.bc.gov.common.model.patient.PatientWithLabOrderAndLatTestsDto import ca.bc.gov.common.model.patient.PatientWithSpecialAuthorityDto @@ -41,6 +43,9 @@ import ca.bc.gov.common.model.relation.PatientWithVaccineAndDosesDto import ca.bc.gov.common.model.relation.VaccineWithDosesDto import ca.bc.gov.common.model.services.DiagnosticImagingDataDto import ca.bc.gov.common.model.services.OrganDonorDto +import ca.bc.gov.common.model.settings.AppFeatureDto +import ca.bc.gov.common.model.settings.AppFeatureWithQuickAccessTilesDto +import ca.bc.gov.common.model.settings.QuickAccessTileDto import ca.bc.gov.common.model.specialauthority.SpecialAuthorityDto import ca.bc.gov.common.model.test.CovidOrderDto import ca.bc.gov.common.model.test.CovidOrderWithCovidTestAndPatientDto @@ -75,12 +80,15 @@ import ca.bc.gov.data.datasource.local.entity.medication.DispensingPharmacyEntit import ca.bc.gov.data.datasource.local.entity.medication.MedicationRecordEntity import ca.bc.gov.data.datasource.local.entity.medication.MedicationSummaryEntity import ca.bc.gov.data.datasource.local.entity.notification.NotificationEntity +import ca.bc.gov.data.datasource.local.entity.relations.AppFeatureWithQuickAccessTiles import ca.bc.gov.data.datasource.local.entity.relations.MedicationWithSummaryAndPharmacy import ca.bc.gov.data.datasource.local.entity.relations.PatientWithClinicalDocuments import ca.bc.gov.data.datasource.local.entity.relations.PatientWithCovidOrderAndCovidTest import ca.bc.gov.data.datasource.local.entity.relations.PatientWithData +import ca.bc.gov.data.datasource.local.entity.relations.PatientWithDependentAndListOder import ca.bc.gov.data.datasource.local.entity.relations.PatientWithHealthVisits import ca.bc.gov.data.datasource.local.entity.relations.PatientWithHospitalVisits +import ca.bc.gov.data.datasource.local.entity.relations.PatientWithImmunizationRecommendations import ca.bc.gov.data.datasource.local.entity.relations.PatientWithImmunizationRecordAndForecast import ca.bc.gov.data.datasource.local.entity.relations.PatientWithLabOrdersAndLabTests import ca.bc.gov.data.datasource.local.entity.relations.PatientWithMedicationRecords @@ -89,6 +97,8 @@ import ca.bc.gov.data.datasource.local.entity.relations.PatientWithVaccineAndDos import ca.bc.gov.data.datasource.local.entity.relations.VaccineRecordWithDose import ca.bc.gov.data.datasource.local.entity.services.DiagnosticImagingDataEntity import ca.bc.gov.data.datasource.local.entity.services.OrganDonorEntity +import ca.bc.gov.data.datasource.local.entity.settings.AppFeatureEntity +import ca.bc.gov.data.datasource.local.entity.settings.QuickAccessTileEntity import ca.bc.gov.data.datasource.local.entity.specialauthority.SpecialAuthorityEntity import ca.bc.gov.data.datasource.local.entity.userprofile.UserProfileEntity import java.time.Instant @@ -475,3 +485,33 @@ fun NotificationEntity.toDto() = NotificationDto( actionType = NotificationActionTypeDto.getByValue(actionType), date = date, ) + +fun AppFeatureEntity.toDto() = AppFeatureDto( + id, + name, + hasManageableQuickAccessLinks, + showAsQuickAccess +) + +fun QuickAccessTileEntity.toDto() = QuickAccessTileDto( + id, + featureId, + tileName, + tilePayload, + showAsQuickAccess +) + +fun AppFeatureWithQuickAccessTiles.toDto() = AppFeatureWithQuickAccessTilesDto( + appFeature.toDto(), + quickAccessTiles.map { it.toDto() } +) + +fun PatientWithImmunizationRecommendations.toDto() = PatientWithImmunizationRecommendationsDto( + patient = patient.toDto(), + recommendations = recommendations.map { it.toDto() }.sortedByDescending { record -> record.agentDueDate } +) + +fun PatientWithDependentAndListOder.toDto() = PatientWithDependentAndListOrderDto( + patient = patient.toDto(), + dependents = dependentAndListOrder.map { it.toDto() } +) diff --git a/data/src/main/java/ca/bc/gov/data/model/mapper/ResponseToDtoMapper.kt b/data/src/main/java/ca/bc/gov/data/model/mapper/ResponseToDtoMapper.kt index 8e107f7b6..c34ca3671 100644 --- a/data/src/main/java/ca/bc/gov/data/model/mapper/ResponseToDtoMapper.kt +++ b/data/src/main/java/ca/bc/gov/data/model/mapper/ResponseToDtoMapper.kt @@ -402,12 +402,23 @@ fun Recommendation.toDto(): ImmunizationRecommendationsDto { ) } -fun BannerPayload.toDto() = BannerDto( - title = this.title, - body = this.body, - startDate = this.startDate.toDateTimeZ(), - endDate = this.endDate.toDateTimeZ() -) +fun BannerPayload.toDto(): BannerDto? { + val startDate = this.startDate.toDateTimeZ() + val endDate = this.endDate.toDateTimeZ() + val currentTime = System.currentTimeMillis() + return if (startDate.toEpochMilli() <= currentTime && + currentTime < endDate.toEpochMilli() + ) { + BannerDto( + title = this.title, + body = this.body, + startDate = this.startDate.toDateTimeZ(), + endDate = this.endDate.toDateTimeZ() + ) + } else { + null + } +} fun DependentPayload.toDto() = DependentDto( hdid = dependentInformation.hdid, diff --git a/gradle.properties b/gradle.properties index dbc950620..b74d1f3be 100644 --- a/gradle.properties +++ b/gradle.properties @@ -18,4 +18,7 @@ android.useAndroidX=true # Automatically convert third-party libraries to use AndroidX android.enableJetifier=false # Kotlin code style for this project: "official" or "obsolete": -kotlin.code.style=official \ No newline at end of file +kotlin.code.style=official +android.defaults.buildfeatures.buildconfig=true +android.nonTransitiveRClass=false +android.nonFinalResIds=false \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 668ed1765..1b9c27278 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ #Mon Oct 20 12:09:37 PST 2022 distributionBase=GRADLE_USER_HOME -distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-bin.zip distributionPath=wrapper/dists zipStorePath=wrapper/dists zipStoreBase=GRADLE_USER_HOME diff --git a/preference/src/main/java/ca/bc/gov/preference/EncryptedPreferenceStorage.kt b/preference/src/main/java/ca/bc/gov/preference/EncryptedPreferenceStorage.kt index 643cea227..879ea07ff 100644 --- a/preference/src/main/java/ca/bc/gov/preference/EncryptedPreferenceStorage.kt +++ b/preference/src/main/java/ca/bc/gov/preference/EncryptedPreferenceStorage.kt @@ -40,6 +40,7 @@ class EncryptedPreferenceStorage @Inject constructor( private const val APP_VERSION_CODE = "APP_VERSION_CODE" private const val RE_ON_BOARDING_REQUIRED = "RE_ON_BOARDING_REQUIRED" private const val PREVIOUS_ON_BOARDING_SCREEN_NAME = "PREVIOUS_ON_BOARDING_SCREEN_NAME" + private const val QUICK_ACCESS_TILE_MANAGEMENT_TUTORIAL = "QUICK_ACCESS_TILE_MANAGEMENT_TUTORIAL" } var cookies: MutableSet? @@ -196,4 +197,12 @@ class EncryptedPreferenceStorage @Inject constructor( encryptedSharedPreferences.edit().putBoolean(RE_ON_BOARDING_REQUIRED, value) .apply() } + + var isQuickAccessTileTutorialRequired: Boolean + get() = encryptedSharedPreferences.getBoolean(QUICK_ACCESS_TILE_MANAGEMENT_TUTORIAL, true) + set(value) { + encryptedSharedPreferences.edit() + .putBoolean(QUICK_ACCESS_TILE_MANAGEMENT_TUTORIAL, value) + .apply() + } } diff --git a/repository/build.gradle b/repository/build.gradle index 413ce1962..8521db9a3 100644 --- a/repository/build.gradle +++ b/repository/build.gradle @@ -67,6 +67,7 @@ android { kotlinOptions { jvmTarget = '17' } + namespace 'ca.bc.gov.repository' } dependencies { diff --git a/repository/src/main/AndroidManifest.xml b/repository/src/main/AndroidManifest.xml index cc1e52ac2..568741e54 100644 --- a/repository/src/main/AndroidManifest.xml +++ b/repository/src/main/AndroidManifest.xml @@ -1,2 +1,2 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/repository/src/main/java/ca/bc/gov/repository/DependentsRepository.kt b/repository/src/main/java/ca/bc/gov/repository/DependentsRepository.kt index 416e809df..639866d7c 100644 --- a/repository/src/main/java/ca/bc/gov/repository/DependentsRepository.kt +++ b/repository/src/main/java/ca/bc/gov/repository/DependentsRepository.kt @@ -11,6 +11,7 @@ import ca.bc.gov.common.model.immunization.ImmunizationDto import ca.bc.gov.common.model.labtest.LabOrderWithLabTestDto import ca.bc.gov.common.model.patient.PatientWithClinicalDocumentsDto import ca.bc.gov.common.model.patient.PatientWithCovidOrderAndTestDto +import ca.bc.gov.common.model.patient.PatientWithImmunizationRecommendationsDto import ca.bc.gov.common.model.patient.PatientWithImmunizationRecordAndForecastDto import ca.bc.gov.common.model.patient.PatientWithLabOrderAndLatTestsDto import ca.bc.gov.common.model.test.CovidOrderWithCovidTestDto @@ -219,6 +220,10 @@ class DependentsRepository @Inject constructor( patientLocalDataSource.getPatientWithClinicalDocuments(patientId) ?: throw getDatabaseException(patientId) + suspend fun getPatientWithImmunizationRecommendations(patientId: Long): PatientWithImmunizationRecommendationsDto = + patientLocalDataSource.getPatientWithImmunizationRecommendations(patientId) + ?: throw getDatabaseException(patientId) + suspend fun updateDependentListOrder(list: List) { localDataSource.deleteAllDependentListOrders() diff --git a/repository/src/main/java/ca/bc/gov/repository/OnBoardingRepository.kt b/repository/src/main/java/ca/bc/gov/repository/OnBoardingRepository.kt index b27552bfe..cc938d9e7 100644 --- a/repository/src/main/java/ca/bc/gov/repository/OnBoardingRepository.kt +++ b/repository/src/main/java/ca/bc/gov/repository/OnBoardingRepository.kt @@ -43,9 +43,16 @@ class OnBoardingRepository @Inject constructor( fun checkIfReOnBoardingRequired(currentAppVersionCode: Int) { if (!onBoardingRequired && currentAppVersionCode > previousVersionCode && - !BuildConfig.FLAG_NEW_ON_BOARDING_SCREEN.equals(previousOnBoardingScreenName, ignoreCase = true) + !BuildConfig.FLAG_NEW_ON_BOARDING_SCREEN.equals( + previousOnBoardingScreenName, + ignoreCase = true + ) ) { isReOnBoardingRequired = true } } + + fun checkIfAppUpdated(currentAppVersionCode: Int): Boolean { + return currentAppVersionCode > previousVersionCode + } } diff --git a/repository/src/main/java/ca/bc/gov/repository/bcsc/BcscAuthRepo.kt b/repository/src/main/java/ca/bc/gov/repository/bcsc/BcscAuthRepo.kt index 6c16047c9..c0f85d380 100644 --- a/repository/src/main/java/ca/bc/gov/repository/bcsc/BcscAuthRepo.kt +++ b/repository/src/main/java/ca/bc/gov/repository/bcsc/BcscAuthRepo.kt @@ -9,8 +9,11 @@ import ca.bc.gov.common.const.AUTH_ERROR_DO_LOGIN import ca.bc.gov.common.const.MUST_CALL_MOBILE_CONFIG import ca.bc.gov.common.exceptions.MyHealthException import ca.bc.gov.common.model.AuthParametersDto +import ca.bc.gov.common.model.UserAuthenticationStatus import ca.bc.gov.data.datasource.local.PatientLocalDataSource import ca.bc.gov.preference.EncryptedPreferenceStorage +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow import net.openid.appauth.AuthState import net.openid.appauth.AuthorizationException import net.openid.appauth.AuthorizationRequest @@ -39,6 +42,22 @@ class BcscAuthRepo( private lateinit var authService: AuthorizationService private lateinit var authServiceConfiguration: AuthorizationServiceConfiguration + val userAuthenticationStatus: Flow = flow { + val authString = encryptedPreferenceStorage.authState + authString?.let { + val authState = AuthState.jsonDeserialize(it) + if (authState.isAuthorized) { + if (authState.needsTokenRefresh) { + emit(UserAuthenticationStatus.SESSION_TIME_OUT) + } else { + emit(UserAuthenticationStatus.AUTHENTICATED) + } + } else { + emit(UserAuthenticationStatus.UN_AUTHENTICATED) + } + } ?: emit(UserAuthenticationStatus.UN_AUTHENTICATED) + } + private fun setAuthState(authState: AuthState?) { if (authState != null) { authState.lastTokenResponse?.additionalParameters?.get("refresh_expires_in")?.toLong() diff --git a/repository/src/main/java/ca/bc/gov/repository/di/RepositoriesModule.kt b/repository/src/main/java/ca/bc/gov/repository/di/RepositoriesModule.kt index 9233bf6db..070ef5ca6 100644 --- a/repository/src/main/java/ca/bc/gov/repository/di/RepositoriesModule.kt +++ b/repository/src/main/java/ca/bc/gov/repository/di/RepositoriesModule.kt @@ -1,6 +1,7 @@ package ca.bc.gov.repository.di import android.content.Context +import ca.bc.gov.data.datasource.local.AppFeatureLocalDataSource import ca.bc.gov.data.datasource.local.CommentLocalDataSource import ca.bc.gov.data.datasource.local.CovidOrderLocalDataSource import ca.bc.gov.data.datasource.local.CovidTestLocalDataSource @@ -15,6 +16,7 @@ import ca.bc.gov.data.datasource.local.MedicationRecordLocalDataSource import ca.bc.gov.data.datasource.local.NotificationLocalDataSource import ca.bc.gov.data.datasource.local.OrganDonorLocalDataSource import ca.bc.gov.data.datasource.local.PatientLocalDataSource +import ca.bc.gov.data.datasource.local.QuickActionTileLocalDataSource import ca.bc.gov.data.datasource.local.VaccineRecordLocalDataSource import ca.bc.gov.data.datasource.remote.BannerRemoteDataSource import ca.bc.gov.data.datasource.remote.CommentRemoteDataSource @@ -56,6 +58,9 @@ import ca.bc.gov.repository.scanner.QrScanner import ca.bc.gov.repository.services.DiagnosticImagingRepository import ca.bc.gov.repository.services.OrganDonorRepository import ca.bc.gov.repository.services.PatientServicesRepository +import ca.bc.gov.repository.settings.AppFeatureRepository +import ca.bc.gov.repository.settings.AppFeatureWithQuickAccessTilesRepository +import ca.bc.gov.repository.settings.QuickAccessTileRepository import ca.bc.gov.repository.testrecord.CovidOrderRepository import ca.bc.gov.repository.testrecord.CovidTestRepository import ca.bc.gov.repository.utils.Base64ToInputImageConverter @@ -348,4 +353,24 @@ class RepositoriesModule { fun provideDiagnosticImagingRepository( localDataSource: DiagnosticImagingLocalDataSource ): DiagnosticImagingRepository = DiagnosticImagingRepository(localDataSource) + + @Provides + @Singleton + fun provideAppFeatureRepository( + appFeatureLocalDataSource: AppFeatureLocalDataSource + ): AppFeatureRepository = AppFeatureRepository(appFeatureLocalDataSource) + + @Provides + @Singleton + fun providesQuickAccessTileRepository( + quickActionTileLocalDataSource: QuickActionTileLocalDataSource + ): QuickAccessTileRepository = QuickAccessTileRepository(quickActionTileLocalDataSource) + + @Provides + @Singleton + fun providesAppFeatureWithQuickAccessTilesRepository( + appFeatureRepository: AppFeatureRepository, + preferenceStorage: EncryptedPreferenceStorage + ): AppFeatureWithQuickAccessTilesRepository = + AppFeatureWithQuickAccessTilesRepository(appFeatureRepository, preferenceStorage) } diff --git a/repository/src/main/java/ca/bc/gov/repository/immunization/ImmunizationRecommendationRepository.kt b/repository/src/main/java/ca/bc/gov/repository/immunization/ImmunizationRecommendationRepository.kt index ff04c4393..5c1523363 100644 --- a/repository/src/main/java/ca/bc/gov/repository/immunization/ImmunizationRecommendationRepository.kt +++ b/repository/src/main/java/ca/bc/gov/repository/immunization/ImmunizationRecommendationRepository.kt @@ -12,6 +12,8 @@ class ImmunizationRecommendationRepository @Inject constructor( fun getAllRecommendations(): Flow> = localDataSource.getAllRecommendations() + suspend fun hasRecommendations() = localDataSource.hasRecommendations() + suspend fun insert(immunizationRecommendation: ImmunizationRecommendationsDto): Long = localDataSource.insert(immunizationRecommendation) diff --git a/repository/src/main/java/ca/bc/gov/repository/patient/PatientRepository.kt b/repository/src/main/java/ca/bc/gov/repository/patient/PatientRepository.kt index 73e058349..f7939a423 100644 --- a/repository/src/main/java/ca/bc/gov/repository/patient/PatientRepository.kt +++ b/repository/src/main/java/ca/bc/gov/repository/patient/PatientRepository.kt @@ -116,6 +116,14 @@ class PatientRepository @Inject constructor( patientLocalDataSource.getPatientWithData(patientId) ?: throw getNoRecordFoundException(patientId) + suspend fun getPatientWithImmunizationRecommendations(patientId: Long) = + patientLocalDataSource.getPatientWithImmunizationRecommendations(patientId) + ?: throw getNoRecordFoundException(patientId) + + suspend fun getPatientWithDependents(patientId: Long) = + patientLocalDataSource.getPatientWithDependents(patientId) + ?: throw getNoRecordFoundException(patientId) + private fun getNoRecordFoundException(patientId: Long) = MyHealthException( DATABASE_ERROR, "No record found for patient id= $patientId" ) diff --git a/repository/src/main/java/ca/bc/gov/repository/settings/AppFeatureRepository.kt b/repository/src/main/java/ca/bc/gov/repository/settings/AppFeatureRepository.kt new file mode 100644 index 000000000..5d3ea947a --- /dev/null +++ b/repository/src/main/java/ca/bc/gov/repository/settings/AppFeatureRepository.kt @@ -0,0 +1,16 @@ +package ca.bc.gov.repository.settings + +import ca.bc.gov.common.model.settings.AppFeatureDto +import ca.bc.gov.data.datasource.local.AppFeatureLocalDataSource +import javax.inject.Inject + +class AppFeatureRepository @Inject constructor( + private val appFeatureLocalDataSource: AppFeatureLocalDataSource +) { + + suspend fun insert(appFeatureDto: AppFeatureDto): Long { + return appFeatureLocalDataSource.insert(appFeatureDto) + } + + suspend fun getAppFeaturesWithQuickAccessTiles() = appFeatureLocalDataSource.getAppFeaturesWithQuickAccessTiles() +} diff --git a/repository/src/main/java/ca/bc/gov/repository/settings/AppFeatureWithQuickAccessTilesRepository.kt b/repository/src/main/java/ca/bc/gov/repository/settings/AppFeatureWithQuickAccessTilesRepository.kt new file mode 100644 index 000000000..ab92a10fd --- /dev/null +++ b/repository/src/main/java/ca/bc/gov/repository/settings/AppFeatureWithQuickAccessTilesRepository.kt @@ -0,0 +1,18 @@ +package ca.bc.gov.repository.settings + +import ca.bc.gov.preference.EncryptedPreferenceStorage +import javax.inject.Inject + +class AppFeatureWithQuickAccessTilesRepository @Inject constructor( + private val appFeatureRepository: AppFeatureRepository, + private val preferenceStorage: EncryptedPreferenceStorage +) { + + var isQuickAccessTileTutorialRequired: Boolean + get() = preferenceStorage.isQuickAccessTileTutorialRequired + set(value) { + preferenceStorage.isQuickAccessTileTutorialRequired = value + } + + suspend fun getAppFeaturesWithQuickAccessTiles() = appFeatureRepository.getAppFeaturesWithQuickAccessTiles() +} diff --git a/repository/src/main/java/ca/bc/gov/repository/settings/QuickAccessTileRepository.kt b/repository/src/main/java/ca/bc/gov/repository/settings/QuickAccessTileRepository.kt new file mode 100644 index 000000000..9a72c23a4 --- /dev/null +++ b/repository/src/main/java/ca/bc/gov/repository/settings/QuickAccessTileRepository.kt @@ -0,0 +1,26 @@ +package ca.bc.gov.repository.settings + +import ca.bc.gov.common.model.QuickAccessTileShowAsQuickLinkDto +import ca.bc.gov.common.model.settings.QuickAccessTileDto +import ca.bc.gov.data.datasource.local.QuickActionTileLocalDataSource +import javax.inject.Inject + +class QuickAccessTileRepository @Inject constructor( + private val quickActionTileLocalDataSource: QuickActionTileLocalDataSource +) { + + suspend fun insert(quickAccessTileDto: QuickAccessTileDto) = + quickActionTileLocalDataSource.insert(quickAccessTileDto) + + suspend fun insertAll(tiles: List) = + quickActionTileLocalDataSource.insertAll(tiles) + + suspend fun updateAll(tiles: List) = + quickActionTileLocalDataSource.updateAll(tiles) + + suspend fun update(tile: QuickAccessTileShowAsQuickLinkDto) = + quickActionTileLocalDataSource.update(tile) + + suspend fun update(showAsQuickAccess: Boolean) = + quickActionTileLocalDataSource.update(showAsQuickAccess) +} diff --git a/scripts/versions.gradle b/scripts/versions.gradle index 5dd9abc7e..b00f9cbfd 100644 --- a/scripts/versions.gradle +++ b/scripts/versions.gradle @@ -6,32 +6,33 @@ versions.targetSdkVersion = 33 versions.compileSdkVersion = 33 //App -versions.versionName = '1.10.0' -versions.versionCode = 209 +versions.versionName = '2.0.0' +versions.versionCode = 210 versions.localApiVersion = 2 //Tools & Libs versions.kotlin_gradle_plugin = "1.8.20" -versions.gradle = '7.4.2' +versions.gradle = '8.1.0' versions.annotation = '1.6.0' versions.appcompat = '1.6.1' -versions.core_ktx = "1.10.0" +versions.core_ktx = "1.10.1" versions.constraint_layout = "2.1.4" versions.constraint_compose = "1.0.1" versions.coroutines = "1.6.4" versions.swipe_refresh_layout = "1.1.0" versions.room = "2.5.1" -versions.material = "1.8.0" +versions.material = "1.9.0" versions.recycler = "1.3.0" versions.coordinator = "1.2.0" +versions.constraint_compose = "1.0.1" -versions.navigation = "2.5.3" +versions.navigation = "2.6.0" versions.lifecycle = "2.6.1" versions.compose_compiler = "1.4.6" -versions.compose_bom = "2023.04.01" +versions.compose_bom = "2023.06.01" versions.hilt = "2.45"