diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/bookings/compose/BookingPaymentSection.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/bookings/compose/BookingPaymentSection.kt new file mode 100644 index 00000000000..499950de8ba --- /dev/null +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/bookings/compose/BookingPaymentSection.kt @@ -0,0 +1,175 @@ +package com.woocommerce.android.ui.bookings.compose + +import androidx.annotation.StringRes +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import com.woocommerce.android.R +import com.woocommerce.android.ui.compose.component.WCColoredButton +import com.woocommerce.android.ui.compose.component.WCOutlinedButton +import com.woocommerce.android.ui.compose.preview.LightDarkThemePreviews +import com.woocommerce.android.ui.compose.theme.WooThemeWithBackground + +@Composable +fun BookingPaymentSection( + model: BookingPaymentDetailsModel, + status: BookingStatus, + onMarkAsPaid: () -> Unit, + onMarkAsRefunded: () -> Unit, + onViewOrder: () -> Unit, + modifier: Modifier = Modifier, +) { + Column(modifier = modifier) { + BookingSectionHeader(R.string.booking_payment_header) + HorizontalDivider(thickness = 0.5.dp) + Column( + modifier = Modifier + .background(color = MaterialTheme.colorScheme.surfaceContainer) + .padding(vertical = 16.dp) + ) { + Column( + verticalArrangement = Arrangement.spacedBy(4.dp), + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp) + ) { + PaymentRow(label = R.string.booking_payment_label_service, value = model.service) + PaymentRow(label = R.string.tax, value = model.tax) + PaymentRow(label = R.string.discount, value = model.discount) + PaymentRow(label = R.string.total, value = model.total, fontWeight = FontWeight.Medium) + } + Spacer(modifier = Modifier.height(16.dp)) + HorizontalDivider( + thickness = 0.5.dp, + modifier = Modifier.padding(start = 16.dp) + ) + Spacer(modifier = Modifier.height(16.dp)) + // TODO Change the logic and use Order info when available + if (status == BookingStatus.Paid) { + WCOutlinedButton( + onClick = onMarkAsRefunded, + colors = ButtonDefaults.outlinedButtonColors( + contentColor = MaterialTheme.colorScheme.onSurface + ), + text = stringResource(id = R.string.orderdetail_issue_refund_button), + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp) + ) + } else { + WCColoredButton( + onClick = onMarkAsPaid, + text = stringResource(id = R.string.booking_payment_mark_as_paid), + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp) + ) + } + Spacer(modifier = Modifier.height(8.dp)) + WCOutlinedButton( + onClick = onViewOrder, + colors = ButtonDefaults.outlinedButtonColors( + contentColor = MaterialTheme.colorScheme.onSurface + ), + text = stringResource(id = R.string.booking_payment_view_order), + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp) + ) + } + } +} + +@Composable +private fun PaymentRow( + @StringRes label: Int, + value: String, + fontWeight: FontWeight = FontWeight.Normal, +) { + Row( + horizontalArrangement = Arrangement.SpaceBetween, + modifier = Modifier + .fillMaxWidth() + ) { + PaymentText(stringResource(label), fontWeight = fontWeight) + PaymentText(value, modifier = Modifier.padding(start = 8.dp), fontWeight = fontWeight) + } +} + +@Composable +private fun PaymentText( + text: String, + fontWeight: FontWeight, + modifier: Modifier = Modifier, +) { + Text( + text = text, + style = MaterialTheme.typography.bodyMedium.copy(fontWeight = fontWeight), + color = MaterialTheme.colorScheme.onSurface, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = modifier + ) +} + +data class BookingPaymentDetailsModel( + val service: String, + val tax: String, + val discount: String, + val total: String +) + +@LightDarkThemePreviews +@Composable +private fun BookingPaymentSectionPreview() { + WooThemeWithBackground { + BookingPaymentSection( + model = BookingPaymentDetailsModel( + service = "$55.00", + tax = "$4.50", + discount = "-", + total = "$59.50" + ), + status = BookingStatus.Complete, + onMarkAsPaid = {}, + onViewOrder = {}, + onMarkAsRefunded = {}, + modifier = Modifier.fillMaxWidth() + ) + } +} + +@LightDarkThemePreviews +@Composable +private fun BookingPaymentSectionWithRefundOptionPreview() { + WooThemeWithBackground { + BookingPaymentSection( + model = BookingPaymentDetailsModel( + service = "$55.00", + tax = "$4.50", + discount = "-", + total = "$59.50" + ), + status = BookingStatus.Paid, + onMarkAsPaid = {}, + onViewOrder = {}, + onMarkAsRefunded = {}, + modifier = Modifier.fillMaxWidth() + ) + } +} diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/bookings/details/BookingDetailsFragment.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/bookings/details/BookingDetailsFragment.kt index ca782a6721d..9153f0739e0 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/bookings/details/BookingDetailsFragment.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/bookings/details/BookingDetailsFragment.kt @@ -23,7 +23,13 @@ class BookingDetailsFragment : BaseFragment() { return composeView { BookingDetailsScreen( viewModel = viewModel, - onBack = { findNavController().popBackStack() } + onBack = { findNavController().popBackStack() }, + onViewOrder = { orderId -> + findNavController().navigate( + BookingDetailsFragmentDirections + .actionBookingDetailsFragmentToOrderDetailFragment(orderId) + ) + } ) } } diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/bookings/details/BookingDetailsScreen.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/bookings/details/BookingDetailsScreen.kt index 255e7504ead..0a5383b4884 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/bookings/details/BookingDetailsScreen.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/bookings/details/BookingDetailsScreen.kt @@ -26,6 +26,7 @@ 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.BookingPaymentSection import com.woocommerce.android.ui.bookings.compose.BookingStatus import com.woocommerce.android.ui.bookings.compose.BookingSummary import com.woocommerce.android.ui.bookings.compose.BookingSummaryModel @@ -36,7 +37,8 @@ import com.woocommerce.android.ui.compose.theme.WooThemeWithBackground @Composable fun BookingDetailsScreen( viewModel: BookingDetailsViewModel, - onBack: () -> Unit + onBack: () -> Unit, + onViewOrder: (Long) -> Unit ) { val viewState by viewModel.state.observeAsState() @@ -44,8 +46,7 @@ fun BookingDetailsScreen( BookingDetailsScreen( viewState = it, onBack = onBack, - onCancelBooking = viewModel::onCancelBooking, - onAttendanceStatusSelected = viewModel::onAttendanceStatusSelected + onViewOrder = onViewOrder, ) } } @@ -54,8 +55,7 @@ fun BookingDetailsScreen( fun BookingDetailsScreen( viewState: BookingDetailsViewState, onBack: () -> Unit, - onCancelBooking: () -> Unit, - onAttendanceStatusSelected: (BookingAttendanceStatus) -> Unit + onViewOrder: (Long) -> Unit, ) { val showAttendanceSheet = remember { mutableStateOf(false) } Scaffold( @@ -82,7 +82,7 @@ fun BookingDetailsScreen( ) BookingAppointmentDetails( model = viewState.bookingsAppointmentDetails, - onCancelBooking = onCancelBooking, + onCancelBooking = viewState.onCancelBooking, modifier = Modifier.fillMaxWidth() ) BookingCustomerDetails( @@ -96,11 +96,19 @@ fun BookingDetailsScreen( 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 -> - onAttendanceStatusSelected(status) + viewState.onAttendanceStatusSelected(status) }, onDismiss = { showAttendanceSheet.value = false } ) @@ -133,8 +141,7 @@ private fun BookingDetailsPreview() { ) ), onBack = {}, - onCancelBooking = {}, - onAttendanceStatusSelected = {} + onViewOrder = {} ) } } diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/bookings/details/BookingDetailsViewModel.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/bookings/details/BookingDetailsViewModel.kt index 1a9f704a7ad..e4ccaba1e0e 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/bookings/details/BookingDetailsViewModel.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/bookings/details/BookingDetailsViewModel.kt @@ -21,7 +21,12 @@ class BookingDetailsViewModel @Inject constructor( private val navArgs: BookingDetailsFragmentArgs by savedState.navArgs() - private val _state = MutableStateFlow(BookingDetailsViewState()) + private val _state = MutableStateFlow( + BookingDetailsViewState( + onCancelBooking = ::onCancelBooking, + onAttendanceStatusSelected = ::onAttendanceStatusSelected, + ) + ) val state: LiveData = _state.asLiveData() init { @@ -32,7 +37,7 @@ class BookingDetailsViewModel @Inject constructor( } } - fun onAttendanceStatusSelected(status: BookingAttendanceStatus) { + private fun onAttendanceStatusSelected(status: BookingAttendanceStatus) { _state.update { current -> current.copy( bookingSummary = current.bookingSummary.copy(attendanceStatus = status) @@ -40,7 +45,7 @@ class BookingDetailsViewModel @Inject constructor( } } - fun onCancelBooking() { + private fun onCancelBooking() { // TODO Add logic to Cancel booking } } diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/bookings/details/BookingDetailsViewState.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/bookings/details/BookingDetailsViewState.kt index 3bdb99ff54f..dab64306c79 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/bookings/details/BookingDetailsViewState.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/bookings/details/BookingDetailsViewState.kt @@ -3,11 +3,13 @@ package com.woocommerce.android.ui.bookings.details import com.woocommerce.android.ui.bookings.compose.BookingAppointmentDetailsModel 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", @@ -32,5 +34,13 @@ data class BookingDetailsViewState( "Montgomery AL 36109", "United States" ) - ) + ), + val bookingPaymentDetails: BookingPaymentDetailsModel = BookingPaymentDetailsModel( + service = "$55.00", + tax = "$4.50", + discount = "-", + total = "$59.50" + ), + val onCancelBooking: () -> Unit = {}, + val onAttendanceStatusSelected: (BookingAttendanceStatus) -> Unit = { _ -> } ) diff --git a/WooCommerce/src/main/res/navigation/nav_graph_bookings.xml b/WooCommerce/src/main/res/navigation/nav_graph_bookings.xml index 3ef6a3e1729..a109327019b 100644 --- a/WooCommerce/src/main/res/navigation/nav_graph_bookings.xml +++ b/WooCommerce/src/main/res/navigation/nav_graph_bookings.xml @@ -7,7 +7,7 @@ + android:label="fragment_bookings"> @@ -20,6 +20,13 @@ android:name="bookingId" android:defaultValue="-1L" app:argType="long" /> + + + diff --git a/WooCommerce/src/main/res/values/strings.xml b/WooCommerce/src/main/res/values/strings.xml index 05a035a1c8a..02d6a361169 100644 --- a/WooCommerce/src/main/res/values/strings.xml +++ b/WooCommerce/src/main/res/values/strings.xml @@ -4895,5 +4895,9 @@ The customer arrived and the session took place as planned. The client will no longer be able to attend. The client missed the appointment without canceling in advance. + PAYMENT + Service + Mark as paid + View order diff --git a/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/bookings/details/BookingDetailsViewModelTest.kt b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/bookings/details/BookingDetailsViewModelTest.kt index 09b6c2268f6..ec2b2d98306 100644 --- a/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/bookings/details/BookingDetailsViewModelTest.kt +++ b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/bookings/details/BookingDetailsViewModelTest.kt @@ -3,6 +3,7 @@ package com.woocommerce.android.ui.bookings.details import androidx.lifecycle.SavedStateHandle import com.woocommerce.android.R import com.woocommerce.android.ui.bookings.compose.BookingAttendanceStatus +import com.woocommerce.android.util.getOrAwaitValue import com.woocommerce.android.viewmodel.BaseUnitTest import com.woocommerce.android.viewmodel.ResourceProvider import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -42,7 +43,8 @@ class BookingDetailsViewModelTest : BaseUnitTest() { val viewModel = createViewModel(savedState, resourceProvider) // When - viewModel.onAttendanceStatusSelected(BookingAttendanceStatus.CANCELLED) + val state = viewModel.state.getOrAwaitValue() + state.onAttendanceStatusSelected(BookingAttendanceStatus.CANCELLED) // Then val updated = viewModel.state.value?.bookingSummary?.attendanceStatus