From 0032f144ac88147c4758f5b81c703c4110f13150 Mon Sep 17 00:00:00 2001 From: Hicham Boushaba Date: Fri, 17 May 2024 16:44:02 +0100 Subject: [PATCH 01/11] Update UI model with needed properties --- .../coupons/DashboardCouponsViewModel.kt | 26 +++++++++++++------ 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/dashboard/coupons/DashboardCouponsViewModel.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/dashboard/coupons/DashboardCouponsViewModel.kt index 3207abcad42..5f1f8636f11 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/dashboard/coupons/DashboardCouponsViewModel.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/dashboard/coupons/DashboardCouponsViewModel.kt @@ -5,7 +5,6 @@ import androidx.lifecycle.asLiveData import androidx.lifecycle.viewModelScope import com.woocommerce.android.AppPrefsWrapper import com.woocommerce.android.WooException -import com.woocommerce.android.model.Coupon import com.woocommerce.android.model.CouponPerformanceReport import com.woocommerce.android.ui.analytics.ranges.StatsTimeRange import com.woocommerce.android.ui.analytics.ranges.StatsTimeRangeSelection @@ -15,6 +14,9 @@ import com.woocommerce.android.ui.dashboard.DashboardViewModel import com.woocommerce.android.ui.dashboard.DashboardViewModel.RefreshEvent import com.woocommerce.android.ui.dashboard.data.CouponsCustomDateRangeDataStore import com.woocommerce.android.ui.dashboard.domain.DashboardDateRangeFormatter +import com.woocommerce.android.ui.products.ParameterRepository +import com.woocommerce.android.util.CoroutineDispatchers +import com.woocommerce.android.util.CouponUtils import com.woocommerce.android.util.WooLog import com.woocommerce.android.viewmodel.MultiLiveEvent import com.woocommerce.android.viewmodel.ScopedViewModel @@ -23,6 +25,7 @@ import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.async import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.SharingStarted @@ -51,6 +54,9 @@ class DashboardCouponsViewModel @AssistedInject constructor( private val customDateRangeDataStore: CouponsCustomDateRangeDataStore, private val dateRangeFormatter: DashboardDateRangeFormatter, private val appPrefs: AppPrefsWrapper, + private val couponUtils: CouponUtils, + private val parameterRepository: ParameterRepository, + private val coroutineDispatchers: CoroutineDispatchers ) : ScopedViewModel(savedStateHandle) { companion object { private const val COUPONS_LIMIT = 3 @@ -64,6 +70,10 @@ class DashboardCouponsViewModel @AssistedInject constructor( private val selectedDateRange = getSelectedRange() .shareIn(viewModelScope, started = SharingStarted.WhileSubscribed(), replay = 1) + private val currencyCodeTask = async(coroutineDispatchers.io) { + parameterRepository.getParameters().currencyCode + } + val dateRangeState = combine( selectedDateRange, customDateRangeDataStore.dateRange @@ -152,8 +162,9 @@ class DashboardCouponsViewModel @AssistedInject constructor( ?: error("Coupon not found for id: ${performanceReport.couponId}") CouponUiModel( - coupon = coupon, - performanceReport = performanceReport + code = coupon.code.orEmpty(), + uses = performanceReport.ordersCount, + description = couponUtils.generateSummary(coupon, currencyCodeTask.await()) ) } @@ -232,11 +243,10 @@ class DashboardCouponsViewModel @AssistedInject constructor( ) data class CouponUiModel( - private val coupon: Coupon, - private val performanceReport: CouponPerformanceReport - ) { - val code: String = coupon.code.orEmpty() - } + val code: String, + val uses: Int, + val description: String + ) data class OpenDatePicker(val fromDate: Date, val toDate: Date) : MultiLiveEvent.Event() From 53bf4b18a7e3fd8c0b69a3047520e281cddefa2e Mon Sep 17 00:00:00 2001 From: Hicham Boushaba Date: Fri, 17 May 2024 17:11:18 +0100 Subject: [PATCH 02/11] Add a loading UI --- .../dashboard/coupons/DashboardCouponsCard.kt | 47 ++++++++++++++++++- 1 file changed, 45 insertions(+), 2 deletions(-) diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/dashboard/coupons/DashboardCouponsCard.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/dashboard/coupons/DashboardCouponsCard.kt index 415f59b616d..75b3f320871 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/dashboard/coupons/DashboardCouponsCard.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/dashboard/coupons/DashboardCouponsCard.kt @@ -1,8 +1,11 @@ package com.woocommerce.android.ui.dashboard.coupons +import androidx.compose.foundation.layout.Arrangement 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.material.CircularProgressIndicator +import androidx.compose.foundation.layout.padding import androidx.compose.material.Divider import androidx.compose.material.Text import androidx.compose.runtime.Composable @@ -10,11 +13,13 @@ import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.livedata.observeAsState import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalLifecycleOwner +import androidx.compose.ui.unit.dp import androidx.lifecycle.LiveData import androidx.lifecycle.Observer import com.woocommerce.android.model.DashboardWidget.Type.COUPONS import com.woocommerce.android.ui.analytics.ranges.StatsTimeRange import com.woocommerce.android.ui.analytics.ranges.StatsTimeRangeSelection.SelectionType +import com.woocommerce.android.ui.compose.animations.SkeletonView import com.woocommerce.android.ui.compose.rememberNavController import com.woocommerce.android.ui.compose.viewModelWithFactory import com.woocommerce.android.ui.dashboard.DashboardDateRangeHeader @@ -113,7 +118,7 @@ private fun DashboardCouponsCard( when (viewState) { is Loading -> { - CircularProgressIndicator() + CouponsLoading() } is Loaded -> { @@ -143,3 +148,41 @@ private fun DashboardCouponsList( } } } + +@Composable +private fun CouponsLoading( + modifier: Modifier = Modifier +) { + Column(modifier.padding(horizontal = 16.dp, vertical = 8.dp)) { + Header(Modifier.fillMaxWidth()) + Spacer(modifier = Modifier.height(8.dp)) + repeat(3) { + Column( + verticalArrangement = Arrangement.spacedBy(4.dp), + modifier = Modifier.padding(top = 8.dp) + ) { + Row( + horizontalArrangement = Arrangement.SpaceBetween, + modifier = Modifier.fillMaxWidth() + ) { + SkeletonView(width = 260.dp, height = 16.dp) + SkeletonView(width = 40.dp, height = 16.dp) + } + SkeletonView(width = 120.dp, height = 16.dp) + Spacer(modifier = Modifier) + Divider() + } + } + } +} + +@Composable +private fun Header(modifier: Modifier = Modifier) { + Row( + horizontalArrangement = Arrangement.SpaceBetween, + modifier = modifier + ) { + Text(text = "Coupons") + Text(text = "Uses") + } +} From 6a829acfefad9592c70acb796208a17cd7f26ccf Mon Sep 17 00:00:00 2001 From: Hicham Boushaba Date: Fri, 17 May 2024 17:58:43 +0100 Subject: [PATCH 03/11] Add UI for the coupon items --- .../dashboard/coupons/DashboardCouponsCard.kt | 60 +++++++++++++++++-- WooCommerce/src/main/res/values/strings.xml | 4 ++ 2 files changed, 58 insertions(+), 6 deletions(-) diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/dashboard/coupons/DashboardCouponsCard.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/dashboard/coupons/DashboardCouponsCard.kt index 75b3f320871..45d2bfb8d0b 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/dashboard/coupons/DashboardCouponsCard.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/dashboard/coupons/DashboardCouponsCard.kt @@ -5,17 +5,22 @@ 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.height import androidx.compose.foundation.layout.padding +import androidx.compose.material.ContentAlpha import androidx.compose.material.Divider +import androidx.compose.material.MaterialTheme import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.livedata.observeAsState import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalLifecycleOwner +import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.lifecycle.LiveData import androidx.lifecycle.Observer +import com.woocommerce.android.R import com.woocommerce.android.model.DashboardWidget.Type.COUPONS import com.woocommerce.android.ui.analytics.ranges.StatsTimeRange import com.woocommerce.android.ui.analytics.ranges.StatsTimeRangeSelection.SelectionType @@ -141,11 +146,46 @@ private fun DashboardCouponsList( state: Loaded, modifier: Modifier = Modifier ) { - Column(modifier) { - state.coupons.forEach { - Text(text = it.code) - Divider() + Column(modifier.padding(horizontal = 16.dp, vertical = 8.dp)) { + Header(Modifier.fillMaxWidth()) + Spacer(modifier = Modifier.height(8.dp)) + state.coupons.forEach { couponUiModel -> + CouponListItem( + couponUiModel = couponUiModel + ) + } + } +} + +@Composable +private fun CouponListItem( + couponUiModel: DashboardCouponsViewModel.CouponUiModel, + modifier: Modifier = Modifier +) { + Column( + verticalArrangement = Arrangement.spacedBy(4.dp), + modifier = modifier.padding(top = 8.dp) + ) { + Row( + horizontalArrangement = Arrangement.SpaceBetween, + modifier = Modifier.fillMaxWidth() + ) { + Text( + text = couponUiModel.code, + style = MaterialTheme.typography.subtitle1 + ) + Text( + text = couponUiModel.uses.toString(), + style = MaterialTheme.typography.subtitle1 + ) } + Text( + text = couponUiModel.description, + style = MaterialTheme.typography.body2, + color = MaterialTheme.colors.onSurface.copy(alpha = ContentAlpha.medium) + ) + Spacer(modifier = Modifier) + Divider() } } @@ -182,7 +222,15 @@ private fun Header(modifier: Modifier = Modifier) { horizontalArrangement = Arrangement.SpaceBetween, modifier = modifier ) { - Text(text = "Coupons") - Text(text = "Uses") + Text( + text = stringResource(id = R.string.dashboard_coupons_card_header_coupons), + style = MaterialTheme.typography.subtitle2, + color = MaterialTheme.colors.onSurface.copy(alpha = ContentAlpha.medium) + ) + Text( + text = stringResource(id = R.string.dashboard_coupons_card_header_uses), + style = MaterialTheme.typography.subtitle2, + color = MaterialTheme.colors.onSurface.copy(alpha = ContentAlpha.medium) + ) } } diff --git a/WooCommerce/src/main/res/values/strings.xml b/WooCommerce/src/main/res/values/strings.xml index a180102b550..a96d62c28b3 100644 --- a/WooCommerce/src/main/res/values/strings.xml +++ b/WooCommerce/src/main/res/values/strings.xml @@ -426,6 +426,10 @@ No reviews found No reviews match the selected filter, please try changing the filter View all reviews + + Coupons + Uses + From f0307ae3d51649d1e79681ea335ac0d5796d1160 Mon Sep 17 00:00:00 2001 From: Hicham Boushaba Date: Fri, 17 May 2024 18:48:25 +0100 Subject: [PATCH 04/11] Handle navigation to coupons and add menu --- .../dashboard/coupons/DashboardCouponsCard.kt | 27 ++++++++++++++++++- .../coupons/DashboardCouponsViewModel.kt | 5 ++++ .../main/res/navigation/nav_graph_main.xml | 3 +++ WooCommerce/src/main/res/values/strings.xml | 1 + 4 files changed, 35 insertions(+), 1 deletion(-) diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/dashboard/coupons/DashboardCouponsCard.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/dashboard/coupons/DashboardCouponsCard.kt index 45d2bfb8d0b..ebd0e3d1b3d 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/dashboard/coupons/DashboardCouponsCard.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/dashboard/coupons/DashboardCouponsCard.kt @@ -21,6 +21,8 @@ import androidx.compose.ui.unit.dp import androidx.lifecycle.LiveData import androidx.lifecycle.Observer import com.woocommerce.android.R +import com.woocommerce.android.extensions.navigateSafely +import com.woocommerce.android.model.DashboardWidget import com.woocommerce.android.model.DashboardWidget.Type.COUPONS import com.woocommerce.android.ui.analytics.ranges.StatsTimeRange import com.woocommerce.android.ui.analytics.ranges.StatsTimeRangeSelection.SelectionType @@ -28,6 +30,7 @@ import com.woocommerce.android.ui.compose.animations.SkeletonView import com.woocommerce.android.ui.compose.rememberNavController import com.woocommerce.android.ui.compose.viewModelWithFactory import com.woocommerce.android.ui.dashboard.DashboardDateRangeHeader +import com.woocommerce.android.ui.dashboard.DashboardFragmentDirections import com.woocommerce.android.ui.dashboard.DashboardViewModel import com.woocommerce.android.ui.dashboard.DashboardViewModel.DashboardWidgetMenu import com.woocommerce.android.ui.dashboard.WidgetCard @@ -37,6 +40,8 @@ import com.woocommerce.android.ui.dashboard.coupons.DashboardCouponsViewModel.St import com.woocommerce.android.ui.dashboard.coupons.DashboardCouponsViewModel.State.Error import com.woocommerce.android.ui.dashboard.coupons.DashboardCouponsViewModel.State.Loaded import com.woocommerce.android.ui.dashboard.coupons.DashboardCouponsViewModel.State.Loading +import com.woocommerce.android.ui.dashboard.coupons.DashboardCouponsViewModel.ViewAllCoupons +import com.woocommerce.android.ui.dashboard.defaultHideMenuEntry import com.woocommerce.android.viewmodel.MultiLiveEvent import java.util.Date @@ -67,6 +72,8 @@ fun DashboardCouponsCard( viewState = viewState, onTabSelected = viewModel::onTabSelected, onCustomRangeClick = viewModel::onEditCustomRangeTapped, + onViewAllClick = viewModel::onViewAllClicked, + onHideClick = { parentViewModel.onHideWidgetClicked(DashboardWidget.Type.COUPONS) }, modifier = modifier ) } @@ -85,6 +92,12 @@ private fun HandleEvents( is DashboardCouponsViewModel.OpenDatePicker -> { openDatePicker(event.fromDate.time, event.toDate.time) } + + ViewAllCoupons -> { + navController.navigateSafely( + DashboardFragmentDirections.actionDashboardToCouponListFragment() + ) + } } } @@ -102,11 +115,23 @@ private fun DashboardCouponsCard( viewState: State, onTabSelected: (SelectionType) -> Unit, onCustomRangeClick: () -> Unit, + onViewAllClick: () -> Unit, + onHideClick: () -> Unit, modifier: Modifier = Modifier ) { WidgetCard( titleResource = COUPONS.titleResource, - menu = DashboardWidgetMenu(emptyList()), + menu = DashboardWidgetMenu( + listOf( + DashboardWidget.Type.COUPONS.defaultHideMenuEntry { + onHideClick() + } + ) + ), + button = DashboardViewModel.DashboardWidgetAction( + titleResource = R.string.dashboard_coupons_view_all_button, + action = onViewAllClick + ), isError = viewState is Error, modifier = modifier ) { diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/dashboard/coupons/DashboardCouponsViewModel.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/dashboard/coupons/DashboardCouponsViewModel.kt index 5f1f8636f11..70d7cc143f8 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/dashboard/coupons/DashboardCouponsViewModel.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/dashboard/coupons/DashboardCouponsViewModel.kt @@ -140,6 +140,10 @@ class DashboardCouponsViewModel @AssistedInject constructor( } } + fun onViewAllClicked() { + triggerEvent(ViewAllCoupons) + } + private fun observeCouponUiModels( dateRange: StatsTimeRange, forceRefresh: Boolean @@ -249,6 +253,7 @@ class DashboardCouponsViewModel @AssistedInject constructor( ) data class OpenDatePicker(val fromDate: Date, val toDate: Date) : MultiLiveEvent.Event() + data object ViewAllCoupons : MultiLiveEvent.Event() @AssistedFactory interface Factory { diff --git a/WooCommerce/src/main/res/navigation/nav_graph_main.xml b/WooCommerce/src/main/res/navigation/nav_graph_main.xml index 1e2c5afec36..18648265266 100644 --- a/WooCommerce/src/main/res/navigation/nav_graph_main.xml +++ b/WooCommerce/src/main/res/navigation/nav_graph_main.xml @@ -90,6 +90,9 @@ + Coupons Uses + View all coupons