Skip to content

Commit 55e165a

Browse files
Merge pull request #14682 from woocommerce/issue/WOOMOB-1400_observe_booking
[WOOMOB-1400] - Observe booking object from DB on the details screen
2 parents ef52c54 + 152b266 commit 55e165a

File tree

12 files changed

+457
-156
lines changed

12 files changed

+457
-156
lines changed
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
package com.woocommerce.android.ui.bookings
2+
3+
import com.woocommerce.android.ui.bookings.compose.BookingAppointmentDetailsModel
4+
import com.woocommerce.android.ui.bookings.compose.BookingAttendanceStatus
5+
import com.woocommerce.android.ui.bookings.compose.BookingStatus
6+
import com.woocommerce.android.ui.bookings.compose.BookingSummaryModel
7+
import com.woocommerce.android.ui.bookings.list.BookingListItem
8+
import com.woocommerce.android.util.CurrencyFormatter
9+
import org.wordpress.android.fluxc.persistence.entity.BookingEntity
10+
import java.time.Duration
11+
import java.time.ZoneOffset
12+
import java.time.format.DateTimeFormatter
13+
import java.time.format.FormatStyle
14+
import javax.inject.Inject
15+
16+
class BookingMapper @Inject constructor(
17+
private val currencyFormatter: CurrencyFormatter,
18+
) {
19+
private val summaryDateFormatter: DateTimeFormatter = DateTimeFormatter.ofLocalizedDateTime(
20+
FormatStyle.MEDIUM,
21+
FormatStyle.SHORT
22+
).withZone(ZoneOffset.UTC)
23+
24+
private val detailsDateFormatter: DateTimeFormatter = DateTimeFormatter.ofLocalizedDate(FormatStyle.FULL)
25+
.withZone(ZoneOffset.UTC)
26+
private val timeRangeFormatter: DateTimeFormatter = DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT)
27+
.withZone(ZoneOffset.UTC)
28+
29+
fun Booking.toBookingSummaryModel(): BookingSummaryModel {
30+
return BookingSummaryModel(
31+
date = summaryDateFormatter.format(start),
32+
// TODO replace mocked values when product and customer data are available
33+
name = "Women’s Haircut",
34+
customerName = "Margarita Nikolaevna",
35+
attendanceStatus = BookingAttendanceStatus.BOOKED,
36+
status = status.toUiModel()
37+
)
38+
}
39+
40+
fun Booking.toListItem(): BookingListItem {
41+
return BookingListItem(
42+
id = id.value,
43+
summary = toBookingSummaryModel()
44+
)
45+
}
46+
47+
fun Booking.toAppointmentDetailsModel(): BookingAppointmentDetailsModel {
48+
val durationMinutes = Duration.between(start, end).toMinutes()
49+
return BookingAppointmentDetailsModel(
50+
date = detailsDateFormatter.format(start),
51+
time = "${timeRangeFormatter.format(start)} - ${timeRangeFormatter.format(end)}",
52+
// TODO replace mocked values when available from API
53+
staff = "Marianne Renoir",
54+
location = "238 Willow Creek Drive, Montgomery AL 36109",
55+
duration = "$durationMinutes min",
56+
price = currencyFormatter.formatCurrency(cost, currency)
57+
)
58+
}
59+
60+
private fun BookingEntity.Status.toUiModel(): BookingStatus = when (this) {
61+
BookingEntity.Status.Paid -> BookingStatus.Paid
62+
BookingEntity.Status.PendingConfirmation -> BookingStatus.PendingConfirmation
63+
BookingEntity.Status.Cancelled -> BookingStatus.Cancelled
64+
BookingEntity.Status.Complete -> BookingStatus.Complete
65+
BookingEntity.Status.Confirmed -> BookingStatus.Confirmed
66+
BookingEntity.Status.Unpaid -> BookingStatus.Unpaid
67+
is BookingEntity.Status.Unknown -> BookingStatus.Unknown(this.key)
68+
}
69+
}

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,12 @@ class BookingsRepository @Inject constructor(
4949
filters = filters
5050
)
5151

