Skip to content

Watch: Adds Order Detail View #12817

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
May 23, 2024
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ struct OrderListCellViewModel {
Localization.title(orderNumber: order.number, customerName: customerName)
}

/// For example, Pamela Nguyen
///
var customerName: String {
if let fullName = order.billingAddress?.fullName, fullName.isNotEmpty {
return fullName
Expand All @@ -56,6 +58,14 @@ struct OrderListCellViewModel {
return formatter.string(from: order.dateCreated)
}

/// Time where the order was created
///
var timeCreated: String {
let formatter: DateFormatter = .timeFormatter
formatter.timeZone = .siteTimezone
return formatter.string(from: order.dateCreated)
}

/// Status of the order
///
var status: OrderStatusEnum {
Expand All @@ -70,6 +80,14 @@ struct OrderListCellViewModel {
return orderStatus?.name ?? order.status.rawValue
}

/// The localized unabbreviated total for a given order item, which includes the currency.
///
/// Example: $48,415,504.20
///
func total(for orderItem: OrderItem) -> String {
currencyFormatter.formatAmount(orderItem.total, with: order.currency) ?? "$\(orderItem.total)"
}

#if !os(watchOS)
/// Accessory view that renders the cell's disclosure indicator
///
Expand Down
179 changes: 179 additions & 0 deletions WooCommerce/Woo Watch App/Orders/OrderDetailView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
import SwiftUI

/// View for the order detail
///
struct OrderDetailView: View {

/// Order to render
///
let order: OrdersListView.Order

/// Tracks the selected tab.
///
@State private var selectedTab = Tab.summary

var body: some View {
TabView(selection: $selectedTab) {
// First
summaryView
.tag(Tab.summary)
.padding(.horizontal)

// Second
if order.itemCount > 0 {
productsView
.tag(Tab.products)
}
}
.toolbar {
ToolbarItem(placement: .confirmationAction) {
Text(order.number)
.font(.body)
.foregroundStyle(Colors.wooPurple20)
}
}
.background(
LinearGradient(gradient: Gradient(colors: [Colors.wooPurpleBackground, .black]), startPoint: .top, endPoint: .bottom)
)
}

/// First View: Summary
///
@ViewBuilder private var summaryView: some View {
VStack(alignment: .leading) {

// Date & Time
HStack {
Text(order.date)
Spacer()
Text(order.time)
}
.font(.caption2)
.foregroundStyle(.secondary)

Divider()

// Name, total, status
VStack(alignment: .leading, spacing: Layout.nameSectionSpacing) {
Text(order.name)
.font(.body)
.fixedSize(horizontal: false, vertical: true)

Text(order.total)
.font(.title2)
.bold()

Text(order.status)
.font(.footnote)
.foregroundStyle(Colors.gray5)
}
.padding(.bottom, Layout.mainSectionsPadding)

// Products button
Button(Localization.products(order.itemCount).lowercased()) {
if order.itemCount > 0 {
self.selectedTab = .products
}
}
.font(.caption2)
.buttonStyle(.borderless)
.frame(maxWidth: .greatestFiniteMagnitude, alignment: .leading)
.padding()
.background(Colors.whiteTransparent)
.cornerRadius(Layout.buttonCornerRadius)
}
}

/// Second View: Product List
///
@ViewBuilder private var productsView: some View {
List {
Section {
ForEach(order.items) { orderItem in
itemRow(orderItem)
}
} header: {
Text(Localization.products(order.itemCount))
.font(.caption2)
}
.listStyle(.plain)
.listRowBackground(Color.clear)
.listRowInsets(.init(top: 0, leading: Layout.itemlistPadding, bottom: 0, trailing: Layout.itemlistPadding))
}
}

/// Item Row of the product list
///
@ViewBuilder private func itemRow(_ item: OrdersListView.OrderItem) -> some View {
VStack(alignment: .leading, spacing: .zero) {
HStack(alignment: .top, spacing: Layout.itemRowSpacing) {

// Item count
Text(item.count.formatted(.number))
.font(.caption2)
.foregroundStyle(Colors.wooPurple20)
.padding(Layout.itemCountPadding)
.background(Circle().fill(Colors.whiteTransparent))

// Name and total
VStack(alignment: .leading) {
Text(item.name)
.font(.caption2)

Text(item.total)
.font(.caption2)
.foregroundStyle(.secondary)
}

Spacer()
}
.padding(.vertical)

if item.showDivider {
Divider()
}
}
}
}

private extension OrderDetailView {
enum Tab: Int {
case summary
case products
}

enum Layout {
static let nameSectionSpacing = 2.0
static let mainSectionsPadding = 10.0
static let itemCountPadding = 6.0
static let itemRowSpacing = 8.0
static let buttonCornerRadius = 10.0
static let itemlistPadding = 5.0
}

enum Localization {
static func products(_ count: Int) -> LocalizedString {
if count == 1 {
return AppLocalizedString(
"watch.orders.detail.product-count-singular",
value: "1 Product",
comment: "Singular format for the number of products in the order detail screen."
)
}

let format = AppLocalizedString(
"watch.orders.detail.product-count",
value: "%d Products",
comment: "Plural format for the number of products in the order detail screen."
)
return LocalizedString(format: format, count)
}
}

enum Colors {
static let wooPurpleBackground = Color(red: 79/255.0, green: 54/255.0, blue: 125/255.0)
static let gray5 = Color(red: 220/255.0, green: 220/255.0, blue: 222/255.0)
static let wooPurple20 = Color(red: 190/255.0, green: 160/255.0, blue: 242/255.0)
static let whiteTransparent = Color(white: 1.0, opacity: 0.12)
}
}
30 changes: 19 additions & 11 deletions WooCommerce/Woo Watch App/Orders/OrdersListView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ struct OrdersListView: View {
}

var body: some View {
NavigationSplitView() {
NavigationStack() {
Group {
switch viewModel.viewState {
case .idle:
Expand All @@ -40,8 +40,6 @@ struct OrdersListView: View {
}
}
}
} detail: {
Text("Order Detail")
}
.task {
await viewModel.fetchOrders()
Expand All @@ -53,10 +51,12 @@ struct OrdersListView: View {
@ViewBuilder var loadingView: some View {
List {
OrderListCard(order: .init(date: "----",
time: "----",
number: "----",
name: "----- -----",
price: "----",
status: "------- ------"))
total: "----",
status: "------- ------",
items: []))
}
.redacted(reason: .placeholder)
}
Expand All @@ -81,8 +81,14 @@ struct OrdersListView: View {
@ViewBuilder private func dataView(orders: [Order]) -> some View {
List() {
ForEach(orders, id: \.number) { order in
OrderListCard(order: order)
NavigationLink(value: order) {
OrderListCard(order: order)
}
}
.listRowBackground(OrderListCard.background)
}
.navigationDestination(for: Order.self) { order in
OrderDetailView(order: order)
}
}
}
Expand Down Expand Up @@ -131,18 +137,20 @@ struct OrderListCard: View {
Text(order.name)
.font(.body)

Text(order.price)
Text(order.total)
.font(.body)
.bold()

Text(order.status)
.font(.footnote)
.foregroundStyle(Colors.wooPurple20)
}
.listRowBackground(
LinearGradient(gradient: Gradient(colors: [Colors.wooBackgroundStart, Colors.wooBackgroundEnd]), startPoint: .top, endPoint: .bottom)
.cornerRadius(10)
)
.listRowBackground(Self.background)
}

static var background: some View {
LinearGradient(gradient: Gradient(colors: [Colors.wooBackgroundStart, Colors.wooBackgroundEnd]), startPoint: .top, endPoint: .bottom)
.cornerRadius(10)
}
}

Expand Down
43 changes: 37 additions & 6 deletions WooCommerce/Woo Watch App/Orders/OrdersListViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -36,13 +36,23 @@ final class OrdersListViewModel: ObservableObject {
///
private static func viewOrders(from remoteOrders: [Order], currencySettings: CurrencySettings) -> [OrdersListView.Order] {
remoteOrders.map { order in
// TODO: Provide real list of site statuses.
let orderViewModel = OrderListCellViewModel(order: order, status: nil, currencySettings: currencySettings)

let items = order.items.enumerated().map { index, orderItem in
OrdersListView.OrderItem(id: orderItem.itemID,
name: orderItem.name,
total: orderViewModel.total(for: orderItem),
count: orderItem.quantity,
showDivider: index < (order.items.count - 1) )
}

return OrdersListView.Order(date: orderViewModel.dateCreated,
time: orderViewModel.timeCreated,
number: "#\(order.number)",
name: orderViewModel.customerName,
price: orderViewModel.total ?? "$\(order.total)",
status: orderViewModel.statusString.capitalized)
total: orderViewModel.total ?? "$\(order.total)",
status: orderViewModel.statusString.capitalized,
items: items)
}
}
}
Expand All @@ -60,13 +70,34 @@ extension OrdersListView {
case error
}

/// Represents an order item.
/// Represents an order.
///
struct Order {
struct Order: Identifiable, Hashable {
let date: String
let time: String
let number: String
let name: String
let price: String
let total: String
let status: String
let items: [OrderItem]

// SwiftUI ID
var id: String {
number
}

var itemCount: Int {
items.count
}
}

/// Represents an order item.
///
struct OrderItem: Identifiable, Hashable {
let id: Int64
let name: String
let total: String
let count: Decimal
let showDivider: Bool
}
}
4 changes: 4 additions & 0 deletions WooCommerce/WooCommerce.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -970,6 +970,7 @@
26CA47202BFD823200E54348 /* Date+Woo.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5290ED8219B3FA900A6AF7F /* Date+Woo.swift */; };
26CA47212BFD82AE00E54348 /* DateFormatter+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE4DDB7A20DD312400D32EC8 /* DateFormatter+Helpers.swift */; };
26CA47222BFD82B900E54348 /* TimeZone+Woo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 453227B623C4D6EC00D816B3 /* TimeZone+Woo.swift */; };
26CA47242BFE4C1C00E54348 /* OrderDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26CA47232BFE4C1B00E54348 /* OrderDetailView.swift */; };
26CCBE0B2523B3650073F94D /* RefundProductsTotalTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26CCBE0A2523B3650073F94D /* RefundProductsTotalTableViewCell.swift */; };
26CCBE0D2523C2560073F94D /* RefundProductsTotalTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 26CCBE0C2523C2560073F94D /* RefundProductsTotalTableViewCell.xib */; };
26CE6F342B7D4C27008DB858 /* Error+Timeout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26CE6F332B7D4C27008DB858 /* Error+Timeout.swift */; };
Expand Down Expand Up @@ -3789,6 +3790,7 @@
26C98F9729C1246F00F96503 /* WPComSitePlan+FreeTrial.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WPComSitePlan+FreeTrial.swift"; sourceTree = "<group>"; };
26C98F9A29C18ACE00F96503 /* StorePlanBanner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StorePlanBanner.swift; sourceTree = "<group>"; };
26CA2BC52AAA1773003B16C2 /* OrderNotificationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OrderNotificationView.swift; sourceTree = "<group>"; };
26CA47232BFE4C1B00E54348 /* OrderDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OrderDetailView.swift; sourceTree = "<group>"; };
26CCBE0A2523B3650073F94D /* RefundProductsTotalTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RefundProductsTotalTableViewCell.swift; sourceTree = "<group>"; };
26CCBE0C2523C2560073F94D /* RefundProductsTotalTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = RefundProductsTotalTableViewCell.xib; sourceTree = "<group>"; };
26CE6F332B7D4C27008DB858 /* Error+Timeout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Error+Timeout.swift"; sourceTree = "<group>"; };
Expand Down Expand Up @@ -7644,6 +7646,7 @@
26A0B2D62BFBA536002E9620 /* OrdersListView.swift */,
26373E2D2BFD2F46008E6735 /* OrdersListViewModel.swift */,
26373E2B2BFD13E0008E6735 /* OrdersDataService.swift */,
26CA47232BFE4C1B00E54348 /* OrderDetailView.swift */,
);
path = Orders;
sourceTree = "<group>";
Expand Down Expand Up @@ -13651,6 +13654,7 @@
26373E2C2BFD13E0008E6735 /* OrdersDataService.swift in Sources */,
26FFC50C2BED7C5A0067B3A4 /* WatchDependencies.swift in Sources */,
26CA471F2BFD81E900E54348 /* String+Helpers.swift in Sources */,
26CA47242BFE4C1C00E54348 /* OrderDetailView.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down