diff --git a/WooCommerce/Classes/ViewRelated/Dashboard/Analytics Hub/AnalyticsHubViewModel.swift b/WooCommerce/Classes/ViewRelated/Dashboard/Analytics Hub/AnalyticsHubViewModel.swift index a31ec5c6198..dddf8a200e6 100644 --- a/WooCommerce/Classes/ViewRelated/Dashboard/Analytics Hub/AnalyticsHubViewModel.swift +++ b/WooCommerce/Classes/ViewRelated/Dashboard/Analytics Hub/AnalyticsHubViewModel.swift @@ -189,25 +189,17 @@ private extension AnalyticsHubViewModel { static func revenueCard(currentPeriodStats: OrderStatsV4?, previousPeriodStats: OrderStatsV4?) -> AnalyticsReportCardViewModel { let showSyncError = currentPeriodStats == nil || previousPeriodStats == nil - let totalDelta = StatsDataTextFormatter.createTotalRevenueDelta(from: previousPeriodStats, to: currentPeriodStats) - let netDelta = StatsDataTextFormatter.createNetRevenueDelta(from: previousPeriodStats, to: currentPeriodStats) return AnalyticsReportCardViewModel(title: Localization.RevenueCard.title, leadingTitle: Localization.RevenueCard.leadingTitle, leadingValue: StatsDataTextFormatter.createTotalRevenueText(orderStats: currentPeriodStats, selectedIntervalIndex: nil), - leadingDelta: totalDelta.string, - leadingDeltaColor: totalDelta.direction.deltaBackgroundColor, - leadingDeltaTextColor: totalDelta.direction.deltaTextColor, + leadingDelta: StatsDataTextFormatter.createTotalRevenueDelta(from: previousPeriodStats, to: currentPeriodStats), leadingChartData: StatsIntervalDataParser.getChartData(for: .totalRevenue, from: currentPeriodStats), - leadingChartColor: totalDelta.direction.chartColor, trailingTitle: Localization.RevenueCard.trailingTitle, trailingValue: StatsDataTextFormatter.createNetRevenueText(orderStats: currentPeriodStats), - trailingDelta: netDelta.string, - trailingDeltaColor: netDelta.direction.deltaBackgroundColor, - trailingDeltaTextColor: netDelta.direction.deltaTextColor, + trailingDelta: StatsDataTextFormatter.createNetRevenueDelta(from: previousPeriodStats, to: currentPeriodStats), trailingChartData: StatsIntervalDataParser.getChartData(for: .netRevenue, from: currentPeriodStats), - trailingChartColor: netDelta.direction.chartColor, isRedacted: false, showSyncError: showSyncError, syncErrorMessage: Localization.RevenueCard.noRevenue) @@ -215,25 +207,18 @@ private extension AnalyticsHubViewModel { static func ordersCard(currentPeriodStats: OrderStatsV4?, previousPeriodStats: OrderStatsV4?) -> AnalyticsReportCardViewModel { let showSyncError = currentPeriodStats == nil || previousPeriodStats == nil - let ordersCountDelta = StatsDataTextFormatter.createOrderCountDelta(from: previousPeriodStats, to: currentPeriodStats) - let orderValueDelta = StatsDataTextFormatter.createAverageOrderValueDelta(from: previousPeriodStats, to: currentPeriodStats) return AnalyticsReportCardViewModel(title: Localization.OrderCard.title, leadingTitle: Localization.OrderCard.leadingTitle, leadingValue: StatsDataTextFormatter.createOrderCountText(orderStats: currentPeriodStats, selectedIntervalIndex: nil), - leadingDelta: ordersCountDelta.string, - leadingDeltaColor: ordersCountDelta.direction.deltaBackgroundColor, - leadingDeltaTextColor: ordersCountDelta.direction.deltaTextColor, + leadingDelta: StatsDataTextFormatter.createOrderCountDelta(from: previousPeriodStats, to: currentPeriodStats), leadingChartData: StatsIntervalDataParser.getChartData(for: .orderCount, from: currentPeriodStats), - leadingChartColor: ordersCountDelta.direction.chartColor, trailingTitle: Localization.OrderCard.trailingTitle, trailingValue: StatsDataTextFormatter.createAverageOrderValueText(orderStats: currentPeriodStats), - trailingDelta: orderValueDelta.string, - trailingDeltaColor: orderValueDelta.direction.deltaBackgroundColor, - trailingDeltaTextColor: orderValueDelta.direction.deltaTextColor, + trailingDelta: StatsDataTextFormatter.createAverageOrderValueDelta(from: previousPeriodStats, + to: currentPeriodStats), trailingChartData: StatsIntervalDataParser.getChartData(for: .averageOrderValue, from: currentPeriodStats), - trailingChartColor: orderValueDelta.direction.chartColor, isRedacted: false, showSyncError: showSyncError, syncErrorMessage: Localization.OrderCard.noOrders) @@ -250,9 +235,7 @@ private extension AnalyticsHubViewModel { let itemsSoldDelta = StatsDataTextFormatter.createOrderItemsSoldDelta(from: previousPeriodStats, to: currentPeriodStats) return AnalyticsProductCardViewModel(itemsSold: itemsSold, - delta: itemsSoldDelta.string, - deltaBackgroundColor: itemsSoldDelta.direction.deltaBackgroundColor, - deltaTextColor: itemsSoldDelta.direction.deltaTextColor, + delta: itemsSoldDelta, itemsSoldData: itemSoldRows(from: itemsSoldStats), isRedacted: false, showStatsError: showStatsError, diff --git a/WooCommerce/Classes/ViewRelated/Dashboard/Analytics Hub/AnalyticsProductCardViewModel.swift b/WooCommerce/Classes/ViewRelated/Dashboard/Analytics Hub/AnalyticsProductCardViewModel.swift index 297987dc466..98696f5087a 100644 --- a/WooCommerce/Classes/ViewRelated/Dashboard/Analytics Hub/AnalyticsProductCardViewModel.swift +++ b/WooCommerce/Classes/ViewRelated/Dashboard/Analytics Hub/AnalyticsProductCardViewModel.swift @@ -9,17 +9,9 @@ struct AnalyticsProductCardViewModel { /// let itemsSold: String - /// Items Sold Delta + /// Items Sold Delta Percentage /// - let delta: String - - /// Delta background color. - /// - let deltaBackgroundColor: UIColor - - /// Delta text color. - /// - let deltaTextColor: UIColor + let delta: DeltaPercentage /// Items Solds data to render. /// @@ -45,9 +37,7 @@ extension AnalyticsProductCardViewModel { var redacted: Self { // Values here are placeholders and will be redacted in the UI .init(itemsSold: "1000", - delta: "+50%", - deltaBackgroundColor: .lightGray, - deltaTextColor: .text, + delta: DeltaPercentage(string: "0%", direction: .zero), itemsSoldData: [.init(imageURL: nil, name: "Product Name", details: "Net Sales", value: "$5678")], isRedacted: true, showStatsError: false, @@ -61,9 +51,9 @@ extension AnalyticsProductCardViewModel { extension AnalyticsProductCard { init(viewModel: AnalyticsProductCardViewModel) { self.itemsSold = viewModel.itemsSold - self.delta = viewModel.delta - self.deltaBackgroundColor = viewModel.deltaBackgroundColor - self.deltaTextColor = viewModel.deltaTextColor + self.delta = viewModel.delta.string + self.deltaBackgroundColor = viewModel.delta.direction.deltaBackgroundColor + self.deltaTextColor = viewModel.delta.direction.deltaTextColor self.itemsSoldData = viewModel.itemsSoldData self.isRedacted = viewModel.isRedacted self.showStatsError = viewModel.showStatsError diff --git a/WooCommerce/Classes/ViewRelated/Dashboard/Analytics Hub/AnalyticsReportCardViewModel.swift b/WooCommerce/Classes/ViewRelated/Dashboard/Analytics Hub/AnalyticsReportCardViewModel.swift index 76a37cc712c..2ece38626f3 100644 --- a/WooCommerce/Classes/ViewRelated/Dashboard/Analytics Hub/AnalyticsReportCardViewModel.swift +++ b/WooCommerce/Classes/ViewRelated/Dashboard/Analytics Hub/AnalyticsReportCardViewModel.swift @@ -17,26 +17,14 @@ struct AnalyticsReportCardViewModel { /// let leadingValue: String - /// First Column Delta Value + /// First Column Delta Percentage /// - let leadingDelta: String - - /// First Column delta background color. - /// - let leadingDeltaColor: UIColor - - /// First Column delta text color. - /// - let leadingDeltaTextColor: UIColor + let leadingDelta: DeltaPercentage /// First Column Chart Data /// let leadingChartData: [Double] - /// First Column Chart Color - /// - let leadingChartColor: UIColor - /// Second Column Title /// let trailingTitle: String @@ -45,26 +33,14 @@ struct AnalyticsReportCardViewModel { /// let trailingValue: String - /// Second Column Delta Value + /// Second Column Delta Percentage /// - let trailingDelta: String - - /// Second Column Delta Background Color - /// - let trailingDeltaColor: UIColor - - /// Second Column delta text color. - /// - let trailingDeltaTextColor: UIColor + let trailingDelta: DeltaPercentage /// Second Column Chart Data /// let trailingChartData: [Double] - /// Second Column Chart Color - /// - let trailingChartColor: UIColor - /// Indicates if the values should be hidden (for loading state) /// let isRedacted: Bool @@ -87,18 +63,12 @@ extension AnalyticsReportCardViewModel { .init(title: title, leadingTitle: leadingTitle, leadingValue: "$1000", - leadingDelta: "+50%", - leadingDeltaColor: .lightGray, - leadingDeltaTextColor: .text, + leadingDelta: DeltaPercentage(string: "0%", direction: .zero), leadingChartData: [], - leadingChartColor: .lightGray, trailingTitle: trailingTitle, trailingValue: "$1000", - trailingDelta: "+50%", - trailingDeltaColor: .lightGray, - trailingDeltaTextColor: .text, + trailingDelta: DeltaPercentage(string: "0%", direction: .zero), trailingChartData: [], - trailingChartColor: .lightGray, isRedacted: true, showSyncError: false, syncErrorMessage: "") @@ -112,18 +82,18 @@ extension AnalyticsReportCard { self.title = viewModel.title self.leadingTitle = viewModel.leadingTitle self.leadingValue = viewModel.leadingValue - self.leadingDelta = viewModel.leadingDelta - self.leadingDeltaColor = viewModel.leadingDeltaColor - self.leadingDeltaTextColor = viewModel.leadingDeltaTextColor + self.leadingDelta = viewModel.leadingDelta.string + self.leadingDeltaColor = viewModel.leadingDelta.direction.deltaBackgroundColor + self.leadingDeltaTextColor = viewModel.leadingDelta.direction.deltaTextColor self.leadingChartData = viewModel.leadingChartData - self.leadingChartColor = viewModel.leadingChartColor + self.leadingChartColor = viewModel.leadingDelta.direction.chartColor self.trailingTitle = viewModel.trailingTitle self.trailingValue = viewModel.trailingValue - self.trailingDelta = viewModel.trailingDelta - self.trailingDeltaColor = viewModel.trailingDeltaColor - self.trailingDeltaTextColor = viewModel.trailingDeltaTextColor + self.trailingDelta = viewModel.trailingDelta.string + self.trailingDeltaColor = viewModel.trailingDelta.direction.deltaBackgroundColor + self.trailingDeltaTextColor = viewModel.trailingDelta.direction.deltaTextColor self.trailingChartData = viewModel.trailingChartData - self.trailingChartColor = viewModel.trailingChartColor + self.trailingChartColor = viewModel.trailingDelta.direction.chartColor self.isRedacted = viewModel.isRedacted self.showSyncError = viewModel.showSyncError self.syncErrorMessage = viewModel.syncErrorMessage diff --git a/WooCommerce/Classes/ViewRelated/Dashboard/Analytics Hub/DeltaPercentage.swift b/WooCommerce/Classes/ViewRelated/Dashboard/Analytics Hub/DeltaPercentage.swift new file mode 100644 index 00000000000..86fe5a0e587 --- /dev/null +++ b/WooCommerce/Classes/ViewRelated/Dashboard/Analytics Hub/DeltaPercentage.swift @@ -0,0 +1,84 @@ +import Foundation +import class UIKit.UIColor + +/// Represents a formatted delta percentage string and its direction of change +struct DeltaPercentage { + /// The delta percentage formatted as a localized string (e.g. `+100%`) + let string: String + + /// The direction of change + let direction: Direction + + init(string: String, direction: Direction) { + self.string = string + self.direction = direction + } + + /// Convenience initializer + /// - Parameters: + /// - value: The percentage expressed as a `Decimal` (e.g. 0.5 for 50%). + /// - formatter: The formatter used to format the value as a string. + init(value: Decimal, formatter: NumberFormatter) { + self.string = formatter.string(from: value as NSNumber) ?? "" + self.direction = { + if value > 0 { + return .positive + } else if value < 0 { + return .negative + } else { + return .zero + } + }() + } + + /// Represents the direction of change for a delta value + enum Direction { + case positive + case negative + case zero + + /// Background color for a `DeltaTag` + var deltaBackgroundColor: UIColor { + switch self { + case .positive: + return Constants.green + case .negative: + return Constants.red + case .zero: + return Constants.lightGray + } + } + + /// Text color for a `DeltaTag` + var deltaTextColor: UIColor { + switch self { + case .positive, .negative: + return .textInverted + case .zero: + return .text + } + } + + /// Line color for an `AnalyticsLineChart` + var chartColor: UIColor { + switch self { + case .positive: + return Constants.green + case .negative: + return Constants.red + case .zero: + return Constants.darkGray + } + } + } +} + +// MARK: Constants +extension DeltaPercentage { + enum Constants { + static let green: UIColor = .withColorStudio(.green, shade: .shade50) + static let red: UIColor = .withColorStudio(.red, shade: .shade40) + static let lightGray: UIColor = .withColorStudio(.gray, shade: .shade0) + static let darkGray: UIColor = .withColorStudio(.gray, shade: .shade30) + } +} diff --git a/WooCommerce/Classes/ViewRelated/Dashboard/Factories/StatsDataTextFormatter.swift b/WooCommerce/Classes/ViewRelated/Dashboard/Factories/StatsDataTextFormatter.swift index f065ed95ef2..422e03d1acf 100644 --- a/WooCommerce/Classes/ViewRelated/Dashboard/Factories/StatsDataTextFormatter.swift +++ b/WooCommerce/Classes/ViewRelated/Dashboard/Factories/StatsDataTextFormatter.swift @@ -156,7 +156,7 @@ struct StatsDataTextFormatter { /// static func createOrderItemsSoldDelta(from previousPeriod: OrderStatsV4?, to currentPeriod: OrderStatsV4?) -> DeltaPercentage { guard let previousPeriod, let currentPeriod else { - return DeltaPercentage(value: 0) // Missing data: 0% change + return DeltaPercentage(value: 0, formatter: deltaNumberFormatter) // Missing data: 0% change } let previousItemsSold = Double(previousPeriod.totals.totalItemsSold) let currentItemsSold = Double(currentPeriod.totals.totalItemsSold) @@ -172,90 +172,27 @@ extension StatsDataTextFormatter { /// static func createDeltaPercentage(from previousValue: Decimal?, to currentValue: Decimal?) -> DeltaPercentage { guard let previousValue, let currentValue, previousValue != currentValue else { - return DeltaPercentage(value: 0) // Missing or equal values: 0% change + return DeltaPercentage(value: 0, formatter: deltaNumberFormatter) // Missing or equal values: 0% change } // If the previous value was 0, return a 100% or -100% change guard previousValue != 0 else { let deltaValue: Decimal = currentValue > 0 ? 1 : -1 - return DeltaPercentage(value: deltaValue) + return DeltaPercentage(value: deltaValue, formatter: deltaNumberFormatter) } - return DeltaPercentage(value: (currentValue - previousValue) / previousValue) + return DeltaPercentage(value: (currentValue - previousValue) / previousValue, formatter: deltaNumberFormatter) } /// Creates the `DeltaPercentage` for the percent change from the previous `Double` value to the current `Double` value /// static func createDeltaPercentage(from previousValue: Double?, to currentValue: Double?) -> DeltaPercentage { guard let previousValue, let currentValue else { - return DeltaPercentage(value: 0) // Missing data: 0% change + return DeltaPercentage(value: 0, formatter: deltaNumberFormatter) // Missing data: 0% change } return createDeltaPercentage(from: Decimal(previousValue), to: Decimal(currentValue)) } - - /// Represents a formatted delta percentage string and its direction of change - struct DeltaPercentage { - /// The delta percentage formatted as a localized string (e.g. `+100%`) - let string: String - - /// The direction of change - let direction: Direction - - init(value: Decimal) { - self.string = deltaNumberFormatter.string(from: value as NSNumber) ?? Constants.placeholderText - self.direction = { - if value > 0 { - return .positive - } else if value < 0 { - return .negative - } else { - return .zero - } - }() - } - - /// Represents the direction of change for a delta value - enum Direction { - case positive - case negative - case zero - - /// Background color for a `DeltaTag` - var deltaBackgroundColor: UIColor { - switch self { - case .positive: - return Constants.green - case .negative: - return Constants.red - case .zero: - return Constants.lightGray - } - } - - /// Text color for a `DeltaTag` - var deltaTextColor: UIColor { - switch self { - case .positive, .negative: - return .textInverted - case .zero: - return .text - } - } - - /// Line color for an `AnalyticsLineChart` - var chartColor: UIColor { - switch self { - case .positive: - return Constants.green - case .negative: - return Constants.red - case .zero: - return Constants.darkGray - } - } - } - } } // MARK: - Private helpers @@ -326,10 +263,6 @@ private extension StatsDataTextFormatter { enum Constants { static let placeholderText = "-" - static let green: UIColor = .withColorStudio(.green, shade: .shade50) - static let red: UIColor = .withColorStudio(.red, shade: .shade40) - static let lightGray: UIColor = .withColorStudio(.gray, shade: .shade0) - static let darkGray: UIColor = .withColorStudio(.gray, shade: .shade30) } diff --git a/WooCommerce/WooCommerce.xcodeproj/project.pbxproj b/WooCommerce/WooCommerce.xcodeproj/project.pbxproj index 4b13c714e89..855e2a4d1bd 100644 --- a/WooCommerce/WooCommerce.xcodeproj/project.pbxproj +++ b/WooCommerce/WooCommerce.xcodeproj/project.pbxproj @@ -1465,6 +1465,7 @@ CC8413E423F5C48E00EFC277 /* stop.sh in Resources */ = {isa = PBXBuildFile; fileRef = CCFC011123E9E40B00157A78 /* stop.sh */; }; CC8413E523F5C49100EFC277 /* start.sh in Resources */ = {isa = PBXBuildFile; fileRef = CCFC011023E9E3F400157A78 /* start.sh */; }; CC923A1D2847A8E0008EEEBE /* OrderStatusListViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC923A1C2847A8E0008EEEBE /* OrderStatusListViewModelTests.swift */; }; + CCA1D5FE293F537400B40560 /* DeltaPercentage.swift in Sources */ = {isa = PBXBuildFile; fileRef = CCA1D5FD293F537400B40560 /* DeltaPercentage.swift */; }; CCB366AF274518EC007D437A /* EditableOrderViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CCB366AE274518EC007D437A /* EditableOrderViewModelTests.swift */; }; CCC284112768C18500F6CC8B /* ProductInOrder.swift in Sources */ = {isa = PBXBuildFile; fileRef = CCC284102768C18500F6CC8B /* ProductInOrder.swift */; }; CCCC29DD25E5757C0046B96F /* RenameAttributesViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = CCCC29DC25E5757C0046B96F /* RenameAttributesViewController.xib */; }; @@ -3476,6 +3477,7 @@ CC770C8927B1497700CE6ABC /* SearchHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchHeader.swift; sourceTree = ""; }; CC77488D2719A07D0043CDD7 /* ShippingLabelAddressTopBannerFactoryTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShippingLabelAddressTopBannerFactoryTests.swift; sourceTree = ""; }; CC923A1C2847A8E0008EEEBE /* OrderStatusListViewModelTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OrderStatusListViewModelTests.swift; sourceTree = ""; }; + CCA1D5FD293F537400B40560 /* DeltaPercentage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeltaPercentage.swift; sourceTree = ""; }; CCB366AE274518EC007D437A /* EditableOrderViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditableOrderViewModelTests.swift; sourceTree = ""; }; CCC284102768C18500F6CC8B /* ProductInOrder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductInOrder.swift; sourceTree = ""; }; CCCC29DC25E5757C0046B96F /* RenameAttributesViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = RenameAttributesViewController.xib; sourceTree = ""; }; @@ -5346,6 +5348,7 @@ 26E7EE6D29300E8100793045 /* AnalyticsProductCard.swift */, 26E7EE7129301EBC00793045 /* AnalyticsProductCardViewModel.swift */, CC41E70B29310C1F008B3FB9 /* AnalyticsLineChart.swift */, + CCA1D5FD293F537400B40560 /* DeltaPercentage.swift */, ); path = "Analytics Hub"; sourceTree = ""; @@ -9922,6 +9925,7 @@ B95112DA28BF79CA00D9578D /* PaymentsRoute.swift in Sources */, 7E6A019F2725CD76001668D5 /* FilterProductCategoryListViewModel.swift in Sources */, CC53FB3C2757EC7200C4CA4F /* ProductSelectorViewModel.swift in Sources */, + CCA1D5FE293F537400B40560 /* DeltaPercentage.swift in Sources */, 4569D3C325DC008700CDC3E2 /* SiteAddress.swift in Sources */, B9B0391828A6838400DC1C83 /* PermanentNoticeView.swift in Sources */, DEC6C51827466B59006832D3 /* StoreStatsEmptyView.swift in Sources */,