Skip to content

Commit 8977b5b

Browse files
authored
Bookings: Add UI for date time filter (#16282)
2 parents 73cb7b0 + f16120d commit 8977b5b

File tree

3 files changed

+247
-27
lines changed

3 files changed

+247
-27
lines changed
Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
import SwiftUI
2+
3+
/// View for filtering bookings by date and time range
4+
struct BookingDateTimeFilterView: View {
5+
/// States for the date pickers are required to be non-optional
6+
@State private var fromDate: Date
7+
@State private var toDate: Date
8+
9+
/// Separate states for the selected dates, which can be optional
10+
@State private var selectedFromDate: Date?
11+
@State private var selectedToDate: Date?
12+
13+
@State private var expandedPicker: PickerType?
14+
15+
enum PickerType: Hashable {
16+
case fromDate
17+
case fromTime
18+
case toDate
19+
case toTime
20+
}
21+
22+
private let onSelection: (Date?, Date?) -> Void
23+
24+
init(startDate: Date?,
25+
endDate: Date?,
26+
onSelection: @escaping (Date?, Date?) -> Void) {
27+
self.fromDate = startDate ?? Date().startOfDay(timezone: .current)
28+
self.selectedFromDate = startDate
29+
self.toDate = endDate ?? Date().endOfDay(timezone: .current)
30+
self.selectedToDate = endDate
31+
self.onSelection = onSelection
32+
}
33+
34+
var body: some View {
35+
List {
36+
// From Section
37+
Section {
38+
dateRow(type: .fromDate, displayedDate: $fromDate, selectedDate: selectedFromDate)
39+
timeRow(type: .fromTime, displayedDate: $fromDate, selectedDate: selectedFromDate)
40+
41+
} header: {
42+
Text(Localization.from.uppercased())
43+
.footnoteStyle()
44+
}
45+
46+
// To Section
47+
Section {
48+
dateRow(type: .toDate, displayedDate: $toDate, selectedDate: selectedToDate)
49+
timeRow(type: .toTime, displayedDate: $toDate, selectedDate: selectedToDate)
50+
} header: {
51+
Text(Localization.to.uppercased())
52+
.footnoteStyle()
53+
}
54+
}
55+
.listStyle(.plain)
56+
.navigationTitle(Localization.title)
57+
.navigationBarTitleDisplayMode(.inline)
58+
.background(Color(.listBackground))
59+
.onChange(of: fromDate) { _, newValue in
60+
selectedFromDate = newValue
61+
}
62+
.onChange(of: toDate) { _, newValue in
63+
selectedToDate = newValue
64+
}
65+
.onChange(of: selectedFromDate) { _, newValue in
66+
onSelection(newValue, selectedToDate)
67+
}
68+
.onChange(of: selectedToDate) { _, newValue in
69+
onSelection(selectedFromDate, newValue)
70+
}
71+
}
72+
}
73+
74+
private extension BookingDateTimeFilterView {
75+
@ViewBuilder
76+
func dateRow(type: PickerType, displayedDate: Binding<Date>, selectedDate: Date?) -> some View {
77+
Button {
78+
withAnimation {
79+
expandedPicker = expandedPicker == type ? nil : type
80+
/// auto selects dates upon expanding the date picker
81+
if selectedDate == nil {
82+
switch type {
83+
case .fromDate, .fromTime:
84+
selectedFromDate = displayedDate.wrappedValue
85+
case .toDate, .toTime:
86+
selectedToDate = displayedDate.wrappedValue
87+
}
88+
}
89+
}
90+
} label: {
91+
HStack {
92+
Text(Localization.date)
93+
.foregroundColor(.primary)
94+
Spacer()
95+
if let selectedDate {
96+
Text(selectedDate, style: .date)
97+
.foregroundColor(.secondary)
98+
}
99+
Image(systemName: "chevron.forward")
100+
.foregroundStyle(Color(.tertiaryLabel))
101+
.fontWeight(.medium)
102+
}
103+
}
104+
105+
if expandedPicker == type {
106+
DatePicker(
107+
"",
108+
selection: displayedDate,
109+
in: dateRange(for: type),
110+
displayedComponents: .date
111+
)
112+
.datePickerStyle(.graphical)
113+
.labelsHidden()
114+
}
115+
}
116+
117+
@ViewBuilder
118+
func timeRow(type: PickerType, displayedDate: Binding<Date>, selectedDate: Date?) -> some View {
119+
Button {
120+
withAnimation {
121+
expandedPicker = expandedPicker == type ? nil : type
122+
}
123+
} label: {
124+
HStack {
125+
Text(Localization.time)
126+
.foregroundColor(.primary)
127+
Spacer()
128+
if let selectedDate {
129+
Text(selectedDate, style: .time)
130+
.foregroundColor(.secondary)
131+
}
132+
Image(systemName: "chevron.forward")
133+
.foregroundStyle(Color(.tertiaryLabel))
134+
.fontWeight(.medium)
135+
}
136+
}
137+
138+
if expandedPicker == type {
139+
DatePicker(
140+
"",
141+
selection: displayedDate,
142+
in: dateRange(for: type),
143+
displayedComponents: .hourAndMinute
144+
)
145+
.datePickerStyle(.wheel)
146+
.labelsHidden()
147+
.frame(maxWidth: .infinity, alignment: .center)
148+
}
149+
}
150+
151+
func dateRange(for type: PickerType) -> ClosedRange<Date> {
152+
switch type {
153+
case .fromDate, .fromTime:
154+
return Date.distantPast...toDate
155+
case .toDate, .toTime:
156+
return fromDate...Date.distantFuture
157+
}
158+
}
159+
}
160+
161+
private extension BookingDateTimeFilterView {
162+
enum Localization {
163+
static let title = NSLocalizedString(
164+
"bookingDateTimeFilterView.title",
165+
value: "Date & time",
166+
comment: "Title of the date time picker for booking filter"
167+
)
168+
static let from = NSLocalizedString(
169+
"bookingDateTimeFilterView.from",
170+
value: "From",
171+
comment: "Title of From section in the date time picker for booking filter"
172+
)
173+
static let to = NSLocalizedString(
174+
"bookingDateTimeFilterView.to",
175+
value: "To",
176+
comment: "Title of the To section in the date time picker for booking filter"
177+
)
178+
static let date = NSLocalizedString(
179+
"bookingDateTimeFilterView.date",
180+
value: "Date",
181+
comment: "Title of the Date row in the date time picker for booking filter"
182+
)
183+
static let time = NSLocalizedString(
184+
"bookingDateTimeFilterView.time",
185+
value: "Time",
186+
comment: "Title of the Time row in the date time picker for booking filter"
187+
)
188+
}
189+
}
190+
191+
#Preview {
192+
NavigationView {
193+
BookingDateTimeFilterView(startDate: nil, endDate: nil, onSelection: { _, _ in })
194+
}
195+
}

