Skip to content

Commit e712965

Browse files
authored
Bookings: Integrate filter API for booking list (#16302)
2 parents 89d3065 + baa0954 commit e712965

File tree

14 files changed

+274
-127
lines changed

14 files changed

+274
-127
lines changed

Modules/Sources/Networking/Remote/BookingsRemote.swift

Lines changed: 65 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,7 @@ public protocol BookingsRemoteProtocol {
99
func loadAllBookings(for siteID: Int64,
1010
pageNumber: Int,
1111
pageSize: Int,
12-
startDateBefore: String?,
13-
startDateAfter: String?,
12+
filters: BookingFilters?,
1413
searchQuery: String?,
1514
order: BookingsRemote.Order) async throws -> [Booking]
1615

@@ -31,6 +30,35 @@ public protocol BookingsRemoteProtocol {
3130
pageSize: Int) async throws -> [BookingResource]
3231
}
3332

33+
/// Filters for booking queries
34+
public struct BookingFilters {
35+
public let productIDs: [Int64]
36+
public let customerIDs: [Int64]
37+
public let resourceIDs: [Int64]
38+
public let startDateBefore: String?
39+
public let startDateAfter: String?
40+
public let bookingStatuses: [String]
41+
public let attendanceStatuses: [String]
42+
43+
public init(
44+
productIDs: [Int64] = [],
45+
customerIDs: [Int64] = [],
46+
resourceIDs: [Int64] = [],
47+
startDateBefore: String? = nil,
48+
startDateAfter: String? = nil,
49+
bookingStatuses: [String] = [],
50+
attendanceStatuses: [String] = []
51+
) {
52+
self.productIDs = productIDs
53+
self.customerIDs = customerIDs
54+
self.resourceIDs = resourceIDs
55+
self.startDateBefore = startDateBefore
56+
self.startDateAfter = startDateAfter
57+
self.bookingStatuses = bookingStatuses
58+
self.attendanceStatuses = attendanceStatuses
59+
}
60+
}
61+
3462
/// Booking: Remote Endpoints
3563
///
3664
public final class BookingsRemote: Remote, BookingsRemoteProtocol {
@@ -43,30 +71,51 @@ public final class BookingsRemote: Remote, BookingsRemoteProtocol {
4371
/// - siteID: Site for which we'll fetch remote bookings.
4472
/// - pageNumber: Number of page that should be retrieved.
4573
/// - pageSize: Number of bookings to be retrieved per page.
46-
/// - startDateBefore: Filter bookings with start date before this timestamp.
47-
/// - startDateAfter: Filter bookings with start date after this timestamp.
74+
/// - filters: Optional filters for bookings (products, customers, resources, dates, statuses).
4875
/// - searchQuery: Search query to filter bookings.
4976
/// - order: Sort order for bookings (ascending or descending).
5077
///
5178
public func loadAllBookings(for siteID: Int64,
5279
pageNumber: Int = Default.pageNumber,
5380
pageSize: Int = Default.pageSize,
54-
startDateBefore: String? = nil,
55-
startDateAfter: String? = nil,
81+
filters: BookingFilters? = nil,
5682
searchQuery: String? = nil,
5783
order: Order) async throws -> [Booking] {
58-
var parameters = [
84+
var parameters: [String: Any] = [
5985
ParameterKey.page: String(pageNumber),
6086
ParameterKey.perPage: String(pageSize),
6187
ParameterKey.order: order.rawValue
6288
]
6389

64-
if let startDateBefore = startDateBefore {
65-
parameters[ParameterKey.startDateBefore] = startDateBefore
66-
}
90+
// Apply filters if provided
91+
if let filters {
92+
if filters.productIDs.isNotEmpty {
93+
parameters[ParameterKey.product] = filters.productIDs.map(String.init)
94+
}
95+
96+
if filters.customerIDs.isNotEmpty {
97+
parameters[ParameterKey.customer] = filters.customerIDs.map(String.init)
98+
}
99+
100+
if filters.resourceIDs.isNotEmpty {
101+
parameters[ParameterKey.resource] = filters.resourceIDs.map(String.init)
102+
}
103+
104+
if let startDateBefore = filters.startDateBefore {
105+
parameters[ParameterKey.startDateBefore] = startDateBefore
106+
}
107+
108+
if let startDateAfter = filters.startDateAfter {
109+
parameters[ParameterKey.startDateAfter] = startDateAfter
110+
}
111+
112+
if filters.bookingStatuses.isNotEmpty {
113+
parameters[ParameterKey.bookingStatus] = filters.bookingStatuses
114+
}
67115

68-
if let startDateAfter = startDateAfter {
69-
parameters[ParameterKey.startDateAfter] = startDateAfter
116+
if filters.attendanceStatuses.isNotEmpty {
117+
parameters[ParameterKey.attendanceStatus] = filters.attendanceStatuses
118+
}
70119
}
71120

72121
if let searchQuery = searchQuery, !searchQuery.isEmpty {
@@ -195,6 +244,10 @@ public extension BookingsRemote {
195244
static let startDateAfter: String = "start_date_after"
196245
static let search: String = "search"
197246
static let order: String = "order"
247+
static let product: String = "product"
248+
static let customer: String = "customer"
249+
static let resource: String = "resource"
250+
static let bookingStatus: String = "booking_status"
198251
static let attendanceStatus = "attendance_status"
199252
}
200253
}

Modules/Sources/Yosemite/Actions/BookingAction.swift

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,7 @@ public enum BookingAction: Action {
1313
case synchronizeBookings(siteID: Int64,
1414
pageNumber: Int,
1515
pageSize: Int = BookingsRemote.Default.pageSize,
16-
startDateBefore: String? = nil,
17-
startDateAfter: String? = nil,
16+
filters: BookingFilters? = nil,
1817
order: BookingsRemote.Order = .descending,
1918
shouldClearCache: Bool = false,
2019
onCompletion: (Result<Bool, Error>) -> Void)
@@ -40,8 +39,7 @@ public enum BookingAction: Action {
4039
searchQuery: String,
4140
pageNumber: Int,
4241
pageSize: Int = BookingsRemote.Default.pageSize,
43-
startDateBefore: String? = nil,
44-
startDateAfter: String? = nil,
42+
filters: BookingFilters? = nil,
4543
order: BookingsRemote.Order = .descending,
4644
onCompletion: (Result<[Booking], Error>) -> Void)
4745

Modules/Sources/Yosemite/Model/Model.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ public typealias BlazeTargetOptions = Networking.BlazeTargetOptions
2626
public typealias BlazeTargetLocation = Networking.BlazeTargetLocation
2727
public typealias BlazeTargetTopic = Networking.BlazeTargetTopic
2828
public typealias Booking = Networking.Booking
29+
public typealias BookingFilters = Networking.BookingFilters
2930
public typealias BookingStatus = Networking.BookingStatus
3031
public typealias BookingOrderInfo = Networking.BookingOrderInfo
3132
public typealias BookingCustomerInfo = Networking.BookingCustomerInfo

Modules/Sources/Yosemite/Stores/BookingStore.swift

Lines changed: 9 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -39,26 +39,24 @@ public class BookingStore: Store {
3939
}
4040

4141
switch action {
42-
case let .synchronizeBookings(siteID, pageNumber, pageSize, startDateBefore, startDateAfter, order, shouldClearCache, onCompletion):
42+
case let .synchronizeBookings(siteID, pageNumber, pageSize, filters, order, shouldClearCache, onCompletion):
4343
synchronizeBookings(siteID: siteID,
4444
pageNumber: pageNumber,
4545
pageSize: pageSize,
46-
startDateBefore: startDateBefore,
47-
startDateAfter: startDateAfter,
46+
filters: filters,
4847
order: order,
4948
shouldClearCache: shouldClearCache,
5049
onCompletion: onCompletion)
5150
case .synchronizeBooking(siteID: let siteID, bookingID: let bookingID, onCompletion: let onCompletion):
5251
synchronizeBooking(siteID: siteID, bookingID: bookingID, onCompletion: onCompletion)
5352
case let .checkIfStoreHasBookings(siteID, onCompletion):
5453
checkIfStoreHasBookings(siteID: siteID, onCompletion: onCompletion)
55-
case let .searchBookings(siteID, searchQuery, pageNumber, pageSize, startDateBefore, startDateAfter, order, onCompletion):
54+
case let .searchBookings(siteID, searchQuery, pageNumber, pageSize, filters, order, onCompletion):
5655
searchBookings(siteID: siteID,
5756
searchQuery: searchQuery,
5857
pageNumber: pageNumber,
5958
pageSize: pageSize,
60-
startDateBefore: startDateBefore,
61-
startDateAfter: startDateAfter,
59+
filters: filters,
6260
order: order,
6361
onCompletion: onCompletion)
6462
case let .fetchResource(siteID, resourceID, onCompletion):
@@ -85,8 +83,7 @@ private extension BookingStore {
8583
func synchronizeBookings(siteID: Int64,
8684
pageNumber: Int,
8785
pageSize: Int,
88-
startDateBefore: String?,
89-
startDateAfter: String?,
86+
filters: BookingFilters?,
9087
order: BookingsRemote.Order,
9188
shouldClearCache: Bool,
9289
onCompletion: @escaping (Result<Bool, Error>) -> Void) {
@@ -95,8 +92,7 @@ private extension BookingStore {
9592
let bookings = try await remote.loadAllBookings(for: siteID,
9693
pageNumber: pageNumber,
9794
pageSize: pageSize,
98-
startDateBefore: startDateBefore,
99-
startDateAfter: startDateAfter,
95+
filters: filters,
10096
searchQuery: nil,
10197
order: order)
10298

@@ -175,8 +171,7 @@ private extension BookingStore {
175171
let bookings = try await remote.loadAllBookings(for: siteID,
176172
pageNumber: 1,
177173
pageSize: 1,
178-
startDateBefore: nil,
179-
startDateAfter: nil,
174+
filters: nil,
180175
searchQuery: nil,
181176
order: .descending)
182177
let hasRemoteBookings = !bookings.isEmpty
@@ -194,17 +189,15 @@ private extension BookingStore {
194189
searchQuery: String,
195190
pageNumber: Int,
196191
pageSize: Int,
197-
startDateBefore: String?,
198-
startDateAfter: String?,
192+
filters: BookingFilters?,
199193
order: BookingsRemote.Order,
200194
onCompletion: @escaping (Result<[Booking], Error>) -> Void) {
201195
Task { @MainActor in
202196
do {
203197
let bookings = try await remote.loadAllBookings(for: siteID,
204198
pageNumber: pageNumber,
205199
pageSize: pageSize,
206-
startDateBefore: startDateBefore,
207-
startDateAfter: startDateAfter,
200+
filters: filters,
208201
searchQuery: searchQuery,
209202
order: order)
210203
let orders = try await ordersRemote.loadOrders(
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import CoreData
2+
3+
extension NSPredicate {
4+
public static func createBookingPredicate(siteID: Int64, filters: BookingFilters) -> NSPredicate {
5+
let siteIDPredicate = NSPredicate(format: "siteID == %lld", siteID)
6+
7+
let productIDsPredicate = filters.productIDs.isNotEmpty ? NSPredicate(format: "productID IN %@", filters.productIDs) : nil
8+
9+
let customerIDsPredicate = filters.customerIDs.isNotEmpty ? NSPredicate(format: "customerID IN %@", filters.customerIDs) : nil
10+
11+
let resourceIDsPredicate = filters.resourceIDs.isNotEmpty ? NSPredicate(format: "resourceID IN %@", filters.resourceIDs) : nil
12+
13+
let startDateBeforePredicate = filters.startDateBefore.flatMap { dateString -> NSPredicate? in
14+
guard let date = ISO8601DateFormatter().date(from: dateString) else { return nil }
15+
return NSPredicate(format: "startDate < %@", date as NSDate)
16+
}
17+
18+
let startDateAfterPredicate = filters.startDateAfter.flatMap { dateString -> NSPredicate? in
19+
guard let date = ISO8601DateFormatter().date(from: dateString) else { return nil }
20+
return NSPredicate(format: "startDate > %@", date as NSDate)
21+
}
22+
23+
let bookingStatusesPredicate = filters.bookingStatuses.isNotEmpty ? NSPredicate(format: "statusKey IN %@", filters.bookingStatuses) : nil
24+
25+
let attendanceStatusesPredicate = filters.attendanceStatuses.isNotEmpty ?
26+
NSPredicate(format: "attendanceStatusKey IN %@", filters.attendanceStatuses) : nil
27+
28+
let subpredicates = [
29+
siteIDPredicate,
30+
productIDsPredicate,
31+
customerIDsPredicate,
32+
resourceIDsPredicate,
33+
startDateBeforePredicate,
34+
startDateAfterPredicate,
35+
bookingStatusesPredicate,
36+
attendanceStatusesPredicate
37+
].compactMap({ $0 })
38+
39+
return NSCompoundPredicate(andPredicateWithSubpredicates: subpredicates)
40+
}
41+
}
42+
43+
extension ResultsController where T: StorageBooking {
44+
public func updatePredicate(siteID: Int64, filters: BookingFilters) {
45+
self.predicate = NSPredicate.createBookingPredicate(siteID: siteID, filters: filters)
46+
}
47+
}

Modules/Tests/NetworkingTests/Remote/BookingsRemoteTests.swift

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -44,14 +44,17 @@ struct BookingsRemoteTests {
4444
let startDateBefore = "2024-12-31T23:59:59"
4545
let startDateAfter = "2024-01-01T00:00:00"
4646
let searchQuery = "test search"
47+
let filters = BookingFilters(
48+
startDateBefore: startDateBefore,
49+
startDateAfter: startDateAfter
50+
)
4751
network.simulateResponse(requestUrlSuffix: "bookings", filename: "booking-list")
4852

4953
// When
5054
_ = try await remote.loadAllBookings(for: sampleSiteID,
5155
pageNumber: 2,
5256
pageSize: 50,
53-
startDateBefore: startDateBefore,
54-
startDateAfter: startDateAfter,
57+
filters: filters,
5558
searchQuery: searchQuery,
5659
order: .ascending)
5760

@@ -74,8 +77,7 @@ struct BookingsRemoteTests {
7477

7578
// When
7679
_ = try await remote.loadAllBookings(for: sampleSiteID,
77-
startDateBefore: nil,
78-
startDateAfter: nil,
80+
filters: nil,
7981
searchQuery: nil,
8082
order: .descending)
8183

Modules/Tests/YosemiteTests/Mocks/MockBookingsRemote.swift

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,7 @@ final class MockBookingsRemote: BookingsRemoteProtocol {
3333
func loadAllBookings(for siteID: Int64,
3434
pageNumber: Int,
3535
pageSize: Int,
36-
startDateBefore: String?,
37-
startDateAfter: String?,
36+
filters: BookingFilters?,
3837
searchQuery: String?,
3938
order: BookingsRemote.Order) async throws -> [Booking] {
4039
guard let result = loadAllBookingsResult else {

WooCommerce/Classes/Bookings/BookingFilters/BookingDateTimeFilterView.swift

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,10 +63,22 @@ struct BookingDateTimeFilterView: View {
6363
selectedToDate = newValue
6464
}
6565
.onChange(of: selectedFromDate) { _, newValue in
66-
onSelection(newValue, selectedToDate)
66+
guard let newValue else {
67+
return onSelection(nil, selectedToDate)
68+
}
69+
/// Bookings backend treats dates as local time with no time zone.
70+
/// Convert the date to keep the selected components but with UTC as time zone.
71+
let convertedDate = convertToUTCDate(newValue)
72+
onSelection(convertedDate, selectedToDate)
6773
}
6874
.onChange(of: selectedToDate) { _, newValue in
69-
onSelection(selectedFromDate, newValue)
75+
guard let newValue else {
76+
return onSelection(selectedFromDate, nil)
77+
}
78+
/// Bookings backend treats dates as local time with no time zone.
79+
/// Convert the date to keep the selected components but with UTC as time zone.
80+
let convertedDate = convertToUTCDate(newValue)
81+
onSelection(selectedFromDate, convertedDate)
7082
}
7183
}
7284
}
@@ -156,6 +168,19 @@ private extension BookingDateTimeFilterView {
156168
return fromDate...Date.distantFuture
157169
}
158170
}
171+
172+
/// Converts a date by extracting its components in the local timezone
173+
/// and reconstructing a new date with those same components in UTC.
174+
/// This effectively treats the selected date/time as if it were in UTC.
175+
func convertToUTCDate(_ date: Date) -> Date {
176+
let localCalendar = Calendar.current
177+
let components = localCalendar.dateComponents([.year, .month, .day, .hour, .minute, .second], from: date)
178+
179+
var utcCalendar = Calendar(identifier: .gregorian)
180+
utcCalendar.timeZone = TimeZone(identifier: "UTC")!
181+
182+
return utcCalendar.date(from: components) ?? date
183+
}
159184
}
160185

161186
private extension BookingDateTimeFilterView {

WooCommerce/Classes/Bookings/BookingFilters/BookingFiltersViewModel.swift

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,18 @@ final class BookingFiltersViewModel: FilterListViewModel {
135135

136136
return readable.joined(separator: ", ")
137137
}
138+
139+
var bookingFilters: BookingFilters {
140+
BookingFilters(
141+
productIDs: products.map { $0.productID },
142+
customerIDs: customers.map { $0.customerID },
143+
resourceIDs: teamMembers.map { $0.resourceID },
144+
startDateBefore: dateRange?.endDate?.ISO8601Format(),
145+
startDateAfter: dateRange?.startDate?.ISO8601Format(),
146+
bookingStatuses: paymentStatuses.map { $0.rawValue },
147+
attendanceStatuses: attendanceStatuses.map { $0.rawValue }
148+
)
149+
}
138150
}
139151
}
140152

WooCommerce/Classes/Bookings/BookingList/BookingListContainerViewModel.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,8 @@ final class BookingListContainerViewModel: ObservableObject {
108108
func updateFilters(_ filters: BookingFiltersViewModel.Filters) {
109109
self.filters = filters
110110
self.numberOfActiveFilters = filters.numberOfActiveFilters
111-
// TODO: Apply filters to All tab
111+
allListViewModel.updateFilters(filters)
112+
allSearchViewModel.updateFilters(filters)
112113
}
113114
}
114115

0 commit comments

Comments
 (0)