52+
fun observeBooking(bookingId: Long): Flow<Booking?> =
53+
bookingsStore.observeBooking(
54+
site = selectedSite.get(),
55+
bookingId = bookingId
56+
)
57+
5258
data class FetchResult(
5359
val bookings: List<Booking>,
5460
val hasMorePages: Boolean

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

Lines changed: 70 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ import com.woocommerce.android.ui.bookings.compose.BookingAttendanceSection
2626
import com.woocommerce.android.ui.bookings.compose.BookingAttendanceStatus
2727
import com.woocommerce.android.ui.bookings.compose.BookingAttendanceStatusBottomSheet
2828
import com.woocommerce.android.ui.bookings.compose.BookingCustomerDetails
29+
import com.woocommerce.android.ui.bookings.compose.BookingCustomerDetailsModel
30+
import com.woocommerce.android.ui.bookings.compose.BookingPaymentDetailsModel
2931
import com.woocommerce.android.ui.bookings.compose.BookingPaymentSection
3032
import com.woocommerce.android.ui.bookings.compose.BookingStatus
3133
import com.woocommerce.android.ui.bookings.compose.BookingSummary
@@ -76,42 +78,44 @@ fun BookingDetailsScreen(
7678
.verticalScroll(rememberScrollState())
7779
.padding(innerPadding)
7880
) {
79-
BookingSummary(
80-
model = viewState.bookingSummary,
81-
modifier = Modifier.fillMaxWidth()
82-
)
83-
BookingAppointmentDetails(
84-
model = viewState.bookingsAppointmentDetails,
85-
onCancelBooking = viewState.onCancelBooking,
86-
modifier = Modifier.fillMaxWidth()
87-
)
88-
BookingCustomerDetails(
89-
model = viewState.bookingCustomerDetails,
90-
modifier = Modifier.fillMaxWidth()
91-
)
92-
BookingAttendanceSection(
93-
status = viewState.bookingSummary.attendanceStatus,
94-
onClick = { showAttendanceSheet.value = true },
95-
modifier = Modifier.fillMaxWidth()
96-
)
97-
BookingPaymentSection(
98-
model = viewState.bookingPaymentDetails,
99-
status = viewState.bookingSummary.status,
100-
onMarkAsPaid = { onViewOrder(viewState.orderId) },
101-
onViewOrder = { onViewOrder(viewState.orderId) },
102-
onMarkAsRefunded = { onViewOrder(viewState.orderId) },
103-
modifier = Modifier.fillMaxWidth()
104-
)
105-
}
106-
if (showAttendanceSheet.value) {
107-
BookingAttendanceStatusBottomSheet(
108-
onSelect = { status ->
109-
viewState.onAttendanceStatusSelected(status)
110-
},
111-
onDismiss = { showAttendanceSheet.value = false }
112-
)
81+
viewState.bookingUiState?.let {
82+
BookingSummary(
83+
model = viewState.bookingUiState.bookingSummary,
84+
modifier = Modifier.fillMaxWidth()
85+
)
86+
BookingAppointmentDetails(
87+
model = viewState.bookingUiState.bookingsAppointmentDetails,
88+
onCancelBooking = viewState.onCancelBooking,
89+
modifier = Modifier.fillMaxWidth()
90+
)
91+
BookingCustomerDetails(
92+
model = viewState.bookingUiState.bookingCustomerDetails,
93+
modifier = Modifier.fillMaxWidth()
94+
)
95+
BookingAttendanceSection(
96+
status = viewState.bookingUiState.bookingSummary.attendanceStatus,
97+
onClick = { showAttendanceSheet.value = true },
98+
modifier = Modifier.fillMaxWidth()
99+
)
100+
BookingPaymentSection(
101+
model = viewState.bookingUiState.bookingPaymentDetails,
102+
status = viewState.bookingUiState.bookingSummary.status,
103+
onMarkAsPaid = { onViewOrder(viewState.orderId) },
104+
onViewOrder = { onViewOrder(viewState.orderId) },
105+
onMarkAsRefunded = { onViewOrder(viewState.orderId) },
106+
modifier = Modifier.fillMaxWidth()
107+
)
108+
}
113109
}
114110
}
111+
if (showAttendanceSheet.value) {
112+
BookingAttendanceStatusBottomSheet(
113+
onSelect = { status ->
114+
viewState.onAttendanceStatusSelected(status)
115+
},
116+
onDismiss = { showAttendanceSheet.value = false }
117+
)
118+
}
115119
}
116120
}
117121