WooCommerce/Classes/Bookings/BookingFilters/BookingFiltersViewModel.swift

Lines changed: 37 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -205,10 +205,8 @@ extension BookingFiltersViewModel.BookingListFilter {
205205
listSelectorConfig: .staticOptions(options: options),
206206
selectedValue: filters.paymentStatus)
207207
case .dateTime:
208-
// TODO: Implement date range selector when available
209-
let options: [BookingDateRangeFilter?] = [nil]
210208
return FilterTypeViewModel(title: title,
211-
listSelectorConfig: .staticOptions(options: options),
209+
listSelectorConfig: .bookingDateTime,
212210
selectedValue: filters.dateRange)
213211
}
214212
}
@@ -256,36 +254,49 @@ extension BookingProductFilter: FilterType {
256254

257255
extension BookingDateRangeFilter: FilterType {
258256
var description: String {
259-
// TODO: Format dates nicely when implementing date range selector
260-
if let startDate = startDate, let endDate = endDate {
261-
let formatter = DateFormatter()
262-
formatter.dateStyle = .short
263-
return "\(formatter.string(from: startDate)) - \(formatter.string(from: endDate))"
264-
} else if let startDate = startDate {
265-
let formatter = DateFormatter()
266-
formatter.dateStyle = .short
267-
return NSLocalizedString(
268-
"bookingDateRangeFilter.from",
269-
value: "From \(formatter.string(from: startDate))",
270-
comment: "Description for booking date range filter with only start date")
271-
} else if let endDate = endDate {
272-
let formatter = DateFormatter()
273-
formatter.dateStyle = .short
274-
return NSLocalizedString(
275-
"bookingDateRangeFilter.until",
276-
value: "Until \(formatter.string(from: endDate))",
277-
comment: "Description for booking date range filter with only end date")
257+
if let startDate, let endDate {
258+
[
259+
startDate.formatted(date: .abbreviated, time: .omitted),
260+
endDate.formatted(date: .abbreviated, time: .omitted)
261+
].joined(separator: " - ")
262+
} else if let startDate {
263+
String.localizedStringWithFormat(
264+
Localization.dateRangeFrom,
265+
startDate.formatted(date: .abbreviated, time: .shortened)
266+
)
267+
} else if let endDate {
268+
String.localizedStringWithFormat(
269+
Localization.dateRangeUntil,
270+
endDate.formatted(date: .abbreviated, time: .shortened)
271+
)
278272
} else {
279-
return NSLocalizedString(
280-
"bookingDateRangeFilter.any",
281-
value: "Any",
282-
comment: "Description for booking date range filter with no dates selected")
273+
Localization.dateRangeAny
283274
}
284275
}
285276

286277
var isActive: Bool {
287278
startDate != nil || endDate != nil
288279
}
280+
281+
private enum Localization {
282+
static let dateRangeFrom = NSLocalizedString(
283+
"bookingFiltersViewModel.dateRangeFilter.from",
284+
value: "From %1$@",
285+
comment: "Description for booking date range filter with only start date. " +
286+
"Placeholder is a date. Reads as: From October 27, 2025."
287+
)
288+
static let dateRangeUntil = NSLocalizedString(
289+
"bookingFiltersViewModel.dateRangeFilter.until",
290+
value: "Until %1$@",
291+
comment: "Description for booking date range filter with only end date. " +
292+
"Placeholder is a date. Reads as: Until October 27, 2025."
293+
)
294+
static let dateRangeAny = NSLocalizedString(
295+
"bookingFiltersViewModel.dateRangeFilter.any",
296+
value: "Any",
297+
comment: "Description for booking date range filter with no dates selected"
298+
)
299+
}
289300
}
290301

291302
// MARK: - Constants

WooCommerce/Classes/ViewRelated/Filters/FilterListViewController.swift

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,8 @@ enum FilterListValueSelectorConfig {
9898
case bookingResource(siteID: Int64)
9999
// Filter list selector for bookable product
100100
case bookableProduct(siteID: Int64)
101-
101+
// Filter list selector for booking date time
102+
case bookingDateTime
102103
}
103104

104105
/// Contains data for rendering a filter type row.
@@ -404,6 +405,19 @@ private extension FilterListViewController {
404405
)
405406
let hostingController = UIHostingController(rootView: memberListSelectorView)
406407
listSelector.navigationController?.pushViewController(hostingController, animated: true)
408+
case .bookingDateTime:
409+
let selectedDateRange = selected.selectedValue as? BookingDateRangeFilter
410+
let dateTimeFilterView = BookingDateTimeFilterView(
411+
startDate: selectedDateRange?.startDate,
412+
endDate: selectedDateRange?.endDate,
413+
onSelection: { [weak self] startDate, endDate in
414+
selected.selectedValue = BookingDateRangeFilter(startDate: startDate, endDate: endDate)
415+
self?.updateUI(numberOfActiveFilters: self?.viewModel.filterTypeViewModels.numberOfActiveFilters ?? 0)
416+
self?.listSelector.reloadData()
417+
}
418+
)
419+
let hostingController = UIHostingController(rootView: dateTimeFilterView)
420+
listSelector.navigationController?.pushViewController(hostingController, animated: true)
407421
}
408422
}
409423
}

0 commit comments

Comments
 (0)