Skip to content

Commit de66262

Browse files
Merge pull request #14735 from woocommerce/issue/WOOMOB-1463_booking_filter_persistence
[WOOMOB-1463] Booking filter persistence
2 parents 72aa1c6 + 5df1b51 commit de66262

File tree

11 files changed

+428
-80
lines changed

11 files changed

+428
-80
lines changed

WooCommerce/src/main/kotlin/com/woocommerce/android/datastore/DataStoreModule.kt

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import androidx.datastore.preferences.preferencesDataStoreFile
1111
import com.automattic.android.tracks.crashlogging.CrashLogging
1212
import com.woocommerce.android.datastore.DataStoreType.ANALYTICS_CONFIGURATION
1313
import com.woocommerce.android.datastore.DataStoreType.ANALYTICS_UI_CACHE
14+
import com.woocommerce.android.datastore.DataStoreType.BOOKINGS_FILTERS
1415
import com.woocommerce.android.datastore.DataStoreType.COUPONS
1516
import com.woocommerce.android.datastore.DataStoreType.DASHBOARD_STATS
1617
import com.woocommerce.android.datastore.DataStoreType.LAST_UPDATE
@@ -196,4 +197,22 @@ class DataStoreModule {
196197
},
197198
scope = CoroutineScope(appCoroutineScope.coroutineContext + Dispatchers.IO)
198199
)
200+
201+
@Provides
202+
@Singleton
203+
@DataStoreQualifier(BOOKINGS_FILTERS)
204+
fun provideBookingsFiltersDataStore(
205+
appContext: Context,
206+
crashLogging: CrashLogging,
207+
@AppCoroutineScope appCoroutineScope: CoroutineScope
208+
): DataStore<Preferences> = PreferenceDataStoreFactory.create(
209+
produceFile = { appContext.preferencesDataStoreFile("bookings_filters") },
210+
corruptionHandler = ReplaceFileCorruptionHandler {
211+
crashLogging.recordEvent(
212+
"Corrupted data store. DataStore Type: ${BOOKINGS_FILTERS.name}"
213+
)
214+
emptyPreferences()
215+
},
216+
scope = CoroutineScope(appCoroutineScope.coroutineContext + Dispatchers.IO)
217+
)
199218
}

WooCommerce/src/main/kotlin/com/woocommerce/android/datastore/DataStoreType.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,6 @@ enum class DataStoreType {
99
COUPONS,
1010
LAST_UPDATE,
1111
SITE_PICKER_WOO_VISIBLE_SITES,
12-
SHIPPING_LABELS_DATA
12+
SHIPPING_LABELS_DATA,
13+
BOOKINGS_FILTERS
1314
}

WooCommerce/src/main/kotlin/com/woocommerce/android/ui/bookings/filter/BookingFilterListScreen.kt

