Skip to content

Commit 4e1797b

Browse files
[Bookings] Customer data for Booking Details (#16214)
2 parents d4900bb + 7c2634e commit 4e1797b

File tree

10 files changed

+436
-119
lines changed

10 files changed

+436
-119
lines changed

Modules/Sources/Yosemite/Actions/CustomerAction.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,4 +100,7 @@ public enum CustomerAction: Action {
100100
///- `siteID`: The site for which customers should be delete.
101101
///- `onCompletion`: Invoked when the operation finishes.
102102
case deleteAllCustomers(siteID: Int64, onCompletion: () -> Void)
103+
104+
/// Loads a customer for the specified `siteID` and `customerID` from storage.
105+
case loadCustomer(siteID: Int64, customerID: Int64, onCompletion: (Result<Customer, Error>) -> Void)
103106
}

Modules/Sources/Yosemite/Stores/CustomerStore.swift

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,8 @@ public final class CustomerStore: Store {
8080
synchronizeAllCustomers(siteID: siteID, pageNumber: pageNumber, pageSize: pageSize, onCompletion: onCompletion)
8181
case .deleteAllCustomers(siteID: let siteID, onCompletion: let onCompletion):
8282
deleteAllCustomers(from: siteID, onCompletion: onCompletion)
83+
case let .loadCustomer(siteID, customerID, onCompletion):
84+
loadCustomer(siteID: siteID, customerID: customerID, onCompletion: onCompletion)
8385
}
8486
}
8587

@@ -251,6 +253,16 @@ public final class CustomerStore: Store {
251253
}, completion: onCompletion, on: .main)
252254
}
253255

256+
private func loadCustomer(siteID: Int64, customerID: Int64, onCompletion: @escaping (Result<Networking.Customer, Error>) -> Void) {
257+
let customers = storageManager.viewStorage.loadCustomers(siteID: siteID, matching: [customerID])
258+
if let storageCustomer = customers.first {
259+
let customer = storageCustomer.toReadOnly()
260+
onCompletion(.success(customer))
261+
} else {
262+
onCompletion(.failure(CustomerStoreError.notFound))
263+
}
264+
}
265+
254266
/// Maps CustomerSearchResult to Customer objects
255267
///
256268
/// - Parameters:
@@ -429,3 +441,9 @@ private extension CustomerStore {
429441
storageCustomer.update(with: readOnlyCustomer)
430442
}
431443
}
444+
445+
// MARK: - Errors
446+
447+
enum CustomerStoreError: Error {
448+
case notFound
449+
}

WooCommerce/Classes/View Modifiers/View+Tappable.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ private struct TappableViewModifier: ViewModifier {
88
onTap()
99
} label: {
1010
content
11+
.contentShape(Rectangle())
1112
}
1213
.buttonStyle(.plain)
1314
}

WooCommerce/Classes/ViewModels/Booking Details/BookingDetailsViewModel.swift

Lines changed: 100 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,25 @@
11
import Foundation
2-
import struct Networking.Booking
2+
import Yosemite
3+
import SwiftUI // Added for withAnimation
34

