Skip to content

Commit b72811e

Browse files
authored
Bookings: Add filter view UI (#16269)
2 parents 1fac92b + 3e27a99 commit b72811e

File tree

8 files changed

+434
-9
lines changed

8 files changed

+434
-9
lines changed
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
// periphery:ignore:all - will be used for booking filters
2+
import Foundation
3+
4+
/// Used to filter bookings by date range
5+
///
6+
public struct BookingDateRangeFilter: Codable, Equatable, Hashable {
7+
/// Start date of the range (inclusive)
8+
///
9+
public let startDate: Date?
10+
11+
/// End date of the range (inclusive)
12+
///
13+
public let endDate: Date?
14+
15+
public init(startDate: Date? = nil,
16+
endDate: Date? = nil) {
17+
self.startDate = startDate
18+
self.endDate = endDate
19+
}
20+
21+
enum CodingKeys: String, CodingKey {
22+
case startDate = "start_date"
23+
case endDate = "end_date"
24+
}
25+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
// periphery:ignore:all - will be used for booking filters
2+
import Foundation
3+
4+
/// Used to filter bookings by product
5+
///
6+
public struct BookingProductFilter: Codable, Hashable {
7+
/// ID of the product
8+
///
9+
public let id: Int64
10+
11+
/// Name of the product
12+
///
13+
public let name: String
14+
15+
public init(id: Int64,
16+
name: String) {
17+
self.id = id
18+
self.name = name
19+
}
20+
}

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 BookingStatus = Networking.BookingStatus
2930
public typealias BookingOrderInfo = Networking.BookingOrderInfo
3031
public typealias BookingCustomerInfo = Networking.BookingCustomerInfo
3132
public typealias BookingPaymentInfo = Networking.BookingPaymentInfo
Lines changed: 336 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,336 @@
1+
import Foundation
2+
import Yosemite
3+
4+
final class BookingFiltersViewModel: FilterListViewModel {
5+
let filterActionTitle = Localization.filterActionTitle
6+
let filterTypeViewModels: [FilterTypeViewModel]
7+
let shouldShowHistory = false
8+
let source = FilterSource.booking
9+
10+
private let teamMemberFilterViewModel: FilterTypeViewModel
11+
private let productFilterViewModel: FilterTypeViewModel
12+
private let customerFilterViewModel: FilterTypeViewModel
13+
private let attendanceStatusFilterViewModel: FilterTypeViewModel
14+
private let paymentStatusFilterViewModel: FilterTypeViewModel
15+
private let dateTimeFilterViewModel: FilterTypeViewModel
16+
17+
18+
init(filter: Filters,
19+
siteID: Int64) {
20+
teamMemberFilterViewModel = BookingListFilter.teamMember(siteID: siteID).createViewModel(filters: filter)
21+
productFilterViewModel = BookingListFilter.product(siteID: siteID).createViewModel(filters: filter)
22+
customerFilterViewModel = BookingListFilter.customer(siteID: siteID).createViewModel(filters: filter)
23+
attendanceStatusFilterViewModel = BookingListFilter.attendanceStatus.createViewModel(filters: filter)
24+
paymentStatusFilterViewModel = BookingListFilter.paymentStatus.createViewModel(filters: filter)
25+
dateTimeFilterViewModel = BookingListFilter.dateTime.createViewModel(filters: filter)
26+
27+
filterTypeViewModels = [
28+
teamMemberFilterViewModel,
29+
productFilterViewModel,
30+
customerFilterViewModel,
31+
attendanceStatusFilterViewModel,
32+
paymentStatusFilterViewModel,
33+
dateTimeFilterViewModel
34+
]
35+
}
36+
37+
var criteria: Filters {
38+
let teamMember = teamMemberFilterViewModel.selectedValue as? BookingResource
39+
let product = productFilterViewModel.selectedValue as? BookingProductFilter
40+
let customer = customerFilterViewModel.selectedValue as? CustomerFilter
41+
let attendanceStatus = attendanceStatusFilterViewModel.selectedValue as? BookingAttendanceStatus
42+
let paymentStatus = paymentStatusFilterViewModel.selectedValue as? BookingStatus
43+
let dateRange = dateTimeFilterViewModel.selectedValue as? BookingDateRangeFilter
44+
let numberOfActiveFilters = filterTypeViewModels.numberOfActiveFilters
45+
46+
return Filters(teamMember: teamMember,
47+
product: product,
48+
attendanceStatus: attendanceStatus,
49+
paymentStatus: paymentStatus,
50+
customer: customer,
51+
dateRange: dateRange,
52+
numberOfActiveFilters: numberOfActiveFilters)
53+
}
54+
55+
func retrieveFilterHistory() async throws -> [Filters] {
56+
// TODO: Implement when booking filter history is available
57+
return []
58+
}
59+
60+
func applyPastFilter(_ filter: Filters) {
61+
// TODO: Implement when booking filter history is available
62+
}
63+
64+
func saveSelectedFilterToHistory(_ filter: Filters) {
65+
// TODO: Implement when booking filter history storage is available
66+
}
67+
68+
func removeFilterFromHistory(_ filter: Filters) {
69+
// TODO: Implement when booking filter history storage is available
70+
}
71+
72+
func clearAllFilterHistory() {
73+
// TODO: Implement when booking filter history storage is available
74+
}
75+
76+
func clearAll() {
77+
teamMemberFilterViewModel.selectedValue = BookingResource?.none
78+
productFilterViewModel.selectedValue = BookingProductFilter?.none
79+
customerFilterViewModel.selectedValue = CustomerFilter?.none
80+
attendanceStatusFilterViewModel.selectedValue = BookingAttendanceStatus?.none
81+
paymentStatusFilterViewModel.selectedValue = BookingStatus?.none
82+
dateTimeFilterViewModel.selectedValue = BookingDateRangeFilter?.none
83+
}
84+
85+
typealias Criteria = Filters
86+
87+
struct Filters: Equatable, HumanReadable {
88+
89+
let teamMember: BookingResource?
90+
let product: BookingProductFilter?
91+
let attendanceStatus: BookingAttendanceStatus?
92+
let paymentStatus: BookingStatus?
93+
let customer: CustomerFilter?
94+
let dateRange: BookingDateRangeFilter?
95+
96+
let numberOfActiveFilters: Int
97+
98+
init() {
99+
teamMember = nil
100+
product = nil
101+
attendanceStatus = nil
102+
paymentStatus = nil
103+
customer = nil
104+
dateRange = nil
105+
numberOfActiveFilters = 0
106+
}
107+
108+
init(teamMember: BookingResource?,
109+
product: BookingProductFilter?,
110+
attendanceStatus: BookingAttendanceStatus?,
111+
paymentStatus: BookingStatus?,
112+
customer: CustomerFilter?,
113+
dateRange: BookingDateRangeFilter?,
114+
numberOfActiveFilters: Int) {
115+
self.teamMember = teamMember
116+
self.product = product
117+
self.attendanceStatus = attendanceStatus
118+
self.paymentStatus = paymentStatus
119+
self.customer = customer
120+
self.dateRange = dateRange
121+
self.numberOfActiveFilters = numberOfActiveFilters
122+
}
123+
124+
var readableString: String {
125+
var readable: [String] = []
126+
if let teamMember {
127+
readable.append(teamMember.name)
128+
}
129+
if let product {
130+
readable.append(product.name)
131+
}
132+
if let attendanceStatus {
133+
readable.append(attendanceStatus.localizedTitle)
134+
}
135+
if let paymentStatus {
136+
readable.append(paymentStatus.localizedTitle)
137+
}
138+
if let customer {
139+
readable.append(customer.description)
140+
}
141+
if let dateRange {
142+
readable.append(dateRange.description)
143+
}
144+
145+
return readable.joined(separator: ", ")
146+
}
147+
}
148+
}
149+
150+
extension BookingFiltersViewModel {
151+
/// Rows listed in the order they appear on screen
152+
///
153+
enum BookingListFilter {
154+
case teamMember(siteID: Int64)
155+
case product(siteID: Int64)
156+
case attendanceStatus
157+
case paymentStatus
158+
case customer(siteID: Int64)
159+
case dateTime
160+
}
161+
}
162+
163+
private extension BookingFiltersViewModel.BookingListFilter {
164+
var title: String {
165+
switch self {
166+
case .teamMember:
167+
return Localization.rowTitleTeamMember
168+
case .product:
169+
return Localization.rowTitleProduct
170+
case .customer:
171+
return Localization.rowTitleCustomer
172+
case .attendanceStatus:
173+
return Localization.rowTitleAttendanceStatus
174+
case .paymentStatus:
175+
return Localization.rowTitlePaymentStatus
176+
case .dateTime:
177+
return Localization.rowTitleDateTime
178+
}
179+
}
180+
}
181+
182+
extension BookingFiltersViewModel.BookingListFilter {
183+
func createViewModel(filters: BookingFiltersViewModel.Filters) -> FilterTypeViewModel {
184+
switch self {
185+
case .teamMember:
186+
// TODO: Implement team member selector when available
187+
// For now, using static options with nil (Any option)
188+
let options: [BookingResource?] = [nil]
189+
return FilterTypeViewModel(title: title,
190+
listSelectorConfig: .staticOptions(options: options),
191+
selectedValue: filters.teamMember)
192+
case .product(let siteID):
193+
return FilterTypeViewModel(title: title,
194+
listSelectorConfig: .products(siteID: siteID),
195+
selectedValue: filters.product)
196+
case .customer(let siteID):
197+
return FilterTypeViewModel(title: title,
198+
listSelectorConfig: .customer(siteID: siteID),
199+
selectedValue: filters.customer)
200+
case .attendanceStatus:
201+
let options: [BookingAttendanceStatus?] = [nil, .booked, .checkedIn, .cancelled, .noShow]
202+
return FilterTypeViewModel(title: title,
203+
listSelectorConfig: .staticOptions(options: options),
204+
selectedValue: filters.attendanceStatus)
205+
case .paymentStatus:
206+
let options: [BookingStatus?] = [nil, .complete, .paid, .unpaid, .cancelled, .pendingConfirmation, .confirmed]
207+
return FilterTypeViewModel(title: title,
208+
listSelectorConfig: .staticOptions(options: options),
209+
selectedValue: filters.paymentStatus)
210+
case .dateTime:
211+
// TODO: Implement date range selector when available
212+
let options: [BookingDateRangeFilter?] = [nil]
213+
return FilterTypeViewModel(title: title,
214+
listSelectorConfig: .staticOptions(options: options),
215+
selectedValue: filters.dateRange)
216+
}
217+
}
218+
}
219+
220+
// MARK: - FilterType conformance
221+
extension BookingResource: FilterType {
222+
var description: String { name }
223+
var isActive: Bool { true }
224+
}
225+
226+
extension BookingAttendanceStatus: FilterType {
227+
var description: String { localizedTitle }
228+
229+
var isActive: Bool {
230+
switch self {
231+
case .booked, .checkedIn, .cancelled, .noShow:
232+
return true
233+
case .unknown:
234+
return false
235+
}
236+
}
237+
}
238+
239+
extension BookingStatus: FilterType {
240+
var description: String { localizedTitle }
241+
242+
var isActive: Bool {
243+
switch self {
244+
case .complete, .paid, .unpaid, .cancelled, .pendingConfirmation, .confirmed:
245+
return true
246+
case .unknown:
247+
return false
248+
}
249+
}
250+
}
251+
252+
extension BookingProductFilter: FilterType {
253+
/// The user-facing description of the filter value.
254+
var description: String { name }
255+
256+
/// Whether the filter is set to a non-empty value.
257+
var isActive: Bool { true }
258+
}
259+
260+
extension BookingDateRangeFilter: FilterType {
261+
var description: String {
262+
// TODO: Format dates nicely when implementing date range selector
263+
if let startDate = startDate, let endDate = endDate {
264+
let formatter = DateFormatter()
265+
formatter.dateStyle = .short
266+
return "\(formatter.string(from: startDate)) - \(formatter.string(from: endDate))"
267+
} else if let startDate = startDate {
268+
let formatter = DateFormatter()
269+
formatter.dateStyle = .short
270+
return NSLocalizedString(
271+
"bookingDateRangeFilter.from",
272+
value: "From \(formatter.string(from: startDate))",
273+
comment: "Description for booking date range filter with only start date")
274+
} else if let endDate = endDate {
275+
let formatter = DateFormatter()
276+
formatter.dateStyle = .short
277+
return NSLocalizedString(
278+
"bookingDateRangeFilter.until",
279+
value: "Until \(formatter.string(from: endDate))",
280+
comment: "Description for booking date range filter with only end date")
281+
} else {
282+
return NSLocalizedString(
283+
"bookingDateRangeFilter.any",
284+
value: "Any",
285+
comment: "Description for booking date range filter with no dates selected")
286+
}
287+
}
288+
289+
var isActive: Bool {
290+
startDate != nil || endDate != nil
291+
}
292+
}
293+
294+
// MARK: - Constants
295+
private extension BookingFiltersViewModel {
296+
enum Localization {
297+
static let filterActionTitle = NSLocalizedString(
298+
"bookingFilters.filterActionTitle",
299+
value: "Show bookings",
300+
comment: "Button title for applying filters to a list of bookings.")
301+
}
302+
}
303+
304+
private extension BookingFiltersViewModel.BookingListFilter {
305+
enum Localization {
306+
static let rowTitleTeamMember = NSLocalizedString(
307+
"bookingFilters.rowTitleTeamMember",
308+
value: "Team Member",
309+
comment: "Row title for filtering bookings by team member.")
310+
311+
static let rowTitleProduct = NSLocalizedString(
312+
"bookingFilters.rowTitleProduct",
313+
value: "Service / Event",
314+
comment: "Row title for filtering bookings by product.")
315+
316+
static let rowTitleCustomer = NSLocalizedString(
317+
"bookingFilters.rowTitleCustomer",
318+
value: "Customer name",
319+
comment: "Row title for filtering bookings by customer.")
320+
321+
static let rowTitleAttendanceStatus = NSLocalizedString(
322+
"bookingFilters.rowTitleAttendanceStatus",
323+
value: "Attendance Status",
324+
comment: "Row title for filtering bookings by attendance status.")
325+
326+
static let rowTitlePaymentStatus = NSLocalizedString(
327+
"bookingFilters.rowTitlePaymentStatus",
328+
value: "Payment Status",
329+
comment: "Row title for filtering bookings by payment status.")
330+
331+
static let rowTitleDateTime = NSLocalizedString(
332+
"bookingFilters.rowTitleDateTime",
333+
value: "Date & time",
334+
comment: "Row title for filtering bookings by date range.")
335+
}
336+
}

0 commit comments

Comments
 (0)