fix order (minus add address and selectedaddressid)

This commit is contained in:
shaulascr
2025-04-11 10:36:43 +07:00
parent e8bb99a8d7
commit 809f7d7696
26 changed files with 1701 additions and 694 deletions

View File

@ -1,12 +1,16 @@
package com.alya.ecommerce_serang.data.api.dto
import com.alya.ecommerce_serang.data.api.response.cart.CartItemsItem
data class CheckoutData(
val orderRequest: OrderRequest,
// Additional UI-related data
val productName: String,
val productImageUrl: String,
val productPrice: Double,
val sellerName: String,
val sellerImageUrl: String,
val sellerId: Int
val orderRequest: Any, // Can be OrderRequest or OrderRequestBuy
val productName: String? = "",
val productImageUrl: String = "",
val productPrice: Double = 0.0,
val sellerName: String = "",
val sellerImageUrl: String? = null,
val sellerId: Int = 0,
val quantity: Int = 1,
val isBuyNow: Boolean = false,
val cartItems: List<CartItemsItem> = emptyList()
)

View File

@ -0,0 +1,33 @@
package com.alya.ecommerce_serang.data.api.dto
import com.google.gson.annotations.SerializedName
data class OrderRequestBuy (
@SerializedName("address_id")
val addressId : Int,
@SerializedName("payment_method_id")
val paymentMethodId : Int,
@SerializedName("ship_price")
val shipPrice : Int,
@SerializedName("ship_name")
val shipName : String,
@SerializedName("ship_service")
val shipService : String,
@SerializedName("is_negotiable")
val isNego: Boolean,
@SerializedName("product_id")
val productId: Int,
@SerializedName("quantity")
val quantity : Int,
@SerializedName("ship_etd")
val shipEtd: String
)

View File

@ -1,4 +1,4 @@
package com.alya.ecommerce_serang.data.api.response.order
package com.alya.ecommerce_serang.data.api.response.cart
import com.google.gson.annotations.SerializedName

View File