45
final class BookingDetailsViewModel: ObservableObject {
5-
let sections: [Section]
6-
let navigationTitle: String
6+
private let stores: StoresManager
77

88
private let booking: Booking
9+
private let customerContent = CustomerContent()
910

10-
init(booking: Booking) {
11-
self.booking = booking
11+
let navigationTitle: String
12+
@Published private(set) var sections: [Section] = []
1213

14+
init(booking: Booking, stores: StoresManager = ServiceLocator.stores) {
15+
self.booking = booking
16+
self.stores = stores
1317
navigationTitle = Self.navigationTitle(for: booking)
18+
setupSections()
19+
}
1420

15-
let headerSection = Section.init(
21+
private func setupSections() {
22+
let headerSection = Section(
1623
content: .header(HeaderContent(booking))
1724
)
1825

@@ -27,21 +34,6 @@ final class BookingDetailsViewModel: ObservableObject {
2734
content: .attendance(AttendanceContent())
2835
)
2936

30-
let customerSection = Section(
31-
header: .title(Localization.customerSectionHeaderTitle.uppercased()),
32-
content: .customer(
33-
/// Temporary hardcode
34-
CustomerContent(
35-
nameText: "Margarita Nikolaevna",
36-
emailText: "[email protected]",
37-
phoneText: "+1 742582943798",
38-
billingAddressText: """
39-
238 Willow Creek Drive
Montgomery
AL 36109
40-
"""
41-
)
42-
)
43-
)
44-
4537
let paymentSection = Section(
4638
header: .title(Localization.paymentSectionHeaderTitle.uppercased()),
4739
content: .payment(PaymentContent(booking: booking))
@@ -55,14 +47,100 @@ final class BookingDetailsViewModel: ObservableObject {
5547
sections = [
5648
headerSection,
5749
appointmentDetailsSection,
58-
customerSection,
5950
attendanceSection,
6051
paymentSection,
6152
bookingNotes
6253
]
6354
}
6455
}
6556

57+
// MARK: Local Data
58+
59+
extension BookingDetailsViewModel {
60+
func loadLocalData() {
61+
loadCustomerData()
62+
}
63+
}
64+
65+
private extension BookingDetailsViewModel {
66+
func loadCustomerData() {
67+
guard booking.customerID > 0 else {
68+
return
69+
}
70+
71+
let action = CustomerAction.loadCustomer(siteID: booking.siteID, customerID: booking.customerID) { [weak self] result in
72+
guard let self = self else { return }
73+
if case .success(let customer) = result {
74+
self.updateCustomerSection(with: customer)
75+
}
76+
}
77+
stores.dispatch(action)
78+
}
79+
}
80+
81+
// MARK: Syncing
82+
83+
extension BookingDetailsViewModel {
84+
func syncData() async {
85+
await syncCustomer()
86+
}
87+
}
88+
89+
private extension BookingDetailsViewModel {
90+
func syncCustomer() async {
91+
guard shouldSyncCustomer else {
92+
return
93+
}
94+
95+
do {
96+
let fetchedCustomer = try await retrieveCustomer()
97+
updateCustomerSection(with: fetchedCustomer)
98+
} catch {
99+
DDLogError("⛔️ Error synchronizing Customer for Booking: \(error)")
100+
}
101+
}
102+
103+
@MainActor
104+
func retrieveCustomer() async throws -> Customer {
105+
try await withCheckedThrowingContinuation { continuation in
106+
let action = CustomerAction.retrieveCustomer(
107+
siteID: booking.siteID,
108+
customerID: booking.customerID
109+
) { result in
110+
switch result {
111+
case .success(let customer):
112+
continuation.resume(returning: customer)
113+
case .failure(let error):
114+
continuation.resume(throwing: error)
115+
}
116+
}
117+
stores.dispatch(action)
118+
}
119+
}
120+
121+
func updateCustomerSection(with customer: Customer) {
122+
customerContent.update(with: customer)
123+
124+
// Avoid adding if it already exists
125+
guard !sections.contains(where: { if case .customer = $0.content { return true } else { return false } }) else {
126+
return
127+
}
128+
129+
let customerSection = Section(
130+
header: .title(Localization.customerSectionHeaderTitle.uppercased()),
131+
content: .customer(customerContent)
132+
)
133+
withAnimation {
134+
sections.insert(customerSection, at: 2)
135+
}
136+
}
137+
138+
/// Returns true when the `customerID` is non-zero and customer section doesn't exist
139+
var shouldSyncCustomer: Bool {
140+
return booking.customerID > 0
141+
}
142+
}
143+
66144
extension BookingDetailsViewModel {
67145
var cancellationAlertMessage: String {
68146
// Temporary hardcoded
Lines changed: 37 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,42 @@
11
import Foundation
2+
import Networking
23

34
extension BookingDetailsViewModel {
4-
struct CustomerContent {
5-
let nameText: String
6-
let emailText: String
7-
let phoneText: String
8-
let billingAddressText: String?
5+
final class CustomerContent: ObservableObject {
6+
@Published var nameText: String?
7+
@Published var emailText: String?
8+
@Published var phoneText: String?
9+
@Published var billingAddressText: String?
10+
11+
func update(with customer: Customer) {
12+
let name = [
13+
customer.firstName,
14+
customer.lastName
15+
]
16+
.compactMap { $0 }
17+
.filter { !$0.isEmpty }
18+
.joined(separator: " ")
19+
20+
let billingAddress = customer.billing.flatMap(formatAddress)
21+
22+
nameText = name
23+
emailText = customer.email
24+
phoneText = customer.billing?.phone ?? ""
25+
billingAddressText = billingAddress
26+
}
27+
28+
private func formatAddress(_ address: Address) -> String {
29+
[
30+
address.address1,
31+
address.address2,
32+
address.city,
33+
address.state,
34+
address.postcode,
35+
address.country
36+
]
37+
.compactMap { $0 }
38+
.filter { !$0.isEmpty }
39+
.joined(separator: "\n")
40+
}
941
}
1042
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import SwiftUI
2+
3+
private extension BookingDetailsView {
4+
struct RowTextStyle: ViewModifier {
5+
func body(content: Content) -> some View {
6+
content
7+
.font(TextFont.bodyMedium)
8+
.foregroundStyle(.primary)
9+
.multilineTextAlignment(.leading)
10+
}
11+
}
12+
}
13+
14+
extension View {
15+
func rowTextStyle() -> some View {
16+
self.modifier(BookingDetailsView.RowTextStyle())
17+
}
18+
}

0 commit comments

Comments
 (0)