diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 47fc23d..cab34b5 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -18,8 +18,8 @@ - + @@ -55,7 +57,7 @@ android:name=".ui.order.detail.PaymentActivity" android:exported="false" /> - val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars()) - v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom) - insets - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/alya/ecommerce_serang/data/api/response/customer/cart/DeleteCartResponse.kt b/app/src/main/java/com/alya/ecommerce_serang/data/api/response/customer/cart/DeleteCartResponse.kt new file mode 100644 index 0000000..6de4d81 --- /dev/null +++ b/app/src/main/java/com/alya/ecommerce_serang/data/api/response/customer/cart/DeleteCartResponse.kt @@ -0,0 +1,9 @@ +package com.alya.ecommerce_serang.data.api.response.customer.cart + +import com.google.gson.annotations.SerializedName + +data class DeleteCartResponse( + + @field:SerializedName("message") + val message: String +) diff --git a/app/src/main/java/com/alya/ecommerce_serang/data/api/response/customer/cart/ListCartResponse.kt b/app/src/main/java/com/alya/ecommerce_serang/data/api/response/customer/cart/ListCartResponse.kt index ae121a9..c152396 100644 --- a/app/src/main/java/com/alya/ecommerce_serang/data/api/response/customer/cart/ListCartResponse.kt +++ b/app/src/main/java/com/alya/ecommerce_serang/data/api/response/customer/cart/ListCartResponse.kt @@ -4,14 +4,14 @@ import com.google.gson.annotations.SerializedName data class ListCartResponse( - @field:SerializedName("data") - val data: List, + @field:SerializedName("data") + val data: List, - @field:SerializedName("message") + @field:SerializedName("message") val message: String ) -data class DataItem( +data class DataItemCart( @field:SerializedName("store_id") val storeId: Int, diff --git a/app/src/main/java/com/alya/ecommerce_serang/data/api/response/customer/order/CreateReviewResponse.kt b/app/src/main/java/com/alya/ecommerce_serang/data/api/response/customer/order/CreateReviewResponse.kt new file mode 100644 index 0000000..966229b --- /dev/null +++ b/app/src/main/java/com/alya/ecommerce_serang/data/api/response/customer/order/CreateReviewResponse.kt @@ -0,0 +1,15 @@ +package com.alya.ecommerce_serang.data.api.response.customer.order + +import com.google.gson.annotations.SerializedName + +data class CreateReviewResponse( + + @field:SerializedName("order_item_id") + val orderItemId: Int, + + @field:SerializedName("rating") + val rating: Int, + + @field:SerializedName("review_text") + val reviewText: 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 5419b1e..efac13b 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 @@ -11,6 +11,7 @@ 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.ReviewProductItem import com.alya.ecommerce_serang.data.api.dto.SearchRequest import com.alya.ecommerce_serang.data.api.dto.UpdateCart import com.alya.ecommerce_serang.data.api.dto.UpdateChatRequest @@ -22,10 +23,12 @@ import com.alya.ecommerce_serang.data.api.response.chat.ChatListResponse import com.alya.ecommerce_serang.data.api.response.chat.SendChatResponse import com.alya.ecommerce_serang.data.api.response.chat.UpdateChatResponse import com.alya.ecommerce_serang.data.api.response.customer.cart.AddCartResponse +import com.alya.ecommerce_serang.data.api.response.customer.cart.DeleteCartResponse import com.alya.ecommerce_serang.data.api.response.customer.cart.ListCartResponse import com.alya.ecommerce_serang.data.api.response.customer.cart.UpdateCartResponse import com.alya.ecommerce_serang.data.api.response.customer.order.CourierCostResponse import com.alya.ecommerce_serang.data.api.response.customer.order.CreateOrderResponse +import com.alya.ecommerce_serang.data.api.response.customer.order.CreateReviewResponse import com.alya.ecommerce_serang.data.api.response.customer.order.ListCityResponse import com.alya.ecommerce_serang.data.api.response.customer.order.ListProvinceResponse import com.alya.ecommerce_serang.data.api.response.customer.order.OrderDetailResponse @@ -197,6 +200,11 @@ interface ApiService { @Body updateCart: UpdateCart ): Response + @DELETE("cart/delete/{id}") + suspend fun deleteCart( + @Path("id") cartItemId : Int + ):Response + @POST("couriercost") suspend fun countCourierCost( @Body courierCost : CourierCostRequest @@ -232,6 +240,11 @@ interface ApiService { @Part complaintimg: MultipartBody.Part ): Response + @POST("review") + suspend fun createReview( + @Body contentReview : ReviewProductItem + ): Response + @POST("search") suspend fun saveSearchQuery( @Body searchRequest: SearchRequest 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 3378880..210d900 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 @@ -2,28 +2,30 @@ package com.alya.ecommerce_serang.data.repository import android.util.Log import com.alya.ecommerce_serang.data.api.dto.AddEvidenceMultipartRequest +import com.alya.ecommerce_serang.data.api.dto.CartItem import com.alya.ecommerce_serang.data.api.dto.CompletedOrderRequest 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.dto.OrdersItem +import com.alya.ecommerce_serang.data.api.dto.UpdateCart import com.alya.ecommerce_serang.data.api.dto.UserProfile -import com.alya.ecommerce_serang.data.api.response.customer.cart.DataItem +import com.alya.ecommerce_serang.data.api.response.customer.cart.DataItemCart +import com.alya.ecommerce_serang.data.api.response.customer.order.CourierCostResponse import com.alya.ecommerce_serang.data.api.response.customer.order.CreateOrderResponse +import com.alya.ecommerce_serang.data.api.response.customer.order.ListCityResponse +import com.alya.ecommerce_serang.data.api.response.customer.order.ListProvinceResponse import com.alya.ecommerce_serang.data.api.response.customer.order.OrderDetailResponse import com.alya.ecommerce_serang.data.api.response.customer.order.OrderListResponse import com.alya.ecommerce_serang.data.api.response.customer.product.ProductResponse -import com.alya.ecommerce_serang.data.api.response.order.AddEvidenceResponse -import com.alya.ecommerce_serang.data.api.response.order.ComplaintResponse -import com.alya.ecommerce_serang.data.api.response.order.CompletedOrderResponse -import com.alya.ecommerce_serang.data.api.response.customer.order.CourierCostResponse -import com.alya.ecommerce_serang.data.api.response.customer.order.ListCityResponse -import com.alya.ecommerce_serang.data.api.response.customer.order.ListProvinceResponse import com.alya.ecommerce_serang.data.api.response.customer.product.StoreProduct import com.alya.ecommerce_serang.data.api.response.customer.product.StoreResponse import com.alya.ecommerce_serang.data.api.response.customer.profile.AddressResponse import com.alya.ecommerce_serang.data.api.response.customer.profile.CreateAddressResponse +import com.alya.ecommerce_serang.data.api.response.order.AddEvidenceResponse +import com.alya.ecommerce_serang.data.api.response.order.ComplaintResponse +import com.alya.ecommerce_serang.data.api.response.order.CompletedOrderResponse import com.alya.ecommerce_serang.data.api.retrofit.ApiService import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow @@ -155,7 +157,7 @@ class OrderRepository(private val apiService: ApiService) { } } - suspend fun getCart(): Result> { + suspend fun getCart(): Result> { return try { val response = apiService.getCart() @@ -178,6 +180,42 @@ class OrderRepository(private val apiService: ApiService) { } } + + + suspend fun updateCart(updateCart: UpdateCart): Result { + return try { + val response = apiService.updateCart(updateCart) + + if (response.isSuccessful) { + Result.Success(response.body()?.message ?: "Cart updated successfully") + } else { + val errorMsg = response.errorBody()?.string() ?: "Failed to update cart" + Log.e("Order Repository", "Error updating cart: $errorMsg") + Result.Error(Exception(errorMsg)) + } + } catch (e: Exception) { + Log.e("Order Repository", "Exception updating cart", e) + Result.Error(e) + } + } + + suspend fun deleteCartItem(cartItemId: Int): Result { + return try { + val response = apiService.deleteCart(cartItemId) + + if (response.isSuccessful) { + Result.Success(response.body()?.message ?: "Item removed from cart") + } else { + val errorMsg = response.errorBody()?.string() ?: "Failed to remove item from cart" + Log.e("Order Repository", "Error deleting cart item: $errorMsg") + Result.Error(Exception(errorMsg)) + } + } catch (e: Exception) { + Log.e("Order Repository", "Exception deleting cart item", e) + Result.Error(e) + } + } + suspend fun fetchStoreDetail(storeId: Int): Result { return try { val response = apiService.getDetailStore(storeId) diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/cart/CartActivity.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/cart/CartActivity.kt new file mode 100644 index 0000000..7b27eeb --- /dev/null +++ b/app/src/main/java/com/alya/ecommerce_serang/ui/cart/CartActivity.kt @@ -0,0 +1,185 @@ +package com.alya.ecommerce_serang.ui.cart + +import android.content.Intent +import android.os.Bundle +import android.view.View +import android.widget.Toast +import androidx.activity.enableEdgeToEdge +import androidx.activity.viewModels +import androidx.appcompat.app.AppCompatActivity +import androidx.constraintlayout.widget.ConstraintLayout +import androidx.core.view.ViewCompat +import androidx.core.view.WindowCompat +import androidx.core.view.WindowInsetsCompat +import androidx.recyclerview.widget.LinearLayoutManager +import com.alya.ecommerce_serang.R +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.databinding.ActivityCartBinding +import com.alya.ecommerce_serang.ui.order.CheckoutActivity +import com.alya.ecommerce_serang.utils.BaseViewModelFactory +import com.alya.ecommerce_serang.utils.SessionManager +import java.text.NumberFormat +import java.util.Locale + +class CartActivity : AppCompatActivity() { + private lateinit var binding: ActivityCartBinding + private lateinit var apiService: ApiService + private lateinit var sessionManager: SessionManager + private lateinit var storeAdapter: StoreAdapter + + private val viewModel: CartViewModel by viewModels { + BaseViewModelFactory { + val apiService = ApiConfig.getApiService(sessionManager) + val orderRepository = OrderRepository(apiService) + CartViewModel(orderRepository) + } + } + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + binding = ActivityCartBinding.inflate(layoutInflater) + setContentView(binding.root) + + sessionManager = SessionManager(this) + apiService = ApiConfig.getApiService(sessionManager) + + WindowCompat.setDecorFitsSystemWindows(window, false) + + enableEdgeToEdge() + + // Apply insets to your root layout + ViewCompat.setOnApplyWindowInsetsListener(binding.root) { view, windowInsets -> + val systemBars = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()) + view.setPadding( + systemBars.left, + systemBars.top, + systemBars.right, + systemBars.bottom + ) + windowInsets + } + + setupRecyclerView() + setupListeners() + observeViewModel() + viewModel.getCart() + } + + private fun setupRecyclerView() { + storeAdapter = StoreAdapter( + onStoreCheckChanged = { storeId, isChecked -> + if (isChecked) { + viewModel.toggleStoreSelection(storeId) + } else { + viewModel.toggleStoreSelection(storeId) + } + }, + onItemCheckChanged = { cartItemId, storeId, isChecked -> + viewModel.toggleItemSelection(cartItemId, storeId) + }, + onItemQuantityChanged = { cartItemId, quantity -> + viewModel.updateCartItem(cartItemId, quantity) + }, + onItemDeleted = { cartItemId -> + viewModel.deleteCartItem(cartItemId) + } + ) + + binding.rvCart.apply { + layoutManager = LinearLayoutManager(this@CartActivity) + adapter = storeAdapter + } + } + + private fun setupListeners() { + binding.cbSelectAll.setOnCheckedChangeListener { _, _ -> + viewModel.toggleSelectAll() + } + + binding.btnCheckout.setOnClickListener { + if (viewModel.totalSelectedCount.value ?: 0 > 0) { + val selectedItems = viewModel.prepareCheckout() + if (selectedItems.isNotEmpty()) { + // Navigate to checkout + val intent = Intent(this, CheckoutActivity::class.java) + // You would pass the selected items to the next activity + // intent.putExtra("SELECTED_ITEMS", selectedItems) + startActivity(intent) + } + } else { + Toast.makeText(this, "Pilih produk terlebih dahulu", Toast.LENGTH_SHORT).show() + } + } + + binding.btnShopNow.setOnClickListener { + // Navigate to product listing/home + //implement home or search activity + finish() + } + } + + private fun observeViewModel() { + viewModel.cartItems.observe(this) { cartItems -> + if (cartItems.isNullOrEmpty()) { + showEmptyState(true) + } else { + showEmptyState(false) + storeAdapter.submitList(cartItems) + } + } + + viewModel.isLoading.observe(this) { isLoading -> + // Show/hide loading indicator if needed + } + + viewModel.errorMessage.observe(this) { errorMessage -> + errorMessage?.let { + Toast.makeText(this, it, Toast.LENGTH_SHORT).show() + } + } + + viewModel.totalPrice.observe(this) { totalPrice -> + binding.tvTotalPrice.text = formatCurrency(totalPrice) + } + + viewModel.totalSelectedCount.observe(this) { count -> + binding.btnCheckout.text = "Beli ($count)" + } + + viewModel.selectedItems.observe(this) { selectedItems -> + viewModel.selectedStores.value?.let { selectedStores -> + viewModel.activeStoreId.value?.let { activeStoreId -> + storeAdapter.updateSelectedItems(selectedItems, selectedStores, activeStoreId) + } + } + } + + viewModel.allSelected.observe(this) { allSelected -> + // Update the "select all" checkbox without triggering the listener + val selectCbAll = binding.cbSelectAll + selectCbAll.setOnCheckedChangeListener(null) + selectCbAll.isChecked = allSelected + selectCbAll.setOnCheckedChangeListener { _, _ -> + viewModel.toggleSelectAll() + } + } + } + + private fun showEmptyState(isEmpty: Boolean) { + if (isEmpty) { + binding.rvCart.visibility = View.GONE + binding.emptyStateLayout.visibility = View.VISIBLE + findViewById(R.id.bottomCheckoutLayout).visibility = View.GONE + } else { + binding.rvCart.visibility = View.VISIBLE + binding.emptyStateLayout.visibility = View.GONE + findViewById(R.id.bottomCheckoutLayout).visibility = View.VISIBLE + } + } + + private fun formatCurrency(amount: Int): String { + val format = NumberFormat.getCurrencyInstance(Locale("id", "ID")) + return format.format(amount).replace("Rp", "Rp ") + } +} \ No newline at end of file diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/cart/CartViewModel.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/cart/CartViewModel.kt new file mode 100644 index 0000000..7841376 --- /dev/null +++ b/app/src/main/java/com/alya/ecommerce_serang/ui/cart/CartViewModel.kt @@ -0,0 +1,298 @@ +package com.alya.ecommerce_serang.ui.cart + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.alya.ecommerce_serang.data.api.dto.UpdateCart +import com.alya.ecommerce_serang.data.api.response.customer.cart.CartItemsItem +import com.alya.ecommerce_serang.data.api.response.customer.cart.DataItemCart +import com.alya.ecommerce_serang.data.repository.OrderRepository +import com.alya.ecommerce_serang.data.repository.Result +import kotlinx.coroutines.launch + +class CartViewModel(private val repository: OrderRepository) : ViewModel() { + + private val _cartItems = MutableLiveData>() + val cartItems: LiveData> = _cartItems + + private val _isLoading = MutableLiveData() + val isLoading: LiveData = _isLoading + + private val _errorMessage = MutableLiveData() + val errorMessage: LiveData = _errorMessage + + private val _totalPrice = MutableLiveData(0) + val totalPrice: LiveData = _totalPrice + + private val _selectedItems = MutableLiveData>(HashSet()) + val selectedItems: LiveData> = _selectedItems + + private val _selectedStores = MutableLiveData>(HashSet()) + val selectedStores: LiveData> = _selectedStores + + private val _totalSelectedCount = MutableLiveData(0) + val totalSelectedCount: LiveData = _totalSelectedCount + + // Track the currently active store ID for checkout + private val _activeStoreId = MutableLiveData(null) + val activeStoreId: LiveData = _activeStoreId + + // Track if all items are selected + private val _allSelected = MutableLiveData(false) + val allSelected: LiveData = _allSelected + + fun getCart() { + _isLoading.value = true + _errorMessage.value = null + + viewModelScope.launch { + when (val result = repository.getCart()) { + is com.alya.ecommerce_serang.data.repository.Result.Success -> { + _cartItems.value = result.data + _isLoading.value = false + } + is com.alya.ecommerce_serang.data.repository.Result.Error -> { + _errorMessage.value = result.exception.message + _isLoading.value = false + } + is Result.Loading -> { + null + } + } + } + } + + fun updateCartItem(cartItemId: Int, quantity: Int) { + _isLoading.value = true + + viewModelScope.launch { + try { + val updateCart = UpdateCart(cartItemId, quantity) + val result = repository.updateCart(updateCart) + + if (result is com.alya.ecommerce_serang.data.repository.Result.Success) { + // Refresh cart data after successful update + getCart() + calculateTotalPrice() + } else { + _errorMessage.value = (result as com.alya.ecommerce_serang.data.repository.Result.Error).exception.message + } + } catch (e: Exception) { + _errorMessage.value = e.message + } finally { + _isLoading.value = false + } + } + } + + fun deleteCartItem(cartItemId: Int) { + _isLoading.value = true + + viewModelScope.launch { + try { + val result = repository.deleteCartItem(cartItemId) + + if (result is com.alya.ecommerce_serang.data.repository.Result.Success) { + // Remove the item from selected items if it was selected + val currentSelectedItems = _selectedItems.value ?: HashSet() + if (currentSelectedItems.contains(cartItemId)) { + currentSelectedItems.remove(cartItemId) + _selectedItems.value = currentSelectedItems + } + + // Refresh cart data after successful deletion + getCart() + calculateTotalPrice() + } else { + _errorMessage.value = (result as Result.Error).exception.message + } + } catch (e: Exception) { + _errorMessage.value = e.message + } finally { + _isLoading.value = false + } + } + } + + fun toggleItemSelection(cartItemId: Int, storeId: Int) { + val currentSelectedItems = _selectedItems.value ?: HashSet() + val currentSelectedStores = _selectedStores.value ?: HashSet() + + if (currentSelectedItems.contains(cartItemId)) { + currentSelectedItems.remove(cartItemId) + + // Check if there are no more selected items for this store + val storeHasSelectedItems = _cartItems.value?.find { it.storeId == storeId } + ?.cartItems?.any { currentSelectedItems.contains(it.cartItemId) } ?: false + + if (!storeHasSelectedItems) { + currentSelectedStores.remove(storeId) + + // If this was the active store, set active store to null + if (_activeStoreId.value == storeId) { + _activeStoreId.value = null + } + } + } else { + // If there's an active store different from this item's store, deselect all items first + if (_activeStoreId.value != null && _activeStoreId.value != storeId) { + currentSelectedItems.clear() + currentSelectedStores.clear() + } + + currentSelectedItems.add(cartItemId) + currentSelectedStores.add(storeId) + + // Set the active store + _activeStoreId.value = storeId + } + + _selectedItems.value = currentSelectedItems + _selectedStores.value = currentSelectedStores + + calculateTotalPrice() + updateTotalSelectedCount() + checkAllSelected() + } + + fun toggleStoreSelection(storeId: Int) { + val currentSelectedItems = _selectedItems.value ?: HashSet() + val currentSelectedStores = _selectedStores.value ?: HashSet() + val storeItems = _cartItems.value?.find { it.storeId == storeId }?.cartItems ?: emptyList() + + if (currentSelectedStores.contains(storeId)) { + // Deselect all items of this store + currentSelectedStores.remove(storeId) + storeItems.forEach { currentSelectedItems.remove(it.cartItemId) } + + // If this was the active store, set active store to null + if (_activeStoreId.value == storeId) { + _activeStoreId.value = null + } + } else { + // If there's another active store, deselect all items first + if (_activeStoreId.value != null && _activeStoreId.value != storeId) { + currentSelectedItems.clear() + currentSelectedStores.clear() + } + + // Select all items of this store + currentSelectedStores.add(storeId) + storeItems.forEach { currentSelectedItems.add(it.cartItemId) } + + // Set this as the active store + _activeStoreId.value = storeId + } + + _selectedItems.value = currentSelectedItems + _selectedStores.value = currentSelectedStores + + calculateTotalPrice() + updateTotalSelectedCount() + checkAllSelected() + } + + fun toggleSelectAll() { + val allItems = _cartItems.value ?: emptyList() + val currentSelected = _allSelected.value ?: false + + if (currentSelected) { + // Deselect all + _selectedItems.value = HashSet() + _selectedStores.value = HashSet() + _activeStoreId.value = null + _allSelected.value = false + } else { + // If we have multiple stores, we need a special handling + if (allItems.size > 1) { + // Select all items from the first store only + val firstStore = allItems.firstOrNull() + if (firstStore != null) { + val selectedItems = HashSet() + firstStore.cartItems.forEach { selectedItems.add(it.cartItemId) } + + _selectedItems.value = selectedItems + _selectedStores.value = HashSet().apply { add(firstStore.storeId) } + _activeStoreId.value = firstStore.storeId + } + } else { + // Single store, select all items + val selectedItems = HashSet() + val selectedStores = HashSet() + + allItems.forEach { dataItem -> + selectedStores.add(dataItem.storeId) + dataItem.cartItems.forEach { cartItem -> + selectedItems.add(cartItem.cartItemId) + } + } + + _selectedItems.value = selectedItems + _selectedStores.value = selectedStores + + if (allItems.isNotEmpty()) { + _activeStoreId.value = allItems[0].storeId + } + } + + _allSelected.value = true + } + + calculateTotalPrice() + updateTotalSelectedCount() + } + + private fun calculateTotalPrice() { + val selectedItems = _selectedItems.value ?: HashSet() + var total = 0 + + _cartItems.value?.forEach { dataItem -> + dataItem.cartItems.forEach { cartItem -> + if (selectedItems.contains(cartItem.cartItemId)) { + total += cartItem.price * cartItem.quantity + } + } + } + + _totalPrice.value = total + } + + private fun updateTotalSelectedCount() { + _totalSelectedCount.value = _selectedItems.value?.size ?: 0 + } + + private fun checkAllSelected() { + val allItems = _cartItems.value ?: emptyList() + val selectedItems = _selectedItems.value ?: HashSet() + + // If there are multiple stores, "all selected" is true only if all items of the active store are selected + val activeStoreId = _activeStoreId.value + val isAllSelected = if (activeStoreId != null) { + val activeStoreItems = allItems.find { it.storeId == activeStoreId }?.cartItems ?: emptyList() + activeStoreItems.all { selectedItems.contains(it.cartItemId) } + } else { + // No active store, so check if all items of any store are selected + allItems.any { dataItem -> + dataItem.cartItems.all { selectedItems.contains(it.cartItemId) } + } + } + + _allSelected.value = isAllSelected + } + + fun prepareCheckout(): List { + val selectedItemsIds = _selectedItems.value ?: HashSet() + val result = mutableListOf() + + _cartItems.value?.forEach { dataItem -> + dataItem.cartItems.forEach { cartItem -> + if (selectedItemsIds.contains(cartItem.cartItemId)) { + result.add(cartItem) + } + } + } + + return result + } +} \ No newline at end of file diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/cart/StoreAdapter.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/cart/StoreAdapter.kt new file mode 100644 index 0000000..6cfeb79 --- /dev/null +++ b/app/src/main/java/com/alya/ecommerce_serang/ui/cart/StoreAdapter.kt @@ -0,0 +1,231 @@ +package com.alya.ecommerce_serang.ui.cart + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.CheckBox +import android.widget.ImageButton +import android.widget.ImageView +import android.widget.TextView +import androidx.constraintlayout.widget.ConstraintLayout +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView +import com.alya.ecommerce_serang.R +import com.alya.ecommerce_serang.data.api.response.customer.cart.CartItemsItem +import com.alya.ecommerce_serang.data.api.response.customer.cart.DataItemCart +import com.bumptech.glide.Glide +import java.text.NumberFormat +import java.util.Locale + +class StoreAdapter( + private val onStoreCheckChanged: (Int, Boolean) -> Unit, + private val onItemCheckChanged: (Int, Int, Boolean) -> Unit, + private val onItemQuantityChanged: (Int, Int) -> Unit, + private val onItemDeleted: (Int) -> Unit +) : ListAdapter(StoreDiffCallback()) { + + private var selectedItems = HashSet() + private var selectedStores = HashSet() + private var activeStoreId: Int? = null + + companion object { + private const val VIEW_TYPE_STORE = 0 + private const val VIEW_TYPE_ITEM = 1 + } + + fun updateSelectedItems(selectedItems: HashSet, selectedStores: HashSet, activeStoreId: Int?) { + this.selectedItems = selectedItems + this.selectedStores = selectedStores + this.activeStoreId = activeStoreId + notifyDataSetChanged() + } + + override fun getItemViewType(position: Int): Int { + var itemCount = 0 + for (store in currentList) { + // Store header + if (position == itemCount) { + return VIEW_TYPE_STORE + } + itemCount++ + + // Check if position is in the range of this store's items + if (position < itemCount + store.cartItems.size) { + return VIEW_TYPE_ITEM + } + itemCount += store.cartItems.size + } + return -1 + } + + override fun getItemCount(): Int { + var count = 0 + for (store in currentList) { + // One for store header + count++ + // Plus the items in this store + count += store.cartItems.size + } + return count + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { + return when (viewType) { + VIEW_TYPE_STORE -> { + val view = LayoutInflater.from(parent.context) + .inflate(R.layout.item_store_cart, parent, false) + StoreViewHolder(view) + } + VIEW_TYPE_ITEM -> { + val view = LayoutInflater.from(parent.context) + .inflate(R.layout.item_cart_product, parent, false) + CartItemViewHolder(view) + } + else -> throw IllegalArgumentException("Invalid view type") + } + } + + override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { + val (storeIndex, itemIndex) = getStoreAndItemIndex(position) + val store = currentList[storeIndex] + + when (holder) { + is StoreViewHolder -> { + holder.bind(store, selectedStores.contains(store.storeId), activeStoreId == store.storeId) { isChecked -> + onStoreCheckChanged(store.storeId, isChecked) + } + } + is CartItemViewHolder -> { + val cartItem = store.cartItems[itemIndex] + val isSelected = selectedItems.contains(cartItem.cartItemId) + val isEnabled = activeStoreId == null || activeStoreId == store.storeId + + holder.bind( + cartItem, + isSelected, + isEnabled, + { isChecked -> onItemCheckChanged(cartItem.cartItemId, store.storeId, isChecked) }, + { quantity -> onItemQuantityChanged(cartItem.cartItemId, quantity) }, + { onItemDeleted(cartItem.cartItemId) } + ) + } + } + } + + private fun getStoreAndItemIndex(position: Int): Pair { + var itemCount = 0 + for (storeIndex in currentList.indices) { + // Store header position + if (position == itemCount) { + return Pair(storeIndex, -1) + } + itemCount++ + + // Check if position is in the range of this store's items + val store = currentList[storeIndex] + if (position < itemCount + store.cartItems.size) { + return Pair(storeIndex, position - itemCount) + } + itemCount += store.cartItems.size + } + throw IllegalArgumentException("Invalid position") + } + + class StoreViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { + private val cbStore: CheckBox = itemView.findViewById(R.id.cbStore) + private val tvStoreName: TextView = itemView.findViewById(R.id.tvStoreName) + + fun bind(store: DataItemCart, isChecked: Boolean, isActiveStore: Boolean, onCheckedChange: (Boolean) -> Unit) { + tvStoreName.text = store.storeName + + // Set checkbox state without triggering listener + cbStore.setOnCheckedChangeListener(null) + cbStore.isChecked = isChecked + + // Only enable checkbox if this store is active or no store is active + cbStore.setOnCheckedChangeListener { _, isChecked -> + onCheckedChange(isChecked) + } + } + } + + class CartItemViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { + private val cbItem: CheckBox = itemView.findViewById(R.id.cbItem) + private val ivProduct: ImageView = itemView.findViewById(R.id.ivProduct) + private val tvProductName: TextView = itemView.findViewById(R.id.tvProductName) + private val tvPrice: TextView = itemView.findViewById(R.id.tvPrice) + private val btnMinus: ImageButton = itemView.findViewById(R.id.btnMinus) + private val tvQuantity: TextView = itemView.findViewById(R.id.tvQuantity) + private val btnPlus: ImageButton = itemView.findViewById(R.id.btnPlus) + private val quantityController: ConstraintLayout = itemView.findViewById(R.id.quantityController) + + fun bind( + cartItem: CartItemsItem, + isChecked: Boolean, + isEnabled: Boolean, + onCheckedChange: (Boolean) -> Unit, + onQuantityChanged: (Int) -> Unit, + onDelete: () -> Unit + ) { + tvProductName.text = cartItem.productName + tvPrice.text = formatCurrency(cartItem.price) + tvQuantity.text = cartItem.quantity.toString() + + // Load product image + Glide.with(itemView.context) + .load("https://example.com/images/${cartItem.productId}.jpg") // Assume image URL based on product ID + .placeholder(R.drawable.placeholder_image) + .error(R.drawable.placeholder_image) + .into(ivProduct) + + // Set checkbox state without triggering listener + cbItem.setOnCheckedChangeListener(null) + cbItem.isChecked = isChecked + cbItem.isEnabled = isEnabled + + cbItem.setOnCheckedChangeListener { _, isChecked -> + onCheckedChange(isChecked) + } + + // Quantity control + btnMinus.setOnClickListener { + val currentQty = tvQuantity.text.toString().toInt() + if (currentQty > 1) { + val newQty = currentQty - 1 + tvQuantity.text = newQty.toString() + onQuantityChanged(newQty) + } else { + // If quantity would be 0, delete the item + onDelete() + } + } + + btnPlus.setOnClickListener { + val currentQty = tvQuantity.text.toString().toInt() + val newQty = currentQty + 1 + tvQuantity.text = newQty.toString() + onQuantityChanged(newQty) + } + + // Disable quantity controls if item is not from active store + btnMinus.isEnabled = isEnabled + btnPlus.isEnabled = isEnabled + } + + private fun formatCurrency(amount: Int): String { + val format = NumberFormat.getCurrencyInstance(Locale("id", "ID")) + return format.format(amount).replace("Rp", "Rp ") + } + } +} + +class StoreDiffCallback : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: DataItemCart, newItem: DataItemCart): Boolean { + return oldItem.storeId == newItem.storeId + } + + override fun areContentsTheSame(oldItem: DataItemCart, newItem: DataItemCart): Boolean { + return oldItem == newItem + } +} \ No newline at end of file diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/home/HomeFragment.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/home/HomeFragment.kt index c19d02e..a902329 100644 --- a/app/src/main/java/com/alya/ecommerce_serang/ui/home/HomeFragment.kt +++ b/app/src/main/java/com/alya/ecommerce_serang/ui/home/HomeFragment.kt @@ -21,6 +21,7 @@ import com.alya.ecommerce_serang.data.api.dto.ProductsItem import com.alya.ecommerce_serang.data.api.retrofit.ApiConfig import com.alya.ecommerce_serang.data.repository.ProductRepository import com.alya.ecommerce_serang.databinding.FragmentHomeBinding +import com.alya.ecommerce_serang.ui.cart.CartActivity import com.alya.ecommerce_serang.ui.notif.NotificationActivity import com.alya.ecommerce_serang.ui.product.DetailProductActivity import com.alya.ecommerce_serang.utils.BaseViewModelFactory @@ -129,6 +130,8 @@ class HomeFragment : Fragment() { // Setup cart and notification buttons binding.searchContainer.btnCart.setOnClickListener { // Navigate to cart + val intent = Intent(requireContext(), CartActivity::class.java) + startActivity(intent) } binding.searchContainer.btnNotification.setOnClickListener { 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 7c964fa..aa3b14e 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 @@ -9,7 +9,7 @@ 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.customer.cart.CartItemsItem -import com.alya.ecommerce_serang.data.api.response.customer.cart.DataItem +import com.alya.ecommerce_serang.data.api.response.customer.cart.DataItemCart import com.alya.ecommerce_serang.data.api.response.customer.product.PaymentInfoItem import com.alya.ecommerce_serang.data.api.response.customer.profile.AddressesItem import com.alya.ecommerce_serang.data.repository.OrderRepository @@ -100,7 +100,7 @@ class CheckoutViewModel(private val repository: OrderRepository) : ViewModel() { if (cartResult is Result.Success) { // Find matching cart items val matchingItems = mutableListOf() - var storeData: DataItem? = null + var storeData: DataItemCart? = null for (store in cartResult.data) { val storeItems = store.cartItems.filter { it.cartItemId in cartItemIds } 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 8de79a8..b06d44f 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 @@ -28,6 +28,7 @@ import com.alya.ecommerce_serang.data.api.retrofit.ApiService import com.alya.ecommerce_serang.data.repository.ProductRepository import com.alya.ecommerce_serang.data.repository.Result import com.alya.ecommerce_serang.databinding.ActivityDetailProductBinding +import com.alya.ecommerce_serang.ui.cart.CartActivity import com.alya.ecommerce_serang.ui.chat.ChatActivity import com.alya.ecommerce_serang.ui.home.HorizontalProductAdapter import com.alya.ecommerce_serang.ui.order.CheckoutActivity @@ -205,9 +206,19 @@ class DetailProductActivity : AppCompatActivity() { } } + val searchContainerView = binding.searchContainer + searchContainerView.btnCart.setOnClickListener{ + navigateToCart() + } + setupRecyclerViewOtherProducts() } + private fun navigateToCart() { + val intent = Intent(this, CartActivity::class.java) + startActivity(intent) + } + private fun updateUI(product: Product){ binding.tvProductName.text = product.productName binding.tvPrice.text = formatCurrency(product.price.toDouble()) @@ -296,6 +307,7 @@ class DetailProductActivity : AppCompatActivity() { private fun showAddToCartPopup(productId: Int) { showQuantityDialog(productId, false) + } private fun showQuantityDialog(productId: Int, isBuyNow: Boolean) { diff --git a/app/src/main/res/drawable/baseline_add_24.xml b/app/src/main/res/drawable/baseline_add_24.xml index 2ae27b8..40a961e 100644 --- a/app/src/main/res/drawable/baseline_add_24.xml +++ b/app/src/main/res/drawable/baseline_add_24.xml @@ -1,4 +1,4 @@ - + diff --git a/app/src/main/res/layout/activity_cart.xml b/app/src/main/res/layout/activity_cart.xml index 246f39a..22692f8 100644 --- a/app/src/main/res/layout/activity_cart.xml +++ b/app/src/main/res/layout/activity_cart.xml @@ -5,6 +5,128 @@ android:id="@+id/main" android:layout_width="match_parent" android:layout_height="match_parent" - tools:context=".data.api.response.customer.cart.CartActivity"> + tools:context=".ui.cart.CartActivity"> + + + + + + + + + + + + + + +