diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/model/DashboardWidget.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/model/DashboardWidget.kt index 94ced538fcc..913a11cde69 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/model/DashboardWidget.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/model/DashboardWidget.kt @@ -30,6 +30,7 @@ data class DashboardWidget( POPULAR_PRODUCTS(R.string.my_store_widget_top_products_title, "top_performers"), BLAZE(R.string.my_store_widget_blaze_title, "blaze"), REVIEWS(R.string.my_store_widget_reviews_title, "reviews"), + ORDERS(R.string.my_store_widget_orders_title, "orders"), } sealed interface Status : Parcelable { diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/model/Order.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/model/Order.kt index 0b76ac65b63..495cda870e6 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/model/Order.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/model/Order.kt @@ -66,6 +66,10 @@ data class Order( @IgnoredOnParcel val isRefundAvailable = !isOrderFullyRefunded && quantityOfItemsWhichPossibleToRefund > 0 && isOrderPaid + @IgnoredOnParcel + val billingName + get() = getBillingName("") + val hasMultipleShippingLines: Boolean get() = shippingLines.size > 1 diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/model/OrderMapper.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/model/OrderMapper.kt index 19e7fab520f..e5b2070049e 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/model/OrderMapper.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/model/OrderMapper.kt @@ -22,7 +22,7 @@ import org.wordpress.android.fluxc.model.order.ShippingLine as WCShippingLine class OrderMapper @Inject constructor( private val getLocations: GetLocations, - private val dateUtils: DateUtils + private val dateUtils: DateUtils, ) { fun toAppModel(databaseEntity: OrderEntity): Order { val metaDataList = databaseEntity.getMetaDataList() diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/compose/component/Tag.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/compose/component/Tag.kt index 903caf183e4..afc1c53b392 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/compose/component/Tag.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/compose/component/Tag.kt @@ -16,6 +16,7 @@ import androidx.compose.ui.res.dimensionResource import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp import com.woocommerce.android.R @Composable @@ -24,26 +25,26 @@ fun WCTag( modifier: Modifier = Modifier, textColor: Color = colorResource(id = R.color.tag_text_main), backgroundColor: Color = colorResource(R.color.tag_bg_main), - textStyle: TextStyle = MaterialTheme.typography.caption + textStyle: TextStyle = MaterialTheme.typography.caption, + fontWeight: FontWeight = FontWeight.Bold ) { Box( modifier = modifier - .clip(RoundedCornerShape(percent = 35)) + .clip(RoundedCornerShape(4.dp)) .background(backgroundColor) .padding( - start = dimensionResource(id = R.dimen.minor_50), - end = dimensionResource(id = R.dimen.minor_50), + horizontal = dimensionResource(id = R.dimen.minor_50), ) ) { Text( modifier = Modifier.padding( horizontal = dimensionResource(id = R.dimen.minor_75), - vertical = dimensionResource(id = R.dimen.minor_25) + vertical = dimensionResource(id = R.dimen.minor_50) ), text = text, style = textStyle, color = textColor, - fontWeight = FontWeight.Bold + fontWeight = fontWeight ) } } diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/dashboard/DashboardContainer.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/dashboard/DashboardContainer.kt index 25975bf4e3b..228c9ad8d5e 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/dashboard/DashboardContainer.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/dashboard/DashboardContainer.kt @@ -35,6 +35,7 @@ import com.woocommerce.android.ui.compose.component.WCOutlinedButton import com.woocommerce.android.ui.dashboard.DashboardViewModel.DashboardEvent.OpenRangePicker import com.woocommerce.android.ui.dashboard.blaze.DashboardBlazeCard import com.woocommerce.android.ui.dashboard.onboarding.DashboardOnboardingCard +import com.woocommerce.android.ui.dashboard.orders.DashboardOrdersCard import com.woocommerce.android.ui.dashboard.reviews.DashboardReviewsCard import com.woocommerce.android.ui.dashboard.stats.DashboardStatsCard import com.woocommerce.android.ui.dashboard.topperformers.DashboardTopPerformersWidgetCard @@ -134,6 +135,11 @@ private fun ConfigurableWidgetCard( modifier = modifier ) + DashboardWidget.Type.ORDERS -> DashboardOrdersCard( + parentViewModel = dashboardViewModel, + modifier = modifier + ) + DashboardWidget.Type.REVIEWS -> DashboardReviewsCard( parentViewModel = dashboardViewModel, modifier = modifier diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/dashboard/data/DashboardDataStore.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/dashboard/data/DashboardDataStore.kt index b6a41551f6c..44ebd38311e 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/dashboard/data/DashboardDataStore.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/dashboard/data/DashboardDataStore.kt @@ -88,6 +88,7 @@ class DashboardDataStore @Inject constructor( private val supportedWidgets: List = DashboardWidget.Type.entries .filter { FeatureFlag.DYNAMIC_DASHBOARD_M2.isEnabled() || + it != DashboardWidget.Type.ORDERS && it != DashboardWidget.Type.REVIEWS } } diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/dashboard/data/DashboardRepository.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/dashboard/data/DashboardRepository.kt index d276b28b3de..6bf239e69ae 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/dashboard/data/DashboardRepository.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/dashboard/data/DashboardRepository.kt @@ -19,7 +19,7 @@ import javax.inject.Inject class DashboardRepository @Inject constructor( selectedSite: SelectedSite, private val dashboardDataStore: DashboardDataStore, - observeStatsWidgetsStatus: ObserveStatsWidgetsStatus, + observeSiteOrdersState: ObserveSiteOrdersState, observeBlazeWidgetStatus: ObserveBlazeWidgetStatus, observeOnboardingWidgetStatus: ObserveOnboardingWidgetStatus ) { @@ -28,7 +28,7 @@ class DashboardRepository @Inject constructor( SiteComponentEntryPoint::class.java ).siteCoroutineScope() - private val statsWidgetsStatus = observeStatsWidgetsStatus() + private val siteOrdersState = observeSiteOrdersState() .stateIn( scope = siteCoroutineScope, started = SharingStarted.Lazily, @@ -51,11 +51,11 @@ class DashboardRepository @Inject constructor( val widgets = combine( dashboardDataStore.widgets, - statsWidgetsStatus, + siteOrdersState, blazeWidgetStatus, onboardingWidgetStatus - ) { widgets, statsWidgetStatus, blazeWidgetStatus, onboardingWidgetStatus -> - widgets.toDomainModel(statsWidgetStatus, blazeWidgetStatus, onboardingWidgetStatus) + ) { widgets, siteOrdersState, blazeWidgetStatus, onboardingWidgetStatus -> + widgets.toDomainModel(siteOrdersState, blazeWidgetStatus, onboardingWidgetStatus) } suspend fun updateWidgets(widgets: List) = dashboardDataStore.updateDashboard( @@ -77,7 +77,7 @@ class DashboardRepository @Inject constructor( } private fun List.toDomainModel( - statsWidgetsStatus: DashboardWidget.Status, + siteOrdersState: DashboardWidget.Status, blazeWidgetStatus: DashboardWidget.Status, onboardingWidgetStatus: DashboardWidget.Status ): List { @@ -88,10 +88,9 @@ class DashboardRepository @Inject constructor( isSelected = widget.isAdded, status = when (type) { DashboardWidget.Type.STATS, - DashboardWidget.Type.POPULAR_PRODUCTS -> statsWidgetsStatus - + DashboardWidget.Type.ORDERS, + DashboardWidget.Type.POPULAR_PRODUCTS -> siteOrdersState DashboardWidget.Type.BLAZE -> blazeWidgetStatus - DashboardWidget.Type.ONBOARDING -> onboardingWidgetStatus else -> DashboardWidget.Status.Available diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/dashboard/data/ObserveStatsWidgetsStatus.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/dashboard/data/ObserveSiteOrdersState.kt similarity index 97% rename from WooCommerce/src/main/kotlin/com/woocommerce/android/ui/dashboard/data/ObserveStatsWidgetsStatus.kt rename to WooCommerce/src/main/kotlin/com/woocommerce/android/ui/dashboard/data/ObserveSiteOrdersState.kt index 74c81330b01..46de65908d2 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/dashboard/data/ObserveStatsWidgetsStatus.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/dashboard/data/ObserveSiteOrdersState.kt @@ -16,7 +16,7 @@ import org.wordpress.android.fluxc.store.WCOrderStore.HasOrdersResult import javax.inject.Inject @OptIn(ExperimentalCoroutinesApi::class) -class ObserveStatsWidgetsStatus @Inject constructor( +class ObserveSiteOrdersState @Inject constructor( private val selectedSite: SelectedSite, private val orderStore: WCOrderStore, private val coroutineDispatchers: CoroutineDispatchers 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 new file mode 100644 index 00000000000..ec0a06ca709 --- /dev/null +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/dashboard/orders/DashboardOrdersCard.kt @@ -0,0 +1,298 @@ +package com.woocommerce.android.ui.dashboard.orders + +import androidx.compose.foundation.layout.Column +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.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.colorResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.constraintlayout.compose.ConstraintLayout +import androidx.lifecycle.LiveData +import androidx.lifecycle.Observer +import androidx.navigation.NavGraph.Companion.findStartDestination +import androidx.navigation.navOptions +import com.woocommerce.android.R +import com.woocommerce.android.extensions.navigateSafely +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.DashboardViewModel +import com.woocommerce.android.ui.dashboard.WidgetCard +import com.woocommerce.android.ui.dashboard.WidgetError +import com.woocommerce.android.ui.dashboard.orders.DashboardOrdersViewModel.NavigateToOrders +import com.woocommerce.android.ui.dashboard.orders.DashboardOrdersViewModel.ViewState.Content +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.viewmodel.MultiLiveEvent.Event + +@Composable +fun DashboardOrdersCard( + parentViewModel: DashboardViewModel, + modifier: Modifier = Modifier, + viewModel: DashboardOrdersViewModel = viewModelWithFactory { factory: DashboardOrdersViewModel.Factory -> + factory.create(parentViewModel) + } +) { + viewModel.viewState.observeAsState().value?.let { state -> + WidgetCard( + titleResource = state.title, + menu = viewModel.menu, + button = viewModel.button, + modifier = modifier, + isError = state is Error + ) { + when (state) { + is Content -> { + TopOrders( + orders = state.orders + ) + } + is Error -> WidgetError( + onContactSupportClicked = parentViewModel::onContactSupportClicked, + onRetryClicked = viewModel::onRefresh + ) + is Loading -> Loading() + } + } + } + + HandleEvents(viewModel.event) +} + +@Composable +private fun HandleEvents( + event: LiveData +) { + val navController = rememberNavController() + val lifecycleOwner = LocalLifecycleOwner.current + + DisposableEffect(event, navController, lifecycleOwner) { + val observer = Observer { event: Event -> + when (event) { + is NavigateToOrders -> { + navController.navigateSafely( + resId = R.id.orders, + navOptions = navOptions { + popUpTo(navController.graph.findStartDestination().id) { + saveState = true + } + } + ) + } + } + } + + event.observe(lifecycleOwner, observer) + + onDispose { + event.removeObserver(observer) + } + } +} + +@Composable +fun TopOrders( + orders: List +) { + Column { + orders.forEach { order -> + OrderListItem(order) + + Divider( + modifier = Modifier + .fillMaxWidth() + .padding(start = 16.dp) + ) + } + } +} + +@Composable +private fun Loading() { + Column { + repeat(3) { + LoadingItem() + Divider( + modifier = Modifier + .fillMaxWidth() + .padding(start = 16.dp) + ) + } + } +} + +@Suppress("DestructuringDeclarationWithTooManyEntries") +@Composable +private fun LoadingItem() { + ConstraintLayout( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp) + ) { + val (number, date, name, status, total) = createRefs() + SkeletonView( + modifier = Modifier + .height(22.dp) + .width(50.dp) + .constrainAs(number) { + top.linkTo(parent.top) + start.linkTo(parent.start) + } + ) + + SkeletonView( + modifier = Modifier + .padding(start = 16.dp) + .height(22.dp) + .width(70.dp) + .constrainAs(date) { + top.linkTo(parent.top) + start.linkTo(number.end) + } + ) + + SkeletonView( + modifier = Modifier + .padding(top = 8.dp) + .height(20.dp) + .width(120.dp) + .constrainAs(name) { + top.linkTo(number.bottom) + start.linkTo(parent.start) + } + ) + + SkeletonView( + modifier = Modifier + .height(22.dp) + .width(100.dp) + .constrainAs(status) { + top.linkTo(parent.top) + end.linkTo(parent.end) + }, + ) + + SkeletonView( + modifier = Modifier + .padding(top = 8.dp) + .height(20.dp) + .width(70.dp) + .constrainAs(total) { + top.linkTo(status.bottom) + end.linkTo(parent.end) + } + ) + } +} + +@Suppress("DestructuringDeclarationWithTooManyEntries") +@Composable +private fun OrderListItem(order: OrderItem) { + ConstraintLayout( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp) + ) { + val (number, date, name, status, total) = createRefs() + + Text( + text = order.number, + style = MaterialTheme.typography.body1, + color = colorResource(id = R.color.color_on_surface_medium), + modifier = Modifier.constrainAs(number) { + top.linkTo(parent.top) + start.linkTo(parent.start) + } + ) + + Text( + text = order.date, + style = MaterialTheme.typography.body1, + color = colorResource(id = R.color.color_on_surface_medium), + modifier = Modifier + .padding(start = 16.dp) + .constrainAs(date) { + top.linkTo(parent.top) + start.linkTo(number.end) + } + ) + + Text( + text = order.customerName, + style = MaterialTheme.typography.body1, + modifier = Modifier + .padding(top = 8.dp) + .constrainAs(name) { + top.linkTo(number.bottom) + start.linkTo(parent.start) + } + ) + + WCTag( + modifier = Modifier + .constrainAs(status) { + top.linkTo(parent.top) + end.linkTo(parent.end) + }, + text = order.status, + textColor = MaterialTheme.colors.onSurface, + backgroundColor = colorResource(id = order.statusColor), + fontWeight = FontWeight.Normal + ) + + Text( + text = order.totalPrice, + style = MaterialTheme.typography.body1, + modifier = Modifier + .padding(top = 8.dp) + .constrainAs(total) { + top.linkTo(status.bottom) + end.linkTo(parent.end) + } + ) + } +} + +@Composable +@Preview +fun PreviewTopOrders() { + TopOrders( + orders = listOf( + OrderItem( + number = "123", + date = "2021-09-01", + customerName = "John Doe", + status = "Processing", + statusColor = R.color.tag_bg_processing, + totalPrice = "$100.00" + ), + OrderItem( + number = "124", + date = "2021-09-02", + customerName = "Jane Doe", + status = "Completed", + statusColor = R.color.tag_bg_completed, + totalPrice = "$200.00" + ) + ), + ) +} + +@Composable +@Preview +fun PreviewLoadingCard() { + Loading() +} 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 new file mode 100644 index 00000000000..09e19aafc55 --- /dev/null +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/dashboard/orders/DashboardOrdersViewModel.kt @@ -0,0 +1,169 @@ +package com.woocommerce.android.ui.dashboard.orders + +import androidx.annotation.ColorRes +import androidx.annotation.StringRes +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.asLiveData +import androidx.lifecycle.viewModelScope +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 +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.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 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.combine +import kotlinx.coroutines.flow.emitAll +import kotlinx.coroutines.flow.merge +import kotlinx.coroutines.flow.onStart +import kotlinx.coroutines.flow.transformLatest +import kotlinx.coroutines.launch +import java.util.Locale + +@HiltViewModel(assistedFactory = Factory::class) +class DashboardOrdersViewModel @AssistedInject constructor( + savedStateHandle: SavedStateHandle, + @Assisted private val parentViewModel: DashboardViewModel, + private val orderListRepository: OrderListRepository, + private val analyticsTrackerWrapper: AnalyticsTrackerWrapper, + private val currencyFormatter: CurrencyFormatter, + private val resourceProvider: ResourceProvider +) : ScopedViewModel(savedStateHandle) { + companion object { + const val MAX_NUMBER_OF_ORDERS_TO_DISPLAY_IN_CARD = 3 + } + + private val orderStatusMap = MutableSharedFlow>(replay = 1) + private val refreshTrigger = MutableSharedFlow(extraBufferCapacity = 1) + + val menu = DashboardWidgetMenu( + items = listOf( + DashboardWidget.Type.ORDERS.defaultHideMenuEntry { + parentViewModel.onHideWidgetClicked(ORDERS) + } + ) + ) + + val button = DashboardWidgetAction( + titleResource = R.string.dashboard_action_view_all_orders, + action = ::onNavigateToOrders + ) + + @OptIn(ExperimentalCoroutinesApi::class) + val viewState = merge(parentViewModel.refreshTrigger, refreshTrigger) + .onStart { emit(RefreshEvent()) } + .transformLatest { + emit(ViewState.Loading) + emitAll( + 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() + + init { + viewModelScope.launch { + orderStatusMap.tryEmit( + orderListRepository.getCachedOrderStatusOptions().mapValues { (_, value) -> + value.toOrderStatus() + } + ) + } + } + + private val Order.Status.color: Int + get() { + return when (this) { + is Order.Status.Processing -> R.color.tag_bg_processing + is Order.Status.Completed -> R.color.tag_bg_completed + is Order.Status.Failed -> R.color.tag_bg_failed + is Order.Status.OnHold -> R.color.tag_bg_on_hold + else -> R.color.tag_bg_other + } + } + + private fun onNavigateToOrders() { + triggerEvent(NavigateToOrders) + } + + fun onRefresh() { + analyticsTrackerWrapper.track( + AnalyticsEvent.DYNAMIC_DASHBOARD_CARD_RETRY_TAPPED, + mapOf( + AnalyticsTracker.KEY_TYPE to DashboardWidget.Type.ORDERS.trackingIdentifier + ) + ) + refreshTrigger.tryEmit(RefreshEvent(isForced = true)) + } + + sealed class ViewState { + data object Loading : ViewState() + data class Error(val message: String) : ViewState() + data class Content(val orders: List) : ViewState() + + @StringRes val title: Int = ORDERS.titleResource + + data class OrderItem( + val number: String, + val date: String, + val customerName: String, + val status: String, + @ColorRes val statusColor: Int, + val totalPrice: String + ) + } + + @AssistedFactory + interface Factory { + fun create(parentViewModel: DashboardViewModel): DashboardOrdersViewModel + } + + data object NavigateToOrders : MultiLiveEvent.Event() +} 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 14659c1bc7e..b3bb495c90b 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 @@ -2,6 +2,8 @@ package com.woocommerce.android.ui.orders.list import com.woocommerce.android.AppConstants import com.woocommerce.android.WooException +import com.woocommerce.android.model.Order +import com.woocommerce.android.model.OrderMapper import com.woocommerce.android.model.RequestResult import com.woocommerce.android.tools.SelectedSite import com.woocommerce.android.util.ContinuationWrapper @@ -10,6 +12,7 @@ import com.woocommerce.android.util.ContinuationWrapper.ContinuationResult.Succe import com.woocommerce.android.util.CoroutineDispatchers import com.woocommerce.android.util.WooLog import com.woocommerce.android.util.WooLog.T.ORDERS +import kotlinx.coroutines.flow.flow import kotlinx.coroutines.withContext import org.greenrobot.eventbus.Subscribe import org.greenrobot.eventbus.ThreadMode @@ -31,7 +34,8 @@ class OrderListRepository @Inject constructor( private val orderStore: WCOrderStore, private val orderUpdateStore: OrderUpdateStore, private val gatewayStore: WCGatewayStore, - private val selectedSite: SelectedSite + private val selectedSite: SelectedSite, + private val orderMapper: OrderMapper, ) { companion object { private const val TAG = "OrderListRepository" @@ -122,6 +126,37 @@ class OrderListRepository @Inject constructor( } } + fun observeTopOrders(count: Int, isForced: Boolean, statusFilter: Order.Status? = null) = flow { + if (!isForced) { + orderStore.getOrdersForSite(selectedSite.get()) + .asSequence() + .filter { statusFilter == null || it.status == statusFilter.value } + .sortedByDescending { it.dateCreated } + .take(count) + .map { orderMapper.toAppModel(it) } + .toList() + .takeIf { it.isNotEmpty() } + ?.let { orders -> + emit(Result.success(orders)) + } + } + + val result = orderStore.fetchOrders( + site = selectedSite.get(), + count = count, + statusFilter = statusFilter?.value, + deleteOldData = false + ) + + if (result.isError) { + WooLog.e(ORDERS, "Error fetching top orders: ${result.error.message}") + emit(Result.failure(WooException(result.error))) + } else { + val orderList = result.model?.map { orderMapper.toAppModel(it) } ?: emptyList() + emit(Result.success(orderList)) + } + } + @Suppress("unused") @Subscribe(threadMode = ThreadMode.MAIN) fun onOrderStatusOptionsChanged(event: OnOrderStatusOptionsChanged) { diff --git a/WooCommerce/src/main/res/values/strings.xml b/WooCommerce/src/main/res/values/strings.xml index 2581e58ec3a..42550747513 100644 --- a/WooCommerce/src/main/res/values/strings.xml +++ b/WooCommerce/src/main/res/values/strings.xml @@ -390,6 +390,7 @@ View Order View Orders + View all orders We can\'t display your\n store\'s analytics Make sure you are running the latest version of WooCommerce on your site and that you have WooCommerce Admin activated. @@ -410,6 +411,7 @@ Top performers Blaze campaigns Feedback + Most recent orders Most recent reviews Unavailable diff --git a/build.gradle b/build.gradle index c19c855d5e4..1c0b583f557 100644 --- a/build.gradle +++ b/build.gradle @@ -98,7 +98,7 @@ tasks.register("installGitHooks", Copy) { } ext { - fluxCVersion = 'trunk-cfe34975d20df76a2b7b4522dccd2faf53ef0f7c' + fluxCVersion = '3008-f3ff9689a0a998537a624ce9a77b930a341bee48' glideVersion = '4.16.0' coilVersion = '2.1.0' constraintLayoutVersion = '1.2.0'