@@ -37,7 +37,7 @@ final class AnalyticsHubViewModel: ObservableObject {
3737
3838 /// Products Card ViewModel
3939 ///
40- @Published var productCard = AnalyticsHubViewModel . productCard ( currentPeriodStats: nil , previousPeriodStats: nil )
40+ @Published var productCard = AnalyticsHubViewModel . productCard ( currentPeriodStats: nil , previousPeriodStats: nil , itemsSoldStats : nil )
4141
4242 /// Time Range Selection Type
4343 ///
@@ -57,6 +57,10 @@ final class AnalyticsHubViewModel: ObservableObject {
5757 ///
5858 @Published private var previousOrderStats : OrderStatsV4 ? = nil
5959
60+ /// Stats for the current top items sold. Used in the products card.
61+ ///
62+ @Published private var itemsSoldStats : TopEarnerStats ? = nil
63+
6064 /// Time Range selection data defining the current and previous time period
6165 ///
6266 private var timeRangeSelection : AnalyticsHubTimeRangeSelection
@@ -89,9 +93,15 @@ private extension AnalyticsHubViewModel {
8993 async let previousPeriodRequest = retrieveStats ( earliestDateToInclude: previousTimeRange. start,
9094 latestDateToInclude: previousTimeRange. end,
9195 forceRefresh: true )
92- let ( currentPeriodStats, previousPeriodStats) = try await ( currentPeriodRequest, previousPeriodRequest)
96+
97+ async let itemsSoldRequest = retrieveTopItemsSoldStats ( earliestDateToInclude: currentTimeRange. start,
98+ latestDateToInclude: currentTimeRange. end,
99+ forceRefresh: true )
100+
101+ let ( currentPeriodStats, previousPeriodStats, itemsSoldStats) = try await ( currentPeriodRequest, previousPeriodRequest, itemsSoldRequest)
93102 self . currentOrderStats = currentPeriodStats
94103 self . previousOrderStats = previousPeriodStats
104+ self . itemsSoldStats = itemsSoldStats
95105 }
96106
97107 @MainActor
@@ -114,6 +124,25 @@ private extension AnalyticsHubViewModel {
114124 stores. dispatch ( action)
115125 }
116126 }
127+
128+ @MainActor
129+ /// Retrieves top ItemsSold stats using the `retrieveTopEarnerStats` action but without saving results into storage.
130+ ///
131+ func retrieveTopItemsSoldStats( earliestDateToInclude: Date , latestDateToInclude: Date , forceRefresh: Bool ) async throws -> TopEarnerStats {
132+ try await withCheckedThrowingContinuation { continuation in
133+ let action = StatsActionV4 . retrieveTopEarnerStats ( siteID: siteID,
134+ timeRange: . thisYear, // Only needed for storing purposes, we can ignore it.
135+ earliestDateToInclude: earliestDateToInclude,
136+ latestDateToInclude: latestDateToInclude,
137+ quantity: Constants . maxNumberOfTopItemsSold,
138+ forceRefresh: forceRefresh,
139+ saveInStorage: false ,
140+ onCompletion: { result in
141+ continuation. resume ( with: result)
142+ } )
143+ stores. dispatch ( action)
144+ }
145+ }
117146}
118147
119148// MARK: Data - UI mapping
@@ -128,16 +157,19 @@ private extension AnalyticsHubViewModel {
128157 func switchToErrorState( ) {
129158 self . currentOrderStats = nil
130159 self . previousOrderStats = nil
160+ self . itemsSoldStats = nil
131161 }
132162
133163 func bindViewModelsWithData( ) {
134- Publishers . CombineLatest ( $currentOrderStats, $previousOrderStats)
135- . sink { [ weak self] currentOrderStats, previousOrderStats in
164+ Publishers . CombineLatest3 ( $currentOrderStats, $previousOrderStats, $itemsSoldStats )
165+ . sink { [ weak self] currentOrderStats, previousOrderStats, itemsSoldStats in
136166 guard let self else { return }
137167
138168 self . revenueCard = AnalyticsHubViewModel . revenueCard ( currentPeriodStats: currentOrderStats, previousPeriodStats: previousOrderStats)
139169 self . ordersCard = AnalyticsHubViewModel . ordersCard ( currentPeriodStats: currentOrderStats, previousPeriodStats: previousOrderStats)
140- self . productCard = AnalyticsHubViewModel . productCard ( currentPeriodStats: currentOrderStats, previousPeriodStats: previousOrderStats)
170+ self . productCard = AnalyticsHubViewModel . productCard ( currentPeriodStats: currentOrderStats,
171+ previousPeriodStats: previousOrderStats,
172+ itemsSoldStats: itemsSoldStats)
141173
142174 } . store ( in: & subscriptions)
143175
@@ -197,25 +229,38 @@ private extension AnalyticsHubViewModel {
197229 syncErrorMessage: Localization . OrderCard. noOrders)
198230 }
199231
200- static func productCard( currentPeriodStats: OrderStatsV4 ? , previousPeriodStats: OrderStatsV4 ? ) -> AnalyticsProductCardViewModel {
232+ /// Helper function to create a `AnalyticsProductCardViewModel` from the fetched stats.
233+ ///
234+ static func productCard( currentPeriodStats: OrderStatsV4 ? ,
235+ previousPeriodStats: OrderStatsV4 ? ,
236+ itemsSoldStats: TopEarnerStats ? ) -> AnalyticsProductCardViewModel {
201237 let showSyncError = currentPeriodStats == nil || previousPeriodStats == nil
202238 let itemsSold = StatsDataTextFormatter . createItemsSoldText ( orderStats: currentPeriodStats)
203239 let itemsSoldDelta = StatsDataTextFormatter . createOrderItemsSoldDelta ( from: previousPeriodStats, to: currentPeriodStats)
204240
205- let imageURL = URL ( string: " https://s0.wordpress.com/i/store/mobile/plans-premium.png " )
206241 return AnalyticsProductCardViewModel ( itemsSold: itemsSold,
207242 delta: itemsSoldDelta. string,
208243 deltaBackgroundColor: Constants . deltaColor ( for: itemsSoldDelta. direction) ,
209- itemsSoldData: [ // Temporary data
210- . init( imageURL: imageURL, name: " Tabletop Photos " , details: " Net Sales: $1,232 " , value: " 32 " ) ,
211- . init( imageURL: imageURL, name: " Kentya Palm " , details: " Net Sales: $800 " , value: " 10 " ) ,
212- . init( imageURL: imageURL, name: " Love Ficus " , details: " Net Sales: $599 " , value: " 5 " ) ,
213- . init( imageURL: imageURL, name: " Bird Of Paradise " , details: " Net Sales: $23.50 " , value: " 2 " )
214- ] ,
244+ itemsSoldData: itemSoldRows ( from: itemsSoldStats) ,
215245 isRedacted: false ,
216246 showSyncError: showSyncError)
217247 }
218248
249+ /// Helper functions to create `TopPerformersRow.Data` items rom the provided `TopEarnerStats`.
250+ ///
251+ static func itemSoldRows( from itemSoldStats: TopEarnerStats ? ) -> [ TopPerformersRow . Data ] {
252+ guard let items = itemSoldStats? . items else {
253+ return [ ]
254+ }
255+
256+ return items. map { item in
257+ TopPerformersRow . Data ( imageURL: URL ( string: item. imageUrl ?? " " ) ,
258+ name: item. productName ?? " " ,
259+ details: Localization . ProductCard. netSales ( value: item. totalString) ,
260+ value: " \( item. quantity) " )
261+ }
262+ }
263+
219264 static func timeRangeCard( timeRangeSelection: AnalyticsHubTimeRangeSelection ) -> AnalyticsTimeRangeCardViewModel {
220265 return AnalyticsTimeRangeCardViewModel ( selectedRangeTitle: timeRangeSelection. rangeSelectionDescription,
221266 currentRangeSubtitle: timeRangeSelection. currentRangeDescription,
@@ -226,6 +271,8 @@ private extension AnalyticsHubViewModel {
226271// MARK: - Constants
227272private extension AnalyticsHubViewModel {
228273 enum Constants {
274+ static let maxNumberOfTopItemsSold = 5
275+
229276 static func deltaColor( for direction: StatsDataTextFormatter . DeltaPercentage . Direction ) -> UIColor {
230277 switch direction {
231278 case . positive:
@@ -252,5 +299,12 @@ private extension AnalyticsHubViewModel {
252299 static let noOrders = NSLocalizedString ( " Unable to load order analytics " ,
253300 comment: " Text displayed when there is an error loading order stats data. " )
254301 }
302+
303+ enum ProductCard {
304+ static func netSales( value: String ) -> String {
305+ String . localizedStringWithFormat ( NSLocalizedString ( " Net sales: %@ " , comment: " Label for the total sales of a product in the Analytics Hub " ) ,
306+ value)
307+ }
308+ }
255309 }
256310}
0 commit comments