This commit is contained in:
shaulascr
2025-05-11 17:25:51 +07:00
parent 3157122293
commit 5bf89f3b86
18 changed files with 1107 additions and 41 deletions

View File

@ -18,8 +18,8 @@
<application
android:name=".app.App"
android:allowBackup="true"
android:enableOnBackInvokedCallback="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:enableOnBackInvokedCallback="true"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
@ -30,9 +30,11 @@
android:usesCleartextTraffic="true"
tools:targetApi="31">
<activity
android:name=".ui.chat.ChatActivity"
android:name=".ui.cart.MainActivity"
android:exported="false" />
<!-- <provider -->
<activity
android:name=".ui.chat.ChatActivity"
android:exported="false" /> <!-- <provider -->
<!-- android:name="androidx.startup.InitializationProvider" -->
<!-- android:authorities="${applicationId}.androidx-startup" -->
<!-- tools:node="remove" /> -->
@ -55,7 +57,7 @@
android:name=".ui.order.detail.PaymentActivity"
android:exported="false" />
<activity
android:name=".data.api.response.customer.cart.CartActivity"
android:name=".ui.cart.CartActivity"
android:exported="false" />
<activity
android:name=".ui.order.address.EditAddressActivity"

View File

@ -0,0 +1,14 @@
package com.alya.ecommerce_serang.data.api.dto
import com.google.gson.annotations.SerializedName
data class ReviewProductItem (
@SerializedName("order_item_id")
val orderItemId : Int,
@SerializedName("rating")
val rating : Int,
@SerializedName("review_text")
val reviewTxt : String
)

View File

@ -1,21 +0,0 @@
package com.alya.ecommerce_serang.data.api.response.customer.cart
import android.os.Bundle
import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import com.alya.ecommerce_serang.R
class CartActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContentView(R.layout.activity_cart)
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets ->
val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
insets
}
}
}

View File

@ -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
)

View File

