Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)
Comment on lines +17 to +22
Copy link
Contributor Author

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.


private val detailsDateFormatter: DateTimeFormatter = DateTimeFormatter.ofLocalizedDate(FormatStyle.FULL)
.withZone(ZoneOffset.UTC)
private val timeRangeFormatter: DateTimeFormatter = DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT)
.withZone(ZoneOffset.UTC)

fun Booking.toBookingSummaryModel(): BookingSummaryModel {
return BookingSummaryModel(
date = summaryDateFormatter.format(start),
// TODO replace mocked values when product and customer data are available
name = "Women’s Haircut",
customerName = "Margarita Nikolaevna",
attendanceStatus = BookingAttendanceStatus.BOOKED,
status = status.toUiModel()
)
}

fun Booking.toListItem(): BookingListItem {
return BookingListItem(
id = id.value,
summary = toBookingSummaryModel()
)
}

fun Booking.toAppointmentDetailsModel(): BookingAppointmentDetailsModel {
val durationMinutes = Duration.between(start, end).toMinutes()
return BookingAppointmentDetailsModel(
date = detailsDateFormatter.format(start),
time = "${timeRangeFormatter.format(start)} - ${timeRangeFormatter.format(end)}",
// TODO replace mocked values when available from API
staff = "Marianne Renoir",
location = "238 Willow Creek Drive, Montgomery AL 36109",
duration = "$durationMinutes min",
price = currencyFormatter.formatCurrency(cost, currency)
)
}

private fun BookingEntity.Status.toUiModel(): BookingStatus = when (this) {
BookingEntity.Status.Paid -> BookingStatus.Paid
BookingEntity.Status.PendingConfirmation -> BookingStatus.PendingConfirmation
BookingEntity.Status.Cancelled -> BookingStatus.Cancelled
BookingEntity.Status.Complete -> BookingStatus.Complete
BookingEntity.Status.Confirmed -> BookingStatus.Confirmed
BookingEntity.Status.Unpaid -> BookingStatus.Unpaid
is BookingEntity.Status.Unknown -> BookingStatus.Unknown(this.key)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,12 @@ class BookingsRepository @Inject constructor(
filters = filters
)

fun observeBooking(bookingId: Long): Flow<Booking?> =
bookingsStore.observeBooking(
site = selectedSite.get(),
bookingId = bookingId
)

data class FetchResult(
val bookings: List<Booking>,
val hasMorePages: Boolean
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -76,42 +78,44 @@ 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,
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,
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 }
)
}
}
}

Expand All @@ -122,21 +126,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 = "margarita@example.com",
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 = {}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,48 +4,87 @@ 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.update
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.filterNotNull
import javax.inject.Inject

@HiltViewModel
class BookingDetailsViewModel @Inject constructor(
savedState: SavedStateHandle,
resourceProvider: ResourceProvider,
bookingsRepository: BookingsRepository,
private val bookingMapper: BookingMapper,
) : ScopedViewModel(savedState) {

private val navArgs: BookingDetailsFragmentArgs by savedState.navArgs()

private val _state = MutableStateFlow(
BookingDetailsViewState(
onCancelBooking = ::onCancelBooking,
onAttendanceStatusSelected = ::onAttendanceStatusSelected,
)
)
val state: LiveData<BookingDetailsViewState> = _state.asLiveData()
private val booking = bookingsRepository.observeBooking(navArgs.bookingId)

// Temporary, the booking status should come from the stored object
private val bookingAttendanceStatus = MutableStateFlow<BookingAttendanceStatus?>(null)

init {
_state.update {
it.copy(
toolbarTitle = resourceProvider.getString(R.string.booking_details_title, navArgs.bookingId),
val state: LiveData<BookingDetailsViewState> = combine(
booking.filterNotNull(),
bookingAttendanceStatus
) { booking, attendanceStatus ->
with(bookingMapper) {
BookingDetailsViewState(
toolbarTitle = resourceProvider.getString(R.string.booking_details_title, booking.id.value),
orderId = booking.orderId,
bookingUiState = buildBookingUiState(booking, attendanceStatus),
onCancelBooking = ::onCancelBooking,
onAttendanceStatusSelected = ::onAttendanceStatusSelected
)
}
}
}.asLiveData()

private fun onAttendanceStatusSelected(status: BookingAttendanceStatus) {
_state.update { current ->
current.copy(
bookingSummary = current.bookingSummary.copy(attendanceStatus = status)
)
}
// Temporary, the booking status should come from the stored object
bookingAttendanceStatus.value = status
}

private fun onCancelBooking() {
// TODO Add logic to Cancel booking
}

private fun BookingMapper.buildBookingUiState(
booking: Booking,
attendanceStatus: BookingAttendanceStatus?
): BookingUiState = BookingUiState(
bookingSummary = booking.toBookingSummaryModel().let {
if (attendanceStatus != null) {
it.copy(attendanceStatus = attendanceStatus)
} else {
it
}
},
bookingsAppointmentDetails = booking.toAppointmentDetailsModel(),
bookingCustomerDetails = BookingCustomerDetailsModel(
name = "Margarita Nikolaevna",
email = "margarita@example.com",
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
Expand Up @@ -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 = "margarita@example.com",
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
Copy link
Contributor Author

Choose a reason for hiding this comment

The 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.

Loading