diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/dashboard/orders/DashboardOrdersCard.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/dashboard/orders/DashboardOrdersCard.kt index ec0a06ca709..55d64facefd 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/dashboard/orders/DashboardOrdersCard.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/dashboard/orders/DashboardOrdersCard.kt @@ -14,6 +14,7 @@ import androidx.compose.runtime.livedata.observeAsState import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.res.colorResource +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @@ -28,6 +29,7 @@ import com.woocommerce.android.ui.compose.animations.SkeletonView import com.woocommerce.android.ui.compose.component.WCTag import com.woocommerce.android.ui.compose.rememberNavController import com.woocommerce.android.ui.compose.viewModelWithFactory +import com.woocommerce.android.ui.dashboard.DashboardFilterableCardHeader import com.woocommerce.android.ui.dashboard.DashboardViewModel import com.woocommerce.android.ui.dashboard.WidgetCard import com.woocommerce.android.ui.dashboard.WidgetError @@ -36,6 +38,7 @@ import com.woocommerce.android.ui.dashboard.orders.DashboardOrdersViewModel.View import com.woocommerce.android.ui.dashboard.orders.DashboardOrdersViewModel.ViewState.Error import com.woocommerce.android.ui.dashboard.orders.DashboardOrdersViewModel.ViewState.Loading import com.woocommerce.android.ui.dashboard.orders.DashboardOrdersViewModel.ViewState.OrderItem +import com.woocommerce.android.ui.orders.filters.data.OrderStatusOption import com.woocommerce.android.viewmodel.MultiLiveEvent.Event @Composable @@ -57,6 +60,9 @@ fun DashboardOrdersCard( when (state) { is Content -> { TopOrders( + selectedFilter = state.selectedFilter, + filterOptions = state.filterOptions, + onFilterSelected = viewModel::onFilterSelected, orders = state.orders ) } @@ -103,11 +109,39 @@ private fun HandleEvents( } } +@Composable +private fun Header( + selectedFilter: OrderStatusOption, + filterOptions: List, + onFilterSelected: (OrderStatusOption) -> Unit, + modifier: Modifier = Modifier +) { + Column(modifier) { + DashboardFilterableCardHeader( + title = stringResource(id = R.string.dashboard_reviews_card_header_title), + currentFilter = selectedFilter, + filterList = filterOptions, + onFilterSelected = onFilterSelected, + mapper = { it.label } + ) + + Divider() + } +} + @Composable fun TopOrders( + selectedFilter: OrderStatusOption, + filterOptions: List, + onFilterSelected: (OrderStatusOption) -> Unit, orders: List ) { Column { + Header( + selectedFilter = selectedFilter, + filterOptions = filterOptions, + onFilterSelected = onFilterSelected + ) orders.forEach { order -> OrderListItem(order) @@ -288,6 +322,27 @@ fun PreviewTopOrders() { totalPrice = "$200.00" ) ), + selectedFilter = OrderStatusOption( + key = "processing", + label = "Processing", + statusCount = 1, + isSelected = true + ), + filterOptions = listOf( + OrderStatusOption( + key = "processing", + label = "Processing", + statusCount = 1, + isSelected = true + ), + OrderStatusOption( + key = "completed", + label = "Completed", + statusCount = 1, + isSelected = false + ) + ), + onFilterSelected = {} ) } diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/dashboard/orders/DashboardOrdersViewModel.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/dashboard/orders/DashboardOrdersViewModel.kt index 09e19aafc55..e97941860da 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/dashboard/orders/DashboardOrdersViewModel.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/dashboard/orders/DashboardOrdersViewModel.kt @@ -9,12 +9,10 @@ import com.woocommerce.android.R import com.woocommerce.android.analytics.AnalyticsEvent import com.woocommerce.android.analytics.AnalyticsTracker import com.woocommerce.android.analytics.AnalyticsTrackerWrapper -import com.woocommerce.android.extensions.capitalize import com.woocommerce.android.extensions.formatToMMMdd import com.woocommerce.android.model.DashboardWidget import com.woocommerce.android.model.DashboardWidget.Type.ORDERS import com.woocommerce.android.model.Order -import com.woocommerce.android.model.toOrderStatus import com.woocommerce.android.ui.dashboard.DashboardViewModel import com.woocommerce.android.ui.dashboard.DashboardViewModel.DashboardWidgetAction import com.woocommerce.android.ui.dashboard.DashboardViewModel.DashboardWidgetMenu @@ -22,25 +20,31 @@ import com.woocommerce.android.ui.dashboard.DashboardViewModel.RefreshEvent import com.woocommerce.android.ui.dashboard.defaultHideMenuEntry import com.woocommerce.android.ui.dashboard.orders.DashboardOrdersViewModel.Factory import com.woocommerce.android.ui.dashboard.orders.DashboardOrdersViewModel.ViewState.Content +import com.woocommerce.android.ui.orders.filters.data.OrderStatusOption +import com.woocommerce.android.ui.orders.filters.domain.GetOrderStatusFilterOptions import com.woocommerce.android.ui.orders.list.OrderListRepository import com.woocommerce.android.util.CurrencyFormatter import com.woocommerce.android.viewmodel.MultiLiveEvent import com.woocommerce.android.viewmodel.ResourceProvider import com.woocommerce.android.viewmodel.ScopedViewModel +import com.woocommerce.android.viewmodel.getStateFlow import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.emitAll +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.merge import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.transformLatest import kotlinx.coroutines.launch -import java.util.Locale +@Suppress("LongParameterList") @HiltViewModel(assistedFactory = Factory::class) class DashboardOrdersViewModel @AssistedInject constructor( savedStateHandle: SavedStateHandle, @@ -48,14 +52,26 @@ class DashboardOrdersViewModel @AssistedInject constructor( private val orderListRepository: OrderListRepository, private val analyticsTrackerWrapper: AnalyticsTrackerWrapper, private val currencyFormatter: CurrencyFormatter, - private val resourceProvider: ResourceProvider + private val resourceProvider: ResourceProvider, + private val getOrderStatusFilterOptions: GetOrderStatusFilterOptions ) : ScopedViewModel(savedStateHandle) { companion object { const val MAX_NUMBER_OF_ORDERS_TO_DISPLAY_IN_CARD = 3 + const val DEFAULT_FILTER_OPTION_STATUS = "all" } - private val orderStatusMap = MutableSharedFlow>(replay = 1) - private val refreshTrigger = MutableSharedFlow(extraBufferCapacity = 1) + private val defaultFilterOption = OrderStatusOption( + key = DEFAULT_FILTER_OPTION_STATUS, + label = resourceProvider.getString(R.string.orderfilters_default_filter_value), + statusCount = 0, + isSelected = true + ) + + private val statusOptions = MutableStateFlow(listOf(defaultFilterOption)) + private val _refreshTrigger = MutableSharedFlow(extraBufferCapacity = 1) + private val refreshTrigger = merge(parentViewModel.refreshTrigger, _refreshTrigger) + .onStart { emit(RefreshEvent()) } + private val selectedFilter = savedStateHandle.getStateFlow(viewModelScope, DEFAULT_FILTER_OPTION_STATUS) val menu = DashboardWidgetMenu( items = listOf( @@ -71,49 +87,61 @@ class DashboardOrdersViewModel @AssistedInject constructor( ) @OptIn(ExperimentalCoroutinesApi::class) - val viewState = merge(parentViewModel.refreshTrigger, refreshTrigger) - .onStart { emit(RefreshEvent()) } - .transformLatest { + val viewState = selectedFilter.flatMapLatest { status -> + refreshTrigger.map { Pair(status, it) } + }.transformLatest { (filterStatus, refresh) -> + val statusFilter = filterStatus + .takeIf { it != DEFAULT_FILTER_OPTION_STATUS } + ?.let { Order.Status.fromValue(it) } + val hasOrders = orderListRepository.hasOrdersLocally(statusFilter) + if (refresh.isForced || !hasOrders) { emit(ViewState.Loading) - emitAll( + } + emitAll( + combine( orderListRepository.observeTopOrders( count = MAX_NUMBER_OF_ORDERS_TO_DISPLAY_IN_CARD, - isForced = it.isForced - ).combine(orderStatusMap) { result, statusMap -> - result.fold( - onSuccess = { orders -> - Content( - orders.map { order -> - val status = statusMap[order.status.value]?.label - ?: order.status.value.capitalize(Locale.getDefault()) - - ViewState.OrderItem( - number = "#${order.number}", - date = order.dateCreated.formatToMMMdd(), - customerName = order.billingName.ifEmpty { - resourceProvider.getString(R.string.orderdetail_customer_name_default) - }, - status = status, - statusColor = order.status.color, - totalPrice = currencyFormatter.formatCurrency(order.total, order.currency) - ) - } - ) - }, - onFailure = { error -> - ViewState.Error(error.message ?: "") - } - ) - } - ) - }.asLiveData() + isForced = refresh.isForced, + statusFilter = statusFilter + ), + statusOptions + ) { result, statusOptions -> + result.fold( + onSuccess = { orders -> + Content( + orders = orders.map { order -> + val status = statusOptions + .first { option -> option.key == order.status.value }.label + + ViewState.OrderItem( + number = "#${order.number}", + date = order.dateCreated.formatToMMMdd(), + customerName = order.billingName.ifEmpty { + resourceProvider.getString(R.string.orderdetail_customer_name_default) + }, + status = status, + statusColor = order.status.color, + totalPrice = currencyFormatter.formatCurrency(order.total, order.currency) + ) + }, + filterOptions = statusOptions, + selectedFilter = statusOptions.first { it.key == filterStatus } + ) + }, + onFailure = { error -> + ViewState.Error(error.message ?: "") + } + ) + } + ) + }.asLiveData() init { viewModelScope.launch { - orderStatusMap.tryEmit( - orderListRepository.getCachedOrderStatusOptions().mapValues { (_, value) -> - value.toOrderStatus() - } + statusOptions.tryEmit( + getOrderStatusFilterOptions() + .toMutableList() + .apply { add(0, defaultFilterOption) } ) } } @@ -140,13 +168,21 @@ class DashboardOrdersViewModel @AssistedInject constructor( AnalyticsTracker.KEY_TYPE to DashboardWidget.Type.ORDERS.trackingIdentifier ) ) - refreshTrigger.tryEmit(RefreshEvent(isForced = true)) + _refreshTrigger.tryEmit(RefreshEvent(isForced = true)) + } + + fun onFilterSelected(filter: OrderStatusOption) { + selectedFilter.value = filter.key } sealed class ViewState { data object Loading : ViewState() data class Error(val message: String) : ViewState() - data class Content(val orders: List) : ViewState() + data class Content( + val orders: List, + val filterOptions: List, + val selectedFilter: OrderStatusOption + ) : ViewState() @StringRes val title: Int = ORDERS.titleResource diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/list/OrderListRepository.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/list/OrderListRepository.kt index b3bb495c90b..b9d07a8b4b7 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/list/OrderListRepository.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/list/OrderListRepository.kt @@ -78,7 +78,7 @@ class OrderListRepository @Inject constructor( if (selectedSite.exists()) { val statusOptions = orderStore.getOrderStatusOptionsForSite(selectedSite.get()) if (statusOptions.isNotEmpty()) { - statusOptions.map { it.statusKey to it }.toMap() + statusOptions.associateBy { it.statusKey } } else { emptyMap() } @@ -126,6 +126,10 @@ class OrderListRepository @Inject constructor( } } + suspend fun hasOrdersLocally(statusFilter: Order.Status? = null) = + orderStore.getOrdersForSite(selectedSite.get()) + .any { statusFilter == null || it.status == statusFilter.value } + fun observeTopOrders(count: Int, isForced: Boolean, statusFilter: Order.Status? = null) = flow { if (!isForced) { orderStore.getOrdersForSite(selectedSite.get())