@@ -122,21 +126,39 @@ private fun BookingDetailsPreview() {
122126
BookingDetailsScreen(
123127
viewState = BookingDetailsViewState(
124128
toolbarTitle = "Booking #12345",
125-
bookingSummary = BookingSummaryModel(
126-
date = "05/07/2025, 11:00 AM",
127-
name = "Women’s Haircut",
128-
customerName = "Margarita Nikolaevna",
129-
attendanceStatus = BookingAttendanceStatus.CHECKED_IN,
130-
status = BookingStatus.Paid
129+
bookingUiState = BookingUiState(
130+
bookingSummary = BookingSummaryModel(
131+
date = "05/07/2025, 11:00 AM",
132+
name = "Women’s Haircut",
133+
customerName = "Margarita Nikolaevna",
134+
attendanceStatus = BookingAttendanceStatus.CHECKED_IN,
135+
status = BookingStatus.Paid
136+
),
137+
bookingsAppointmentDetails = BookingAppointmentDetailsModel(
138+
date = "Monday, 05 July 2025",
139+
time = "11:00 am - 12:00 pm",
140+
staff = "Marianne Renoir",
141+
location = "238 Willow Creek Drive, Montgomery AL 36109",
142+
duration = "60 min",
143+
price = "$55.00"
144+
),
145+
bookingCustomerDetails = BookingCustomerDetailsModel(
146+
name = "Margarita Nikolaevna",
147+
email = "[email protected]",
148+
phone = "+1 555-123-4567",
149+
billingAddressLines = listOf(
150+
"238 Willow Creek Drive",
151+
"Montgomery AL 36109",
152+
"United States"
153+
)
154+
),
155+
bookingPaymentDetails = BookingPaymentDetailsModel(
156+
service = "$55.00",
157+
tax = "$4.50",
158+
discount = "-",
159+
total = "$59.50"
160+
)
131161
),
132-
bookingsAppointmentDetails = BookingAppointmentDetailsModel(
133-
date = "Monday, 05 July 2025",
134-
time = "11:00 am - 12:00 pm",
135-
staff = "Marianne Renoir",
136-
location = "238 Willow Creek Drive, Montgomery AL 36109",
137-
duration = "60 min",
138-
price = "$55.00"
139-
)
140162
),
141163
onBack = {},
142164
onViewOrder = {}

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

Lines changed: 57 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -4,48 +4,87 @@ import androidx.lifecycle.LiveData
44
import androidx.lifecycle.SavedStateHandle
55
import androidx.lifecycle.asLiveData
66
import com.woocommerce.android.R
7+
import com.woocommerce.android.ui.bookings.Booking
8+
import com.woocommerce.android.ui.bookings.BookingMapper
9+
import com.woocommerce.android.ui.bookings.BookingsRepository
710
import com.woocommerce.android.ui.bookings.compose.BookingAttendanceStatus
11+
import com.woocommerce.android.ui.bookings.compose.BookingCustomerDetailsModel
12+
import com.woocommerce.android.ui.bookings.compose.BookingPaymentDetailsModel
813
import com.woocommerce.android.viewmodel.ResourceProvider
914
import com.woocommerce.android.viewmodel.ScopedViewModel
1015
import com.woocommerce.android.viewmodel.navArgs
1116
import dagger.hilt.android.lifecycle.HiltViewModel
1217
import kotlinx.coroutines.flow.MutableStateFlow
13-
import kotlinx.coroutines.flow.update
18+
import kotlinx.coroutines.flow.combine
19+
import kotlinx.coroutines.flow.filterNotNull
1420
import javax.inject.Inject
1521

1622
@HiltViewModel
1723
class BookingDetailsViewModel @Inject constructor(
1824
savedState: SavedStateHandle,
1925
resourceProvider: ResourceProvider,
26+
bookingsRepository: BookingsRepository,
27+
private val bookingMapper: BookingMapper,
2028
) : ScopedViewModel(savedState) {
2129

2230
private val navArgs: BookingDetailsFragmentArgs by savedState.navArgs()
2331

24-
private val _state = MutableStateFlow(
25-
BookingDetailsViewState(
26-
onCancelBooking = ::onCancelBooking,
27-
onAttendanceStatusSelected = ::onAttendanceStatusSelected,
28-
)
29-
)
30-
val state: LiveData<BookingDetailsViewState> = _state.asLiveData()
32+
private val booking = bookingsRepository.observeBooking(navArgs.bookingId)
33+
34+
// Temporary, the booking status should come from the stored object
35+
private val bookingAttendanceStatus = MutableStateFlow<BookingAttendanceStatus?>(null)
3136

32-
init {
33-
_state.update {
34-
it.copy(
35-
toolbarTitle = resourceProvider.getString(R.string.booking_details_title, navArgs.bookingId),
37+
val state: LiveData<BookingDetailsViewState> = combine(
38+
booking.filterNotNull(),
39+
bookingAttendanceStatus
40+
) { booking, attendanceStatus ->
41+
with(bookingMapper) {
42+
BookingDetailsViewState(
43+
toolbarTitle = resourceProvider.getString(R.string.booking_details_title, booking.id.value),
44+
orderId = booking.orderId,
45+
bookingUiState = buildBookingUiState(booking, attendanceStatus),
46+
onCancelBooking = ::onCancelBooking,
47+
onAttendanceStatusSelected = ::onAttendanceStatusSelected
3648
)
3749
}
38-
}
50+
}.asLiveData()
3951

4052
private fun onAttendanceStatusSelected(status: BookingAttendanceStatus) {
41-
_state.update { current ->
42-
current.copy(
43-
bookingSummary = current.bookingSummary.copy(attendanceStatus = status)
44-
)
45-
}
53+
// Temporary, the booking status should come from the stored object
54+
bookingAttendanceStatus.value = status
4655
}
4756

4857
private fun onCancelBooking() {
4958
// TODO Add logic to Cancel booking
5059
}
60+
61+
private fun BookingMapper.buildBookingUiState(
62+
booking: Booking,
63+
attendanceStatus: BookingAttendanceStatus?
64+
): BookingUiState = BookingUiState(
65+
bookingSummary = booking.toBookingSummaryModel().let {
66+
if (attendanceStatus != null) {
67+
it.copy(attendanceStatus = attendanceStatus)
68+
} else {
69+
it
70+
}
71+
},
72+
bookingsAppointmentDetails = booking.toAppointmentDetailsModel(),
73+
bookingCustomerDetails = BookingCustomerDetailsModel(
74+
name = "Margarita Nikolaevna",
75+
email = "[email protected]",
76+
phone = "+1 555-123-4567",
77+
billingAddressLines = listOf(
78+
"238 Willow Creek Drive",
79+
"Montgomery AL 36109",
80+
"United States"
81+
)
82+
),
83+
bookingPaymentDetails = BookingPaymentDetailsModel(
84+
service = "$55.00",
85+
tax = "$4.50",
86+
discount = "-",
87+
total = "$59.50"
88+
)
89+
)
5190
}

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

Lines changed: 8 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -4,43 +4,19 @@ import com.woocommerce.android.ui.bookings.compose.BookingAppointmentDetailsMode
44
import com.woocommerce.android.ui.bookings.compose.BookingAttendanceStatus
55
import com.woocommerce.android.ui.bookings.compose.BookingCustomerDetailsModel
66
import com.woocommerce.android.ui.bookings.compose.BookingPaymentDetailsModel
7-
import com.woocommerce.android.ui.bookings.compose.BookingStatus
87
import com.woocommerce.android.ui.bookings.compose.BookingSummaryModel
98

109
data class BookingDetailsViewState(
1110
val toolbarTitle: String = "",
1211
val orderId: Long = 0L,
13-
val bookingSummary: BookingSummaryModel = BookingSummaryModel(
14-
date = "05/07/2025, 11:00 AM",
15-
name = "Women’s Haircut",
16-
customerName = "Margarita Nikolaevna",
17-
attendanceStatus = BookingAttendanceStatus.NO_SHOW,
18-
status = BookingStatus.Paid
19-
),
20-
val bookingsAppointmentDetails: BookingAppointmentDetailsModel = BookingAppointmentDetailsModel(
21-
date = "Monday, 05 July 2025",
22-
time = "11:00 am - 12:00 pm",
23-
staff = "Marianne Renoir",
24-
location = "238 Willow Creek Drive, Montgomery AL 36109",
25-
duration = "60 min",
26-
price = "$55.00"
27-
),
28-
val bookingCustomerDetails: BookingCustomerDetailsModel = BookingCustomerDetailsModel(
29-
name = "Margarita Nikolaevna",
30-
email = "[email protected]",
31-
phone = "+1 555-123-4567",
32-
billingAddressLines = listOf(
33-
"238 Willow Creek Drive",
34-
"Montgomery AL 36109",
35-
"United States"
36-
)
37-
),
38-
val bookingPaymentDetails: BookingPaymentDetailsModel = BookingPaymentDetailsModel(
39-
service = "$55.00",
40-
tax = "$4.50",
41-
discount = "-",
42-
total = "$59.50"
43-
),
12+
val bookingUiState: BookingUiState? = null,
4413
val onCancelBooking: () -> Unit = {},
4514
val onAttendanceStatusSelected: (BookingAttendanceStatus) -> Unit = { _ -> }
4615
)
16+
17+
data class BookingUiState(
18+
val bookingSummary: BookingSummaryModel,
19+
val bookingsAppointmentDetails: BookingAppointmentDetailsModel,
20+
val bookingCustomerDetails: BookingCustomerDetailsModel,
21+
val bookingPaymentDetails: BookingPaymentDetailsModel,
22+
)

0 commit comments

Comments
 (0)