Skip to content

Commit 4b6fed3

Browse files
authored
Bookings: Update customer filter to support multiple selections (#16291)
2 parents fe18ea4 + b5c3d62 commit 4b6fed3

File tree

16 files changed

+393
-92
lines changed

16 files changed

+393
-92
lines changed

Modules/Sources/Networking/Model/Customer.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import Codegen
44
/// Represents a Customer entity:
55
/// https://woocommerce.github.io/woocommerce-rest-api-docs/#customer-properties
66
///
7-
public struct Customer: Codable, GeneratedCopiable, GeneratedFakeable, Equatable {
7+
public struct Customer: Codable, GeneratedCopiable, GeneratedFakeable, Equatable, Hashable {
88
/// The siteID for the customer
99
public let siteID: Int64
1010

Modules/Sources/Yosemite/Actions/CustomerAction.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,8 @@ public enum CustomerAction: Action {
5050
///- `filter`: Filter to perform the search.
5151
///- `retrieveFullCustomersData`: If `true`, retrieves all customers data one by one after the search request. It will be removed once
5252
/// `betterCustomerSelectionInOrder` is finished for performance reasons.
53-
///- `onCompletion`: Invoked when the operation finishes.
54-
/// - `result.success()`: On success.
53+
///- `onCompletion`: Invoked when the operation finishes. Returns true if there are more customers to be synced in the search results.
54+
/// - `result.success(Bool)`: On success, returns whether there are more pages available.
5555
/// - `result.failure(Error)`: Error fetching data
5656
case searchCustomers(
5757
siteID: Int64,
@@ -63,7 +63,7 @@ public enum CustomerAction: Action {
6363
retrieveFullCustomersData: Bool,
6464
filter: CustomerSearchFilter,
6565
filterEmpty: WCAnalyticsCustomerRemote.FilterEmpty? = nil,
66-
onCompletion: (Result<(), Error>) -> Void)
66+
onCompletion: (Result<Bool, Error>) -> Void)
6767

6868
/// Searches for WCAnalyticsCustomers by keyword and stores the results.
6969
///
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import Foundation
2+
3+
/// Used to filter bookings by customers
4+
///
5+
public struct BookingCustomerFilter: Codable, Hashable {
6+
/// ID of the customer
7+
/// periphery:ignore - to be used later when applying filter
8+
///
9+
public let customerID: Int64
10+
11+
/// Name of the customer
12+
///
13+
public let name: String
14+
15+
public init(customerID: Int64, name: String) {
16+
self.customerID = customerID
17+
self.name = name
18+
}
19+
}

Modules/Sources/Yosemite/Stores/CustomerStore.swift

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ public final class CustomerStore: Store {
9090
/// - Parameters:
9191
/// - siteID: The site for which the array of Customers should be fetched.
9292
/// - keyword: Keyword that we pass to the `?query={keyword}` endpoint to perform the search
93-
/// - onCompletion: Invoked when the operation finishes.
93+
/// - onCompletion: Invoked when the operation finishes. Returns true if there are more pages available.
9494
///
9595
func searchCustomers(
9696
for siteID: Int64,
@@ -102,7 +102,7 @@ public final class CustomerStore: Store {
102102
retrieveFullCustomersData: Bool,
103103
filter: CustomerSearchFilter,
104104
filterEmpty: WCAnalyticsCustomerRemote.FilterEmpty?,
105-
onCompletion: @escaping (Result<(), Error>) -> Void) {
105+
onCompletion: @escaping (Result<Bool, Error>) -> Void) {
106106
wcAnalyticsCustomerRemote.searchCustomers(for: siteID,
107107
pageNumber: pageNumber,
108108
pageSize: pageSize,
@@ -114,15 +114,24 @@ public final class CustomerStore: Store {
114114
guard let self else { return }
115115
switch result {
116116
case .success(let customers):
117+
let hasNextPage = customers.count == pageSize
117118
if retrieveFullCustomersData {
118-
self.mapSearchResultsToCustomerObjects(for: siteID, with: keyword, with: customers, onCompletion: onCompletion)
119+
self.mapSearchResultsToCustomerObjects(for: siteID,
120+
with: keyword,
121+
with: customers,
122+
onCompletion: { result in
123+
switch result {
124+
case .success: onCompletion(.success(hasNextPage))
125+
case .failure(let error): onCompletion(.failure(error))
126+
}
127+
})
119128
} else {
120129
self.upsertCustomersAndSave(siteID: siteID,
121130
readOnlyCustomers: customers,
122131
shouldDeleteExistingCustomers: pageNumber == 1,
123132
keyword: keyword,
124133
onCompletion: {
125-
onCompletion(.success(()))
134+
onCompletion(.success(hasNextPage))
126135
})
127136
}
128137
case .failure(let error):
@@ -262,7 +271,7 @@ public final class CustomerStore: Store {
262271
private func mapSearchResultsToCustomerObjects(for siteID: Int64,
263272
with keyword: String,
264273
with searchResults: [WCAnalyticsCustomer],
265-
onCompletion: @escaping (Result<(), Error>) -> Void) {
274+
onCompletion: @escaping (Result<Void, Error>) -> Void) {
266275
var customers = [Customer]()
267276
let group = DispatchGroup()
268277
for result in searchResults {

Modules/Tests/YosemiteTests/Stores/CustomerStoreTests.swift

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,7 @@ final class CustomerStoreTests: XCTestCase {
143143
network.simulateError(requestUrlSuffix: "", error: expectedError)
144144

145145
// When
146-
let result = waitFor { promise in
146+
let result: Result<Bool, Error> = waitFor { promise in
147147
let action = CustomerAction.searchCustomers(
148148
siteID: self.dummySiteID,
149149
pageNumber: 1,
@@ -172,7 +172,7 @@ final class CustomerStoreTests: XCTestCase {
172172
XCTAssertEqual(viewStorage.countObjects(ofType: Storage.CustomerSearchResult.self), 0)
173173

174174
// When
175-
let response = waitFor { promise in
175+
let response: Result<Bool, Error> = waitFor { promise in
176176
let action = CustomerAction.searchCustomers(siteID: self.dummySiteID,
177177
pageNumber: 1,
178178
pageSize: 25,
@@ -188,6 +188,8 @@ final class CustomerStoreTests: XCTestCase {
188188

189189
// Then
190190
XCTAssertTrue(response.isSuccess)
191+
// Verify hasNextPage is false since we received 2 customers with pageSize 25
192+
XCTAssertEqual(try? response.get(), false)
191193
XCTAssertEqual(viewStorage.countObjects(ofType: Storage.Customer.self), 2)
192194
XCTAssertEqual(viewStorage.countObjects(ofType: Storage.CustomerSearchResult.self), 1)
193195

WooCommerce/Classes/Bookings/BookingFilters/BookableProductListSyncable.swift

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,14 @@ struct BookableProductListSyncable: ListSyncable {
99

1010
let siteID: Int64
1111

12-
var title: String { Localization.title }
12+
let title = Localization.title
1313

14-
var emptyStateMessage: String { Localization.noMembersFound }
14+
let emptyStateMessage = Localization.noMembersFound
15+
let emptyItemTitlePlaceholder: String? = nil
16+
17+
let searchConfiguration: ListSearchConfiguration? = nil
18+
19+
let selectionDisabledMessage: String? = nil
1520

1621
// MARK: - ResultsController Configuration
1722

@@ -46,12 +51,22 @@ struct BookableProductListSyncable: ListSyncable {
4651
)
4752
}
4853

54+
/// Creates the action to search items with keyword
55+
func createSearchAction(keyword: String, pageNumber: Int, pageSize: Int, completion: @escaping (Result<Bool, Error>) -> Void) -> Action {
56+
fatalError("Searching is not supported")
57+
}
58+
4959
// MARK: - Display Configuration
5060

5161
func displayName(for item: Product) -> String {
5262
item.name
5363
}
5464

65+
/// Returns the description for an item
66+
func description(for item: Product) -> String? { nil }
67+
68+
func selectionEnabled(for item: Product) -> Bool { true }
69+
5570
func filterItem(for item: Product) -> BookingProductFilter {
5671
BookingProductFilter(productID: item.productID, name: item.name)
5772
}

WooCommerce/Classes/Bookings/BookingFilters/BookingFiltersViewModel.swift

Lines changed: 20 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ final class BookingFiltersViewModel: FilterListViewModel {
3737
var criteria: Filters {
3838
let teamMembers = (teamMemberFilterViewModel.selectedValue as? MultipleFilterSelection)?.items as? [BookingResource] ?? []
3939
let products = (productFilterViewModel.selectedValue as? MultipleFilterSelection)?.items as? [BookingProductFilter] ?? []
40-
let customer = customerFilterViewModel.selectedValue as? CustomerFilter
40+
let customers = (customerFilterViewModel.selectedValue as? MultipleFilterSelection)?.items as? [BookingCustomerFilter] ?? []
4141
let attendanceStatuses = (attendanceStatusFilterViewModel.selectedValue as? MultipleFilterSelection)?.items as? [BookingAttendanceStatus] ?? []
4242
let paymentStatuses = (paymentStatusFilterViewModel.selectedValue as? MultipleFilterSelection)?.items as? [BookingStatus] ?? []
4343
let dateRange = dateTimeFilterViewModel.selectedValue as? BookingDateRangeFilter
@@ -47,7 +47,7 @@ final class BookingFiltersViewModel: FilterListViewModel {
4747
products: products,
4848
attendanceStatuses: attendanceStatuses,
4949
paymentStatuses: paymentStatuses,
50-
customer: customer,
50+
customers: customers,
5151
dateRange: dateRange,
5252
numberOfActiveFilters: numberOfActiveFilters)
5353
}
@@ -90,7 +90,7 @@ final class BookingFiltersViewModel: FilterListViewModel {
9090
let products: [BookingProductFilter]
9191
let attendanceStatuses: [BookingAttendanceStatus]
9292
let paymentStatuses: [BookingStatus]
93-
let customer: CustomerFilter?
93+
let customers: [BookingCustomerFilter]
9494
let dateRange: BookingDateRangeFilter?
9595

9696
let numberOfActiveFilters: Int
@@ -100,7 +100,7 @@ final class BookingFiltersViewModel: FilterListViewModel {
100100
products = []
101101
attendanceStatuses = []
102102
paymentStatuses = []
103-
customer = nil
103+
customers = []
104104
dateRange = nil
105105
numberOfActiveFilters = 0
106106
}
@@ -109,14 +109,14 @@ final class BookingFiltersViewModel: FilterListViewModel {
109109
products: [BookingProductFilter],
110110
attendanceStatuses: [BookingAttendanceStatus],
111111
paymentStatuses: [BookingStatus],
112-
customer: CustomerFilter?,
112+
customers: [BookingCustomerFilter],
113113
dateRange: BookingDateRangeFilter?,
114114
numberOfActiveFilters: Int) {
115115
self.teamMembers = teamMembers
116116
self.products = products
117117
self.attendanceStatuses = attendanceStatuses
118118
self.paymentStatuses = paymentStatuses
119-
self.customer = customer
119+
self.customers = customers
120120
self.dateRange = dateRange
121121
self.numberOfActiveFilters = numberOfActiveFilters
122122
}
@@ -127,9 +127,8 @@ final class BookingFiltersViewModel: FilterListViewModel {
127127
attendanceStatuses.map { $0.localizedTitle } +
128128
paymentStatuses.map { $0.localizedTitle }
129129

130-
if let customer {
131-
readable.append(customer.description)
132-
}
130+
readable += customers.map { $0.name }
131+
133132
if let dateRange {
134133
readable.append(dateRange.description)
135134
}
@@ -184,8 +183,8 @@ extension BookingFiltersViewModel.BookingListFilter {
184183
selectedValue: MultipleFilterSelection(items: filters.products))
185184
case .customer(let siteID):
186185
return FilterTypeViewModel(title: title,
187-
listSelectorConfig: .customer(siteID: siteID, source: .booking),
188-
selectedValue: filters.customer)
186+
listSelectorConfig: .bookingCustomers(siteID: siteID),
187+
selectedValue: MultipleFilterSelection(items: filters.customers))
189188
case .attendanceStatus:
190189
let options: [BookingAttendanceStatus?] = [.booked, .checkedIn, .cancelled, .noShow]
191190
return FilterTypeViewModel(title: title,
@@ -244,6 +243,14 @@ extension BookingProductFilter: FilterType {
244243
var isActive: Bool { true }
245244
}
246245

246+
extension BookingCustomerFilter: FilterType {
247+
/// The user-facing description of the filter value.
248+
var description: String { name }
249+
250+
/// Whether the filter is set to a non-empty value.
251+
var isActive: Bool { true }
252+
}
253+
247254
extension BookingDateRangeFilter: FilterType {
248255
var description: String {
249256
if let startDate, let endDate {
@@ -314,8 +321,8 @@ private extension BookingFiltersViewModel.BookingListFilter {
314321
comment: "Row title for filtering bookings by product.")
315322

316323
static let rowTitleCustomer = NSLocalizedString(
317-
"bookingFilters.rowTitleCustomer",
318-
value: "Customer name",
324+
"bookingFilters.rowCustomer",
325+
value: "Customer",
319326
comment: "Row title for filtering bookings by customer.")
320327

321328
static let rowTitleAttendanceStatus = NSLocalizedString(

0 commit comments

Comments
 (0)