Skip to content

Commit e2b71a5

Browse files
Merge pull request #14753 from woocommerce/issue/WOOMOB-1454-fetch-staff-member-name
[CIAB] Fetch staff member name in booking details
2 parents e6154f2 + 7185888 commit e2b71a5

File tree

18 files changed

+4655
-115
lines changed

18 files changed

+4655
-115
lines changed

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

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import com.woocommerce.android.ui.bookings.compose.BookingAppointmentDetailsMode
77
import com.woocommerce.android.ui.bookings.compose.BookingAttendanceStatus
88
import com.woocommerce.android.ui.bookings.compose.BookingCustomerDetailsModel
99
import com.woocommerce.android.ui.bookings.compose.BookingPaymentDetailsModel
10+
import com.woocommerce.android.ui.bookings.compose.BookingStaffMemberStatus
1011
import com.woocommerce.android.ui.bookings.compose.BookingStatus
1112
import com.woocommerce.android.ui.bookings.compose.BookingSummaryModel
1213
import com.woocommerce.android.ui.bookings.list.BookingListItem
@@ -54,13 +55,15 @@ class BookingMapper @Inject constructor(
5455
)
5556
}
5657

57-
fun Booking.toAppointmentDetailsModel(): BookingAppointmentDetailsModel {
58+
fun Booking.toAppointmentDetailsModel(
59+
staffMemberStatus: BookingStaffMemberStatus?
60+
): BookingAppointmentDetailsModel {
5861
val durationMinutes = Duration.between(start, end).toMinutes()
5962
return BookingAppointmentDetailsModel(
6063
date = detailsDateFormatter.format(start),
6164
time = "${timeRangeFormatter.format(start)} - ${timeRangeFormatter.format(end)}",
65+
staff = staffMemberStatus,
6266
// TODO replace mocked values when available from API
63-
staff = "Marianne Renoir",
6467
location = "238 Willow Creek Drive, Montgomery AL 36109",
6568
duration = "$durationMinutes min",
6669
price = currencyFormatter.formatCurrency(cost, currency)

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

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,12 @@ package com.woocommerce.android.ui.bookings
33
import com.woocommerce.android.WooException
44
import com.woocommerce.android.tools.SelectedSite
55
import kotlinx.coroutines.flow.Flow
6+
import kotlinx.coroutines.flow.flowOf
67
import org.wordpress.android.fluxc.network.rest.wpcom.wc.bookings.BookingsFilterOption
78
import org.wordpress.android.fluxc.network.rest.wpcom.wc.bookings.BookingsOrderOption
89
import org.wordpress.android.fluxc.network.rest.wpcom.wc.bookings.BookingsStore
910
import org.wordpress.android.fluxc.persistence.entity.BookingEntity
11+
import org.wordpress.android.fluxc.persistence.entity.BookingResourceEntity
1012
import javax.inject.Inject
1113

1214
class BookingsRepository @Inject constructor(
@@ -62,23 +64,48 @@ class BookingsRepository @Inject constructor(
6264

6365
suspend fun fetchBooking(
6466
bookingId: Long
65-
): Result<Unit> {
67+
): Result<Booking> {
6668
val result = bookingsStore.fetchBooking(
6769
site = selectedSite.get(),
6870
bookingId = bookingId
6971
)
72+
return if (result.isError) {
73+
Result.failure(WooException(result.error))
74+
} else {
75+
Result.success(result.model!!)
76+
}
77+
}
78+
79+
suspend fun fetchResource(
80+
resourceId: Long
81+
): Result<Unit> {
82+
val result = bookingsStore.fetchResource(
83+
site = selectedSite.get(),
84+
resourceId = resourceId
85+
)
7086
return if (result.isError) {
7187
Result.failure(WooException(result.error))
7288
} else {
7389
Result.success(Unit)
7490
}
7591
}
7692

93+
fun observeResource(resourceId: Long): Flow<BookingResource?> {
94+
return if (resourceId == 0L) {
95+
flowOf(null)
96+
} else {
97+
bookingsStore.observeResource(
98+
site = selectedSite.get(),
99+
resourceId = resourceId
100+
)
101+
}
102+
}
103+
77104
data class FetchResult(
78105
val bookings: List<Booking>,
79106
val hasMorePages: Boolean
80107
)
81108
}
82109

83110
typealias Booking = BookingEntity
84-
typealias BookingStatus = BookingEntity.Status
111+
typealias BookingResource = BookingResourceEntity

WooCommerce/src/main/kotlin/com/woocommerce/android/ui/bookings/compose/BookingAppointmentDetails.kt

Lines changed: 57 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package com.woocommerce.android.ui.bookings.compose
33
import androidx.annotation.StringRes
44
import androidx.compose.foundation.background
55
import androidx.compose.foundation.layout.Arrangement
6+
import androidx.compose.foundation.layout.Box
67
import androidx.compose.foundation.layout.Column
78
import androidx.compose.foundation.layout.Row
89
import androidx.compose.foundation.layout.fillMaxWidth
@@ -13,10 +14,12 @@ import androidx.compose.material3.MaterialTheme
1314
import androidx.compose.material3.Text
1415
import androidx.compose.runtime.Composable
1516
import androidx.compose.ui.Modifier
17+
import androidx.compose.ui.platform.LocalDensity
1618
import androidx.compose.ui.res.stringResource
1719
import androidx.compose.ui.text.style.TextOverflow
1820
import androidx.compose.ui.unit.dp
1921
import com.woocommerce.android.R
22+
import com.woocommerce.android.ui.compose.animations.SkeletonView
2023
import com.woocommerce.android.ui.compose.component.WCOutlinedButton
2124
import com.woocommerce.android.ui.compose.preview.LightDarkThemePreviews
2225
import com.woocommerce.android.ui.compose.theme.WooThemeWithBackground
@@ -39,10 +42,32 @@ fun BookingAppointmentDetails(
3942
label = R.string.booking_appointment_label_time,
4043
value = model.time
4144
)
42-
AppointmentDetailsRow(
43-
label = R.string.booking_appointment_label_staff,
44-
value = model.staff
45-
)
45+
model.staff?.let {
46+
AppointmentDetailsRow(
47+
label = R.string.booking_appointment_label_staff
48+
) {
49+
when (it) {
50+
is BookingStaffMemberStatus.Loaded, is BookingStaffMemberStatus.Unavailable -> {
51+
Text(
52+
text = (it as? BookingStaffMemberStatus.Loaded)?.name ?: "-",
53+
style = MaterialTheme.typography.bodyMedium,
54+
color = MaterialTheme.colorScheme.onSurfaceVariant,
55+
maxLines = 1,
56+
overflow = TextOverflow.Ellipsis
57+
)
58+
}
59+
60+
BookingStaffMemberStatus.Loading -> {
61+
SkeletonView(
62+
width = 80.dp,
63+
height = with(LocalDensity.current) {
64+
MaterialTheme.typography.bodyMedium.fontSize.toDp()
65+
},
66+
)
67+
}
68+
}
69+
}
70+
}
4671
AppointmentDetailsRow(
4772
label = R.string.booking_appointment_label_location,
4873
value = model.location
@@ -72,7 +97,10 @@ fun BookingAppointmentDetails(
7297
}
7398

7499
@Composable
75-
fun AppointmentDetailsRow(@StringRes label: Int, value: String) {
100+
private fun AppointmentDetailsRow(
101+
@StringRes label: Int,
102+
value: @Composable () -> Unit
103+
) {
76104
Column {
77105
Row(
78106
horizontalArrangement = Arrangement.SpaceBetween,
@@ -81,14 +109,9 @@ fun AppointmentDetailsRow(@StringRes label: Int, value: String) {
81109
.padding(horizontal = 16.dp, vertical = 12.dp)
82110
) {
83111
BookingDetailsLabel(label)
84-
Text(
85-
modifier = Modifier.padding(start = 8.dp),
86-
text = value,
87-
style = MaterialTheme.typography.bodyMedium,
88-
color = MaterialTheme.colorScheme.onSurfaceVariant,
89-
maxLines = 1,
90-
overflow = TextOverflow.Ellipsis
91-
)
112+
Box(Modifier.padding(start = 8.dp)) {
113+
value()
114+
}
92115
}
93116
HorizontalDivider(
94117
modifier = Modifier.padding(start = 16.dp),
@@ -97,15 +120,34 @@ fun AppointmentDetailsRow(@StringRes label: Int, value: String) {
97120
}
98121
}
99122

123+
@Composable
124+
private fun AppointmentDetailsRow(@StringRes label: Int, value: String) {
125+
AppointmentDetailsRow(label = label) {
126+
Text(
127+
text = value,
128+
style = MaterialTheme.typography.bodyMedium,
129+
color = MaterialTheme.colorScheme.onSurfaceVariant,
130+
maxLines = 1,
131+
overflow = TextOverflow.Ellipsis
132+
)
133+
}
134+
}
135+
100136
data class BookingAppointmentDetailsModel(
101137
val date: String,
102138
val time: String,
103-
val staff: String,
139+
val staff: BookingStaffMemberStatus?,
104140
val location: String,
105141
val duration: String,
106142
val price: String
107143
)
108144

145+
sealed interface BookingStaffMemberStatus {
146+
data object Loading : BookingStaffMemberStatus
147+
data class Loaded(val name: String) : BookingStaffMemberStatus
148+
data object Unavailable : BookingStaffMemberStatus
149+
}
150+
109151
@LightDarkThemePreviews
110152
@Composable
111153
private fun BookingAppointmentDetailsPreview() {
@@ -114,7 +156,7 @@ private fun BookingAppointmentDetailsPreview() {
114156
model = BookingAppointmentDetailsModel(
115157
date = "05/07/2025, 11:00 AM",
116158
time = "11:00 am - 12:00 pm",
117-
staff = "Marianne Renoir",
159+
staff = BookingStaffMemberStatus.Loading,
118160
location = "238 Willow Creek Drive, Montgomery AL 36109",
119161
duration = "60 min",
120162
price = "$55.00"

WooCommerce/src/main/kotlin/com/woocommerce/android/ui/bookings/details/BookingDetailsScreen.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import com.woocommerce.android.ui.bookings.compose.BookingCustomerDetails
3636
import com.woocommerce.android.ui.bookings.compose.BookingCustomerDetailsModel
3737
import com.woocommerce.android.ui.bookings.compose.BookingPaymentDetailsModel
3838
import com.woocommerce.android.ui.bookings.compose.BookingPaymentSection
39+
import com.woocommerce.android.ui.bookings.compose.BookingStaffMemberStatus
3940
import com.woocommerce.android.ui.bookings.compose.BookingStatus
4041
import com.woocommerce.android.ui.bookings.compose.BookingSummary
4142
import com.woocommerce.android.ui.bookings.compose.BookingSummaryModel
@@ -225,7 +226,7 @@ private fun BookingDetailsPreview() {
225226
bookingsAppointmentDetails = BookingAppointmentDetailsModel(
226227
date = "Monday, 05 July 2025",
227228
time = "11:00 am - 12:00 pm",
228-
staff = "Marianne Renoir",
229+
staff = BookingStaffMemberStatus.Loaded("Marianne Renoir"),
229230
location = "238 Willow Creek Drive, Montgomery AL 36109",
230231
duration = "60 min",
231232
price = "$55.00"

WooCommerce/src/main/kotlin/com/woocommerce/android/ui/bookings/details/BookingDetailsViewModel.kt

Lines changed: 69 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,22 +3,34 @@ package com.woocommerce.android.ui.bookings.details
33
import androidx.lifecycle.LiveData
44
import androidx.lifecycle.SavedStateHandle
55
import androidx.lifecycle.asLiveData
6+
import androidx.lifecycle.viewModelScope
67
import com.woocommerce.android.R
78
import com.woocommerce.android.tools.NetworkStatus
89
import com.woocommerce.android.ui.bookings.Booking
910
import com.woocommerce.android.ui.bookings.BookingMapper
11+
import com.woocommerce.android.ui.bookings.BookingResource
1012
import com.woocommerce.android.ui.bookings.BookingsRepository
1113
import com.woocommerce.android.ui.bookings.compose.BookingAttendanceStatus
14+
import com.woocommerce.android.ui.bookings.compose.BookingStaffMemberStatus
1215
import com.woocommerce.android.viewmodel.MultiLiveEvent
1316
import com.woocommerce.android.viewmodel.ResourceProvider
1417
import com.woocommerce.android.viewmodel.ScopedViewModel
1518
import com.woocommerce.android.viewmodel.navArgs
1619
import dagger.hilt.android.lifecycle.HiltViewModel
20+
import kotlinx.coroutines.ExperimentalCoroutinesApi
21+
import kotlinx.coroutines.async
22+
import kotlinx.coroutines.awaitAll
1723
import kotlinx.coroutines.flow.MutableStateFlow
24+
import kotlinx.coroutines.flow.SharingStarted
1825
import kotlinx.coroutines.flow.combine
26+
import kotlinx.coroutines.flow.first
27+
import kotlinx.coroutines.flow.flatMapLatest
28+
import kotlinx.coroutines.flow.flowOf
29+
import kotlinx.coroutines.flow.shareIn
1930
import kotlinx.coroutines.launch
2031
import javax.inject.Inject
2132

33+
@OptIn(ExperimentalCoroutinesApi::class)
2234
@HiltViewModel
2335
class BookingDetailsViewModel @Inject constructor(
2436
savedState: SavedStateHandle,
@@ -31,6 +43,11 @@ class BookingDetailsViewModel @Inject constructor(
3143
private val navArgs: BookingDetailsFragmentArgs by savedState.navArgs()
3244

3345
private val booking = bookingsRepository.observeBooking(navArgs.bookingId)
46+
.shareIn(scope = viewModelScope, started = SharingStarted.WhileSubscribed(), replay = 1)
47+
48+
private val resource = booking.flatMapLatest { booking ->
49+
booking?.resourceId?.let { bookingsRepository.observeResource(it) } ?: flowOf(null)
50+
}
3451

3552
private val loadingState = MutableStateFlow<BookingDetailsLoadingState>(BookingDetailsLoadingState.Idle)
3653

@@ -40,14 +57,19 @@ class BookingDetailsViewModel @Inject constructor(
4057
val state: LiveData<BookingDetailsViewState> = combine(
4158
booking,
4259
bookingAttendanceStatus,
43-
loadingState
44-
) { booking, attendanceStatus, loadingState ->
60+
loadingState,
61+
resource
62+
) { booking, attendanceStatus, loadingState, resource ->
4563
with(bookingMapper) {
4664
BookingDetailsViewState(
4765
toolbarTitle = booking?.id?.value?.let { id ->
4866
resourceProvider.getString(R.string.booking_details_title, id)
4967
} ?: "",
50-
bookingUiState = if (booking != null) buildBookingUiState(booking, attendanceStatus) else null,
68+
bookingUiState = if (booking != null) {
69+
buildBookingUiState(booking, resource, attendanceStatus, loadingState)
70+
} else {
71+
null
72+
},
5173
loadingState = loadingState,
5274
onCancelBooking = ::onCancelBooking,
5375
onAttendanceStatusSelected = ::onAttendanceStatusSelected,
@@ -60,17 +82,30 @@ class BookingDetailsViewModel @Inject constructor(
6082
fetchBooking(BookingDetailsLoadingState.Loading)
6183
}
6284

63-
private fun fetchBooking(state: BookingDetailsLoadingState = BookingDetailsLoadingState.Refreshing) {
85+
private fun fetchBooking(
86+
initialLoadingState: BookingDetailsLoadingState = BookingDetailsLoadingState.Refreshing
87+
) {
6488
launch {
6589
if (!networkStatus.isConnected()) {
6690
triggerEvent(MultiLiveEvent.Event.ShowSnackbar(R.string.offline_error))
6791
return@launch
6892
}
69-
loadingState.value = state
70-
bookingsRepository.fetchBooking(navArgs.bookingId)
71-
.onFailure {
72-
triggerEvent(MultiLiveEvent.Event.ShowSnackbar(R.string.bookings_fetch_error))
73-
}
93+
94+
loadingState.value = initialLoadingState
95+
96+
val bookingTask = async {
97+
bookingsRepository.fetchBooking(navArgs.bookingId)
98+
}
99+
val resourceTask = async {
100+
val booking = booking.first() ?: bookingTask.await().getOrNull()
101+
val resourceId = booking?.resourceId?.takeIf { it != 0L } ?: return@async Result.success(Unit)
102+
bookingsRepository.fetchResource(resourceId)
103+
}
104+
105+
if (awaitAll(bookingTask, resourceTask).any { it.isFailure }) {
106+
triggerEvent(MultiLiveEvent.Event.ShowSnackbar(R.string.bookings_fetch_error))
107+
}
108+
74109
loadingState.value = BookingDetailsLoadingState.Idle
75110
}
76111
}
@@ -86,7 +121,9 @@ class BookingDetailsViewModel @Inject constructor(
86121

87122
private suspend fun BookingMapper.buildBookingUiState(
88123
booking: Booking,
89-
attendanceStatus: BookingAttendanceStatus?
124+
resource: BookingResource?,
125+
attendanceStatus: BookingAttendanceStatus?,
126+
loadingState: BookingDetailsLoadingState
90127
): BookingUiState = BookingUiState(
91128
orderId = booking.orderId,
92129
bookingSummary = booking.toBookingSummaryModel().let {
@@ -96,8 +133,29 @@ class BookingDetailsViewModel @Inject constructor(
96133
it
97134
}
98135
},
99-
bookingsAppointmentDetails = booking.toAppointmentDetailsModel(),
136+
bookingsAppointmentDetails = booking.toAppointmentDetailsModel(
137+
staffMemberStatus = buildStaffMemberStatus(
138+
resourceId = booking.resourceId,
139+
resource = resource,
140+
loadingState = loadingState
141+
)
142+
),
100143
bookingCustomerDetails = booking.order.customerInfo.toCustomerDetailsModel(),
101144
bookingPaymentDetails = booking.order.paymentInfo?.toPaymentDetailsModel(booking.currency)
102145
)
146+
147+
private fun buildStaffMemberStatus(
148+
resourceId: Long,
149+
resource: BookingResource?,
150+
loadingState: BookingDetailsLoadingState
151+
): BookingStaffMemberStatus? {
152+
return when {
153+
resourceId == 0L -> null
154+
resource != null -> BookingStaffMemberStatus.Loaded(resource.name)
155+
loadingState == BookingDetailsLoadingState.Loading ||
156+
loadingState == BookingDetailsLoadingState.Refreshing -> BookingStaffMemberStatus.Loading
157+
158+
else -> BookingStaffMemberStatus.Unavailable
159+
}
160+
}
103161
}

0 commit comments

Comments
 (0)