@ -4,15 +4,36 @@ import com.google.gson.annotations.SerializedName
data class DetailStoreProductResponse(
@field:SerializedName("store")
@field:SerializedName("store")
val store: StoreProduct,
@field:SerializedName("message")
@field:SerializedName("message")
val message: String
)
data class PaymentInfoItem(
@field:SerializedName("qris_image")
val qrisImage: String,
@field:SerializedName("bank_num")
val bankNum: String,
@field:SerializedName("name")
val name: String
)
data class StoreProduct(
@field:SerializedName("store_id")
val storeId: Int,
@field:SerializedName("shipping_service")
val shippingService: List<ShippingServiceItem>,
@field:SerializedName("store_rating")
val storeRating: String,
@field:SerializedName("store_name")
val storeName: String,
@ -22,12 +43,21 @@ data class StoreProduct(
@field:SerializedName("store_type")
val storeType: String,
@field:SerializedName("payment_info")
val paymentInfo: List<PaymentInfoItem>,
@field:SerializedName("store_location")
val storeLocation: String,
@field:SerializedName("store_image")
val storeImage: String? = null,
val storeImage: String,
@field:SerializedName("status")
val status: String
)
data class ShippingServiceItem(
@field:SerializedName("courier")
val courier: String
)

View File

@ -5,6 +5,7 @@ import com.alya.ecommerce_serang.data.api.dto.CourierCostRequest
import com.alya.ecommerce_serang.data.api.dto.CreateAddressRequest
import com.alya.ecommerce_serang.data.api.dto.LoginRequest
import com.alya.ecommerce_serang.data.api.dto.OrderRequest
import com.alya.ecommerce_serang.data.api.dto.OrderRequestBuy
import com.alya.ecommerce_serang.data.api.dto.OtpRequest
import com.alya.ecommerce_serang.data.api.dto.RegisterRequest
import com.alya.ecommerce_serang.data.api.dto.UpdateCart
@ -13,11 +14,11 @@ import com.alya.ecommerce_serang.data.api.response.auth.OtpResponse
import com.alya.ecommerce_serang.data.api.response.auth.RegisterResponse
import com.alya.ecommerce_serang.data.api.response.cart.AddCartResponse
import com.alya.ecommerce_serang.data.api.response.cart.ListCartResponse
import com.alya.ecommerce_serang.data.api.response.cart.UpdateCartResponse
import com.alya.ecommerce_serang.data.api.response.order.CourierCostResponse
import com.alya.ecommerce_serang.data.api.response.order.CreateOrderResponse
import com.alya.ecommerce_serang.data.api.response.order.ListCityResponse
import com.alya.ecommerce_serang.data.api.response.order.ListProvinceResponse
import com.alya.ecommerce_serang.data.api.response.order.UpdateCartResponse
import com.alya.ecommerce_serang.data.api.response.product.AllProductResponse
import com.alya.ecommerce_serang.data.api.response.product.CategoryResponse
import com.alya.ecommerce_serang.data.api.response.product.DetailStoreProductResponse
@ -80,6 +81,11 @@ interface ApiService {
@Body request: OrderRequest
): Response<CreateOrderResponse>
@POST("order")
suspend fun postOrderBuyNow(
@Body request: OrderRequestBuy
): Response<CreateOrderResponse>
@GET("profile/address")
suspend fun getAddress(
): Response<AddressResponse>

View File

@ -4,11 +4,14 @@ import android.util.Log
import com.alya.ecommerce_serang.data.api.dto.CourierCostRequest
import com.alya.ecommerce_serang.data.api.dto.CreateAddressRequest
import com.alya.ecommerce_serang.data.api.dto.OrderRequest
import com.alya.ecommerce_serang.data.api.dto.OrderRequestBuy
import com.alya.ecommerce_serang.data.api.response.cart.DataItem
import com.alya.ecommerce_serang.data.api.response.order.CourierCostResponse
import com.alya.ecommerce_serang.data.api.response.order.CreateOrderResponse
import com.alya.ecommerce_serang.data.api.response.order.ListCityResponse
import com.alya.ecommerce_serang.data.api.response.order.ListProvinceResponse
import com.alya.ecommerce_serang.data.api.response.product.ProductResponse
import com.alya.ecommerce_serang.data.api.response.product.StoreProduct
import com.alya.ecommerce_serang.data.api.response.product.StoreResponse
import com.alya.ecommerce_serang.data.api.response.profile.AddressResponse
import com.alya.ecommerce_serang.data.api.response.profile.CreateAddressResponse
@ -21,31 +24,163 @@ class OrderRepository(private val apiService: ApiService) {
return try {
val response = apiService.getDetailProduct(productId)
if (response.isSuccessful) {
response.body()
val productResponse = response.body()
Log.d("Order Repository", "Product detail fetched successfully: ${productResponse?.product?.productName}")
productResponse
} else {
Log.e("ProductRepository", "Error: ${response.errorBody()?.string()}")
val errorBody = response.errorBody()?.string() ?: "Unknown error"
Log.e("Order Repository", "Error fetching product detail. Code: ${response.code()}, Error: $errorBody")
null
}
} catch (e: Exception) {
Log.e("Order Repository", "Exception fetching product", e)
null
}
}
suspend fun createOrder(orderRequest: OrderRequest): Response<CreateOrderResponse> {
return apiService.postOrder(orderRequest)
return try {
Log.d("Order Repository", "Creating order. Request details: $orderRequest")
val response = apiService.postOrder(orderRequest)
if (response.isSuccessful) {
Log.d("Order Repository", "Order created successfully. Response: ${response.body()}")
} else {
val errorBody = response.errorBody()?.string() ?: "Unknown error"
Log.e("Order Repository", "Order creation failed. Code: ${response.code()}, Error: $errorBody")
}
response
} catch (e: Exception) {
Log.e("Order Repository", "Exception creating order", e)
throw e
}
}
suspend fun createOrderBuyNow(orderRequestBuy: OrderRequestBuy): Response<CreateOrderResponse> {
return try {
Log.d("Order Repository", "Creating buy now order. Request details: $orderRequestBuy")
val response = apiService.postOrderBuyNow(orderRequestBuy)
if (response.isSuccessful) {
Log.d("Order Repository", "Buy now order created successfully. Response: ${response.body()}")
} else {
val errorBody = response.errorBody()?.string() ?: "Unknown error"
Log.e("Order Repository", "Buy now order creation failed. Code: ${response.code()}, Error: $errorBody")
}
response
} catch (e: Exception) {
Log.e("Order Repository", "Exception creating buy now order", e)
throw e
}
}
suspend fun getStore(): StoreResponse? {
val response = apiService.getStore()
return if (response.isSuccessful) response.body() else null
return try {
val response = apiService.getStore()
if (response.isSuccessful) {
val storeResponse = response.body()
Log.d("Order Repository", "Store information fetched successfully. Store count: ${storeResponse?.store?.storeName}")
storeResponse
} else {
val errorBody = response.errorBody()?.string() ?: "Unknown error"
Log.e("Order Repository", "Error fetching store. Code: ${response.code()}, Error: $errorBody")
null
}
} catch (e: Exception) {
Log.e("Order Repository", "Exception getting store", e)
null
}
}
suspend fun getAddress(): AddressResponse?{
val response = apiService.getAddress()
return if (response.isSuccessful) response.body() else null
suspend fun getAddress(): AddressResponse? {
return try {
val response = apiService.getAddress()
if (response.isSuccessful) {
val addressResponse = response.body()
Log.d("Order Repository", "Address information fetched successfully. Address count: ${addressResponse?.addresses?.size}")
addressResponse
} else {
val errorBody = response.errorBody()?.string() ?: "Unknown error"
Log.e("Order Repository", "Error fetching addresses. Code: ${response.code()}, Error: $errorBody")
null
}
} catch (e: Exception) {
Log.e("Order Repository", "Exception getting addresses", e)
null
}
}
suspend fun getCountCourierCost(courierCost: CourierCostRequest): Result<CourierCostResponse> {
return try {
Log.d("Order Repository", "Calculating courier cost. Request: $courierCost")
val response = apiService.countCourierCost(courierCost)
if (response.isSuccessful) {
response.body()?.let { courierCostResponse ->
Log.d("Order Repository", "Courier cost calculation successful. Courier costs: ${courierCostResponse.courierCosts.size}")
Result.Success(courierCostResponse)
} ?: run {
Result.Error(Exception("Failed to get courier cost: Empty response"))
}
} else {
val errorMsg = response.errorBody()?.string() ?: "Unknown error"
Log.e("Order Repository", "Error calculating courier cost. Code: ${response.code()}, Error: $errorMsg")
Result.Error(Exception(errorMsg))
}
} catch (e: Exception) {
Log.e("Order Repository", "Exception calculating courier cost", e)
Result.Error(e)
}
}
suspend fun getCart(): Result<List<DataItem>> {
return try {
val response = apiService.getCart()
if (response.isSuccessful) {
val cartData = response.body()?.data
if (!cartData.isNullOrEmpty()) {
Result.Success(cartData)
} else {
Log.e("Order Repository", "Cart data is empty")
Result.Error(Exception("Cart is empty"))
}
} else {
val errorMsg = response.errorBody()?.string() ?: "Unknown error"
Log.e("Order Repository", "Error fetching cart: $errorMsg")
Result.Error(Exception(errorMsg))
}
} catch (e: Exception) {
Log.e("Order Repository", "Exception fetching cart", e)
Result.Error(e)
}
}
suspend fun fetchStoreDetail(storeId: Int): Result<StoreProduct?> {
return try {
val response = apiService.getDetailStore(storeId)
if (response.isSuccessful) {
val store = response.body()?.store
if (store != null) {
Result.Success(store)
} else {
Result.Error(Exception("Store details not found"))
}
} else {
val errorMsg = response.errorBody()?.string() ?: "Unknown error"
Log.e("Order Repository", "Error fetching store: $errorMsg")
Result.Error(Exception(errorMsg))
}
} catch (e: Exception) {
Log.e("Order Repository", "Exception fetching store details", e)
Result.Error(e)
}
}
//post data with message/response
suspend fun addAddress(createAddressRequest: CreateAddressRequest): Result<CreateAddressResponse> {
return try {
val response = apiService.createAddress(createAddressRequest)
@ -72,19 +207,4 @@ class OrderRepository(private val apiService: ApiService) {
return if (response.isSuccessful) response.body() else null
}
suspend fun getCountCourierCost(courierCost: CourierCostRequest): Result<CourierCostResponse>{
return try {
val response = apiService.countCourierCost(courierCost)
if (response.isSuccessful){
response.body()?.let {
Result.Success(it)
} ?: Result.Error(Exception("Add Address failed"))
} else {
Log.e("OrderRepository", "Error: ${response.errorBody()?.string()}")
Result.Error(Exception(response.errorBody()?.string() ?: "Unknown error"))
}
} catch (e: Exception) {
Result.Error(e)
}
}
}

View File

@ -7,6 +7,7 @@ import com.alya.ecommerce_serang.data.api.dto.ProductsItem
import com.alya.ecommerce_serang.data.api.response.cart.AddCartResponse
import com.alya.ecommerce_serang.data.api.response.product.ProductResponse
import com.alya.ecommerce_serang.data.api.response.product.ReviewsItem
import com.alya.ecommerce_serang.data.api.response.product.StoreProduct
import com.alya.ecommerce_serang.data.api.retrofit.ApiService
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
@ -21,26 +22,37 @@ class ProductRepository(private val apiService: ApiService) {
if (response.isSuccessful) {
// Return a Result.Success with the list of products
Result.Success(response.body()?.products ?: emptyList())
val products = response.body()?.products ?: emptyList()
Log.d(TAG, "Products fetched successfully. Total products: ${products.size}")
// Optional: Log some product details
products.take(3).forEach { product ->
Log.d(TAG, "Sample Product - ID: ${product.id}, Name: ${product.name}, Price: ${product.price}")
}
Result.Success(products)
} else {
// Return a Result.Error with a custom Exception
Log.e("ProductRepository", "Error: ${response.errorBody()?.string()}")
val errorBody = response.errorBody()?.string() ?: "Unknown error"
Log.e(TAG, "Failed to fetch products. Code: ${response.code()}, Error: $errorBody")
Result.Error(Exception("Failed to fetch products. Code: ${response.code()}"))
}
} catch (e: Exception) {
// Return a Result.Error with the exception caught
Log.e(TAG, "Exception while fetching products", e)
Result.Error(e)
}
}
suspend fun fetchProductDetail(productId: Int): ProductResponse? {
return try {
Log.d(TAG, "Fetching product detail for ID: $productId")
val response = apiService.getDetailProduct(productId)
if (response.isSuccessful) {
response.body()
val productResponse = response.body()
Log.d(TAG, "Product detail fetched successfully. Product: ${productResponse?.product?.productName}")
productResponse
} else {
Log.e("ProductRepository", "Error: ${response.errorBody()?.string()}")
val errorBody = response.errorBody()?.string() ?: "Unknown error"
Log.e(TAG, "Error fetching product detail. Code: ${response.code()}, Error: $errorBody")
null
}
} catch (e: Exception) {
@ -51,19 +63,18 @@ class ProductRepository(private val apiService: ApiService) {
suspend fun getAllCategories(): Result<List<CategoryItem>> =
withContext(Dispatchers.IO) {
try {
Log.d("Categories", "Attempting to fetch categories")
val response = apiService.allCategory()
if (response.isSuccessful) {
val categories = response.body()?.category ?: emptyList()
Log.d("Categories", "Fetched categories: $categories")
Log.d("ProductRepository", "Fetched categories: $categories")
categories.forEach { Log.d("Category Image", "Category: ${it.name}, Image: ${it.image}") }
Result.Success(categories)
} else {
Result.Error(Exception("Failed to fetch categories. Code: ${response.code()}"))
}
} catch (e: Exception) {
Log.e("Categories", "Error fetching categories", e)
Log.e("ProductRepository", "Error fetching categories", e)
Result.Error(e)
}
}
@ -83,20 +94,46 @@ class ProductRepository(private val apiService: ApiService) {
}
suspend fun addToCart(request: CartItem): Result<AddCartResponse> {
return try{
return try {
val response = apiService.addCart(request)
if (response.isSuccessful){
if (response.isSuccessful) {
response.body()?.let {
Result.Success(it)
} ?: Result.Error(Exception("Add Cart failed"))
} else {
Log.e("OrderRepository", "Error: ${response.errorBody()?.string()}")
Log.e("ProductRepository", "Error: ${response.errorBody()?.string()}")
Result.Error(Exception(response.errorBody()?.string() ?: "Unknown Error"))
}
} catch (e: Exception){
} catch (e: Exception) {
Result.Error(e)
}
}
suspend fun fetchStoreDetail(storeId: Int): Result<StoreProduct?> {
return try {
val response = apiService.getDetailStore(storeId)
if (response.isSuccessful) {
val store = response.body()?.store
if (store != null) {
Result.Success(store)
} else {
Result.Error(Throwable("Empty response body"))
}
} else {
val errorMsg = response.errorBody()?.string() ?: "Unknown error"
Log.e("ProductRepository", "Error: $errorMsg")
Result.Error(Throwable(errorMsg))
}
} catch (e: Exception) {
Result.Error(e)
}
}
companion object {
private const val TAG = "ProductRepository"
}
}
// suspend fun fetchStoreDetail(storeId: Int): Store? {

View File

@ -0,0 +1,79 @@
package com.alya.ecommerce_serang.ui.order
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.alya.ecommerce_serang.R
import com.alya.ecommerce_serang.data.api.dto.CheckoutData
import com.alya.ecommerce_serang.data.api.response.cart.CartItemsItem
import com.alya.ecommerce_serang.databinding.ItemOrderProductBinding
import com.alya.ecommerce_serang.databinding.ItemOrderSellerBinding
import com.bumptech.glide.Glide
import java.text.NumberFormat
import java.util.Locale
class CartCheckoutAdapter(private val checkoutData: CheckoutData) :
RecyclerView.Adapter<CartCheckoutAdapter.SellerViewHolder>() {
class SellerViewHolder(val binding: ItemOrderSellerBinding) : RecyclerView.ViewHolder(binding.root)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SellerViewHolder {
val binding = ItemOrderSellerBinding.inflate(
LayoutInflater.from(parent.context), parent, false
)
return SellerViewHolder(binding)
}
override fun getItemCount(): Int = 1 // Only one seller
override fun onBindViewHolder(holder: SellerViewHolder, position: Int) {
with(holder.binding) {
// Set seller name
tvStoreName.text = checkoutData.sellerName
// Set up products RecyclerView with multiple items
rvSellerOrderProduct.apply {
layoutManager = LinearLayoutManager(context)
adapter = MultiCartItemsAdapter(checkoutData.cartItems)
isNestedScrollingEnabled = false
}
}
}
}
class MultiCartItemsAdapter(private val cartItems: List<CartItemsItem>) :
RecyclerView.Adapter<MultiCartItemsAdapter.CartItemViewHolder>() {
class CartItemViewHolder(val binding: ItemOrderProductBinding) : RecyclerView.ViewHolder(binding.root)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CartItemViewHolder {
val binding = ItemOrderProductBinding.inflate(
LayoutInflater.from(parent.context), parent, false
)
return CartItemViewHolder(binding)
}
override fun getItemCount(): Int = cartItems.size
override fun onBindViewHolder(holder: CartItemViewHolder, position: Int) {
val item = cartItems[position]
with(holder.binding) {
// Set cart item details
tvProductName.text = item.productName
tvProductQuantity.text = "${item.quantity} buah"
tvProductPrice.text = formatCurrency(item.price.toDouble())
// Load placeholder image
Glide.with(ivProduct.context)
.load(R.drawable.placeholder_image)
.into(ivProduct)
}
}
private fun formatCurrency(amount: Double): String {
val formatter = NumberFormat.getCurrencyInstance(Locale("in", "ID"))
return formatter.format(amount).replace(",00", "")
}
}

View File

@ -1,109 +1,78 @@
package com.alya.ecommerce_serang.ui.order
import android.app.Activity
import android.app.ProgressDialog
import android.content.Context
import android.content.Intent
import android.os.Build
import android.os.Bundle
import android.os.Looper
import android.util.Log
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.ViewModelProvider
import androidx.recyclerview.widget.LinearLayoutManager
import com.alya.ecommerce_serang.data.api.dto.CheckoutData
import com.alya.ecommerce_serang.data.api.dto.OrderRequest
import com.alya.ecommerce_serang.data.api.dto.OrderRequestBuy
import com.alya.ecommerce_serang.data.api.response.product.PaymentItem
import com.alya.ecommerce_serang.data.api.retrofit.ApiConfig
import com.alya.ecommerce_serang.data.api.retrofit.ApiService
import com.alya.ecommerce_serang.data.repository.OrderRepository
import com.alya.ecommerce_serang.data.repository.ProductRepository
import com.alya.ecommerce_serang.databinding.ActivityCheckoutBinding
import com.alya.ecommerce_serang.databinding.ActivityDetailProductBinding
import com.alya.ecommerce_serang.ui.order.address.AddressActivity
import com.alya.ecommerce_serang.utils.BaseViewModelFactory
import com.alya.ecommerce_serang.utils.SessionManager
import com.google.gson.Gson
import java.text.NumberFormat
import java.util.Locale
class CheckoutActivity : AppCompatActivity() {
private lateinit var binding: ActivityCheckoutBinding
private lateinit var apiService: ApiService
private lateinit var sessionManager: SessionManager
private var itemOrderAdapter: CheckoutSellerAdapter? = null
private var paymentAdapter: PaymentMethodAdapter? = null
private val viewModel: CheckoutViewModel by viewModels {
BaseViewModelFactory {
val apiService = ApiConfig.getApiService(sessionManager)
val productRepository = ProductRepository(apiService)
val orderRepository = OrderRepository(apiService)
CheckoutViewModel(orderRepository)
}
}
private var orderRequest: OrderRequest? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityCheckoutBinding.inflate(layoutInflater)
setContentView(binding.root)
sessionManager = SessionManager(this)
apiService = ApiConfig.getApiService(sessionManager)
// Get order request from intent
getOrderRequestFromIntent()
// Setup UI components
setupToolbar()
setupObservers()
setupClickListeners()
// Load data if order request is available
orderRequest?.let {
viewModel.loadCheckoutData(it)
// Update shipping method display
binding.tvShippingMethod.text = "${it.shipName} ${it.shipService} (${it.shipEtd} hari)"
} ?: run {
// Handle case when order request is not available
Toast.makeText(this, "Error: Order request data not found", Toast.LENGTH_SHORT).show()
finish()
}
processIntentData()
}
private fun getOrderRequestFromIntent() {
// Check for direct OrderRequest object
if (intent.hasExtra(EXTRA_ORDER_REQUEST)) {
orderRequest = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
intent.getSerializableExtra(EXTRA_ORDER_REQUEST, OrderRequest::class.java)
} else {
@Suppress("DEPRECATION")
intent.getSerializableExtra(EXTRA_ORDER_REQUEST) as? OrderRequest
}
}
// Check for JSON string
else if (intent.hasExtra(EXTRA_ORDER_REQUEST_JSON)) {
val jsonString = intent.getStringExtra(EXTRA_ORDER_REQUEST_JSON)
try {
orderRequest = Gson().fromJson(jsonString, OrderRequest::class.java)
} catch (e: Exception) {
Log.e(TAG, "Error parsing order request JSON", e)
}
}
// Check for individual fields
else if (intent.hasExtra(EXTRA_ADDRESS_ID) && intent.hasExtra(EXTRA_PRODUCT_ID)) {
orderRequest = OrderRequest(
address_id = intent.getIntExtra(EXTRA_ADDRESS_ID, 0),
payment_method_id = intent.getIntExtra(EXTRA_PAYMENT_METHOD_ID, 0),
ship_price = intent.getIntExtra(EXTRA_SHIP_PRICE, 0),
ship_name = intent.getStringExtra(EXTRA_SHIP_NAME) ?: "",
ship_service = intent.getStringExtra(EXTRA_SHIP_SERVICE) ?: "",
is_negotiable = intent.getBooleanExtra(EXTRA_IS_NEGOTIABLE, false),
product_id = intent.getIntExtra(EXTRA_PRODUCT_ID, 0),
quantity = intent.getIntExtra(EXTRA_QUANTITY, 0),
ship_etd = intent.getStringExtra(EXTRA_SHIP_ETD) ?: ""
private fun processIntentData() {
// Determine if this is Buy Now or Cart checkout
val isBuyNow = intent.hasExtra(EXTRA_PRODUCT_ID) && !intent.hasExtra(EXTRA_CART_ITEM_IDS)
if (isBuyNow) {
// Process Buy Now flow
viewModel.initializeBuyNow(
storeId = intent.getIntExtra(EXTRA_STORE_ID, 0),
storeName = intent.getStringExtra(EXTRA_STORE_NAME),
productId = intent.getIntExtra(EXTRA_PRODUCT_ID, 0),
productName = intent.getStringExtra(EXTRA_PRODUCT_NAME),
productImage = intent.getStringExtra(EXTRA_PRODUCT_IMAGE),
quantity = intent.getIntExtra(EXTRA_QUANTITY, 1),
price = intent.getDoubleExtra(EXTRA_PRICE, 0.0)
)
} else {
// Process Cart checkout flow
val cartItemIds = intent.getIntArrayExtra(EXTRA_CART_ITEM_IDS)?.toList() ?: emptyList()
if (cartItemIds.isNotEmpty()) {
viewModel.initializeFromCart(cartItemIds)
} else {
Toast.makeText(this, "Error: No cart items specified", Toast.LENGTH_SHORT).show()
finish()
}
}
}
@ -116,88 +85,186 @@ class CheckoutActivity : AppCompatActivity() {
private fun setupObservers() {
// Observe checkout data
viewModel.checkoutData.observe(this) { data ->
setupSellerOrderRecyclerView(data)
setupProductRecyclerView(data)
updateOrderSummary()
// Load payment methods
viewModel.getPaymentMethods { paymentMethods ->
if (paymentMethods.isNotEmpty()) {
setupPaymentMethodsRecyclerView(paymentMethods)
}
}
}
// Observe address details
viewModel.addressDetails.observe(this) { address ->
binding.tvPlacesAddress.text = address.label
binding.tvAddress.text = address.fullAddress
binding.tvPlacesAddress.text = address?.recipient
binding.tvAddress.text = "${address?.street}, ${address?.subdistrict}"
}
// Observe payment details
viewModel.paymentDetails.observe(this) { payment ->
binding.tvPaymentMethod.text = payment.name
// Update selected payment in adapter
payment?.id?.let { paymentAdapter?.setSelectedPaymentId(it) }
}
// Observe loading state
viewModel.isLoading.observe(this) { isLoading ->
// Show/hide loading indicator
// binding.progressBar.isVisible = isLoading
binding.btnPay.isEnabled = !isLoading
// Show/hide loading indicator if you have one
}
// Observe error messages
viewModel.errorMessage.observe(this) { message ->
if (message.isNotEmpty()) {
Toast.makeText(this, message, Toast.LENGTH_SHORT).show()
}
}
// Observe order creation
viewModel.orderCreated.observe(this) { created ->
if (created) {
Toast.makeText(this, "Order successfully created!", Toast.LENGTH_SHORT).show()
setResult(RESULT_OK)
finish()
}
}
}
private fun setupSellerOrderRecyclerView(checkoutData: CheckoutData) {
val adapter = CheckoutSellerAdapter(checkoutData)
binding.rvSellerOrder.apply {
private fun setupProductRecyclerView(checkoutData: CheckoutData) {
val adapter = if (checkoutData.isBuyNow || checkoutData.cartItems.size <= 1) {
CheckoutSellerAdapter(checkoutData)
} else {
CartCheckoutAdapter(checkoutData)
}
binding.rvProductItems.apply {
layoutManager = LinearLayoutManager(this@CheckoutActivity)
this.adapter = adapter
isNestedScrollingEnabled = false
}
}
private fun setupPaymentMethodsRecyclerView(paymentMethods: List<PaymentItem>) {
paymentAdapter = PaymentMethodAdapter(paymentMethods) { payment ->
// When a payment method is selected
viewModel.setPaymentMethod(payment.id)
}
binding.rvPaymentMethods.apply {
layoutManager = LinearLayoutManager(this@CheckoutActivity)
adapter = paymentAdapter
}
}
private fun updateOrderSummary() {
viewModel.checkoutData.value?.let { data ->
// Calculate subtotal (product price * quantity)
val subtotal = data.productPrice * data.orderRequest.quantity
binding.tvSubtotal.text = formatCurrency(subtotal)
// Update price information
binding.tvItemTotal.text = formatCurrency(viewModel.calculateSubtotal())
// Calculate total (subtotal + shipping)
val total = subtotal + data.orderRequest.ship_price
// Get shipping price
val shipPrice = if (data.isBuyNow) {
(data.orderRequest as OrderRequestBuy).shipPrice.toDouble()
} else {
(data.orderRequest as OrderRequest).shipPrice.toDouble()
}
binding.tvShippingFee.text = formatCurrency(shipPrice)
// Update total
val total = viewModel.calculateTotal()
binding.tvTotal.text = formatCurrency(total)
binding.tvBottomTotal.text = formatCurrency(total)
}
}
private fun updateShippingUI(shipName: String, shipService: String, shipEtd: String, shipPrice: Int) {
if (shipName.isNotEmpty() && shipService.isNotEmpty()) {
// Display shipping name and service in one line
binding.tvCourierName.text = "$shipName $shipService"
binding.tvDeliveryEstimate.text = "$shipEtd hari kerja"
binding.tvShippingPrice.text = formatCurrency(shipPrice.toDouble())
binding.rbJne.isChecked = true
}
}
private fun setupClickListeners() {
// Setup address selection
// Address selection
binding.tvChangeAddress.setOnClickListener {
// Launch address selection activity
startActivityForResult(
Intent(this, AddressSelectionActivity::class.java),
REQUEST_ADDRESS
)
val intent = Intent(this, AddressActivity::class.java)
addressSelectionLauncher.launch(intent)
}
// Setup payment button
// Shipping method selection
binding.layoutShippingMethod.setOnClickListener {
val addressId = viewModel.addressDetails.value?.id ?: 0
if (addressId <= 0) {
Toast.makeText(this, "Please select delivery address first", Toast.LENGTH_SHORT).show()
return@setOnClickListener
}
// Launch shipping selection with address and product info
val intent = Intent(this, ShippingActivity::class.java)
intent.putExtra(ShippingActivity.EXTRA_ADDRESS_ID, addressId)
// Add product info for courier cost calculation
val currentData = viewModel.checkoutData.value
if (currentData != null) {
if (currentData.isBuyNow) {
val buyRequest = currentData.orderRequest as OrderRequestBuy
intent.putExtra(ShippingActivity.EXTRA_PRODUCT_ID, buyRequest.productId)
intent.putExtra(ShippingActivity.EXTRA_QUANTITY, buyRequest.quantity)
} else {
// For cart, we'll pass the first item's info
val firstItem = currentData.cartItems.firstOrNull()
if (firstItem != null) {
intent.putExtra(ShippingActivity.EXTRA_PRODUCT_ID, firstItem.productId)
intent.putExtra(ShippingActivity.EXTRA_QUANTITY, firstItem.quantity)
}
}
}
shippingSelectionLauncher.launch(intent)
}
// Create order button
binding.btnPay.setOnClickListener {
// Create the order by sending API request
if (validateOrder()) {
createOrder()
viewModel.createOrder()
}
}
// Setup voucher section
binding.layoutVoucher.setOnClickListener {
Toast.makeText(this, "Select Voucher", Toast.LENGTH_SHORT).show()
// Voucher section (if implemented)
binding.layoutVoucher?.setOnClickListener {
Toast.makeText(this, "Voucher feature not implemented", Toast.LENGTH_SHORT).show()
}
}
// Setup shipping method
binding.layoutShippingMethod.setOnClickListener {
// Launch shipping method selection
val orderRequest = this.orderRequest ?: return@setOnClickListener
val intent = Intent(this, ShippingMethodActivity::class.java)
intent.putExtra(ShippingMethodActivity.EXTRA_PRODUCT_ID, orderRequest.product_id)
startActivityForResult(intent, REQUEST_SHIPPING)
private val addressSelectionLauncher = registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) { result ->
if (result.resultCode == RESULT_OK) {
val addressId = result.data?.getIntExtra(AddressActivity.EXTRA_ADDRESS_ID, 0) ?: 0
if (addressId > 0) {
viewModel.setSelectedAddress(addressId)
}
}
}
// Setup payment method
binding.layoutPaymentMethod.setOnClickListener {
// Launch payment method selection
startActivityForResult(
Intent(this, PaymentMethodActivity::class.java),
REQUEST_PAYMENT
)
private val shippingSelectionLauncher = registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) { result ->
if (result.resultCode == RESULT_OK) {
val data = result.data ?: return@registerForActivityResult
val shipName = data.getStringExtra(ShippingActivity.EXTRA_SHIP_NAME) ?: return@registerForActivityResult
val shipService = data.getStringExtra(ShippingActivity.EXTRA_SHIP_SERVICE) ?: return@registerForActivityResult
val shipPrice = data.getIntExtra(ShippingActivity.EXTRA_SHIP_PRICE, 0)
val shipEtd = data.getStringExtra(ShippingActivity.EXTRA_SHIP_ETD) ?: ""
// Update shipping in ViewModel
viewModel.setShippingMethod(shipName, shipService, shipPrice, shipEtd)
// Update UI - display shipping name and service in one line
updateShippingUI(shipName, shipService, shipEtd, shipPrice)
}
}
@ -207,22 +274,27 @@ class CheckoutActivity : AppCompatActivity() {
}
private fun validateOrder(): Boolean {
val orderRequest = this.orderRequest ?: return false
// Check address
if (orderRequest.address_id <= 0) {
// Check if address is selected
if (viewModel.addressDetails.value == null) {
Toast.makeText(this, "Silakan pilih alamat pengiriman", Toast.LENGTH_SHORT).show()
return false
}
// Check shipping method
if (orderRequest.ship_name.isEmpty() || orderRequest.ship_service.isEmpty()) {
// Check if shipping is selected
val checkoutData = viewModel.checkoutData.value ?: return false
val shipName = if (checkoutData.isBuyNow) {
(checkoutData.orderRequest as OrderRequestBuy).shipName
} else {
(checkoutData.orderRequest as OrderRequest).shipName
}
if (shipName.isEmpty()) {
Toast.makeText(this, "Silakan pilih metode pengiriman", Toast.LENGTH_SHORT).show()
return false
}
// Check payment method
if (orderRequest.payment_method_id <= 0) {
// Check if payment method is selected
if (viewModel.paymentDetails.value == null) {
Toast.makeText(this, "Silakan pilih metode pembayaran", Toast.LENGTH_SHORT).show()
return false
}
@ -230,163 +302,51 @@ class CheckoutActivity : AppCompatActivity() {
return true
}
private fun createOrder() {
val orderRequest = this.orderRequest ?: return
// Show progress dialog
val progressDialog = ProgressDialog(this)
progressDialog.setMessage("Membuat pesanan...")
progressDialog.setCancelable(false)
progressDialog.show()
// In a real app, you would send the order request to your API
// For now, we'll simulate an API call
Handler(Looper.getMainLooper()).postDelayed({
progressDialog.dismiss()
// Show success message
Toast.makeText(this, "Pesanan berhasil dibuat!", Toast.LENGTH_SHORT).show()
// Create intent result with the order request
val resultIntent = Intent()
resultIntent.putExtra(EXTRA_ORDER_REQUEST, orderRequest)
setResult(RESULT_OK, resultIntent)
// Return to previous screen
finish()
}, 1500)
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (resultCode == RESULT_OK) {
when (requestCode) {
REQUEST_ADDRESS -> {
// Handle address selection result
val addressId = data?.getIntExtra(AddressSelectionActivity.EXTRA_ADDRESS_ID, 0) ?: 0
if (addressId > 0) {
orderRequest?.address_id = addressId
// Reload address details
orderRequest?.let { request ->
viewModelScope.launch {
val addressDetails = repository.getAddressDetails(request.address_id)
binding.tvPlacesAddress.text = addressDetails.label
binding.tvAddress.text = addressDetails.fullAddress
}
}
}
}
REQUEST_SHIPPING -> {
// Handle shipping method selection result
data?.let { intent ->
val shipName = intent.getStringExtra(ShippingMethodActivity.EXTRA_SHIP_NAME) ?: return
val shipService = intent.getStringExtra(ShippingMethodActivity.EXTRA_SHIP_SERVICE) ?: return
val shipPrice = intent.getIntExtra(ShippingMethodActivity.EXTRA_SHIP_PRICE, 0)
val shipEtd = intent.getStringExtra(ShippingMethodActivity.EXTRA_SHIP_ETD) ?: ""
// Update order request
orderRequest?.apply {
this.ship_name = shipName
this.ship_service = shipService
this.ship_price = shipPrice
this.ship_etd = shipEtd
}
// Update UI
binding.tvShippingMethod.text = "$shipName $shipService ($shipEtd hari)"
updateOrderSummary()
}
}
REQUEST_PAYMENT -> {
// Handle payment method selection result
val paymentMethodId = data?.getIntExtra(PaymentMethodActivity.EXTRA_PAYMENT_METHOD_ID, 0) ?: 0
if (paymentMethodId > 0) {
orderRequest?.payment_method_id = paymentMethodId
// Reload payment method details
orderRequest?.let { request ->
viewModelScope.launch {
val paymentDetails = repository.getPaymentMethodDetails(request.payment_method_id)
binding.tvPaymentMethod.text = paymentDetails.name
}
}
}
}
}
}
}
companion object {
private const val TAG = "CheckoutActivity"
// Request codes
const val REQUEST_ADDRESS = 100
const val REQUEST_SHIPPING = 101
const val REQUEST_PAYMENT = 102
// Intent extras
const val EXTRA_ORDER_REQUEST = "extra_order_request"
const val EXTRA_ORDER_REQUEST_JSON = "extra_order_request_json"
const val EXTRA_CART_ITEM_IDS = "extra_cart_item_ids"
const val EXTRA_STORE_ID = "STORE_ID"
const val EXTRA_STORE_NAME = "STORE_NAME"
const val EXTRA_PRODUCT_ID = "PRODUCT_ID"
const val EXTRA_PRODUCT_NAME = "PRODUCT_NAME"
const val EXTRA_PRODUCT_IMAGE = "PRODUCT_IMAGE"
const val EXTRA_QUANTITY = "QUANTITY"
const val EXTRA_PRICE = "PRICE"
// Individual field extras
const val EXTRA_ADDRESS_ID = "extra_address_id"
const val EXTRA_PAYMENT_METHOD_ID = "extra_payment_method_id"
const val EXTRA_SHIP_PRICE = "extra_ship_price"
const val EXTRA_SHIP_NAME = "extra_ship_name"
const val EXTRA_SHIP_SERVICE = "extra_ship_service"
const val EXTRA_IS_NEGOTIABLE = "extra_is_negotiable"
const val EXTRA_PRODUCT_ID = "extra_product_id"
const val EXTRA_QUANTITY = "extra_quantity"
const val EXTRA_SHIP_ETD = "extra_ship_etd"
// Helper methods for starting activity
// Start methods for various ways to launch the activity
// Start with OrderRequest object
fun start(context: Context, orderRequest: OrderRequest) {
val intent = Intent(context, CheckoutActivity::class.java)
intent.putExtra(EXTRA_ORDER_REQUEST, orderRequest)
context.startActivity(intent)
}
// Start with OrderRequest JSON
fun startWithJson(context: Context, orderRequestJson: String) {
val intent = Intent(context, CheckoutActivity::class.java)
intent.putExtra(EXTRA_ORDER_REQUEST_JSON, orderRequestJson)
context.startActivity(intent)
}
// Start with individual fields
fun start(
// For Buy Now
fun startForBuyNow(
context: Context,
addressId: Int,
paymentMethodId: Int,
shipPrice: Int,
shipName: String,
shipService: String,
isNegotiable: Boolean,
storeId: Int,
storeName: String?,
productId: Int,
productName: String?,
productImage: String?,
quantity: Int,
shipEtd: String
price: Double
) {
val intent = Intent(context, CheckoutActivity::class.java).apply {
putExtra(EXTRA_ADDRESS_ID, addressId)
putExtra(EXTRA_PAYMENT_METHOD_ID, paymentMethodId)
putExtra(EXTRA_SHIP_PRICE, shipPrice)
putExtra(EXTRA_SHIP_NAME, shipName)
putExtra(EXTRA_SHIP_SERVICE, shipService)
putExtra(EXTRA_IS_NEGOTIABLE, isNegotiable)
putExtra(EXTRA_STORE_ID, storeId)
putExtra(EXTRA_STORE_NAME, storeName)
putExtra(EXTRA_PRODUCT_ID, productId)
putExtra(EXTRA_PRODUCT_NAME, productName)
putExtra(EXTRA_PRODUCT_IMAGE, productImage)
putExtra(EXTRA_QUANTITY, quantity)
putExtra(EXTRA_SHIP_ETD, shipEtd)
putExtra(EXTRA_PRICE, price)
}
context.startActivity(intent)
}
// Launch for result with OrderRequest
fun startForResult(activity: Activity, orderRequest: OrderRequest, requestCode: Int) {
val intent = Intent(activity, CheckoutActivity::class.java)
intent.putExtra(EXTRA_ORDER_REQUEST, orderRequest)
activity.startActivityForResult(intent, requestCode)
// For Cart checkout
fun startForCart(
context: Context,
cartItemIds: List<Int>
) {
val intent = Intent(context, CheckoutActivity::class.java).apply {
putExtra(EXTRA_CART_ITEM_IDS, cartItemIds.toIntArray())
}
context.startActivity(intent)
}
}
}

View File

@ -4,10 +4,8 @@ import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.alya.ecommerce_serang.R
import com.alya.ecommerce_serang.data.api.dto.CheckoutData
import com.alya.ecommerce_serang.databinding.ItemOrderSellerBinding
import com.bumptech.glide.Glide
// Adapter for seller section that contains the product
class CheckoutSellerAdapter(private val checkoutData: CheckoutData) :
@ -17,29 +15,28 @@ class CheckoutSellerAdapter(private val checkoutData: CheckoutData) :
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SellerViewHolder {
val binding = ItemOrderSellerBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
LayoutInflater.from(parent.context), parent, false
)
return SellerViewHolder(binding)
}
override fun getItemCount(): Int = 1 // Only one seller based on your JSON
override fun getItemCount(): Int = 1 // Only one seller
override fun onBindViewHolder(holder: SellerViewHolder, position: Int) {
with(holder.binding) {
tvListProductOrder.text = checkoutData.sellerName
// Set seller name
tvStoreName.text = checkoutData.sellerName
// Load seller image
Glide.with(ivSellerOrder.context)
.load(checkoutData.sellerImageUrl)
.placeholder(R.drawable.placeholder_image)
.into(ivSellerOrder)
// Set up nested RecyclerView for the product
// Set up products RecyclerView
rvSellerOrderProduct.apply {
layoutManager = LinearLayoutManager(context)
adapter = CheckoutProductAdapter(checkoutData)
adapter = if (checkoutData.isBuyNow) {
// Single product for Buy Now
SingleProductAdapter(checkoutData)
} else {
// Single cart item
SingleCartItemAdapter(checkoutData.cartItems.first())
}
isNestedScrollingEnabled = false
}
}

View File

@ -7,9 +7,13 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.alya.ecommerce_serang.data.api.dto.CheckoutData
import com.alya.ecommerce_serang.data.api.dto.OrderRequest
import com.alya.ecommerce_serang.data.api.response.profile.AddressesItem
import com.alya.ecommerce_serang.data.api.dto.OrderRequestBuy
import com.alya.ecommerce_serang.data.api.response.cart.CartItemsItem
import com.alya.ecommerce_serang.data.api.response.cart.DataItem
import com.alya.ecommerce_serang.data.api.response.product.PaymentItem
import com.alya.ecommerce_serang.data.api.response.profile.AddressesItem
import com.alya.ecommerce_serang.data.repository.OrderRepository
import com.alya.ecommerce_serang.data.repository.Result
import kotlinx.coroutines.launch
class CheckoutViewModel(private val repository: OrderRepository) : ViewModel() {
@ -17,55 +21,302 @@ class CheckoutViewModel(private val repository: OrderRepository) : ViewModel() {
private val _checkoutData = MutableLiveData<CheckoutData>()
val checkoutData: LiveData<CheckoutData> = _checkoutData
private val _addressDetails = MutableLiveData<AddressesItem>()
val addressDetails: LiveData<AddressesItem> = _addressDetails
private val _addressDetails = MutableLiveData<AddressesItem?>()
val addressDetails: LiveData<AddressesItem?> = _addressDetails
private val _storePayments = MutableLiveData<List<PaymentItem>>()
val storePayments: LiveData<List<PaymentItem>> = _storePayments
private val _paymentDetails = MutableLiveData<PaymentItem?>()
val paymentDetails: LiveData<PaymentItem?> = _paymentDetails
private val _isLoading = MutableLiveData<Boolean>()
val isLoading: LiveData<Boolean> = _isLoading
fun loadCheckoutData(orderRequest: OrderRequest) {
private val _errorMessage = MutableLiveData<String>()
val errorMessage: LiveData<String> = _errorMessage
private val _orderCreated = MutableLiveData<Boolean>()
val orderCreated: LiveData<Boolean> = _orderCreated
// Initialize "Buy Now" checkout
fun initializeBuyNow(
storeId: Int,
storeName: String?,
productId: Int,
productName: String?,
productImage: String?,
quantity: Int,
price: Double
) {
viewModelScope.launch {
_isLoading.value = true
try {
// Load all necessary data
val productDetails = repository.fetchProductDetail(orderRequest.productIdItem)
val storeDetails = repository.getStoreDetails(productDetails.product.storeId)
// val addressDetails = repository.getAddressDetails(orderRequest.address_id)
// Update LiveData objects
// _addressDetails.value = addressDetails
// Create CheckoutData object
_checkoutData.value = CheckoutData(
orderRequest = orderRequest,
productName = productDetails?.product?.productName,
productImageUrl = productDetails.product.image,
productPrice = productDetails.product.price,
sellerName = storeDetails.store.storeName,
sellerImageUrl = storeDetails.store.storeImage,
sellerId = productDetails.product.storeId
// Create initial OrderRequestBuy object
val orderRequest = OrderRequestBuy(
addressId = 0, // Will be set when user selects address
paymentMethodId = 0, // Will be set when user selects payment
shipPrice = 0, // Will be set when user selects shipping
shipName = "",
shipService = "",
isNego = false, // Default value
productId = productId,
quantity = quantity,
shipEtd = ""
)
storeDetails?.let {
_storePayments.value = it.payment
}
// Create checkout data
_checkoutData.value = CheckoutData(
orderRequest = orderRequest,
productName = productName,
productImageUrl = productImage ?: "",
productPrice = price,
sellerName = storeName ?: "",
sellerId = storeId,
quantity = quantity,
isBuyNow = true
)
} catch (e: Exception) {
// Handle errors
Log.e("CheckoutViewModel", "Error loading checkout data", e)
Log.e(TAG, "Error initializing Buy Now data", e)
_errorMessage.value = "Failed to initialize checkout: ${e.message}"
} finally {
_isLoading.value = false
}
}
}
// Initialize checkout from cart
fun initializeFromCart(cartItemIds: List<Int>) {
viewModelScope.launch {
_isLoading.value = true
try {
// Get cart data
val cartResult = repository.getCart()
if (cartResult is Result.Success) {
// Find matching cart items
val matchingItems = mutableListOf<CartItemsItem>()
var storeData: DataItem? = null
for (store in cartResult.data) {
val storeItems = store.cartItems.filter { it.cartItemId in cartItemIds }
if (storeItems.isNotEmpty()) {
matchingItems.addAll(storeItems)
storeData = store
break
}
}
if (matchingItems.isNotEmpty() && storeData != null) {
// Create initial OrderRequest object
val orderRequest = OrderRequest(
addressId = 0, // Will be set when user selects address
paymentMethodId = 0, // Will be set when user selects payment
shipPrice = 0, // Will be set when user selects shipping
shipName = "",
shipService = "",
isNego = false,
cartItemId = cartItemIds,
shipEtd = ""
)
// Create checkout data
_checkoutData.value = CheckoutData(
orderRequest = orderRequest,
productName = matchingItems.first().productName,
sellerName = storeData.storeName,
sellerId = storeData.storeId,
isBuyNow = false,
cartItems = matchingItems
)
} else {
_errorMessage.value = "No matching cart items found"
}
} else if (cartResult is Result.Error) {
_errorMessage.value = "Failed to fetch cart items: ${cartResult.exception.message}"
}
} catch (e: Exception) {
Log.e(TAG, "Error initializing cart checkout", e)
_errorMessage.value = "Error: ${e.message}"
} finally {
_isLoading.value = false
}
}
}
// Get payment methods from API
fun getPaymentMethods(callback: (List<PaymentItem>) -> Unit) {
viewModelScope.launch {
try {
val storeResponse = repository.getStore()
if (storeResponse != null && storeResponse.payment.isNotEmpty()) {
callback(storeResponse.payment)
} else {
callback(emptyList())
}
} catch (e: Exception) {
Log.e(TAG, "Error fetching payment methods", e)
callback(emptyList())
}
}
}
// Set selected address
fun setSelectedAddress(addressId: Int) {
viewModelScope.launch {
_isLoading.value = true
try {
// Get address details from API
val addressResponse = repository.getAddress()
if (addressResponse != null && !addressResponse.addresses.isNullOrEmpty()) {
val address = addressResponse.addresses.find { it.id == addressId }
if (addressResponse != null && !addressResponse.addresses.isNullOrEmpty()) {
val address = addressResponse.addresses.find { it.id == addressId }
// No need for null check since _addressDetails now accepts nullable values
_addressDetails.value = address
// Update order request with address ID only if address isn't null
if (address != null) {
val currentData = _checkoutData.value ?: return@launch
if (currentData.isBuyNow) {
val buyRequest = currentData.orderRequest as OrderRequestBuy
val updatedRequest = buyRequest.copy(addressId = addressId)
_checkoutData.value = currentData.copy(orderRequest = updatedRequest)
} else {
val cartRequest = currentData.orderRequest as OrderRequest
val updatedRequest = cartRequest.copy(addressId = addressId)
_checkoutData.value = currentData.copy(orderRequest = updatedRequest)
}
}
}
}
} catch (e: Exception) {
_errorMessage.value = "Error loading address: ${e.message}"
} finally {
_isLoading.value = false
}
}
}
// Set shipping method
fun setShippingMethod(shipName: String, shipService: String, shipPrice: Int, shipEtd: String) {
val currentData = _checkoutData.value ?: return
if (currentData.isBuyNow) {
val buyRequest = currentData.orderRequest as OrderRequestBuy
val updatedRequest = buyRequest.copy(
shipName = shipName,
shipService = shipService,
shipPrice = shipPrice,
shipEtd = shipEtd
)
_checkoutData.value = currentData.copy(orderRequest = updatedRequest)
} else {
val cartRequest = currentData.orderRequest as OrderRequest
val updatedRequest = cartRequest.copy(
shipName = shipName,
shipService = shipService,
shipPrice = shipPrice,
shipEtd = shipEtd
)
_checkoutData.value = currentData.copy(orderRequest = updatedRequest)
}
}
// Set payment method
fun setPaymentMethod(paymentId: Int) {
viewModelScope.launch {
try {
val storeResponse = repository.getStore()
if (storeResponse != null) {
val payment = storeResponse.payment.find { it.id == paymentId }
_paymentDetails.value = payment
// Update order request only if payment isn't null
if (payment != null) {
val currentData = _checkoutData.value ?: return@launch
if (currentData.isBuyNow) {
val buyRequest = currentData.orderRequest as OrderRequestBuy
val updatedRequest = buyRequest.copy(paymentMethodId = paymentId)
_checkoutData.value = currentData.copy(orderRequest = updatedRequest)
} else {
val cartRequest = currentData.orderRequest as OrderRequest
val updatedRequest = cartRequest.copy(paymentMethodId = paymentId)
_checkoutData.value = currentData.copy(orderRequest = updatedRequest)
}
}
}
} catch (e: Exception) {
_errorMessage.value = "Error setting payment method: ${e.message}"
}
}
}
// Create order
fun createOrder() {
viewModelScope.launch {
_isLoading.value = true
try {
val data = _checkoutData.value ?: throw Exception("No checkout data available")
val response = if (data.isBuyNow) {
// For Buy Now, use the dedicated endpoint
val buyRequest = data.orderRequest as OrderRequestBuy
repository.createOrderBuyNow(buyRequest)
} else {
// For Cart checkout, use the standard order endpoint
val cartRequest = data.orderRequest as OrderRequest
repository.createOrder(cartRequest)
}
if (response.isSuccessful) {
_orderCreated.value = true
} else {
val errorMsg = response.errorBody()?.string() ?: "Unknown error"
_errorMessage.value = "Failed to create order: $errorMsg"
}
} catch (e: Exception) {
_errorMessage.value = "Error creating order: ${e.message}"
} finally {
_isLoading.value = false
}
}
}
// Calculate total price (subtotal + shipping)
fun calculateTotal(): Double {
val data = checkoutData.value ?: return 0.0
return (data.productPrice * data.orderRequest.quantity) + data.orderRequest.shipPrice
val data = _checkoutData.value ?: return 0.0
return calculateSubtotal() + getShippingPrice()
}
// Calculate subtotal (without shipping)
fun calculateSubtotal(): Double {
val data = _checkoutData.value ?: return 0.0
return if (data.isBuyNow) {
// For Buy Now, use product price * quantity
val buyRequest = data.orderRequest as OrderRequestBuy
data.productPrice * buyRequest.quantity
} else {
// For Cart, sum all items
data.cartItems.sumOf { it.price * it.quantity.toDouble() }
}
}
// Get shipping price
private fun getShippingPrice(): Double {
val data = _checkoutData.value ?: return 0.0
return if (data.isBuyNow) {
(data.orderRequest as OrderRequestBuy).shipPrice.toDouble()
} else {
(data.orderRequest as OrderRequest).shipPrice.toDouble()
}
}
companion object {
private const val TAG = "CheckoutViewModel"
}
}

View File

@ -0,0 +1,91 @@
package com.alya.ecommerce_serang.ui.order
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.alya.ecommerce_serang.R
import com.alya.ecommerce_serang.data.api.response.product.PaymentItem
import com.alya.ecommerce_serang.databinding.ItemPaymentMethodBinding
import com.bumptech.glide.Glide
import com.bumptech.glide.request.RequestOptions
class PaymentMethodAdapter(
private val paymentMethods: List<PaymentItem>,
private val onPaymentSelected: (PaymentItem) -> Unit
) : RecyclerView.Adapter<PaymentMethodAdapter.PaymentMethodViewHolder>() {
// Track the selected position
private var selectedPosition = -1
class PaymentMethodViewHolder(val binding: ItemPaymentMethodBinding) :
RecyclerView.ViewHolder(binding.root)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PaymentMethodViewHolder {
val binding = ItemPaymentMethodBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
return PaymentMethodViewHolder(binding)
}
override fun getItemCount(): Int = paymentMethods.size
override fun onBindViewHolder(holder: PaymentMethodViewHolder, position: Int) {
val payment = paymentMethods[position]
with(holder.binding) {
// Set payment method name
tvPaymentMethodName.text = payment.bankName
// Set radio button state
rbPaymentMethod.isChecked = selectedPosition == position
// Load payment icon if available
if (payment.qrisImage.isNotEmpty()) {
Glide.with(ivPaymentMethod.context)
.load(payment.qrisImage)
.apply(
RequestOptions()
.placeholder(R.drawable.outline_store_24)
.error(R.drawable.outline_store_24))
.into(ivPaymentMethod)
} else {
// Default icon for bank transfers
ivPaymentMethod.setImageResource(R.drawable.outline_store_24)
}
// Handle click on the entire item
root.setOnClickListener {
selectPayment(position)
onPaymentSelected(payment)
}
// Handle click on the radio button
rbPaymentMethod.setOnClickListener {
selectPayment(position)
onPaymentSelected(payment)
}
}
}
// Helper method to handle payment selection
private fun selectPayment(position: Int) {
if (selectedPosition != position) {
val previousPosition = selectedPosition
selectedPosition = position
// Update UI for previous and new selection
notifyItemChanged(previousPosition)
notifyItemChanged(position)
}
}
// Select a payment method programmatically
fun setSelectedPaymentId(paymentId: Int) {
val position = paymentMethods.indexOfFirst { it.id == paymentId }
if (position != -1 && position != selectedPosition) {
selectPayment(position)
}
}
}

View File

@ -5,82 +5,130 @@ import android.os.Bundle
import android.widget.Toast
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import androidx.core.view.isVisible
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.alya.ecommerce_serang.R
import com.alya.ecommerce_serang.data.api.dto.CostProduct
import com.alya.ecommerce_serang.data.api.dto.CourierCostRequest
import com.alya.ecommerce_serang.data.api.retrofit.ApiConfig
import com.alya.ecommerce_serang.data.api.retrofit.ApiService
import com.alya.ecommerce_serang.data.repository.OrderRepository
import com.alya.ecommerce_serang.data.repository.Result
import com.alya.ecommerce_serang.databinding.ActivityCheckoutBinding
import com.alya.ecommerce_serang.databinding.ActivityShippingBinding
import com.alya.ecommerce_serang.utils.BaseViewModelFactory
import com.alya.ecommerce_serang.utils.SessionManager
import com.google.android.material.appbar.MaterialToolbar
import kotlinx.coroutines.launch
class ShippingActivity : AppCompatActivity() {
private lateinit var binding: ActivityCheckoutBinding
private lateinit var apiService: ApiService
private lateinit var binding: ActivityShippingBinding
private lateinit var sessionManager: SessionManager
private lateinit var adapter: ShippingAdapter
private lateinit var shippingAdapter: ShippingAdapter
private val viewModel: ShippingViewModel by viewModels {
BaseViewModelFactory {
val apiService = ApiConfig.getApiService(sessionManager)
val orderRepository = OrderRepository(apiService)
ShippingViewModel(orderRepository)
val repository = OrderRepository(apiService)
ShippingViewModel(repository)
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityCheckoutBinding.inflate(layoutInflater)
binding = ActivityShippingBinding.inflate(layoutInflater)
setContentView(binding.root)
// Initialize SessionManager
sessionManager = SessionManager(this)
apiService = ApiConfig.getApiService(sessionManager)
val recyclerView = findViewById<RecyclerView>(R.id.rv_shipment_order)
adapter = ShippingAdapter { selectedService ->
val intent = Intent().apply {
putExtra("ship_name", selectedService.service)
putExtra("ship_price", selectedService.cost)
putExtra("ship_service", selectedService.description)
}
setResult(RESULT_OK, intent)
// Get data from intent
val addressId = intent.getIntExtra(EXTRA_ADDRESS_ID, 0)
val productId = intent.getIntExtra(EXTRA_PRODUCT_ID, 0)
val quantity = intent.getIntExtra(EXTRA_QUANTITY, 1)
// Validate required information
if (addressId <= 0 || productId <= 0) {
Toast.makeText(this, "Missing required shipping information", Toast.LENGTH_SHORT).show()
finish()
return
}
// Setup UI components
setupToolbar()
setupRecyclerView()
setupObservers()
// Load shipping options
viewModel.loadShippingOptions(addressId, productId, quantity)
}
private fun setupToolbar() {
binding.toolbar.setNavigationOnClickListener {
finish()
}
recyclerView.adapter = adapter
recyclerView.layoutManager = LinearLayoutManager(this)
}
val request = CourierCostRequest(
addressId = intent.getIntExtra("extra_address_id", 0),
itemCost = CostProduct(
productId = intent.getIntExtra("product_id", 0),
quantity = intent.getIntExtra("quantity", 1)
private fun setupRecyclerView() {
shippingAdapter = ShippingAdapter { courierCostsItem, service ->
// Handle shipping method selection
returnSelectedShipping(
courierCostsItem.courier,
service.service,
service.cost,
service.etd
)
)
}
viewModel.fetchShippingServices(request)
binding.rvShipmentOrder.apply {
layoutManager = LinearLayoutManager(this@ShippingActivity)
adapter = shippingAdapter
}
}
lifecycleScope.launch {
viewModel.shippingServices.collect { result ->
result?.let {
when (it) {
is Result.Success -> adapter.submitList(it.data)
is Result.Error -> Toast.makeText(this@ShippingActivity, it.exception.message, Toast.LENGTH_SHORT).show()
is Result.Loading -> null
}
}
private fun setupObservers() {
// Observe shipping options
viewModel.shippingOptions.observe(this) { courierOptions ->
shippingAdapter.submitList(courierOptions)
updateEmptyState(courierOptions.isEmpty() || courierOptions.all { it.services.isEmpty() })
}
// Observe loading state
viewModel.isLoading.observe(this) { isLoading ->
// binding.progressBar.isVisible = isLoading
}
// Observe error messages
viewModel.errorMessage.observe(this) { message ->
if (message.isNotEmpty()) {
Toast.makeText(this, message, Toast.LENGTH_SHORT).show()
}
}
}
findViewById<MaterialToolbar>(R.id.toolbar).setNavigationOnClickListener {
finish()
private fun updateEmptyState(isEmpty: Boolean) {
// binding.layoutEmptyShipping.isVisible = isEmpty
binding.rvShipmentOrder.isVisible = !isEmpty
}
private fun returnSelectedShipping(
shipName: String,
shipService: String,
shipPrice: Int,
shipEtd: String
) {
val intent = Intent().apply {
putExtra(EXTRA_SHIP_NAME, shipName)
putExtra(EXTRA_SHIP_SERVICE, shipService)
putExtra(EXTRA_SHIP_PRICE, shipPrice)
putExtra(EXTRA_SHIP_ETD, shipEtd)
}
setResult(RESULT_OK, intent)
finish()
}
companion object {
// Constants for intent extras
const val EXTRA_ADDRESS_ID = "extra_address_id"
const val EXTRA_PRODUCT_ID = "extra_product_id"
const val EXTRA_QUANTITY = "extra_quantity"
const val EXTRA_SHIP_NAME = "extra_ship_name"
const val EXTRA_SHIP_SERVICE = "extra_ship_service"
const val EXTRA_SHIP_PRICE = "extra_ship_price"
const val EXTRA_SHIP_ETD = "extra_ship_etd"
}
}

View File

@ -1,65 +1,96 @@
package com.alya.ecommerce_serang.ui.order
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.RadioButton
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import com.alya.ecommerce_serang.R
import com.alya.ecommerce_serang.data.api.response.order.CourierCostsItem
import com.alya.ecommerce_serang.data.api.response.order.ServicesItem
import com.alya.ecommerce_serang.databinding.ItemShippingOrderBinding
class ShippingAdapter(
private val onItemSelected: (ServicesItem) -> Unit
private val onItemSelected: (CourierCostsItem, ServicesItem) -> Unit
) : RecyclerView.Adapter<ShippingAdapter.ShippingViewHolder>() {
private var services = listOf<ServicesItem>()
private var selectedPosition = -1
private val courierCostsList = mutableListOf<CourierCostsItem>()
private var selectedPosition = RecyclerView.NO_POSITION
private var selectedCourierPosition = RecyclerView.NO_POSITION
fun submitList(newList: List<ServicesItem>) {
services = newList
fun submitList(courierCostsList: List<CourierCostsItem>) {
this.courierCostsList.clear()
this.courierCostsList.addAll(courierCostsList)
notifyDataSetChanged()
}
inner class ShippingViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
private val courierName = itemView.findViewById<TextView>(R.id.courier_name_cost)
private val estDate = itemView.findViewById<TextView>(R.id.est_date)
private val costPrice = itemView.findViewById<TextView>(R.id.cost_price)
private val radioButton = itemView.findViewById<RadioButton>(R.id.radio_btn_cost)
inner class ShippingViewHolder(
private val binding: ItemShippingOrderBinding
) : RecyclerView.ViewHolder(binding.root) {
fun bind(service: ServicesItem, isSelected: Boolean) {
courierName.text = service.service // already includes courier name from ViewModel
estDate.text = "Estimasi ${service.etd}"
costPrice.text = "Rp${service.cost}"
radioButton.isChecked = isSelected
fun bind(courierCostsItem: CourierCostsItem, service: ServicesItem, isSelected: Boolean) {
binding.apply {
// Combine courier name and service
courierNameCost.text = "${courierCostsItem.courier} - ${service.service}"
estDate.text = "Estimasi ${service.etd} hari"
costPrice.text = "Rp${service.cost}"
itemView.setOnClickListener {
if (adapterPosition != RecyclerView.NO_POSITION) {
selectedPosition = adapterPosition
notifyDataSetChanged()
onItemSelected(service)
// Single click handler for both item and radio button
val onClickAction = {
val newPosition = adapterPosition
if (newPosition != RecyclerView.NO_POSITION) {
// Update selected position
val oldPosition = selectedPosition
selectedPosition = newPosition
selectedCourierPosition = getParentCourierPosition(courierCostsItem)
// Notify only the changed items to improve performance
notifyItemChanged(oldPosition)
notifyItemChanged(newPosition)
// Call the callback with both courier and service
onItemSelected(courierCostsItem, service)
}
}
}
radioButton.setOnClickListener {
if (adapterPosition != RecyclerView.NO_POSITION) {
selectedPosition = adapterPosition
notifyDataSetChanged()
onItemSelected(service)
root.setOnClickListener { onClickAction() }
radioBtnCost.apply {
isChecked = isSelected
setOnClickListener { onClickAction() }
}
}
}
}
private fun getParentCourierPosition(courierCostsItem: CourierCostsItem): Int {
return courierCostsList.indexOfFirst { it == courierCostsItem }
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ShippingViewHolder {
val view = LayoutInflater.from(parent.context).inflate(R.layout.item_shipping_order, parent, false)
return ShippingViewHolder(view)
val binding = ItemShippingOrderBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
return ShippingViewHolder(binding)
}
override fun onBindViewHolder(holder: ShippingViewHolder, position: Int) {
val service = services[position]
holder.bind(service, position == selectedPosition)
// Flatten the nested structure for binding
var currentPosition = 0
for (courierCostsItem in courierCostsList) {
for (service in courierCostsItem.services) {
if (currentPosition == position) {
holder.bind(
courierCostsItem,
service,
currentPosition == selectedPosition
)
return
}
currentPosition++
}
}
}
override fun getItemCount(): Int = services.size
override fun getItemCount(): Int {
return courierCostsList.sumOf { it.services.size }
}
}

View File

@ -1,32 +1,73 @@
package com.alya.ecommerce_serang.ui.order
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.alya.ecommerce_serang.data.api.dto.CostProduct
import com.alya.ecommerce_serang.data.api.dto.CourierCostRequest
import com.alya.ecommerce_serang.data.api.response.order.ServicesItem
import com.alya.ecommerce_serang.data.api.response.order.CourierCostsItem
import com.alya.ecommerce_serang.data.repository.OrderRepository
import com.alya.ecommerce_serang.data.repository.Result
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
class ShippingViewModel(private val repository: OrderRepository): ViewModel() {
class ShippingViewModel(
private val repository: OrderRepository
) : ViewModel() {
private val _shippingServices = MutableStateFlow<Result<List<ServicesItem>>?>(null)
val shippingServices: StateFlow<Result<List<ServicesItem>>?> = _shippingServices
// Shipping options LiveData
private val _shippingOptions = MutableLiveData<List<CourierCostsItem>>()
val shippingOptions: LiveData<List<CourierCostsItem>> = _shippingOptions
// Loading state LiveData
private val _isLoading = MutableLiveData<Boolean>()
val isLoading: LiveData<Boolean> = _isLoading
// Error message LiveData
private val _errorMessage = MutableLiveData<String>()
val errorMessage: LiveData<String> = _errorMessage
/**
* Load shipping options based on address, product, and quantity
*/
fun loadShippingOptions(addressId: Int, productId: Int, quantity: Int) {
// Reset previous state
_isLoading.value = true
_errorMessage.value = ""
// Prepare the request
val request = CourierCostRequest(
addressId = addressId,
itemCost = CostProduct(
productId = productId,
quantity = quantity
)
)
fun fetchShippingServices(request: CourierCostRequest) {
viewModelScope.launch {
val result = repository.getCountCourierCost(request)
if (result is Result.Success) {
val services = result.data.courierCosts.flatMap { courier ->
courier.services.map {
it.copy(service = "${courier.courier} - ${it.service}")
try {
// Fetch courier costs
val result = repository.getCountCourierCost(request)
when (result) {
is Result.Success -> {
// Update shipping options directly with courier costs
_shippingOptions.value = result.data.courierCosts
}
is Result.Error -> {
// Handle error case
_errorMessage.value = result.exception.message ?: "Unknown error occurred"
}
is Result.Loading -> {
// Typically handled by the loading state
}
}
_shippingServices.value = Result.Success(services)
} else if (result is Result.Error) {
_shippingServices.value = Result.Error(result.exception)
} catch (e: Exception) {
// Catch any unexpected exceptions
_errorMessage.value = e.localizedMessage ?: "An unexpected error occurred"
} finally {
// Always set loading to false
_isLoading.value = false
}
}
}

View File

@ -0,0 +1,45 @@
package com.alya.ecommerce_serang.ui.order
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.alya.ecommerce_serang.R
import com.alya.ecommerce_serang.data.api.response.cart.CartItemsItem
import com.alya.ecommerce_serang.databinding.ItemOrderProductBinding
import com.bumptech.glide.Glide
import java.text.NumberFormat
import java.util.Locale
class SingleCartItemAdapter(private val cartItem: CartItemsItem) :
RecyclerView.Adapter<SingleCartItemAdapter.CartItemViewHolder>() {
class CartItemViewHolder(val binding: ItemOrderProductBinding) : RecyclerView.ViewHolder(binding.root)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CartItemViewHolder {
val binding = ItemOrderProductBinding.inflate(
LayoutInflater.from(parent.context), parent, false
)
return CartItemViewHolder(binding)
}
override fun getItemCount(): Int = 1
override fun onBindViewHolder(holder: CartItemViewHolder, position: Int) {
with(holder.binding) {
// Set cart item details
tvProductName.text = cartItem.productName
tvProductQuantity.text = "${cartItem.quantity} buah"
tvProductPrice.text = formatCurrency(cartItem.price.toDouble())
// Load placeholder image
Glide.with(ivProduct.context)
.load(R.drawable.placeholder_image)
.into(ivProduct)
}
}
private fun formatCurrency(amount: Double): String {
val formatter = NumberFormat.getCurrencyInstance(Locale("in", "ID"))
return formatter.format(amount).replace(",00", "")
}
}

View File

@ -5,37 +5,44 @@ import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.alya.ecommerce_serang.R
import com.alya.ecommerce_serang.data.api.dto.CheckoutData
import com.alya.ecommerce_serang.data.api.dto.OrderRequestBuy
import com.alya.ecommerce_serang.databinding.ItemOrderProductBinding
import com.bumptech.glide.Glide
import com.bumptech.glide.request.RequestOptions
import java.text.NumberFormat
import java.util.Locale
class CheckoutProductAdapter(private val checkoutData: CheckoutData) :
RecyclerView.Adapter<CheckoutProductAdapter.ProductViewHolder>() {
class SingleProductAdapter(private val checkoutData: CheckoutData) :
RecyclerView.Adapter<SingleProductAdapter.ProductViewHolder>() {
class ProductViewHolder(val binding: ItemOrderProductBinding) : RecyclerView.ViewHolder(binding.root)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ProductViewHolder {
val binding = ItemOrderProductBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
LayoutInflater.from(parent.context), parent, false
)
return ProductViewHolder(binding)
}
override fun getItemCount(): Int = 1 // Only one product based on your JSON
override fun getItemCount(): Int = 1
override fun onBindViewHolder(holder: ProductViewHolder, position: Int) {
with(holder.binding) {
// Set product details
tvProductName.text = checkoutData.productName
tvProductQuantity.text = "${checkoutData.orderRequest.quantity} buah"
val quantity = (checkoutData.orderRequest as OrderRequestBuy).quantity
tvProductQuantity.text = "$quantity buah"
tvProductPrice.text = formatCurrency(checkoutData.productPrice)
// Load image with Glide
// Load product image
Glide.with(ivProduct.context)
.load(checkoutData.productImageUrl)
.placeholder(R.drawable.placeholder_image)
.apply(
RequestOptions()
.placeholder(R.drawable.placeholder_image)
.error(R.drawable.placeholder_image))
.into(ivProduct)
}
}

View File

@ -91,9 +91,6 @@ class AddAddressViewModel(private val repository: OrderRepository, private val s
selectedCityId = id
}
fun getSelectedProvinceId(): Int? = selectedProvinceId
fun getSelectedCityId(): Int? = selectedCityId
companion object {
private const val TAG = "AddAddressViewModel"
}

View File

@ -26,6 +26,7 @@ class AddressActivity : AppCompatActivity() {
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityAddressBinding.inflate(layoutInflater)
@ -34,6 +35,8 @@ class AddressActivity : AppCompatActivity() {
sessionManager = SessionManager(this)
apiService = ApiConfig.getApiService(sessionManager)
setupToolbar()
adapter = AddressAdapter { selectedId ->
viewModel.selectAddress(selectedId)
}
@ -55,15 +58,28 @@ class AddressActivity : AppCompatActivity() {
adapter.setSelectedAddressId(selectedId)
}
}
private fun setupToolbar() {
binding.toolbar.setNavigationOnClickListener {
finish()
}
}
// private fun updateEmptyState(isEmpty: Boolean) {
// binding.layoutEmptyAddresses.isVisible = isEmpty
// binding.rvAddresses.isVisible = !isEmpty
// }
private fun onBackPressedWithResult() {
viewModel.selectedAddressId.value?.let {
val intent = Intent()
intent.putExtra("selected_address_id", it)
intent.putExtra(EXTRA_ADDRESS_ID, it)
setResult(RESULT_OK, intent)
}
finish()
}
companion object {
const val EXTRA_ADDRESS_ID = "extra_address_id"
}
}
//override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {

View File

@ -1,5 +1,6 @@
package com.alya.ecommerce_serang.ui.product
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.util.Log
@ -29,6 +30,8 @@ import com.alya.ecommerce_serang.utils.BaseViewModelFactory
import com.alya.ecommerce_serang.utils.SessionManager
import com.bumptech.glide.Glide
import com.google.android.material.bottomsheet.BottomSheetDialog
import java.text.NumberFormat
import java.util.Locale
class DetailProductActivity : AppCompatActivity() {
private lateinit var binding: ActivityDetailProductBinding
@ -54,14 +57,6 @@ class DetailProductActivity : AppCompatActivity() {
sessionManager = SessionManager(this)
apiService = ApiConfig.getApiService(sessionManager)
// val productId = intent.getIntExtra("PRODUCT_ID", -1)
// //nanti tambah get store id dari HomeFragment Product.storeId
// if (productId == -1) {
// Log.e("DetailProductActivity", "Invalid Product ID")
// finish() // Close activity if no valid ID
// return
// }
setupUI()
setupObservers()
loadData()
@ -88,8 +83,19 @@ class DetailProductActivity : AppCompatActivity() {
}
}
viewModel.storeDetail.observe(this) { store ->
updateStoreInfo(store)
viewModel.storeDetail.observe(this) { result ->
when (result) {
is Result.Success -> {
updateStoreInfo(result.data)
}
is Result.Error -> {
// Show error message, maybe a Toast or Snackbar
Toast.makeText(this, "Failed to load store: ${result.exception.message}", Toast.LENGTH_SHORT).show()
}
is Result.Loading -> {
// Show loading indicator if needed
}
}
}
viewModel.otherProducts.observe(this) { products ->
@ -99,29 +105,23 @@ class DetailProductActivity : AppCompatActivity() {
viewModel.reviewProduct.observe(this) { reviews ->
setupRecyclerViewReviewsProduct(reviews)
}
//
// viewModel.isLoading.observe(this) { isLoading ->
// binding.progressBar.visibility = if (isLoading) View.VISIBLE else View.GONE
// }
//
// viewModel.error.observe(this) { errorMessage ->
// if (errorMessage.isNotEmpty()) {
// Toast.makeText(this, errorMessage, Toast.LENGTH_LONG).show()
// }
// }
viewModel.isLoading.observe(this) { isLoading ->
binding.progressBarDetailProd.visibility = if (isLoading) View.VISIBLE else View.GONE
}
viewModel.error.observe(this) { errorMessage ->
if (errorMessage.isNotEmpty()) {
Toast.makeText(this, errorMessage, Toast.LENGTH_LONG).show()
}
}
viewModel.addCart.observe(this) { result ->
when (result) {
is com.alya.ecommerce_serang.data.repository.Result.Success -> {
Toast.makeText(this, result.data, Toast.LENGTH_SHORT).show()
// Check if we need to navigate to checkout (for "Buy Now" flow)
if (viewModel.shouldNavigateToCheckout) {
viewModel.shouldNavigateToCheckout = false
navigateToCheckout()
}
is Result.Success -> {
val cartId = result.data.data.cartId
Toast.makeText(this, result.data.message, Toast.LENGTH_SHORT).show()
}
is com.alya.ecommerce_serang.data.repository.Result.Error -> {
is Result.Error -> {
Toast.makeText(this, "Failed to add to cart: ${result.exception.message}", Toast.LENGTH_SHORT).show()
}
is Result.Loading -> {
@ -134,7 +134,21 @@ class DetailProductActivity : AppCompatActivity() {
private fun updateStoreInfo(store: StoreProduct?) {
store?.let {
binding.tvSellerName.text = it.storeName
// Add more store details as needed
binding.tvSellerRating.text = it.storeRating
binding.tvSellerLocation.text = it.storeLocation
// Load store image using Glide
val fullImageUrl = when (val img = it.storeImage) {
is String -> {
if (img.startsWith("/")) BASE_URL + img.substring(1) else img
}
else -> R.drawable.placeholder_image
}
Glide.with(this)
.load(fullImageUrl)
.placeholder(R.drawable.placeholder_image)
.into(binding.ivSellerImage)
}
}
@ -176,14 +190,14 @@ class DetailProductActivity : AppCompatActivity() {
private fun updateUI(product: Product){
binding.tvProductName.text = product.productName
binding.tvPrice.text = product.price
binding.tvPrice.text = formatCurrency(product.price.toDouble())
binding.tvSold.text = product.totalSold.toString()
binding.tvRating.text = product.rating
binding.tvWeight.text = product.weight.toString()
binding.tvStock.text = product.stock.toString()
binding.tvCategory.text = product.productCategory
binding.tvDescription.text = product.description
binding.tvSellerName.text = product.storeId.toString()
val fullImageUrl = when (val img = product.image) {
@ -298,33 +312,63 @@ class DetailProductActivity : AppCompatActivity() {
}
}
btnBuyNow.setOnClickListener {
bottomSheetDialog.dismiss()
val cartItem = CartItem(
productId = productId,
quantity = currentQuantity
)
// For both Buy Now and Add to Cart, we add to cart first
if (isBuyNow) {
// Set flag to navigate to checkout after adding to cart is successful
viewModel.shouldNavigateToCheckout = true
// If it's Buy Now, navigate directly to checkout without adding to cart
navigateToCheckout()
} else {
// If it's Add to Cart, add the item to the cart
val cartItem = CartItem(
productId = productId,
quantity = currentQuantity
)
viewModel.reqCart(cartItem)
}
// Add to cart in both cases
viewModel.reqCart(cartItem)
}
btnClose.setOnClickListener {
bottomSheetDialog.dismiss()
}
bottomSheetDialog.show()
}
}
private fun formatCurrency(amount: Double): String {
val formatter = NumberFormat.getCurrencyInstance(Locale("in", "ID"))
return formatter.format(amount).replace(",00", "")
}
private fun navigateToCheckout() {
val intent = Intent(this, CheckoutActivity::class.java)
startActivity(intent)
val productDetail = viewModel.productDetail.value ?: return
val storeDetail = viewModel.storeDetail.value
if (storeDetail !is Result.Success || storeDetail.data == null) {
Toast.makeText(this, "Store information not available", Toast.LENGTH_SHORT).show()
return
}
// Start checkout activity with buy now flow
CheckoutActivity.startForBuyNow(
context = this,
storeId = productDetail.storeId,
storeName = storeDetail.data.storeName,
productId = productDetail.productId,
productName = productDetail.productName,
productImage = productDetail.image,
quantity = currentQuantity,
price = productDetail.price.toDouble()
)
}
companion object {
const val EXTRA_PRODUCT_ID = "extra_product_id"
fun start(context: Context, productId: Int) {
val intent = Intent(context, DetailProductActivity::class.java)
intent.putExtra(EXTRA_PRODUCT_ID, productId)
context.startActivity(intent)
}
}
}

View File

@ -7,6 +7,7 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.alya.ecommerce_serang.data.api.dto.CartItem
import com.alya.ecommerce_serang.data.api.dto.ProductsItem
import com.alya.ecommerce_serang.data.api.response.cart.AddCartResponse
import com.alya.ecommerce_serang.data.api.response.product.Product
import com.alya.ecommerce_serang.data.api.response.product.ReviewsItem
import com.alya.ecommerce_serang.data.api.response.product.StoreProduct
@ -19,8 +20,8 @@ class ProductViewModel(private val repository: ProductRepository) : ViewModel()
private val _productDetail = MutableLiveData<Product?>()
val productDetail: LiveData<Product?> get() = _productDetail
private val _storeDetail = MutableLiveData<StoreProduct?>()
val storeDetail : LiveData<StoreProduct?> get() = _storeDetail
private val _storeDetail = MutableLiveData<Result<StoreProduct?>>()
val storeDetail : LiveData<Result<StoreProduct?>> get() = _storeDetail
private val _reviewProduct = MutableLiveData<List<ReviewsItem>>()
val reviewProduct: LiveData<List<ReviewsItem>> get() = _reviewProduct
@ -28,8 +29,8 @@ class ProductViewModel(private val repository: ProductRepository) : ViewModel()
private val _otherProducts = MutableLiveData<List<ProductsItem>>()
val otherProducts: LiveData<List<ProductsItem>> get() = _otherProducts
private val _addCart = MutableLiveData<Result<String>>()
val addCart: LiveData<Result<String>> get() = _addCart
private val _addCart = MutableLiveData<Result<AddCartResponse>>()
val addCart: LiveData<Result<AddCartResponse>> get() = _addCart
private val _isLoading = MutableLiveData<Boolean>()
val isLoading: LiveData<Boolean> get() = _isLoading
@ -37,8 +38,6 @@ class ProductViewModel(private val repository: ProductRepository) : ViewModel()
private val _error = MutableLiveData<String>()
val error: LiveData<String> get() = _error
// Flag to indicate if we should navigate to checkout after adding to cart
var shouldNavigateToCheckout: Boolean = false
fun loadProductDetail(productId: Int) {
_isLoading.value = true
@ -47,10 +46,10 @@ class ProductViewModel(private val repository: ProductRepository) : ViewModel()
val result = repository.fetchProductDetail(productId)
_productDetail.value = result?.product
// Load store details if product has a store ID
// result?.product?.storeId?.let { storeId ->
// loadStoreDetail(storeId)
// }
//Load store details if product has a store ID
result?.product?.storeId?.let { storeId ->
loadStoreDetail(storeId)
}
} catch (e: Exception) {
Log.e("ProductViewModel", "Error loading product details: ${e.message}")
_error.value = "Failed to load product details: ${e.message}"
@ -60,16 +59,18 @@ class ProductViewModel(private val repository: ProductRepository) : ViewModel()
}
}
// fun loadStoreDetail(storeId: Int) {
// viewModelScope.launch {
// try {
// val result = repository.fetchStoreDetail(storeId)
// _storeDetail.value = result
// } catch (e: Exception) {
// Log.e("ProductViewModel", "Error loading store details: ${e.message}")
// }
// }
// }
fun loadStoreDetail(storeId: Int) {
viewModelScope.launch {
try {
_storeDetail.value = Result.Loading
val result = repository.fetchStoreDetail(storeId)
_storeDetail.value = result
} catch (e: Exception) {
Log.e("ProductViewModel", "Error loading store details: ${e.message}")
_storeDetail.value = Result.Error(e)
}
}
}
fun loadReviews(productId: Int) {
viewModelScope.launch {
@ -106,17 +107,19 @@ class ProductViewModel(private val repository: ProductRepository) : ViewModel()
}
fun reqCart(request: CartItem){
viewModelScope.launch {
_isLoading.value = true
when (val result = repository.addToCart(request)) {
is Result.Success -> {
val message = result.data.message
_addCart.value =
Result.Success(message)
_addCart.value = result
_isLoading.value = false
}
is Result.Error -> {
_addCart.value = Result.Error(result.exception)
_addCart.value = result
_error.value = result.exception.message ?: "Unknown error"
_isLoading.value = false
}
is Result.Loading -> {
// optional: already emitted above
_isLoading.value = true
}
}
}

View File

@ -1,17 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<shape android:shape="rectangle">
<solid android:color="@android:color/white" />
<stroke
android:width="1dp"
android:color="#E0E0E0" />
<corners android:radius="4dp" />
</shape>
</item>
<item android:right="8dp">
<bitmap
android:gravity="end|center_vertical"
android:src="@drawable/ic_dropdown_arrow" />
</item>
</layer-list>

View File

@ -1,17 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/cardview_light_background"
android:background="@color/black_800"
tools:context=".ui.order.CheckoutActivity">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="@color/white"
android:elevation="2dp"
app:navigationIcon="@drawable/ic_back_24"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
@ -21,199 +24,286 @@
android:id="@+id/scrollView"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_marginBottom="16dp"
app:layout_constraintTop_toBottomOf="@id/toolbar"
app:layout_constraintBottom_toTopOf="@id/btn_pay">
app:layout_constraintBottom_toTopOf="@id/bottom_payment_bar">
<androidx.constraintlayout.widget.ConstraintLayout
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
android:layout_height="wrap_content"
android:orientation="vertical">
<!-- Delivery Address Section -->
<androidx.cardview.widget.CardView
android:id="@+id/card_delivery_address"
android:layout_width="0dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="16dp"
app:cardCornerRadius="8dp"
app:cardElevation="2dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent">
android:layout_marginHorizontal="0dp"
android:layout_marginTop="0dp"
app:cardElevation="0dp">
<androidx.constraintlayout.widget.ConstraintLayout
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="start"
android:padding="8dp">
<ImageView
android:id="@+id/iv_location_checkout"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:tint="@color/blue_500"
android:layout_marginTop="16dp"
android:src="@drawable/baseline_location_pin_24"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"/>
android:orientation="vertical"
android:padding="16dp"
android:background="@color/white">
<LinearLayout
android:id="@+id/linear_address"
android:layout_width="0dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp"
app:layout_constraintStart_toEndOf="@id/iv_location_checkout"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/tv_change_address">
android:orientation="horizontal">
<ImageView
android:id="@+id/iv_location_icon"
android:layout_width="24dp"
android:layout_height="24dp"
android:src="@drawable/baseline_location_pin_24"
android:layout_gravity="center_vertical"
app:tint="#3D84FF" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Alamat Pengiriman"
android:fontFamily="@font/dmsans_semibold"/>
<TextView
android:id="@+id/tv_places_address"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="Rumah"
android:fontFamily="@font/dmsans_medium"/>
<TextView
android:id="@+id/tv_address"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="Jl. Pegangasan Timur"
android:fontFamily="@font/dmsans_medium"/>
android:textSize="16sp"
android:fontFamily="@font/dmsans_medium"
android:layout_marginStart="8dp" />
</LinearLayout>
<TextView
android:id="@+id/tv_change_address"
android:id="@+id/tv_places_address"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Rumah"
android:textColor="#5A5A5A"
android:paddingHorizontal="8dp"
android:paddingVertical="2dp"
android:textSize="12sp"
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp"
android:text="Pilih Alamat"
android:textColor="@color/blue_500"
android:fontFamily="@font/dmsans_medium"
android:clickable="true"
android:focusable="true"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toBottomOf="@id/linear_address"/>
</androidx.constraintlayout.widget.ConstraintLayout>
android:layout_marginStart="32dp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginTop="8dp">
<TextView
android:id="@+id/tv_address"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Jl. Pegangasan Timur"
android:textSize="14sp"
android:layout_marginStart="32dp" />
<TextView
android:id="@+id/tv_change_address"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Pilih Alamat"
android:textColor="#3D84FF"
android:textSize="14sp" />
</LinearLayout>
</LinearLayout>
</androidx.cardview.widget.CardView>
<!-- Product Item Section -->
<View
android:layout_width="match_parent"
android:layout_height="8dp"
android:background="@color/black_50" />
<!-- Product Items Section -->
<androidx.cardview.widget.CardView
android:id="@+id/card_product"
android:layout_width="0dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="16dp"
app:cardCornerRadius="8dp"
app:cardElevation="2dp"
app:layout_constraintTop_toBottomOf="@id/card_delivery_address"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent">
app:cardElevation="0dp">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_seller_order"
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:listitem="@layout/item_order_seller"/>
android:orientation="vertical"
android:background="@color/white"
android:padding="16dp">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_product_items"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
tools:listitem="@layout/item_order_seller" />
</LinearLayout>
</androidx.cardview.widget.CardView>
<View
android:layout_width="match_parent"
android:layout_height="8dp"
android:background="#F5F5F5" />
<!-- Voucher Section -->
<LinearLayout
android:id="@+id/layout_voucher"
android:layout_width="0dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:background="?attr/selectableItemBackground"
android:background="@color/white"
android:padding="16dp"
app:layout_constraintTop_toBottomOf="@id/card_product"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent">
android:gravity="center_vertical">
<!-- <ImageView-->
<!-- android:layout_width="24dp"-->
<!-- android:layout_height="24dp"-->
<!-- android:src="@drawable/ic_voucher_24" />-->
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Gunakan Voucher"
android:fontFamily="@font/dmsans_semibold"/>
android:textSize="14sp"
android:layout_marginStart="8dp" />
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/ic_arrow_right"/>
android:layout_width="24dp"
android:layout_height="24dp"
android:src="@drawable/ic_arrow_right" />
</LinearLayout>
<View
android:layout_width="match_parent"
android:layout_height="8dp"
android:background="@color/black_50" />
<!-- Shipping Method Section -->
<LinearLayout
android:id="@+id/layout_shipping_method"
android:layout_width="0dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:background="?attr/selectableItemBackground"
android:padding="16dp"
app:layout_constraintTop_toBottomOf="@id/layout_voucher"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent">
android:background="@color/white"
android:padding="16dp">
<TextView
android:layout_width="wrap_content"
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Metode Pengiriman"
android:fontFamily="@font/dmsans_medium"/>
android:orientation="horizontal"
android:gravity="center_vertical">
<TextView
android:id="@+id/tv_shipping_method"
android:layout_width="wrap_content"
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Metode Pengiriman"
android:textSize="14sp" />
<TextView
android:id="@+id/tv_shipping_option"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Opsi Pengiriman"
android:textColor="#3D84FF"
android:textSize="14sp" />
</LinearLayout>
<androidx.cardview.widget.CardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Pilih Pengiriman"
android:fontFamily="@font/dmsans_medium"
android:layout_marginTop="4dp"/>
android:layout_marginTop="8dp"
app:cardCornerRadius="8dp"
app:cardElevation="0dp"
app:cardBackgroundColor="#F5F5F5">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="12dp">
<RadioButton
android:id="@+id/rb_jne"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:checked="true" />
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical"
android:layout_marginStart="8dp">
<TextView
android:id="@+id/tv_courier_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="JNE"
android:textSize="16sp"
android:fontFamily="@font/dmsans_medium" />
<TextView
android:id="@+id/tv_delivery_estimate"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="3 - 4 hari kerja"
android:textSize="14sp"
android:textColor="#757575" />
</LinearLayout>
<TextView
android:id="@+id/tv_shipping_price"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Rp15.000"
android:textSize="16sp"
android:fontFamily="@font/dmsans_medium"
android:layout_gravity="center_vertical" />
</LinearLayout>
</androidx.cardview.widget.CardView>
</LinearLayout>
<View
android:layout_width="match_parent"
android:layout_height="8dp"
android:background="@color/black_50" />
<!-- Payment Method Section -->
<LinearLayout
android:id="@+id/layout_payment_method"
android:layout_width="0dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:background="?attr/selectableItemBackground"
android:padding="16dp"
app:layout_constraintTop_toBottomOf="@id/layout_shipping_method"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent">
android:background="@color/white"
android:padding="16dp">
<TextView
android:layout_width="wrap_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Metode Pembayaran"
android:fontFamily="@font/dmsans_medium"/>
android:textSize="14sp"
android:layout_marginBottom="8dp" />
<TextView
android:id="@+id/tv_payment_method"
android:layout_width="wrap_content"
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_payment_methods"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Bank Transfer"
android:fontFamily="@font/dmsans_medium"
android:layout_marginTop="4dp"/>
tools:listitem="@layout/item_payment_method"
tools:itemCount="2" />
</LinearLayout>
<View
android:layout_width="match_parent"
android:layout_height="8dp"
android:background="@color/black_50" />
<!-- Price Summary Section -->
<LinearLayout
android:id="@+id/layout_price_summary"
android:layout_width="0dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp"
app:layout_constraintTop_toBottomOf="@id/layout_payment_method"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent">
android:background="@color/white"
android:padding="16dp">
<LinearLayout
android:layout_width="match_parent"
@ -224,22 +314,43 @@
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Subtotal"
android:fontFamily="@font/dmsans_medium"/>
android:text="1 item"
android:textSize="14sp" />
<TextView
android:id="@+id/tv_subtotal"
android:id="@+id/tv_item_total"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Rp65,000"
android:fontFamily="@font/dmsans_medium"/>
android:text="Rp65.000"
android:textSize="14sp" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginTop="8dp">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Biaya Pengiriman"
android:textSize="14sp" />
<TextView
android:id="@+id/tv_shipping_fee"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Rp15.000"
android:textSize="14sp" />
</LinearLayout>
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="@color/soft_gray"
android:layout_marginVertical="8dp"/>
android:background="@color/black_50"
android:layout_marginVertical="12dp" />
<LinearLayout
android:layout_width="match_parent"
@ -251,28 +362,64 @@
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Total"
android:fontFamily="@font/dmsans_bold"/>
android:textSize="16sp"
android:fontFamily="@font/dmsans_bold" />
<TextView
android:id="@+id/tv_total"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Rp65,000"
android:fontFamily="@font/dmsans_bold"/>
android:text="Rp75.000"
android:textColor="#3D84FF"
android:textSize="16sp"
android:fontFamily="@font/dmsans_bold" />
</LinearLayout>
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
</LinearLayout>
</ScrollView>
<!-- Pay Button -->
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_pay"
android:layout_width="0dp"
<!-- Bottom Payment Bar -->
<LinearLayout
android:id="@+id/bottom_payment_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Bayar"
android:layout_margin="16dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"/>
android:orientation="horizontal"
android:padding="16dp"
android:background="@color/white"
android:elevation="8dp"
app:layout_constraintBottom_toBottomOf="parent">
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Total:"
android:textSize="14sp" />
<TextView
android:id="@+id/tv_bottom_total"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Rp75.000"
android:textColor="#3D84FF"
android:textSize="18sp"
android:fontFamily="@font/dmsans_bold" />
</LinearLayout>
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_pay"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Bayar"
android:textAllCaps="false"
android:paddingHorizontal="32dp"
app:cornerRadius="8dp"
android:backgroundTint="#3D84FF" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -406,6 +406,13 @@
</LinearLayout>
</androidx.core.widget.NestedScrollView>
<ProgressBar
android:id="@+id/progress_bar_detail_prod"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"
android:layout_gravity="center"/>
<!-- Bottom Action Bar -->
<com.google.android.material.bottomappbar.BottomAppBar
android:id="@+id/bottomAppBar"

View File

@ -11,33 +11,34 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_margin="4dp"
android:layout_margin="8dp"
android:gravity="center_vertical">
<ImageView
android:id="@+id/iv_seller_order"
android:layout_width="32dp"
android:layout_height="32dp"
android:src="@drawable/placeholder_image"/>
android:layout_width="24dp"
android:layout_height="24dp"
android:src="@drawable/outline_store_24" />
<TextView
android:id="@+id/tv_list_product_order"
android:id="@+id/tv_store_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Toko Buka"/>
android:text="SnackEnak"
android:textSize="16sp"
android:fontFamily="@font/dmsans_semibold"
android:layout_marginStart="8dp" />
</LinearLayout>
<com.google.android.material.divider.MaterialDivider
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:dividerInsetStart="16dp"
app:dividerInsetEnd="16dp"/>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_seller_order_product"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:listitem="@layout/item_order_product"/>
<com.google.android.material.divider.MaterialDivider
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:dividerInsetStart="16dp"
app:dividerInsetEnd="16dp"/>
</LinearLayout>

View File

@ -0,0 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="16dp"
android:background="?attr/selectableItemBackground"
android:gravity="center_vertical">
<ImageView
android:id="@+id/iv_payment_method"
android:layout_width="32dp"
android:layout_height="32dp"
android:src="@drawable/outline_store_24"
android:layout_marginEnd="16dp"/>
<TextView
android:id="@+id/tv_payment_method_name"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Bank Transfer"
android:fontFamily="@font/dmsans_medium"/>
<RadioButton
android:id="@+id/rb_payment_method"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
</LinearLayout>