diff --git a/app/src/main/java/com/alya/ecommerce_serang/data/api/dto/CheckoutData.kt b/app/src/main/java/com/alya/ecommerce_serang/data/api/dto/CheckoutData.kt index 0c83461..81b9332 100644 --- a/app/src/main/java/com/alya/ecommerce_serang/data/api/dto/CheckoutData.kt +++ b/app/src/main/java/com/alya/ecommerce_serang/data/api/dto/CheckoutData.kt @@ -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 = emptyList() ) \ No newline at end of file diff --git a/app/src/main/java/com/alya/ecommerce_serang/data/api/dto/OrderRequestBuy.kt b/app/src/main/java/com/alya/ecommerce_serang/data/api/dto/OrderRequestBuy.kt new file mode 100644 index 0000000..8e0b154 --- /dev/null +++ b/app/src/main/java/com/alya/ecommerce_serang/data/api/dto/OrderRequestBuy.kt @@ -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 +) \ No newline at end of file diff --git a/app/src/main/java/com/alya/ecommerce_serang/data/api/response/order/UpdateCartResponse.kt b/app/src/main/java/com/alya/ecommerce_serang/data/api/response/cart/UpdateCartResponse.kt similarity index 70% rename from app/src/main/java/com/alya/ecommerce_serang/data/api/response/order/UpdateCartResponse.kt rename to app/src/main/java/com/alya/ecommerce_serang/data/api/response/cart/UpdateCartResponse.kt index 272c7d2..d831255 100644 --- a/app/src/main/java/com/alya/ecommerce_serang/data/api/response/order/UpdateCartResponse.kt +++ b/app/src/main/java/com/alya/ecommerce_serang/data/api/response/cart/UpdateCartResponse.kt @@ -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 diff --git a/app/src/main/java/com/alya/ecommerce_serang/data/api/response/product/DetailStoreProductResponse.kt b/app/src/main/java/com/alya/ecommerce_serang/data/api/response/product/DetailStoreProductResponse.kt index f44b5e8..08df515 100644 --- a/app/src/main/java/com/alya/ecommerce_serang/data/api/response/product/DetailStoreProductResponse.kt +++ b/app/src/main/java/com/alya/ecommerce_serang/data/api/response/product/DetailStoreProductResponse.kt @@ -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, + + @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, + @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 +) diff --git a/app/src/main/java/com/alya/ecommerce_serang/data/api/retrofit/ApiService.kt b/app/src/main/java/com/alya/ecommerce_serang/data/api/retrofit/ApiService.kt index 0895903..4b5a78d 100644 --- a/app/src/main/java/com/alya/ecommerce_serang/data/api/retrofit/ApiService.kt +++ b/app/src/main/java/com/alya/ecommerce_serang/data/api/retrofit/ApiService.kt @@ -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 + @POST("order") + suspend fun postOrderBuyNow( + @Body request: OrderRequestBuy + ): Response + @GET("profile/address") suspend fun getAddress( ): Response diff --git a/app/src/main/java/com/alya/ecommerce_serang/data/repository/OrderRepository.kt b/app/src/main/java/com/alya/ecommerce_serang/data/repository/OrderRepository.kt index 5aa51a0..d2aad68 100644 --- a/app/src/main/java/com/alya/ecommerce_serang/data/repository/OrderRepository.kt +++ b/app/src/main/java/com/alya/ecommerce_serang/data/repository/OrderRepository.kt @@ -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 { - 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 { + 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 { + 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> { + 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 { + 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 { 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{ - 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) - } - } } \ No newline at end of file diff --git a/app/src/main/java/com/alya/ecommerce_serang/data/repository/ProductRepository.kt b/app/src/main/java/com/alya/ecommerce_serang/data/repository/ProductRepository.kt index 43da21b..9743f84 100644 --- a/app/src/main/java/com/alya/ecommerce_serang/data/repository/ProductRepository.kt +++ b/app/src/main/java/com/alya/ecommerce_serang/data/repository/ProductRepository.kt @@ -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> = 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 { - 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 { + 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? { diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/order/CartCheckoutAdapter.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/order/CartCheckoutAdapter.kt new file mode 100644 index 0000000..ee71da9 --- /dev/null +++ b/app/src/main/java/com/alya/ecommerce_serang/ui/order/CartCheckoutAdapter.kt @@ -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() { + + 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) : + RecyclerView.Adapter() { + + 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", "") + } +} \ No newline at end of file diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/order/CheckoutActivity.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/order/CheckoutActivity.kt index fba4599..3e71fcd 100644 --- a/app/src/main/java/com/alya/ecommerce_serang/ui/order/CheckoutActivity.kt +++ b/app/src/main/java/com/alya/ecommerce_serang/ui/order/CheckoutActivity.kt @@ -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) { + 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 + ) { + val intent = Intent(context, CheckoutActivity::class.java).apply { + putExtra(EXTRA_CART_ITEM_IDS, cartItemIds.toIntArray()) + } + context.startActivity(intent) } } } \ No newline at end of file diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/order/CheckoutSellerAdapter.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/order/CheckoutSellerAdapter.kt index 26d8864..1d436b4 100644 --- a/app/src/main/java/com/alya/ecommerce_serang/ui/order/CheckoutSellerAdapter.kt +++ b/app/src/main/java/com/alya/ecommerce_serang/ui/order/CheckoutSellerAdapter.kt @@ -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 } } diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/order/CheckoutViewModel.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/order/CheckoutViewModel.kt index 3f7aa86..8567bf1 100644 --- a/app/src/main/java/com/alya/ecommerce_serang/ui/order/CheckoutViewModel.kt +++ b/app/src/main/java/com/alya/ecommerce_serang/ui/order/CheckoutViewModel.kt @@ -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() val checkoutData: LiveData = _checkoutData - private val _addressDetails = MutableLiveData() - val addressDetails: LiveData = _addressDetails + private val _addressDetails = MutableLiveData() + val addressDetails: LiveData = _addressDetails - private val _storePayments = MutableLiveData>() - val storePayments: LiveData> = _storePayments + private val _paymentDetails = MutableLiveData() + val paymentDetails: LiveData = _paymentDetails private val _isLoading = MutableLiveData() val isLoading: LiveData = _isLoading - fun loadCheckoutData(orderRequest: OrderRequest) { + private val _errorMessage = MutableLiveData() + val errorMessage: LiveData = _errorMessage + + private val _orderCreated = MutableLiveData() + val orderCreated: LiveData = _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) { + 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() + 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) -> 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" } } \ No newline at end of file diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/order/PaymentMethodAdapter.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/order/PaymentMethodAdapter.kt new file mode 100644 index 0000000..0a84fc4 --- /dev/null +++ b/app/src/main/java/com/alya/ecommerce_serang/ui/order/PaymentMethodAdapter.kt @@ -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, + private val onPaymentSelected: (PaymentItem) -> Unit +) : RecyclerView.Adapter() { + + // 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) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/order/ShippingActivity.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/order/ShippingActivity.kt index c462215..bc35d35 100644 --- a/app/src/main/java/com/alya/ecommerce_serang/ui/order/ShippingActivity.kt +++ b/app/src/main/java/com/alya/ecommerce_serang/ui/order/ShippingActivity.kt @@ -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(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(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" } } diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/order/ShippingAdapter.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/order/ShippingAdapter.kt index abc086d..cfbf401 100644 --- a/app/src/main/java/com/alya/ecommerce_serang/ui/order/ShippingAdapter.kt +++ b/app/src/main/java/com/alya/ecommerce_serang/ui/order/ShippingAdapter.kt @@ -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() { - private var services = listOf() - private var selectedPosition = -1 + private val courierCostsList = mutableListOf() + private var selectedPosition = RecyclerView.NO_POSITION + private var selectedCourierPosition = RecyclerView.NO_POSITION - fun submitList(newList: List) { - services = newList + fun submitList(courierCostsList: List) { + this.courierCostsList.clear() + this.courierCostsList.addAll(courierCostsList) notifyDataSetChanged() } - inner class ShippingViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { - private val courierName = itemView.findViewById(R.id.courier_name_cost) - private val estDate = itemView.findViewById(R.id.est_date) - private val costPrice = itemView.findViewById(R.id.cost_price) - private val radioButton = itemView.findViewById(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 } + } } diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/order/ShippingViewModel.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/order/ShippingViewModel.kt index 1fc08c8..17696c7 100644 --- a/app/src/main/java/com/alya/ecommerce_serang/ui/order/ShippingViewModel.kt +++ b/app/src/main/java/com/alya/ecommerce_serang/ui/order/ShippingViewModel.kt @@ -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>?>(null) - val shippingServices: StateFlow>?> = _shippingServices + // Shipping options LiveData + private val _shippingOptions = MutableLiveData>() + val shippingOptions: LiveData> = _shippingOptions + + // Loading state LiveData + private val _isLoading = MutableLiveData() + val isLoading: LiveData = _isLoading + + // Error message LiveData + private val _errorMessage = MutableLiveData() + val errorMessage: LiveData = _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 } } } diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/order/SingleItemCartAdapter.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/order/SingleItemCartAdapter.kt new file mode 100644 index 0000000..7f4d17e --- /dev/null +++ b/app/src/main/java/com/alya/ecommerce_serang/ui/order/SingleItemCartAdapter.kt @@ -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() { + + 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", "") + } +} diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/order/CheckoutProductAdapter.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/order/SingleProductAdapter.kt similarity index 63% rename from app/src/main/java/com/alya/ecommerce_serang/ui/order/CheckoutProductAdapter.kt rename to app/src/main/java/com/alya/ecommerce_serang/ui/order/SingleProductAdapter.kt index 468dd0f..0255a93 100644 --- a/app/src/main/java/com/alya/ecommerce_serang/ui/order/CheckoutProductAdapter.kt +++ b/app/src/main/java/com/alya/ecommerce_serang/ui/order/SingleProductAdapter.kt @@ -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() { +class SingleProductAdapter(private val checkoutData: CheckoutData) : + RecyclerView.Adapter() { 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) } } diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/order/address/AddAddressViewModel.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/order/address/AddAddressViewModel.kt index acf82b2..fd462d2 100644 --- a/app/src/main/java/com/alya/ecommerce_serang/ui/order/address/AddAddressViewModel.kt +++ b/app/src/main/java/com/alya/ecommerce_serang/ui/order/address/AddAddressViewModel.kt @@ -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" } diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/order/address/AddressActivity.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/order/address/AddressActivity.kt index 0311d0e..7fe2b85 100644 --- a/app/src/main/java/com/alya/ecommerce_serang/ui/order/address/AddressActivity.kt +++ b/app/src/main/java/com/alya/ecommerce_serang/ui/order/address/AddressActivity.kt @@ -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?) { diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/product/DetailProductActivity.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/product/DetailProductActivity.kt index 8242847..d0a2c59 100644 --- a/app/src/main/java/com/alya/ecommerce_serang/ui/product/DetailProductActivity.kt +++ b/app/src/main/java/com/alya/ecommerce_serang/ui/product/DetailProductActivity.kt @@ -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) + } } } \ No newline at end of file diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/product/ProductViewModel.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/product/ProductViewModel.kt index a2b90d4..9fd7bce 100644 --- a/app/src/main/java/com/alya/ecommerce_serang/ui/product/ProductViewModel.kt +++ b/app/src/main/java/com/alya/ecommerce_serang/ui/product/ProductViewModel.kt @@ -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() val productDetail: LiveData get() = _productDetail - private val _storeDetail = MutableLiveData() - val storeDetail : LiveData get() = _storeDetail + private val _storeDetail = MutableLiveData>() + val storeDetail : LiveData> get() = _storeDetail private val _reviewProduct = MutableLiveData>() val reviewProduct: LiveData> get() = _reviewProduct @@ -28,8 +29,8 @@ class ProductViewModel(private val repository: ProductRepository) : ViewModel() private val _otherProducts = MutableLiveData>() val otherProducts: LiveData> get() = _otherProducts - private val _addCart = MutableLiveData>() - val addCart: LiveData> get() = _addCart + private val _addCart = MutableLiveData>() + val addCart: LiveData> get() = _addCart private val _isLoading = MutableLiveData() val isLoading: LiveData get() = _isLoading @@ -37,8 +38,6 @@ class ProductViewModel(private val repository: ProductRepository) : ViewModel() private val _error = MutableLiveData() val error: LiveData 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 } } } diff --git a/app/src/main/res/drawable/spinner_address_background.xml b/app/src/main/res/drawable/spinner_address_background.xml deleted file mode 100644 index d70e841..0000000 --- a/app/src/main/res/drawable/spinner_address_background.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/activity_checkout.xml b/app/src/main/res/layout/activity_checkout.xml index 6ffe7e6..4305ca3 100644 --- a/app/src/main/res/layout/activity_checkout.xml +++ b/app/src/main/res/layout/activity_checkout.xml @@ -1,17 +1,20 @@ - + app:layout_constraintBottom_toTopOf="@id/bottom_payment_bar"> - + android:layout_height="wrap_content" + android:orientation="vertical"> + android:layout_marginHorizontal="0dp" + android:layout_marginTop="0dp" + app:cardElevation="0dp"> - - - + android:orientation="vertical" + android:padding="16dp" + android:background="@color/white"> + android:orientation="horizontal"> + + + - - - - + android:textSize="16sp" + android:fontFamily="@font/dmsans_medium" + android:layout_marginStart="8dp" /> - + android:layout_marginStart="32dp" /> + + + + + + + + - + + + + app:cardElevation="0dp"> - + android:orientation="vertical" + android:background="@color/white" + android:padding="16dp"> + + + + + + android:gravity="center_vertical"> + + + + + + android:textSize="14sp" + android:layout_marginStart="8dp" /> + android:layout_width="24dp" + android:layout_height="24dp" + android:src="@drawable/ic_arrow_right" /> + + + android:background="@color/white" + android:padding="16dp"> - + android:orientation="horizontal" + android:gravity="center_vertical"> - + + + + + + android:layout_marginTop="8dp" + app:cardCornerRadius="8dp" + app:cardElevation="0dp" + app:cardBackgroundColor="#F5F5F5"> + + + + + + + + + + + + + + + + + + android:background="@color/white" + android:padding="16dp"> + android:textSize="14sp" + android:layout_marginBottom="8dp" /> - + tools:listitem="@layout/item_payment_method" + tools:itemCount="2" /> + + + android:background="@color/white" + android:padding="16dp"> + android:text="1 item" + android:textSize="14sp" /> + android:text="Rp65.000" + android:textSize="14sp" /> + + + + + + + + android:background="@color/black_50" + android:layout_marginVertical="12dp" /> + android:textSize="16sp" + android:fontFamily="@font/dmsans_bold" /> + android:text="Rp75.000" + android:textColor="#3D84FF" + android:textSize="16sp" + android:fontFamily="@font/dmsans_bold" /> - + - - + + android:orientation="horizontal" + android:padding="16dp" + android:background="@color/white" + android:elevation="8dp" + app:layout_constraintBottom_toBottomOf="parent"> + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_detail_product.xml b/app/src/main/res/layout/activity_detail_product.xml index 23bd083..f9182d4 100644 --- a/app/src/main/res/layout/activity_detail_product.xml +++ b/app/src/main/res/layout/activity_detail_product.xml @@ -406,6 +406,13 @@ + + + android:layout_width="24dp" + android:layout_height="24dp" + android:src="@drawable/outline_store_24" /> + + android:text="SnackEnak" + android:textSize="16sp" + android:fontFamily="@font/dmsans_semibold" + android:layout_marginStart="8dp" /> - - - + \ No newline at end of file diff --git a/app/src/main/res/layout/item_payment_method.xml b/app/src/main/res/layout/item_payment_method.xml new file mode 100644 index 0000000..968abcb --- /dev/null +++ b/app/src/main/res/layout/item_payment_method.xml @@ -0,0 +1,29 @@ + + + + + + + + + \ No newline at end of file