Lines changed: 1 addition & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,6 @@ import androidx.compose.ui.res.stringResource
2020
import androidx.compose.ui.res.vectorResource
2121
import androidx.compose.ui.unit.dp
2222
import com.woocommerce.android.R
23-
import com.woocommerce.android.ui.bookings.filter.BookingFilterListViewModel.BookingFilterListItem
24-
import com.woocommerce.android.ui.bookings.filter.BookingFilterListViewModel.BookingFilterListUiState
2523
import com.woocommerce.android.ui.compose.component.Toolbar
2624
import com.woocommerce.android.ui.compose.component.WCColoredButton
2725
import com.woocommerce.android.ui.compose.component.WCListItemWithInlineSubtitle
@@ -91,40 +89,7 @@ private fun BookingFilterListScreenPreview() {
9189
WooThemeWithBackground {
9290
BookingFilterListScreen(
9391
state = BookingFilterListUiState(
94-
items = listOf(
95-
BookingFilterListItem(
96-
title = R.string.bookings_filter_title_team_member,
97-
value = null
98-
),
99-
BookingFilterListItem(
100-
title = R.string.bookings_filter_title_attendance_status,
101-
value = null
102-
),
103-
BookingFilterListItem(
104-
title = R.string.bookings_filter_title_payment_status,
105-
value = null
106-
),
107-
BookingFilterListItem(
108-
title = R.string.bookings_filter_title_type,
109-
value = null
110-
),
111-
BookingFilterListItem(
112-
title = R.string.bookings_filter_customer_name,
113-
value = null
114-
),
115-
BookingFilterListItem(
116-
title = R.string.bookings_filter_category,
117-
value = null
118-
),
119-
BookingFilterListItem(
120-
title = R.string.bookings_filter_title_date,
121-
value = null
122-
),
123-
BookingFilterListItem(
124-
title = R.string.bookings_filter_title_service_event,
125-
value = null
126-
),
127-
)
92+
initialBookingFilters = null,
12893
)
12994
)
13095
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
package com.woocommerce.android.ui.bookings.filter
2+
3+
import androidx.annotation.StringRes
4+
import com.woocommerce.android.R
5+
import org.wordpress.android.fluxc.network.rest.wpcom.wc.bookings.BookingFilters
6+
import org.wordpress.android.fluxc.network.rest.wpcom.wc.bookings.BookingsFilterOption
7+
8+
data class BookingFilterListUiState(
9+
val initialBookingFilters: BookingFilters? = null,
10+
val newBookingFilters: Set<BookingsFilterOption> = emptySet(),
11+
val onClose: () -> Unit = {},
12+
val onShowBookings: () -> Unit = {},
13+
) {
14+
15+
val items: List<BookingFilterListItem> = initialBookingFilters.defaultBookingFilters().map { option ->
16+
BookingFilterListItem(
17+
title = option.titleRes(),
18+
value = option.value,
19+
)
20+
}
21+
}
22+
23+
data class BookingFilterListItem(
24+
@StringRes val title: Int,
25+
val value: String? = null,
26+
val onClick: () -> Unit = {}
27+
)
28+
29+
@StringRes
30+
fun BookingsFilterOption.titleRes(): Int = when (this) {
31+
BookingsFilterOption.TeamMember -> R.string.bookings_filter_title_team_member
32+
BookingsFilterOption.AttendanceStatus -> R.string.bookings_filter_title_attendance_status
33+
BookingsFilterOption.PaymentStatus -> R.string.bookings_filter_title_payment_status
34+
BookingsFilterOption.BookingType -> R.string.bookings_filter_title_type
35+
is BookingsFilterOption.Customer -> R.string.bookings_filter_customer_name
36+
BookingsFilterOption.Category -> R.string.bookings_filter_category
37+
is BookingsFilterOption.DateRange -> R.string.bookings_filter_title_date
38+
BookingsFilterOption.ServiceEvent -> R.string.bookings_filter_title_service_event
39+
}
40+
41+
private val BookingsFilterOption.value: String?
42+
get() = when (this) {
43+
BookingsFilterOption.TeamMember,
44+
BookingsFilterOption.AttendanceStatus,
45+
BookingsFilterOption.PaymentStatus,
46+
BookingsFilterOption.BookingType,
47+
BookingsFilterOption.Category,
48+
BookingsFilterOption.ServiceEvent,
49+
is BookingsFilterOption.Customer,
50+
is BookingsFilterOption.DateRange -> null
51+
}
52+
53+
private fun BookingFilters?.defaultBookingFilters(): List<BookingsFilterOption> = listOf(
54+
BookingsFilterOption.TeamMember,
55+
BookingsFilterOption.AttendanceStatus,
56+
BookingsFilterOption.PaymentStatus,
57+
BookingsFilterOption.BookingType,
58+
BookingsFilterOption.Customer(customerId = this?.customer?.customerId, customerName = this?.customer?.customerName),
59+
BookingsFilterOption.Category,
60+
BookingsFilterOption.DateRange(before = this?.dateRange?.before, after = this?.dateRange?.after),
61+
BookingsFilterOption.ServiceEvent,
62+
)
Lines changed: 42 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,72 +1,77 @@
11
package com.woocommerce.android.ui.bookings.filter
22

3-
import androidx.annotation.StringRes
43
import androidx.lifecycle.SavedStateHandle
54
import androidx.lifecycle.asLiveData
6-
import com.woocommerce.android.R
5+
import com.woocommerce.android.ui.bookings.filter.data.BookingFilterRepository
76
import com.woocommerce.android.viewmodel.MultiLiveEvent
87
import com.woocommerce.android.viewmodel.ScopedViewModel
98
import dagger.hilt.android.lifecycle.HiltViewModel
109
import kotlinx.coroutines.flow.MutableStateFlow
10+
import kotlinx.coroutines.flow.firstOrNull
11+
import kotlinx.coroutines.flow.update
12+
import kotlinx.coroutines.launch
13+
import org.wordpress.android.fluxc.network.rest.wpcom.wc.bookings.BookingFilters
1114
import org.wordpress.android.fluxc.network.rest.wpcom.wc.bookings.BookingsFilterOption
1215
import javax.inject.Inject
1316

1417
@HiltViewModel
1518
class BookingFilterListViewModel @Inject constructor(
16-
savedStateHandle: SavedStateHandle
19+
savedStateHandle: SavedStateHandle,
20+
private val bookingFilterRepository: BookingFilterRepository,
1721
) : ScopedViewModel(savedStateHandle) {
1822

1923
private val _uiState = MutableStateFlow(
2024
BookingFilterListUiState(
21-
items = defaultBookingFilters().map { BookingFilterListItem(title = it.titleRes()) },
2225
onClose = ::onClose,
2326
onShowBookings = ::onShowBookings
2427
)
2528
)
2629
val uiState = _uiState.asLiveData()
2730

31+
init {
32+
getBookingFilter()
33+
}
34+
35+
private fun getBookingFilter() {
36+
launch {
37+
// We don't observe changes here, just get the current value once
38+
val bookingFilters = bookingFilterRepository.bookingFiltersFlow.firstOrNull()
39+
_uiState.update { current ->
40+
current.copy(initialBookingFilters = bookingFilters)
41+
}
42+
}
43+
}
44+
2845
private fun onClose() {
2946
// TODO Verify unsaved changes and close
3047
triggerEvent(MultiLiveEvent.Event.Exit)
3148
}
3249

3350
private fun onShowBookings() {
34-
// TODO Apply filters and show bookings
51+
launch {
52+
bookingFilterRepository.save(_uiState.value.updatedBookingFilters)
53+
}
3554
triggerEvent(MultiLiveEvent.Event.Exit)
3655
}
56+
}
3757

38-
@StringRes
39-
private fun BookingsFilterOption.titleRes(): Int = when (this) {
40-
BookingsFilterOption.TeamMember -> R.string.bookings_filter_title_team_member
41-
BookingsFilterOption.AttendanceStatus -> R.string.bookings_filter_title_attendance_status
42-
BookingsFilterOption.PaymentStatus -> R.string.bookings_filter_title_payment_status
43-
BookingsFilterOption.BookingType -> R.string.bookings_filter_title_type
44-
is BookingsFilterOption.Customer -> R.string.bookings_filter_customer_name
45-
BookingsFilterOption.Category -> R.string.bookings_filter_category
46-
is BookingsFilterOption.DateRange -> R.string.bookings_filter_title_date
47-
BookingsFilterOption.ServiceEvent -> R.string.bookings_filter_title_service_event
48-
}
49-
50-
private fun defaultBookingFilters(): List<BookingsFilterOption> = listOf(
51-
BookingsFilterOption.TeamMember,
52-
BookingsFilterOption.AttendanceStatus,
53-
BookingsFilterOption.PaymentStatus,
54-
BookingsFilterOption.BookingType,
55-
BookingsFilterOption.Customer(customerId = null),
56-
BookingsFilterOption.Category,
57-
BookingsFilterOption.DateRange(before = null, after = null),
58-
BookingsFilterOption.ServiceEvent,
59-
)
58+
private val BookingFilterListUiState.updatedBookingFilters: BookingFilters
59+
get() {
60+
val initial = initialBookingFilters ?: BookingFilters()
61+
val updates = this@updatedBookingFilters.newBookingFilters
6062

61-
data class BookingFilterListItem(
62-
@StringRes val title: Int,
63-
val value: String? = null,
64-
val onClick: () -> Unit = {}
65-
)
63+
return BookingFilters(
64+
dateRange = updates.getOrDefault(initial.dateRange),
65+
customer = updates.getOrDefault(initial.customer),
66+
teamMember = updates.getOrDefault(initial.teamMember),
67+
attendanceStatus = updates.getOrDefault(initial.attendanceStatus),
68+
paymentStatus = updates.getOrDefault(initial.paymentStatus),
69+
bookingType = updates.getOrDefault(initial.bookingType),
70+
category = updates.getOrDefault(initial.category),
71+
serviceEvent = updates.getOrDefault(initial.serviceEvent),
72+
)
73+
}
6674

67-
data class BookingFilterListUiState(
68-
val items: List<BookingFilterListItem>,
69-
val onClose: () -> Unit = {},
70-
val onShowBookings: () -> Unit = {},
71-
)
75+
private inline fun <reified T> Set<BookingsFilterOption>.getOrDefault(default: T?): T? {
76+
return this.filterIsInstance<T>().firstOrNull() ?: default
7277
}
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
package com.woocommerce.android.ui.bookings.filter.data
2+
3+
import androidx.datastore.core.DataStore
4+
import androidx.datastore.preferences.core.Preferences
5+
import androidx.datastore.preferences.core.edit
6+
import androidx.datastore.preferences.core.longPreferencesKey
7+
import androidx.datastore.preferences.core.stringPreferencesKey
8+
import com.woocommerce.android.datastore.DataStoreQualifier
9+
import com.woocommerce.android.datastore.DataStoreType.BOOKINGS_FILTERS
10+
import com.woocommerce.android.tools.SelectedSite
11+
import kotlinx.coroutines.ExperimentalCoroutinesApi
12+
import kotlinx.coroutines.flow.Flow
13+
import kotlinx.coroutines.flow.distinctUntilChanged
14+
import kotlinx.coroutines.flow.flatMapLatest
15+
import kotlinx.coroutines.flow.map
16+
import org.wordpress.android.fluxc.network.rest.wpcom.wc.bookings.BookingFilters
17+
import org.wordpress.android.fluxc.network.rest.wpcom.wc.bookings.BookingsFilterOption
18+
import java.time.Instant
19+
import javax.inject.Inject
20+
21+
@OptIn(ExperimentalCoroutinesApi::class)
22+
class BookingFilterRepository @Inject constructor(
23+
@DataStoreQualifier(BOOKINGS_FILTERS) private val dataStore: DataStore<Preferences>,
24+
private val selectedSite: SelectedSite,
25+
) {
26+
// Keys are built per-site to keep selections isolated across sites
27+
private fun customerIdKey(siteId: Int) = longPreferencesKey("bfilters_${siteId}_customer_id")
28+
private fun customerNameKey(siteId: Int) = stringPreferencesKey("bfilters_${siteId}_customer_name")
29+
private fun dateBeforeKey(siteId: Int) = longPreferencesKey("bfilters_${siteId}_date_before")
30+
private fun dateAfterKey(siteId: Int) = longPreferencesKey("bfilters_${siteId}_date_after")
31+
32+
private val siteIdFlow = selectedSite.observe().map { it?.id ?: -1 }.distinctUntilChanged()
33+
34+
val bookingFiltersFlow: Flow<BookingFilters> = siteIdFlow.flatMapLatest { siteId ->
35+
dataStore.data.map { prefs ->
36+
BookingFilters(
37+
customer = prefs.getCustomerValue(siteId),
38+
dateRange = prefs.getDateRangeValue(siteId)
39+
)
40+
}
41+
}
42+
43+
suspend fun save(bookingFilters: BookingFilters) {
44+
val siteId = selectedSite.getSelectedSiteId()
45+
dataStore.edit { prefs ->
46+
// Customer
47+
val customerIdKey = customerIdKey(siteId)
48+
val customerNameKey = customerNameKey(siteId)
49+
val customer = bookingFilters.customer
50+
if (customer != null) {
51+
val id = customer.customerId
52+
val name = customer.customerName
53+
if (id != null) prefs[customerIdKey] = id else prefs.remove(customerIdKey)
54+
if (name != null) prefs[customerNameKey] = name else prefs.remove(customerNameKey)
55+
} else {
56+
// Clear if not provided
57+
prefs.remove(customerIdKey)
58+
prefs.remove(customerNameKey)
59+
}
60+
61+
// Date range
62+
val dateRange = bookingFilters.dateRange
63+
val beforeKey = dateBeforeKey(siteId)
64+
val afterKey = dateAfterKey(siteId)
65+
if (dateRange != null) {
66+
val before = dateRange.before?.toEpochMilli()
67+
val after = dateRange.after?.toEpochMilli()
68+
if (before == null) prefs.remove(beforeKey) else prefs[beforeKey] = before
69+
if (after == null) prefs.remove(afterKey) else prefs[afterKey] = after
70+
} else {
71+
// Clear if not provided
72+
prefs.remove(beforeKey)
73+
prefs.remove(afterKey)
74+
}
75+
76+
// Other filters currently have no persisted payload; ignore for now
77+
}
78+
}
79+
80+
private fun Preferences.getCustomerValue(siteId: Int): BookingsFilterOption.Customer? {
81+
val customerId = this[customerIdKey(siteId)]
82+
val customerName = this[customerNameKey(siteId)]
83+
return if (customerId != null || customerName != null) {
84+
BookingsFilterOption.Customer(customerId = customerId, customerName = customerName)
85+
} else {
86+
null
87+
}
88+
}
89+
90+
private fun Preferences.getDateRangeValue(siteId: Int): BookingsFilterOption.DateRange? {
91+
val before = this[dateBeforeKey(siteId)]?.let { Instant.ofEpochMilli(it) }
92+
val after = this[dateAfterKey(siteId)]?.let { Instant.ofEpochMilli(it) }
93+
return if (before != null || after != null) {
94+
BookingsFilterOption.DateRange(before = before, after = after)
95+
} else {
96+
null
97+
}
98+
}
99+
}

WooCommerce/src/main/kotlin/com/woocommerce/android/ui/bookings/list/BookingListFiltersBuilder.kt

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.woocommerce.android.ui.bookings.list
22

3+
import org.wordpress.android.fluxc.network.rest.wpcom.wc.bookings.BookingFilters
34
import org.wordpress.android.fluxc.network.rest.wpcom.wc.bookings.BookingsFilterOption
45
import java.time.Clock
56
import java.time.LocalDate
@@ -35,4 +36,17 @@ class BookingListFiltersBuilder @Inject constructor(
3536
BookingListTab.All -> null
3637
}
3738
}
39+
40+
fun BookingFilters.asList(): List<BookingsFilterOption> {
41+
val filters = mutableListOf<BookingsFilterOption>()
42+
customer?.let { filters.add(it) }
43+
dateRange?.let { filters.add(it) }
44+
teamMember?.let { filters.add(it) }
45+
attendanceStatus?.let { filters.add(it) }
46+
paymentStatus?.let { filters.add(it) }
47+
bookingType?.let { filters.add(it) }
48+
category?.let { filters.add(it) }
49+
serviceEvent?.let { filters.add(it) }
50+
return filters
51+
}
3852
}

0 commit comments

Comments
 (0)