-
Notifications
You must be signed in to change notification settings - Fork 136
[WOOMOB-1400] - Observe booking object from DB on the details screen #14682
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 10 commits
5230f5a
221c2b6
16f6336
6bd0c81
ddc5ed9
13614a8
b43c1fd
c33ad87
4533d98
509794d
b168757
acb05f5
2df6a27
152b266
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,69 @@ | ||||||
| package com.woocommerce.android.ui.bookings | ||||||
|
|
||||||
| import com.woocommerce.android.ui.bookings.compose.BookingAppointmentDetailsModel | ||||||
| import com.woocommerce.android.ui.bookings.compose.BookingAttendanceStatus | ||||||
| import com.woocommerce.android.ui.bookings.compose.BookingStatus | ||||||
| import com.woocommerce.android.ui.bookings.compose.BookingSummaryModel | ||||||
| import com.woocommerce.android.ui.bookings.list.BookingListItem | ||||||
| import com.woocommerce.android.util.CurrencyFormatter | ||||||
| import org.wordpress.android.fluxc.persistence.entity.BookingEntity | ||||||
| import java.time.Duration | ||||||
| import java.time.ZoneOffset | ||||||
| import java.time.format.DateTimeFormatter | ||||||
| import java.time.format.FormatStyle | ||||||
| import javax.inject.Inject | ||||||
|
|
||||||
| class BookingMapper @Inject constructor( | ||||||
| private val currencyFormatter: CurrencyFormatter, | ||||||
| ) { | ||||||
| private val summaryDateFormatter: DateTimeFormatter = DateTimeFormatter.ofLocalizedDateTime( | ||||||
| FormatStyle.MEDIUM, | ||||||
| FormatStyle.SHORT | ||||||
| ).withZone(ZoneOffset.UTC) | ||||||
|
|
||||||
| private val detailsDateFormatter: DateTimeFormatter = DateTimeFormatter.ofPattern("EEEE, dd MMM yyyy") | ||||||
|
||||||
| private val detailsDateFormatter: DateTimeFormatter = DateTimeFormatter.ofPattern("EEEE, dd MMM yyyy") | |
| private val detailsDateFormatter: DateTimeFormatter = DateTimeFormatter.ofLocalizedDate(FormatStyle.FULL) |
The above will update the text from: Monday, 06 Oct 2025 to Monday, 6 October 2025 for UK english set as the locale.
Another approach if we are sure we want to keep the shorter month name is to use something like:
private val detailsDateFormatter: DateTimeFormatter = DateTimeFormatterBuilder()
.appendPattern("EEEE, ")
.append(DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM))
.toFormatter()
.withZone(ZoneOffset.UTC)But personally I think using FormatStyle.FULL is better in this context, WDYT?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Makes sense, I did focus on the format I saw in the Figma, but this will work better across different locales. Thanks!
Outdated
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
np, just to make the role a bit clearer, I'd rename this to toListItem(), WDYT?
AdamGrzybkowski marked this conversation as resolved.
Show resolved
Hide resolved
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -26,6 +26,8 @@ import com.woocommerce.android.ui.bookings.compose.BookingAttendanceSection | |
| import com.woocommerce.android.ui.bookings.compose.BookingAttendanceStatus | ||
| import com.woocommerce.android.ui.bookings.compose.BookingAttendanceStatusBottomSheet | ||
| import com.woocommerce.android.ui.bookings.compose.BookingCustomerDetails | ||
| import com.woocommerce.android.ui.bookings.compose.BookingCustomerDetailsModel | ||
| import com.woocommerce.android.ui.bookings.compose.BookingPaymentDetailsModel | ||
| import com.woocommerce.android.ui.bookings.compose.BookingPaymentSection | ||
| import com.woocommerce.android.ui.bookings.compose.BookingStatus | ||
| import com.woocommerce.android.ui.bookings.compose.BookingSummary | ||
|
|
@@ -76,44 +78,46 @@ fun BookingDetailsScreen( | |
| .verticalScroll(rememberScrollState()) | ||
| .padding(innerPadding) | ||
| ) { | ||
| BookingSummary( | ||
| model = viewState.bookingSummary, | ||
| modifier = Modifier.fillMaxWidth() | ||
| ) | ||
| BookingAppointmentDetails( | ||
| model = viewState.bookingsAppointmentDetails, | ||
| onCancelBooking = viewState.onCancelBooking, | ||
| modifier = Modifier.fillMaxWidth() | ||
| ) | ||
| BookingCustomerDetails( | ||
| model = viewState.bookingCustomerDetails, | ||
| onEmailClick = {}, | ||
| onPhoneClick = {}, | ||
| modifier = Modifier.fillMaxWidth() | ||
| ) | ||
| BookingAttendanceSection( | ||
| status = viewState.bookingSummary.attendanceStatus, | ||
| onClick = { showAttendanceSheet.value = true }, | ||
| modifier = Modifier.fillMaxWidth() | ||
| ) | ||
| BookingPaymentSection( | ||
| model = viewState.bookingPaymentDetails, | ||
| status = viewState.bookingSummary.status, | ||
| onMarkAsPaid = { onViewOrder(viewState.orderId) }, | ||
| onViewOrder = { onViewOrder(viewState.orderId) }, | ||
| onMarkAsRefunded = { onViewOrder(viewState.orderId) }, | ||
| modifier = Modifier.fillMaxWidth() | ||
| ) | ||
| } | ||
| if (showAttendanceSheet.value) { | ||
| BookingAttendanceStatusBottomSheet( | ||
| onSelect = { status -> | ||
| viewState.onAttendanceStatusSelected(status) | ||
| }, | ||
| onDismiss = { showAttendanceSheet.value = false } | ||
| ) | ||
| viewState.bookingUiState?.let { | ||
| BookingSummary( | ||
| model = viewState.bookingUiState.bookingSummary, | ||
| modifier = Modifier.fillMaxWidth() | ||
| ) | ||
| BookingAppointmentDetails( | ||
| model = viewState.bookingUiState.bookingsAppointmentDetails, | ||
| onCancelBooking = viewState.onCancelBooking, | ||
| modifier = Modifier.fillMaxWidth() | ||
| ) | ||
| BookingCustomerDetails( | ||
| model = viewState.bookingUiState.bookingCustomerDetails, | ||
| onEmailClick = {}, | ||
| onPhoneClick = {}, | ||
| modifier = Modifier.fillMaxWidth() | ||
| ) | ||
| BookingAttendanceSection( | ||
| status = viewState.bookingUiState.bookingSummary.attendanceStatus, | ||
| onClick = { showAttendanceSheet.value = true }, | ||
| modifier = Modifier.fillMaxWidth() | ||
| ) | ||
| BookingPaymentSection( | ||
| model = viewState.bookingUiState.bookingPaymentDetails, | ||
| status = viewState.bookingUiState.bookingSummary.status, | ||
| onMarkAsPaid = { onViewOrder(viewState.orderId) }, | ||
| onViewOrder = { onViewOrder(viewState.orderId) }, | ||
| onMarkAsRefunded = { onViewOrder(viewState.orderId) }, | ||
| modifier = Modifier.fillMaxWidth() | ||
| ) | ||
| } | ||
| } | ||
| } | ||
| if (showAttendanceSheet.value) { | ||
| BookingAttendanceStatusBottomSheet( | ||
| onSelect = { status -> | ||
| viewState.onAttendanceStatusSelected(status) | ||
| }, | ||
| onDismiss = { showAttendanceSheet.value = false } | ||
| ) | ||
| } | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -124,21 +128,39 @@ private fun BookingDetailsPreview() { | |
| BookingDetailsScreen( | ||
| viewState = BookingDetailsViewState( | ||
| toolbarTitle = "Booking #12345", | ||
| bookingSummary = BookingSummaryModel( | ||
| date = "05/07/2025, 11:00 AM", | ||
| name = "Women’s Haircut", | ||
| customerName = "Margarita Nikolaevna", | ||
| attendanceStatus = BookingAttendanceStatus.CHECKED_IN, | ||
| status = BookingStatus.Paid | ||
| bookingUiState = BookingUiState( | ||
| bookingSummary = BookingSummaryModel( | ||
| date = "05/07/2025, 11:00 AM", | ||
| name = "Women’s Haircut", | ||
| customerName = "Margarita Nikolaevna", | ||
| attendanceStatus = BookingAttendanceStatus.CHECKED_IN, | ||
| status = BookingStatus.Paid | ||
| ), | ||
| bookingsAppointmentDetails = BookingAppointmentDetailsModel( | ||
| date = "Monday, 05 July 2025", | ||
| time = "11:00 am - 12:00 pm", | ||
| staff = "Marianne Renoir", | ||
| location = "238 Willow Creek Drive, Montgomery AL 36109", | ||
| duration = "60 min", | ||
| price = "$55.00" | ||
| ), | ||
| bookingCustomerDetails = BookingCustomerDetailsModel( | ||
| name = "Margarita Nikolaevna", | ||
| email = "[email protected]", | ||
| phone = "+1 555-123-4567", | ||
| billingAddressLines = listOf( | ||
| "238 Willow Creek Drive", | ||
| "Montgomery AL 36109", | ||
| "United States" | ||
| ) | ||
| ), | ||
| bookingPaymentDetails = BookingPaymentDetailsModel( | ||
| service = "$55.00", | ||
| tax = "$4.50", | ||
| discount = "-", | ||
| total = "$59.50" | ||
| ) | ||
| ), | ||
| bookingsAppointmentDetails = BookingAppointmentDetailsModel( | ||
| date = "Monday, 05 July 2025", | ||
| time = "11:00 am - 12:00 pm", | ||
| staff = "Marianne Renoir", | ||
| location = "238 Willow Creek Drive, Montgomery AL 36109", | ||
| duration = "60 min", | ||
| price = "$55.00" | ||
| ) | ||
| ), | ||
| onBack = {}, | ||
| onViewOrder = {} | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,19 +4,28 @@ import androidx.lifecycle.LiveData | |
| import androidx.lifecycle.SavedStateHandle | ||
| import androidx.lifecycle.asLiveData | ||
| import com.woocommerce.android.R | ||
| import com.woocommerce.android.ui.bookings.Booking | ||
| import com.woocommerce.android.ui.bookings.BookingMapper | ||
| import com.woocommerce.android.ui.bookings.BookingsRepository | ||
| import com.woocommerce.android.ui.bookings.compose.BookingAttendanceStatus | ||
| import com.woocommerce.android.ui.bookings.compose.BookingCustomerDetailsModel | ||
| import com.woocommerce.android.ui.bookings.compose.BookingPaymentDetailsModel | ||
| import com.woocommerce.android.viewmodel.ResourceProvider | ||
| import com.woocommerce.android.viewmodel.ScopedViewModel | ||
| import com.woocommerce.android.viewmodel.navArgs | ||
| import dagger.hilt.android.lifecycle.HiltViewModel | ||
| import kotlinx.coroutines.flow.MutableStateFlow | ||
| import kotlinx.coroutines.flow.launchIn | ||
| import kotlinx.coroutines.flow.onEach | ||
| import kotlinx.coroutines.flow.update | ||
| import javax.inject.Inject | ||
|
|
||
| @HiltViewModel | ||
| class BookingDetailsViewModel @Inject constructor( | ||
| savedState: SavedStateHandle, | ||
| resourceProvider: ResourceProvider, | ||
| private val bookingsRepository: BookingsRepository, | ||
| private val bookingMapper: BookingMapper, | ||
| ) : ScopedViewModel(savedState) { | ||
|
|
||
| private val navArgs: BookingDetailsFragmentArgs by savedState.navArgs() | ||
|
|
@@ -35,17 +44,59 @@ class BookingDetailsViewModel @Inject constructor( | |
| toolbarTitle = resourceProvider.getString(R.string.booking_details_title, navArgs.bookingId), | ||
| ) | ||
| } | ||
| observeBooking(navArgs.bookingId) | ||
|
||
| } | ||
|
|
||
| private fun onAttendanceStatusSelected(status: BookingAttendanceStatus) { | ||
| _state.update { current -> | ||
| current.copy( | ||
| bookingSummary = current.bookingSummary.copy(attendanceStatus = status) | ||
| ) | ||
| val bookingState = _state.value.bookingUiState | ||
| if (bookingState != null) { | ||
| _state.update { current -> | ||
| current.copy( | ||
| bookingUiState = bookingState.copy( | ||
| bookingSummary = bookingState.bookingSummary.copy(attendanceStatus = status) | ||
| ) | ||
| ) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| private fun onCancelBooking() { | ||
| // TODO Add logic to Cancel booking | ||
| } | ||
|
|
||
| private fun observeBooking(bookingId: Long) { | ||
| bookingsRepository.observeBooking(bookingId) | ||
| .onEach { booking -> | ||
| booking?.let { updateStateWithBooking(it) } | ||
| } | ||
| .launchIn(this) | ||
| } | ||
|
|
||
| private fun updateStateWithBooking(booking: Booking) = with(bookingMapper) { | ||
| _state.update { current -> | ||
| current.copy( | ||
| orderId = booking.orderId, | ||
| bookingUiState = BookingUiState( | ||
| bookingSummary = booking.toBookingSummaryModel(), | ||
| bookingsAppointmentDetails = booking.toAppointmentDetailsModel(), | ||
| bookingCustomerDetails = BookingCustomerDetailsModel( | ||
| name = "Margarita Nikolaevna", | ||
| email = "[email protected]", | ||
| phone = "+1 555-123-4567", | ||
| billingAddressLines = listOf( | ||
| "238 Willow Creek Drive", | ||
| "Montgomery AL 36109", | ||
| "United States" | ||
| ) | ||
| ), | ||
| bookingPaymentDetails = BookingPaymentDetailsModel( | ||
| service = "$55.00", | ||
| tax = "$4.50", | ||
| discount = "-", | ||
| total = "$59.50" | ||
| ) | ||
| ), | ||
| ) | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,43 +4,19 @@ import com.woocommerce.android.ui.bookings.compose.BookingAppointmentDetailsMode | |
| import com.woocommerce.android.ui.bookings.compose.BookingAttendanceStatus | ||
| import com.woocommerce.android.ui.bookings.compose.BookingCustomerDetailsModel | ||
| import com.woocommerce.android.ui.bookings.compose.BookingPaymentDetailsModel | ||
| import com.woocommerce.android.ui.bookings.compose.BookingStatus | ||
| import com.woocommerce.android.ui.bookings.compose.BookingSummaryModel | ||
|
|
||
| data class BookingDetailsViewState( | ||
| val toolbarTitle: String = "", | ||
| val orderId: Long = 0L, | ||
| val bookingSummary: BookingSummaryModel = BookingSummaryModel( | ||
| date = "05/07/2025, 11:00 AM", | ||
| name = "Women’s Haircut", | ||
| customerName = "Margarita Nikolaevna", | ||
| attendanceStatus = BookingAttendanceStatus.NO_SHOW, | ||
| status = BookingStatus.Paid | ||
| ), | ||
| val bookingsAppointmentDetails: BookingAppointmentDetailsModel = BookingAppointmentDetailsModel( | ||
| date = "Monday, 05 July 2025", | ||
| time = "11:00 am - 12:00 pm", | ||
| staff = "Marianne Renoir", | ||
| location = "238 Willow Creek Drive, Montgomery AL 36109", | ||
| duration = "60 min", | ||
| price = "$55.00" | ||
| ), | ||
| val bookingCustomerDetails: BookingCustomerDetailsModel = BookingCustomerDetailsModel( | ||
| name = "Margarita Nikolaevna", | ||
| email = "[email protected]", | ||
| phone = "+1 555-123-4567", | ||
| billingAddressLines = listOf( | ||
| "238 Willow Creek Drive", | ||
| "Montgomery AL 36109", | ||
| "United States" | ||
| ) | ||
| ), | ||
| val bookingPaymentDetails: BookingPaymentDetailsModel = BookingPaymentDetailsModel( | ||
| service = "$55.00", | ||
| tax = "$4.50", | ||
| discount = "-", | ||
| total = "$59.50" | ||
| ), | ||
| val bookingUiState: BookingUiState? = null, | ||
| val onCancelBooking: () -> Unit = {}, | ||
| val onAttendanceStatusSelected: (BookingAttendanceStatus) -> Unit = { _ -> } | ||
| ) | ||
|
|
||
| data class BookingUiState( | ||
| val bookingSummary: BookingSummaryModel, | ||
| val bookingsAppointmentDetails: BookingAppointmentDetailsModel, | ||
| val bookingCustomerDetails: BookingCustomerDetailsModel, | ||
| val bookingPaymentDetails: BookingPaymentDetailsModel, | ||
| ) | ||
|
Comment on lines
+17
to
+22
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. All those models would be nullable after this PR, so it's handy to wrap them in a single class to avoid multiple null checks. |
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
cc @hichamboushaba I've used the UTC based on your discussion.