@ -4,14 +4,14 @@ import com.google.gson.annotations.SerializedName
data class ListCartResponse(
@field:SerializedName("data")
val data: List<DataItem>,
@field:SerializedName("data")
val data: List<DataItemCart>,
@field:SerializedName("message")
@field:SerializedName("message")
val message: String
)
data class DataItem(
data class DataItemCart(
@field:SerializedName("store_id")
val storeId: Int,

View File

@ -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
)

View File

@ -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<UpdateCartResponse>
@DELETE("cart/delete/{id}")
suspend fun deleteCart(
@Path("id") cartItemId : Int
):Response<DeleteCartResponse>
@POST("couriercost")
suspend fun countCourierCost(
@Body courierCost : CourierCostRequest
@ -232,6 +240,11 @@ interface ApiService {
@Part complaintimg: MultipartBody.Part
): Response<ComplaintResponse>
@POST("review")
suspend fun createReview(
@Body contentReview : ReviewProductItem
): Response<CreateReviewResponse>
@POST("search")
suspend fun saveSearchQuery(
@Body searchRequest: SearchRequest

View File

@ -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<List<DataItem>> {
suspend fun getCart(): Result<List<DataItemCart>> {
return try {
val response = apiService.getCart()
@ -178,6 +180,42 @@ class OrderRepository(private val apiService: ApiService) {
}
}
suspend fun updateCart(updateCart: UpdateCart): Result<String> {
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<String> {
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<StoreProduct?> {
return try {
val response = apiService.getDetailStore(storeId)

View File

@ -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<ConstraintLayout>(R.id.bottomCheckoutLayout).visibility = View.GONE
} else {
binding.rvCart.visibility = View.VISIBLE
binding.emptyStateLayout.visibility = View.GONE
findViewById<ConstraintLayout>(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 ")
}
}

View File

@ -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<List<DataItemCart>>()
val cartItems: LiveData<List<DataItemCart>> = _cartItems
private val _isLoading = MutableLiveData<Boolean>()
val isLoading: LiveData<Boolean> = _isLoading
private val _errorMessage = MutableLiveData<String?>()
val errorMessage: LiveData<String?> = _errorMessage
private val _totalPrice = MutableLiveData<Int>(0)
val totalPrice: LiveData<Int> = _totalPrice
private val _selectedItems = MutableLiveData<HashSet<Int>>(HashSet())
val selectedItems: LiveData<HashSet<Int>> = _selectedItems
private val _selectedStores = MutableLiveData<HashSet<Int>>(HashSet())
val selectedStores: LiveData<HashSet<Int>> = _selectedStores
private val _totalSelectedCount = MutableLiveData<Int>(0)
val totalSelectedCount: LiveData<Int> = _totalSelectedCount
// Track the currently active store ID for checkout
private val _activeStoreId = MutableLiveData<Int?>(null)
val activeStoreId: LiveData<Int?> = _activeStoreId
// Track if all items are selected
private val _allSelected = MutableLiveData<Boolean>(false)
val allSelected: LiveData<Boolean> = _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<Int>()
firstStore.cartItems.forEach { selectedItems.add(it.cartItemId) }
_selectedItems.value = selectedItems
_selectedStores.value = HashSet<Int>().apply { add(firstStore.storeId) }
_activeStoreId.value = firstStore.storeId
}
} else {
// Single store, select all items
val selectedItems = HashSet<Int>()
val selectedStores = HashSet<Int>()
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<CartItemsItem> {
val selectedItemsIds = _selectedItems.value ?: HashSet()
val result = mutableListOf<CartItemsItem>()
_cartItems.value?.forEach { dataItem ->
dataItem.cartItems.forEach { cartItem ->
if (selectedItemsIds.contains(cartItem.cartItemId)) {
result.add(cartItem)
}
}
}
return result
}
}

View File

@ -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<DataItemCart, RecyclerView.ViewHolder>(StoreDiffCallback()) {
private var selectedItems = HashSet<Int>()
private var selectedStores = HashSet<Int>()
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<Int>, selectedStores: HashSet<Int>, 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<Int, Int> {
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<DataItemCart>() {
override fun areItemsTheSame(oldItem: DataItemCart, newItem: DataItemCart): Boolean {
return oldItem.storeId == newItem.storeId
}
override fun areContentsTheSame(oldItem: DataItemCart, newItem: DataItemCart): Boolean {
return oldItem == newItem
}
}

View File

@ -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 {

View File

@ -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<CartItemsItem>()
var storeData: DataItem? = null
var storeData: DataItemCart? = null
for (store in cartResult.data) {
val storeItems = store.cartItems.filter { it.cartItemId in cartItemIds }

View File

@ -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) {

View File

@ -1,4 +1,4 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#FFFFFF" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#211E1E" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
<path android:fillColor="@android:color/white" android:pathData="M19,13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/>

View File

@ -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">
<include
android:id="@+id/header"
layout="@layout/header" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rvCart"
android:layout_width="match_parent"
android:layout_height="0dp"
android:background="#F5F5F5"
app:layout_constraintBottom_toTopOf="@+id/bottomCheckoutLayout"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/header"/>
<!-- Bottom Checkout Layout -->
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/bottomCheckoutLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@android:color/white"
android:elevation="8dp"
android:padding="16dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent">
<CheckBox
android:id="@+id/cbSelectAll"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Semua"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/tvTotalLabel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Total: "
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@+id/cbSelectAll"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/tvTotalPrice"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Rp0"
android:textColor="@android:color/black"
android:textSize="16sp"
android:textStyle="bold"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@+id/tvTotalLabel"
app:layout_constraintTop_toTopOf="parent" />
<Button
android:id="@+id/btnCheckout"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/bg_button_outline"
android:paddingStart="24dp"
android:paddingEnd="24dp"
android:text="Beli (0)"
android:textColor="@android:color/white"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
<!-- Empty State View (Shown when cart is empty) -->
<LinearLayout
android:id="@+id/emptyStateLayout"
android:layout_width="match_parent"
android:layout_height="0dp"
android:gravity="center"
android:orientation="vertical"
android:visibility="gone"
app:layout_constraintBottom_toTopOf="@+id/bottomCheckoutLayout"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/header">
<ImageView
android:layout_width="120dp"
android:layout_height="120dp"
android:src="@drawable/outline_shopping_cart_24" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="Keranjang Anda kosong"
android:textColor="@android:color/black"
android:textSize="18sp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:gravity="center"
android:paddingStart="32dp"
android:paddingEnd="32dp"
android:text="Silakan tambahkan produk ke keranjang"
android:textColor="#757575"
android:textSize="14sp" />
<Button
android:id="@+id/btnShopNow"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:background="@drawable/bg_button_outline"
android:paddingStart="24dp"
android:paddingEnd="24dp"
android:text="Belanja Sekarang"
android:textColor="@android:color/white" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,104 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@android:color/white"
android:paddingStart="8dp"
android:paddingTop="12dp"
android:paddingEnd="12dp"
android:paddingBottom="12dp">
<CheckBox
android:id="@+id/cbItem"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="@+id/ivProduct"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@+id/ivProduct" />
<ImageView
android:id="@+id/ivProduct"
android:layout_width="60dp"
android:layout_height="60dp"
android:layout_marginStart="8dp"
android:scaleType="centerCrop"
app:layout_constraintStart_toEndOf="@+id/cbItem"
app:layout_constraintTop_toTopOf="parent"
tools:src="@tools:sample/avatars" />
<TextView
android:id="@+id/tvProductName"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:ellipsize="end"
android:maxLines="2"
android:text="Product Name"
android:textColor="@android:color/black"
android:textSize="14sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/ivProduct"
app:layout_constraintTop_toTopOf="@+id/ivProduct" />
<TextView
android:id="@+id/tvPrice"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:text="Rp0"
android:textColor="#0077B6"
android:textSize="14sp"
android:textStyle="bold"
app:layout_constraintStart_toStartOf="@+id/tvProductName"
app:layout_constraintTop_toBottomOf="@+id/tvProductName" />
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/quantityController"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="1.0"
app:layout_constraintStart_toEndOf="@+id/tvPrice"
app:layout_constraintTop_toBottomOf="@+id/tvProductName">
<ImageButton
android:id="@+id/btnMinus"
android:layout_width="30dp"
android:layout_height="30dp"
android:background="@drawable/bg_button_filled"
android:src="@drawable/baseline_add_24"
app:tint="@color/white"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/tvQuantity"
android:layout_width="35dp"
android:layout_height="wrap_content"
android:gravity="center"
android:text="1"
android:textColor="@android:color/black"
android:fontFamily="@font/dmsans_medium"
android:textSize="16sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/btnPlus"
app:layout_constraintStart_toEndOf="@+id/btnMinus"
app:layout_constraintTop_toTopOf="parent" />
<ImageButton
android:id="@+id/btnPlus"
android:layout_width="30dp"
android:layout_height="30dp"
android:background="@drawable/bg_button_filled"
android:src="@drawable/baseline_add_24"
app:tint="@color/white"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,41 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@android:color/white"
android:padding="12dp">
<CheckBox
android:id="@+id/cbStore"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<ImageView
android:id="@+id/ivStore"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginStart="8dp"
android:src="@drawable/outline_store_24"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@+id/cbStore"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/tvStoreName"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:text="Store Name"
android:textColor="@android:color/black"
android:textSize="16sp"
android:textStyle="bold"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/ivStore"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>