Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 65 additions & 12 deletions Modules/Sources/Networking/Remote/BookingsRemote.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,7 @@ public protocol BookingsRemoteProtocol {
func loadAllBookings(for siteID: Int64,
pageNumber: Int,
pageSize: Int,
startDateBefore: String?,
startDateAfter: String?,
filters: BookingFilters?,
searchQuery: String?,
order: BookingsRemote.Order) async throws -> [Booking]

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

/// Filters for booking queries
public struct BookingFilters {
public let productIDs: [Int64]
public let customerIDs: [Int64]
public let resourceIDs: [Int64]
public let startDateBefore: String?
public let startDateAfter: String?
public let bookingStatuses: [String]
public let attendanceStatuses: [String]

public init(
productIDs: [Int64] = [],
customerIDs: [Int64] = [],
resourceIDs: [Int64] = [],
startDateBefore: String? = nil,
startDateAfter: String? = nil,
bookingStatuses: [String] = [],
attendanceStatuses: [String] = []
) {
self.productIDs = productIDs
self.customerIDs = customerIDs
self.resourceIDs = resourceIDs
self.startDateBefore = startDateBefore
self.startDateAfter = startDateAfter
self.bookingStatuses = bookingStatuses
self.attendanceStatuses = attendanceStatuses
}
}

/// Booking: Remote Endpoints
///
public final class BookingsRemote: Remote, BookingsRemoteProtocol {
Expand All @@ -43,30 +71,51 @@ public final class BookingsRemote: Remote, BookingsRemoteProtocol {
/// - siteID: Site for which we'll fetch remote bookings.
/// - pageNumber: Number of page that should be retrieved.
/// - pageSize: Number of bookings to be retrieved per page.
/// - startDateBefore: Filter bookings with start date before this timestamp.
/// - startDateAfter: Filter bookings with start date after this timestamp.
/// - filters: Optional filters for bookings (products, customers, resources, dates, statuses).
/// - searchQuery: Search query to filter bookings.
/// - order: Sort order for bookings (ascending or descending).
///
public func loadAllBookings(for siteID: Int64,
pageNumber: Int = Default.pageNumber,
pageSize: Int = Default.pageSize,
startDateBefore: String? = nil,
startDateAfter: String? = nil,
filters: BookingFilters? = nil,
searchQuery: String? = nil,
order: Order) async throws -> [Booking] {
var parameters = [
var parameters: [String: Any] = [
ParameterKey.page: String(pageNumber),
ParameterKey.perPage: String(pageSize),
ParameterKey.order: order.rawValue
]

if let startDateBefore = startDateBefore {
parameters[ParameterKey.startDateBefore] = startDateBefore
}
// Apply filters if provided
if let filters {
if filters.productIDs.isNotEmpty {
parameters[ParameterKey.product] = filters.productIDs.map(String.init)
}

if filters.customerIDs.isNotEmpty {
parameters[ParameterKey.customer] = filters.customerIDs.map(String.init)
}

if filters.resourceIDs.isNotEmpty {
parameters[ParameterKey.resource] = filters.resourceIDs.map(String.init)
}

if let startDateBefore = filters.startDateBefore {
parameters[ParameterKey.startDateBefore] = startDateBefore
}

if let startDateAfter = filters.startDateAfter {
parameters[ParameterKey.startDateAfter] = startDateAfter
}

if filters.bookingStatuses.isNotEmpty {
parameters[ParameterKey.bookingStatus] = filters.bookingStatuses
}

if let startDateAfter = startDateAfter {
parameters[ParameterKey.startDateAfter] = startDateAfter
if filters.attendanceStatuses.isNotEmpty {
parameters[ParameterKey.attendanceStatus] = filters.attendanceStatuses
}
}

if let searchQuery = searchQuery, !searchQuery.isEmpty {
Expand Down Expand Up @@ -195,6 +244,10 @@ public extension BookingsRemote {
static let startDateAfter: String = "start_date_after"
static let search: String = "search"
static let order: String = "order"
static let product: String = "product"
static let customer: String = "customer"
static let resource: String = "resource"
static let bookingStatus: String = "booking_status"
static let attendanceStatus = "attendance_status"
}
}
6 changes: 2 additions & 4 deletions Modules/Sources/Yosemite/Actions/BookingAction.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,7 @@ public enum BookingAction: Action {
case synchronizeBookings(siteID: Int64,
pageNumber: Int,
pageSize: Int = BookingsRemote.Default.pageSize,
startDateBefore: String? = nil,
startDateAfter: String? = nil,
filters: BookingFilters? = nil,
order: BookingsRemote.Order = .descending,
shouldClearCache: Bool = false,
onCompletion: (Result<Bool, Error>) -> Void)
Expand All @@ -40,8 +39,7 @@ public enum BookingAction: Action {
searchQuery: String,
pageNumber: Int,
pageSize: Int = BookingsRemote.Default.pageSize,
startDateBefore: String? = nil,
startDateAfter: String? = nil,
filters: BookingFilters? = nil,
order: BookingsRemote.Order = .descending,
onCompletion: (Result<[Booking], Error>) -> Void)

Expand Down
1 change: 1 addition & 0 deletions Modules/Sources/Yosemite/Model/Model.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ public typealias BlazeTargetOptions = Networking.BlazeTargetOptions
public typealias BlazeTargetLocation = Networking.BlazeTargetLocation
public typealias BlazeTargetTopic = Networking.BlazeTargetTopic
public typealias Booking = Networking.Booking
public typealias BookingFilters = Networking.BookingFilters
public typealias BookingStatus = Networking.BookingStatus
public typealias BookingOrderInfo = Networking.BookingOrderInfo
public typealias BookingCustomerInfo = Networking.BookingCustomerInfo
Expand Down
25 changes: 9 additions & 16 deletions Modules/Sources/Yosemite/Stores/BookingStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -39,26 +39,24 @@ public class BookingStore: Store {
}

switch action {
case let .synchronizeBookings(siteID, pageNumber, pageSize, startDateBefore, startDateAfter, order, shouldClearCache, onCompletion):
case let .synchronizeBookings(siteID, pageNumber, pageSize, filters, order, shouldClearCache, onCompletion):
synchronizeBookings(siteID: siteID,
pageNumber: pageNumber,
pageSize: pageSize,
startDateBefore: startDateBefore,
startDateAfter: startDateAfter,
filters: filters,
order: order,
shouldClearCache: shouldClearCache,
onCompletion: onCompletion)
case .synchronizeBooking(siteID: let siteID, bookingID: let bookingID, onCompletion: let onCompletion):
synchronizeBooking(siteID: siteID, bookingID: bookingID, onCompletion: onCompletion)
case let .checkIfStoreHasBookings(siteID, onCompletion):
checkIfStoreHasBookings(siteID: siteID, onCompletion: onCompletion)
case let .searchBookings(siteID, searchQuery, pageNumber, pageSize, startDateBefore, startDateAfter, order, onCompletion):
case let .searchBookings(siteID, searchQuery, pageNumber, pageSize, filters, order, onCompletion):
searchBookings(siteID: siteID,
searchQuery: searchQuery,
pageNumber: pageNumber,
pageSize: pageSize,
startDateBefore: startDateBefore,
startDateAfter: startDateAfter,
filters: filters,
order: order,
onCompletion: onCompletion)
case let .fetchResource(siteID, resourceID, onCompletion):
Expand All @@ -85,8 +83,7 @@ private extension BookingStore {
func synchronizeBookings(siteID: Int64,
pageNumber: Int,
pageSize: Int,
startDateBefore: String?,
startDateAfter: String?,
filters: BookingFilters?,
order: BookingsRemote.Order,
shouldClearCache: Bool,
onCompletion: @escaping (Result<Bool, Error>) -> Void) {
Expand All @@ -95,8 +92,7 @@ private extension BookingStore {
let bookings = try await remote.loadAllBookings(for: siteID,
pageNumber: pageNumber,
pageSize: pageSize,
startDateBefore: startDateBefore,
startDateAfter: startDateAfter,
filters: filters,
searchQuery: nil,
order: order)

Expand Down Expand Up @@ -175,8 +171,7 @@ private extension BookingStore {
let bookings = try await remote.loadAllBookings(for: siteID,
pageNumber: 1,
pageSize: 1,
startDateBefore: nil,
startDateAfter: nil,
filters: nil,
searchQuery: nil,
order: .descending)
let hasRemoteBookings = !bookings.isEmpty
Expand All @@ -194,17 +189,15 @@ private extension BookingStore {
searchQuery: String,
pageNumber: Int,
pageSize: Int,
startDateBefore: String?,
startDateAfter: String?,
filters: BookingFilters?,
order: BookingsRemote.Order,
onCompletion: @escaping (Result<[Booking], Error>) -> Void) {
Task { @MainActor in
do {
let bookings = try await remote.loadAllBookings(for: siteID,
pageNumber: pageNumber,
pageSize: pageSize,
startDateBefore: startDateBefore,
startDateAfter: startDateAfter,
filters: filters,
searchQuery: searchQuery,
order: order)
let orders = try await ordersRemote.loadOrders(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import CoreData

extension NSPredicate {
public static func createBookingPredicate(siteID: Int64, filters: BookingFilters) -> NSPredicate {
let siteIDPredicate = NSPredicate(format: "siteID == %lld", siteID)

let productIDsPredicate = filters.productIDs.isNotEmpty ? NSPredicate(format: "productID IN %@", filters.productIDs) : nil

let customerIDsPredicate = filters.customerIDs.isNotEmpty ? NSPredicate(format: "customerID IN %@", filters.customerIDs) : nil

let resourceIDsPredicate = filters.resourceIDs.isNotEmpty ? NSPredicate(format: "resourceID IN %@", filters.resourceIDs) : nil

let startDateBeforePredicate = filters.startDateBefore.flatMap { dateString -> NSPredicate? in
guard let date = ISO8601DateFormatter().date(from: dateString) else { return nil }
return NSPredicate(format: "startDate < %@", date as NSDate)
}

let startDateAfterPredicate = filters.startDateAfter.flatMap { dateString -> NSPredicate? in
guard let date = ISO8601DateFormatter().date(from: dateString) else { return nil }
return NSPredicate(format: "startDate > %@", date as NSDate)
}

let bookingStatusesPredicate = filters.bookingStatuses.isNotEmpty ? NSPredicate(format: "statusKey IN %@", filters.bookingStatuses) : nil

let attendanceStatusesPredicate = filters.attendanceStatuses.isNotEmpty ?
NSPredicate(format: "attendanceStatusKey IN %@", filters.attendanceStatuses) : nil

let subpredicates = [
siteIDPredicate,
productIDsPredicate,
customerIDsPredicate,
resourceIDsPredicate,
startDateBeforePredicate,
startDateAfterPredicate,
bookingStatusesPredicate,
attendanceStatusesPredicate
].compactMap({ $0 })

return NSCompoundPredicate(andPredicateWithSubpredicates: subpredicates)
}
}

extension ResultsController where T: StorageBooking {
public func updatePredicate(siteID: Int64, filters: BookingFilters) {
self.predicate = NSPredicate.createBookingPredicate(siteID: siteID, filters: filters)
}
}
10 changes: 6 additions & 4 deletions Modules/Tests/NetworkingTests/Remote/BookingsRemoteTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -44,14 +44,17 @@ struct BookingsRemoteTests {
let startDateBefore = "2024-12-31T23:59:59"
let startDateAfter = "2024-01-01T00:00:00"
let searchQuery = "test search"
let filters = BookingFilters(
startDateBefore: startDateBefore,
startDateAfter: startDateAfter
)
network.simulateResponse(requestUrlSuffix: "bookings", filename: "booking-list")

// When
_ = try await remote.loadAllBookings(for: sampleSiteID,
pageNumber: 2,
pageSize: 50,
startDateBefore: startDateBefore,
startDateAfter: startDateAfter,
filters: filters,
searchQuery: searchQuery,
order: .ascending)

Expand All @@ -74,8 +77,7 @@ struct BookingsRemoteTests {

// When
_ = try await remote.loadAllBookings(for: sampleSiteID,
startDateBefore: nil,
startDateAfter: nil,
filters: nil,
searchQuery: nil,
order: .descending)

Expand Down
3 changes: 1 addition & 2 deletions Modules/Tests/YosemiteTests/Mocks/MockBookingsRemote.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,7 @@ final class MockBookingsRemote: BookingsRemoteProtocol {
func loadAllBookings(for siteID: Int64,
pageNumber: Int,
pageSize: Int,
startDateBefore: String?,
startDateAfter: String?,
filters: BookingFilters?,
searchQuery: String?,
order: BookingsRemote.Order) async throws -> [Booking] {
guard let result = loadAllBookingsResult else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,10 +63,22 @@ struct BookingDateTimeFilterView: View {
selectedToDate = newValue
}
.onChange(of: selectedFromDate) { _, newValue in
onSelection(newValue, selectedToDate)
guard let newValue else {
return onSelection(nil, selectedToDate)
}
/// Bookings backend treats dates as local time with no time zone.
/// Convert the date to keep the selected components but with UTC as time zone.
let convertedDate = convertToUTCDate(newValue)
onSelection(convertedDate, selectedToDate)
}
.onChange(of: selectedToDate) { _, newValue in
onSelection(selectedFromDate, newValue)
guard let newValue else {
return onSelection(selectedFromDate, nil)
}
/// Bookings backend treats dates as local time with no time zone.
/// Convert the date to keep the selected components but with UTC as time zone.
let convertedDate = convertToUTCDate(newValue)
onSelection(selectedFromDate, convertedDate)
}
}
}
Expand Down Expand Up @@ -156,6 +168,19 @@ private extension BookingDateTimeFilterView {
return fromDate...Date.distantFuture
}
}

/// Converts a date by extracting its components in the local timezone
/// and reconstructing a new date with those same components in UTC.
/// This effectively treats the selected date/time as if it were in UTC.
func convertToUTCDate(_ date: Date) -> Date {
let localCalendar = Calendar.current
let components = localCalendar.dateComponents([.year, .month, .day, .hour, .minute, .second], from: date)

var utcCalendar = Calendar(identifier: .gregorian)
utcCalendar.timeZone = TimeZone(identifier: "UTC")!

return utcCalendar.date(from: components) ?? date
}
}

private extension BookingDateTimeFilterView {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,18 @@ final class BookingFiltersViewModel: FilterListViewModel {

return readable.joined(separator: ", ")
}

var bookingFilters: BookingFilters {
BookingFilters(
productIDs: products.map { $0.productID },
customerIDs: customers.map { $0.customerID },
resourceIDs: teamMembers.map { $0.resourceID },
startDateBefore: dateRange?.endDate?.ISO8601Format(),
startDateAfter: dateRange?.startDate?.ISO8601Format(),
bookingStatuses: paymentStatuses.map { $0.rawValue },
attendanceStatuses: attendanceStatuses.map { $0.rawValue }
)
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,8 @@ final class BookingListContainerViewModel: ObservableObject {
func updateFilters(_ filters: BookingFiltersViewModel.Filters) {
self.filters = filters
self.numberOfActiveFilters = filters.numberOfActiveFilters
// TODO: Apply filters to All tab
allListViewModel.updateFilters(filters)
allSearchViewModel.updateFilters(filters)
}
}

Expand Down
Loading