mirror of
https://github.com/shaulascr/ecommerce_serang.git
synced 2025-08-16 19:57:25 +00:00
Merge branch 'screen-features'
# Conflicts: # app/src/main/AndroidManifest.xml
This commit is contained in:
@ -12,14 +12,9 @@ import com.bumptech.glide.Glide
|
||||
|
||||
class HomeCategoryAdapter(
|
||||
private var categories:List<CategoryItem>,
|
||||
//A lambda function that will be invoked when a category item is clicked.
|
||||
private val onClick:(category:CategoryItem) -> Unit
|
||||
): RecyclerView.Adapter<HomeCategoryAdapter.ViewHolder>() {
|
||||
|
||||
/*
|
||||
ViewHolder is responsible for caching and managing the view references for each item in
|
||||
the RecyclerView.It binds the Category data to the corresponding views within the item layout.
|
||||
*/
|
||||
inner class ViewHolder(private val binding: ItemCategoryHomeBinding): RecyclerView.ViewHolder(binding.root){
|
||||
fun bind(category: CategoryItem) = with(binding) {
|
||||
Log.d("CategoriesAdapter", "Binding category: ${category.name}, Image: ${category.image}")
|
||||
@ -59,7 +54,7 @@ class HomeCategoryAdapter(
|
||||
}
|
||||
|
||||
fun updateLimitedCategory(newCategories: List<CategoryItem>){
|
||||
val limitedCategories = newCategories.take(10)
|
||||
val limitedCategories = newCategories.take(9)
|
||||
updateData(limitedCategories)
|
||||
}
|
||||
}
|
@ -14,10 +14,11 @@ import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.repeatOnLifecycle
|
||||
import androidx.navigation.findNavController
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import com.alya.ecommerce_serang.R
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.alya.ecommerce_serang.data.api.dto.CategoryItem
|
||||
import com.alya.ecommerce_serang.data.api.dto.ProductsItem
|
||||
import com.alya.ecommerce_serang.data.api.response.customer.product.StoreItem
|
||||
import com.alya.ecommerce_serang.data.api.retrofit.ApiConfig
|
||||
import com.alya.ecommerce_serang.data.repository.ProductRepository
|
||||
import com.alya.ecommerce_serang.databinding.FragmentHomeBinding
|
||||
@ -25,8 +26,9 @@ 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.ui.product.category.CategoryProductsActivity
|
||||
import com.alya.ecommerce_serang.ui.product.listproduct.ListCategoryActivity
|
||||
import com.alya.ecommerce_serang.ui.product.listproduct.ListProductActivity
|
||||
import com.alya.ecommerce_serang.utils.BaseViewModelFactory
|
||||
import com.alya.ecommerce_serang.utils.HorizontalMarginItemDecoration
|
||||
import com.alya.ecommerce_serang.utils.SessionManager
|
||||
import com.alya.ecommerce_serang.utils.setLightStatusBar
|
||||
import com.alya.ecommerce_serang.utils.viewmodel.HomeUiState
|
||||
@ -75,11 +77,6 @@ class HomeFragment : Fragment() {
|
||||
}
|
||||
|
||||
private fun setupRecyclerView() {
|
||||
productAdapter = HorizontalProductAdapter(
|
||||
products = emptyList(),
|
||||
onClick = { product -> handleProductClick(product) }
|
||||
)
|
||||
|
||||
categoryAdapter = HomeCategoryAdapter(
|
||||
categories = emptyList(),
|
||||
onClick = { category -> handleCategoryProduct(category)}
|
||||
@ -87,21 +84,28 @@ class HomeFragment : Fragment() {
|
||||
|
||||
binding.newProducts.apply {
|
||||
adapter = productAdapter
|
||||
layoutManager = LinearLayoutManager(
|
||||
context,
|
||||
LinearLayoutManager.HORIZONTAL,
|
||||
false
|
||||
)
|
||||
layoutManager = GridLayoutManager(requireContext(), 2)
|
||||
}
|
||||
|
||||
binding.categories.apply {
|
||||
adapter = categoryAdapter
|
||||
layoutManager = LinearLayoutManager(
|
||||
layoutManager = GridLayoutManager(
|
||||
context,
|
||||
LinearLayoutManager.HORIZONTAL,
|
||||
3, // 3 columns
|
||||
RecyclerView.VERTICAL, // vertical layout
|
||||
false
|
||||
)
|
||||
}
|
||||
|
||||
binding.productshowAll.setOnClickListener {
|
||||
val intent = Intent(requireContext(), ListProductActivity::class.java)
|
||||
startActivity(intent)
|
||||
}
|
||||
|
||||
binding.categoryShowAll.setOnClickListener {
|
||||
val intent = Intent(requireContext(), ListCategoryActivity::class.java)
|
||||
startActivity(intent)
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupSearchView() {
|
||||
@ -155,7 +159,10 @@ class HomeFragment : Fragment() {
|
||||
binding.loading.root.isVisible = false
|
||||
binding.error.root.isVisible = false
|
||||
binding.home.isVisible = true
|
||||
productAdapter?.updateLimitedProducts(state.products)
|
||||
val products = state.products
|
||||
viewModel.loadStoresForProducts(products) // << add this here
|
||||
|
||||
productAdapter?.updateLimitedProducts(products)
|
||||
}
|
||||
is HomeUiState.Error -> {
|
||||
binding.loading.root.isVisible = false
|
||||
@ -168,7 +175,9 @@ class HomeFragment : Fragment() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
viewLifecycleOwner.lifecycleScope.launch {
|
||||
@ -179,30 +188,56 @@ class HomeFragment : Fragment() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
viewLifecycleOwner.lifecycleScope.launch {
|
||||
viewLifecycleOwner.lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||
viewModel.storeMap.collect { storeMap ->
|
||||
val products = (viewModel.uiState.value as? HomeUiState.Success)?.products.orEmpty()
|
||||
if (products.isNotEmpty()) {
|
||||
updateProducts(products, storeMap)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateProducts(products: List<ProductsItem>, storeMap: Map<Int, StoreItem>) {
|
||||
if (products.isEmpty()) {
|
||||
Log.d("HomeFragment", "Product list is empty, hiding RecyclerView")
|
||||
binding.newProducts.visibility = View.VISIBLE
|
||||
} else {
|
||||
Log.d("HomeFragment", "Displaying product list in RecyclerView")
|
||||
binding.newProducts.visibility = View.VISIBLE // <-- Fix here
|
||||
productAdapter = HorizontalProductAdapter(products, onClick = { product ->
|
||||
handleProductClick(product)
|
||||
}, storeMap = storeMap)
|
||||
binding.newProducts.adapter = productAdapter
|
||||
productAdapter?.updateProducts(products)
|
||||
}
|
||||
}
|
||||
|
||||
private fun initUi() {
|
||||
// For LightStatusBar
|
||||
setLightStatusBar()
|
||||
val banners = binding.banners
|
||||
banners.offscreenPageLimit = 1
|
||||
|
||||
val nextItemVisiblePx = resources.getDimension(R.dimen.viewpager_next_item_visible)
|
||||
val currentItemHorizontalMarginPx =
|
||||
resources.getDimension(R.dimen.viewpager_current_item_horizontal_margin)
|
||||
val pageTranslationX = nextItemVisiblePx + currentItemHorizontalMarginPx
|
||||
|
||||
banners.setPageTransformer { page, position ->
|
||||
page.translationX = -pageTranslationX * position
|
||||
page.scaleY = 1 - (0.25f * kotlin.math.abs(position))
|
||||
}
|
||||
|
||||
banners.addItemDecoration(
|
||||
HorizontalMarginItemDecoration(
|
||||
requireContext(),
|
||||
R.dimen.viewpager_current_item_horizontal_margin
|
||||
)
|
||||
)
|
||||
// val banners = binding.banners
|
||||
// banners.offscreenPageLimit = 1
|
||||
//
|
||||
// val nextItemVisiblePx = resources.getDimension(R.dimen.viewpager_next_item_visible)
|
||||
// val currentItemHorizontalMarginPx =
|
||||
// resources.getDimension(R.dimen.viewpager_current_item_horizontal_margin)
|
||||
// val pageTranslationX = nextItemVisiblePx + currentItemHorizontalMarginPx
|
||||
//
|
||||
// banners.setPageTransformer { page, position ->
|
||||
// page.translationX = -pageTranslationX * position
|
||||
// page.scaleY = 1 - (0.25f * kotlin.math.abs(position))
|
||||
// }
|
||||
//
|
||||
// banners.addItemDecoration(
|
||||
// HorizontalMarginItemDecoration(
|
||||
// requireContext(),
|
||||
// R.dimen.viewpager_current_item_horizontal_margin
|
||||
// )
|
||||
// )
|
||||
}
|
||||
|
||||
private fun handleProductClick(product: ProductsItem) {
|
||||
|
@ -8,15 +8,17 @@ import androidx.recyclerview.widget.RecyclerView
|
||||
import com.alya.ecommerce_serang.BuildConfig.BASE_URL
|
||||
import com.alya.ecommerce_serang.R
|
||||
import com.alya.ecommerce_serang.data.api.dto.ProductsItem
|
||||
import com.alya.ecommerce_serang.databinding.ItemProductHorizontalBinding
|
||||
import com.alya.ecommerce_serang.data.api.response.customer.product.StoreItem
|
||||
import com.alya.ecommerce_serang.databinding.ItemProductGridBinding
|
||||
import com.bumptech.glide.Glide
|
||||
|
||||
class HorizontalProductAdapter(
|
||||
private var products: List<ProductsItem>,
|
||||
private val onClick: (ProductsItem) -> Unit
|
||||
private val onClick: (ProductsItem) -> Unit,
|
||||
private val storeMap: Map<Int, StoreItem>
|
||||
) : RecyclerView.Adapter<HorizontalProductAdapter.ProductViewHolder>() {
|
||||
|
||||
inner class ProductViewHolder(private val binding: ItemProductHorizontalBinding) :
|
||||
inner class ProductViewHolder(private val binding: ItemProductGridBinding) :
|
||||
RecyclerView.ViewHolder(binding.root) {
|
||||
|
||||
fun bind(product: ProductsItem) = with(binding) {
|
||||
@ -29,22 +31,25 @@ class HorizontalProductAdapter(
|
||||
|
||||
Log.d("ProductAdapter", "Loading image: $fullImageUrl")
|
||||
|
||||
itemName.text = product.name
|
||||
itemPrice.text = product.price
|
||||
tvProductName.text = product.name
|
||||
tvProductPrice.text = product.price
|
||||
rating.text = product.rating
|
||||
|
||||
// Load image using Glide
|
||||
Glide.with(itemView)
|
||||
.load(fullImageUrl)
|
||||
.placeholder(R.drawable.placeholder_image)
|
||||
.into(imageProduct)
|
||||
.into(ivProductImage)
|
||||
|
||||
val storeName = product.storeId?.let { storeMap[it]?.storeName } ?: "Unknown Store"
|
||||
binding.tvStoreName.text = storeName
|
||||
|
||||
root.setOnClickListener { onClick(product) }
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ProductViewHolder {
|
||||
val binding = ItemProductHorizontalBinding.inflate(
|
||||
val binding = ItemProductGridBinding.inflate(
|
||||
LayoutInflater.from(parent.context), parent, false
|
||||
)
|
||||
return ProductViewHolder(binding)
|
||||
@ -65,11 +70,11 @@ class HorizontalProductAdapter(
|
||||
}
|
||||
|
||||
fun updateLimitedProducts(newProducts: List<ProductsItem>) {
|
||||
val diffCallback = ProductDiffCallback(products, newProducts)
|
||||
val limitedProducts = newProducts.take(10) //limit 10 produk
|
||||
val limitedProducts = newProducts.take(10)
|
||||
val diffCallback = ProductDiffCallback(products, limitedProducts)
|
||||
val diffResult = DiffUtil.calculateDiff(diffCallback)
|
||||
products = limitedProducts
|
||||
diffResult.dispatchUpdatesTo(this)
|
||||
updateProducts(limitedProducts)
|
||||
}
|
||||
|
||||
class ProductDiffCallback(
|
||||
|
@ -117,9 +117,6 @@ class SearchHomeFragment : Fragment() {
|
||||
}
|
||||
|
||||
private fun setupSearchResultsRecyclerView() {
|
||||
searchResultsAdapter = SearchResultsAdapter { product ->
|
||||
navigateToProductDetail(product)
|
||||
}
|
||||
|
||||
binding.searchResultsRecyclerView.apply {
|
||||
adapter = searchResultsAdapter
|
||||
@ -130,9 +127,26 @@ class SearchHomeFragment : Fragment() {
|
||||
private fun observeData() {
|
||||
viewModel.searchResults.observe(viewLifecycleOwner) { products ->
|
||||
|
||||
if (!products.isNullOrEmpty()){
|
||||
viewModel.storeDetail(products)
|
||||
}
|
||||
|
||||
searchResultsAdapter?.submitList(products)
|
||||
binding.noResultsText.isVisible = products.isEmpty() && !viewModel.isSearching.value!!
|
||||
binding.searchResultsRecyclerView.isVisible = products.isNotEmpty()
|
||||
|
||||
}
|
||||
|
||||
viewModel.storeDetail.observe(viewLifecycleOwner) {storeMap ->
|
||||
val products = viewModel.searchResults.value.orEmpty()
|
||||
if (products.isNotEmpty()){
|
||||
searchResultsAdapter = SearchResultsAdapter(
|
||||
onItemClick = {product -> navigateToProductDetail(product) },
|
||||
storeMap = storeMap
|
||||
)
|
||||
binding.searchResultsRecyclerView.adapter = searchResultsAdapter
|
||||
searchResultsAdapter?.submitList(products)
|
||||
}
|
||||
}
|
||||
|
||||
viewModel.isSearching.observe(viewLifecycleOwner) { isSearching ->
|
||||
|
@ -6,6 +6,7 @@ import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.alya.ecommerce_serang.data.api.dto.ProductsItem
|
||||
import com.alya.ecommerce_serang.data.api.response.customer.product.StoreItem
|
||||
import com.alya.ecommerce_serang.data.repository.ProductRepository
|
||||
import com.alya.ecommerce_serang.data.repository.Result
|
||||
import kotlinx.coroutines.launch
|
||||
@ -15,6 +16,9 @@ class SearchHomeViewModel (private val productRepository: ProductRepository) : V
|
||||
private val _searchResults = MutableLiveData<List<ProductsItem>>(emptyList())
|
||||
val searchResults: LiveData<List<ProductsItem>> = _searchResults
|
||||
|
||||
private val _storeDetail = MutableLiveData<Map<Int, StoreItem>>()
|
||||
val storeDetail : LiveData<Map<Int, StoreItem>> get() = _storeDetail
|
||||
|
||||
private val _searchHistory = MutableLiveData<List<String>>(emptyList())
|
||||
val searchHistory: LiveData<List<String>> = _searchHistory
|
||||
|
||||
@ -25,10 +29,10 @@ class SearchHomeViewModel (private val productRepository: ProductRepository) : V
|
||||
val isSearchActive: LiveData<Boolean> = _isSearchActive
|
||||
|
||||
fun searchProducts(query: String) {
|
||||
Log.d("HomeViewModel", "searchProducts called with query: '$query'")
|
||||
Log.d("SearchHomeViewModel", "searchProducts called with query: '$query'")
|
||||
|
||||
if (query.isBlank()) {
|
||||
Log.d("HomeViewModel", "Query is blank, clearing results")
|
||||
Log.d("SearchHomeViewModel", "Query is blank, clearing results")
|
||||
_searchResults.value = emptyList()
|
||||
_isSearchActive.value = false
|
||||
return
|
||||
@ -38,18 +42,18 @@ class SearchHomeViewModel (private val productRepository: ProductRepository) : V
|
||||
_isSearchActive.value = true
|
||||
|
||||
viewModelScope.launch {
|
||||
Log.d("HomeViewModel", "Starting search coroutine")
|
||||
Log.d("SearchHomeViewModeldel", "Starting search coroutine")
|
||||
|
||||
when (val result = productRepository.searchProducts(query)) {
|
||||
is Result.Success -> {
|
||||
Log.d("HomeViewModel", "Search successful, found ${result.data.size} products")
|
||||
Log.d("SearchHomeViewModel", "Search successful, found ${result.data.size} products")
|
||||
_searchResults.postValue(result.data)
|
||||
|
||||
// Double check the state after assignment
|
||||
Log.d("HomeViewModel", "Updated searchResults value has ${result.data.size} items")
|
||||
Log.d("SearchHomeViewModel", "Updated searchResults value has ${result.data.size} items")
|
||||
}
|
||||
is Result.Error -> {
|
||||
Log.e("HomeViewModel", "Search failed", result.exception)
|
||||
Log.e("SearchHomeViewModel", "Search failed", result.exception)
|
||||
_searchResults.postValue(emptyList())
|
||||
}
|
||||
else -> {}
|
||||
@ -58,6 +62,30 @@ class SearchHomeViewModel (private val productRepository: ProductRepository) : V
|
||||
}
|
||||
}
|
||||
|
||||
fun storeDetail(products: List<ProductsItem>){
|
||||
viewModelScope.launch {
|
||||
val map = mutableMapOf<Int, StoreItem>()
|
||||
|
||||
val storeIds = products.mapNotNull { it.storeId }.toSet()
|
||||
for (storeId in storeIds){
|
||||
try {
|
||||
when (val result = productRepository.fetchStoreDetail(storeId)){
|
||||
is Result.Success -> map[storeId] = result.data
|
||||
is Result.Error -> Log.e("SearchHomeViewModel", "Error Loading Store")
|
||||
else -> {}
|
||||
}
|
||||
} catch (e: Exception){
|
||||
Log.e("SearchHomeViewModel", "Exception error for storeId: $storeId", e)
|
||||
}
|
||||
}
|
||||
_storeDetail.value = map
|
||||
}
|
||||
}
|
||||
|
||||
fun loadStoreDetail(storeId: Int) {
|
||||
|
||||
}
|
||||
|
||||
fun clearSearch() {
|
||||
_isSearchActive.value = false
|
||||
_searchResults.value = emptyList()
|
||||
@ -68,7 +96,7 @@ class SearchHomeViewModel (private val productRepository: ProductRepository) : V
|
||||
viewModelScope.launch {
|
||||
when (val result = productRepository.getSearchHistory()) {
|
||||
is Result.Success -> _searchHistory.value = result.data
|
||||
is Result.Error -> Log.e("HomeViewModel", "Failed to load search history", result.exception)
|
||||
is Result.Error -> Log.e("SearchHomeViewModel", "Failed to load search history", result.exception)
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
|
@ -6,13 +6,16 @@ import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.ListAdapter
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.alya.ecommerce_serang.BuildConfig.BASE_URL
|
||||
import com.alya.ecommerce_serang.R
|
||||
import com.alya.ecommerce_serang.data.api.dto.ProductsItem
|
||||
import com.alya.ecommerce_serang.data.api.response.customer.product.StoreItem
|
||||
import com.alya.ecommerce_serang.databinding.ItemProductGridBinding
|
||||
import com.bumptech.glide.Glide
|
||||
|
||||
class SearchResultsAdapter(
|
||||
private val onItemClick: (ProductsItem) -> Unit
|
||||
private val onItemClick: (ProductsItem) -> Unit,
|
||||
private val storeMap: Map<Int, StoreItem>
|
||||
) : ListAdapter<ProductsItem, SearchResultsAdapter.ViewHolder>(DIFF_CALLBACK) {
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||
@ -43,19 +46,23 @@ class SearchResultsAdapter(
|
||||
|
||||
fun bind(product: ProductsItem) {
|
||||
binding.tvProductName.text = product.name
|
||||
binding.tvProductPrice.text = (product.price)
|
||||
binding.tvProductPrice.text = product.price
|
||||
|
||||
val fullImageUrl = if (product.image.startsWith("/")) {
|
||||
BASE_URL + product.image.removePrefix("/") // Append base URL if the path starts with "/"
|
||||
} else {
|
||||
product.image // Use as is if it's already a full URL
|
||||
}
|
||||
Log.d("ProductAdapter", "Loading image: $fullImageUrl")
|
||||
// Load image with Glide
|
||||
Glide.with(binding.root.context)
|
||||
.load(product.image)
|
||||
.load(fullImageUrl)
|
||||
.placeholder(R.drawable.placeholder_image)
|
||||
// .error(R.drawable.error_image)
|
||||
.into(binding.ivProductImage)
|
||||
|
||||
// Set store name if available
|
||||
product.storeId?.toString().let {
|
||||
binding.tvStoreName.text = it
|
||||
}
|
||||
val storeName = product.storeId?.let { storeMap[it]?.storeName } ?: "Unknown Store"
|
||||
binding.tvStoreName.text = storeName
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -326,10 +326,10 @@ class CheckoutActivity : AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
// Voucher section (if implemented)
|
||||
binding.layoutVoucher?.setOnClickListener {
|
||||
Toast.makeText(this, "Voucher feature not implemented", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
// // Voucher section (if implemented)
|
||||
// binding.layoutVoucher?.setOnClickListener {
|
||||
// Toast.makeText(this, "Voucher feature not implemented", Toast.LENGTH_SHORT).show()
|
||||
// }
|
||||
}
|
||||
|
||||
private val addressSelectionLauncher = registerForActivityResult(
|
||||
|
@ -91,7 +91,7 @@ class HistoryViewModel(private val repository: OrderRepository) : ViewModel() {
|
||||
|
||||
private suspend fun getAllOrdersCombined() {
|
||||
try {
|
||||
val allStatuses = listOf("pending", "unpaid", "processed", "shipped", "completed", "canceled")
|
||||
val allStatuses = listOf("unpaid", "paid", "processed", "shipped", "completed", "canceled")
|
||||
val allOrders = mutableListOf<OrdersItem>()
|
||||
|
||||
// Use coroutineScope to allow launching async blocks
|
||||
@ -211,10 +211,7 @@ class HistoryViewModel(private val repository: OrderRepository) : ViewModel() {
|
||||
|
||||
fun refreshOrders(status: String = "all") {
|
||||
Log.d(TAG, "Refreshing orders with status: $status")
|
||||
// Clear current orders before fetching new ones
|
||||
_orders.value = ViewState.Loading
|
||||
|
||||
// Re-fetch the orders with the current status
|
||||
// Don't set Loading here if you want to show current data while refreshing
|
||||
getOrderList(status)
|
||||
}
|
||||
}
|
@ -4,7 +4,6 @@ import android.app.Activity
|
||||
import android.app.Dialog
|
||||
import android.content.ContextWrapper
|
||||
import android.content.Intent
|
||||
import android.graphics.Color
|
||||
import android.net.Uri
|
||||
import android.provider.MediaStore
|
||||
import android.util.Log
|
||||
@ -15,10 +14,10 @@ import android.view.Window
|
||||
import android.widget.ArrayAdapter
|
||||
import android.widget.AutoCompleteTextView
|
||||
import android.widget.ImageView
|
||||
import android.widget.ProgressBar
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.lifecycle.findViewTreeLifecycleOwner
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
@ -40,9 +39,16 @@ import java.util.TimeZone
|
||||
|
||||
class OrderHistoryAdapter(
|
||||
private val onOrderClickListener: (OrdersItem) -> Unit,
|
||||
private val viewModel: HistoryViewModel // Add this parameter
|
||||
private val viewModel: HistoryViewModel,
|
||||
private val callbacks: OrderActionCallbacks
|
||||
) : RecyclerView.Adapter<OrderHistoryAdapter.OrderViewHolder>() {
|
||||
|
||||
interface OrderActionCallbacks {
|
||||
fun onOrderCancelled(orderId: String, success: Boolean, message: String)
|
||||
fun onOrderCompleted(orderId: Int, success: Boolean, message: String)
|
||||
fun onShowLoading(show: Boolean)
|
||||
}
|
||||
|
||||
private val orders = mutableListOf<OrdersItem>()
|
||||
|
||||
private var fragmentStatus: String = "all"
|
||||
@ -140,28 +146,6 @@ class OrderHistoryAdapter(
|
||||
deadlineLabel.visibility = View.GONE
|
||||
|
||||
when (status) {
|
||||
"pending" -> {
|
||||
statusOrder.apply {
|
||||
visibility = View.VISIBLE
|
||||
text = itemView.context.getString(R.string.pending_orders)
|
||||
}
|
||||
deadlineLabel.apply {
|
||||
visibility = View.VISIBLE
|
||||
text = itemView.context.getString(R.string.dl_pending)
|
||||
}
|
||||
btnLeft.apply {
|
||||
visibility = View.VISIBLE
|
||||
text = itemView.context.getString(R.string.canceled_order_btn)
|
||||
setOnClickListener {
|
||||
showCancelOrderBottomSheet(order.orderId)
|
||||
viewModel.refreshOrders()
|
||||
}
|
||||
}
|
||||
deadlineDate.apply {
|
||||
visibility = View.VISIBLE
|
||||
text = formatDate(order.createdAt)
|
||||
}
|
||||
}
|
||||
"unpaid" -> {
|
||||
statusOrder.apply {
|
||||
visibility = View.VISIBLE
|
||||
@ -176,7 +160,6 @@ class OrderHistoryAdapter(
|
||||
text = itemView.context.getString(R.string.canceled_order_btn)
|
||||
setOnClickListener {
|
||||
showCancelOrderBottomSheet(order.orderId)
|
||||
viewModel.refreshOrders()
|
||||
}
|
||||
}
|
||||
|
||||
@ -198,6 +181,28 @@ class OrderHistoryAdapter(
|
||||
text = formatDatePay(order.updatedAt)
|
||||
}
|
||||
}
|
||||
"paid" -> {
|
||||
statusOrder.apply {
|
||||
visibility = View.VISIBLE
|
||||
text = itemView.context.getString(R.string.paid_orders)
|
||||
}
|
||||
deadlineLabel.apply {
|
||||
visibility = View.VISIBLE
|
||||
text = itemView.context.getString(R.string.dl_paid)
|
||||
}
|
||||
btnLeft.apply {
|
||||
visibility = View.VISIBLE
|
||||
text = itemView.context.getString(R.string.canceled_order_btn)
|
||||
setOnClickListener {
|
||||
showCancelOrderDialog(order.orderId.toString())
|
||||
viewModel.refreshOrders()
|
||||
}
|
||||
}
|
||||
// deadlineDate.apply {
|
||||
// visibility = View.VISIBLE
|
||||
// text = formatDatePay(order.updatedAt)
|
||||
// }
|
||||
}
|
||||
"processed" -> {
|
||||
// Untuk status processed, tampilkan "Hubungi Penjual"
|
||||
statusOrder.apply {
|
||||
@ -239,11 +244,14 @@ class OrderHistoryAdapter(
|
||||
visibility = View.VISIBLE
|
||||
text = itemView.context.getString(R.string.claim_order)
|
||||
setOnClickListener {
|
||||
// Handle click event
|
||||
callbacks.onShowLoading(true)
|
||||
|
||||
// Call ViewModel
|
||||
viewModel.confirmOrderCompleted(order.orderId, "completed")
|
||||
viewModel.refreshOrders()
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
deadlineDate.apply {
|
||||
visibility = View.VISIBLE
|
||||
@ -454,52 +462,32 @@ class OrderHistoryAdapter(
|
||||
}
|
||||
}
|
||||
|
||||
// Show loading indicator
|
||||
val loadingView = View(context).apply {
|
||||
layoutParams = ViewGroup.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.MATCH_PARENT
|
||||
)
|
||||
setBackgroundColor(Color.parseColor("#80000000"))
|
||||
callbacks.onShowLoading(true)
|
||||
|
||||
val progressBar = ProgressBar(context).apply {
|
||||
layoutParams = ViewGroup.LayoutParams(
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT,
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
)
|
||||
}
|
||||
|
||||
// addView(progressBar)
|
||||
// (progressBar.layoutParams as? ViewGroup.MarginLayoutParams)?.apply {
|
||||
// gravity = Gravity.CENTER
|
||||
// }
|
||||
}
|
||||
|
||||
dialog.addContentView(loadingView, loadingView.layoutParams)
|
||||
|
||||
// Call the ViewModel to cancel the order with image
|
||||
// Call ViewModel method but don't observe here
|
||||
viewModel.cancelOrderWithImage(orderId, reason, imageFile)
|
||||
|
||||
// Observe for success/failure
|
||||
viewModel.isSuccess.observe(itemView.findViewTreeLifecycleOwner()!!) { isSuccess ->
|
||||
// Remove loading indicator
|
||||
(loadingView.parent as? ViewGroup)?.removeView(loadingView)
|
||||
// Create a one-time observer that will be removed automatically
|
||||
val observer = object : Observer<Boolean> {
|
||||
override fun onChanged(isSuccess: Boolean) {
|
||||
callbacks.onShowLoading(false)
|
||||
|
||||
if (isSuccess) {
|
||||
Toast.makeText(context, context.getString(R.string.order_canceled_successfully), Toast.LENGTH_SHORT).show()
|
||||
dialog.dismiss()
|
||||
|
||||
// Find the order in the list and remove it or update its status
|
||||
val position = orders.indexOfFirst { it.orderId.toString() == orderId }
|
||||
if (position != -1) {
|
||||
orders.removeAt(position)
|
||||
notifyItemRemoved(position)
|
||||
notifyItemRangeChanged(position, orders.size)
|
||||
if (isSuccess) {
|
||||
val message = viewModel.message.value ?: context.getString(R.string.order_canceled_successfully)
|
||||
callbacks.onOrderCancelled(orderId, true, message)
|
||||
dialog.dismiss()
|
||||
} else {
|
||||
val message = viewModel.message.value ?: context.getString(R.string.failed_to_cancel_order)
|
||||
callbacks.onOrderCancelled(orderId, false, message)
|
||||
}
|
||||
} else {
|
||||
Toast.makeText(context, viewModel.message.value ?: context.getString(R.string.failed_to_cancel_order), Toast.LENGTH_SHORT).show()
|
||||
|
||||
// Remove this observer after first use
|
||||
viewModel.isSuccess.removeObserver(this)
|
||||
}
|
||||
}
|
||||
|
||||
// Add observer only once
|
||||
viewModel.isSuccess.observe(itemView.findViewTreeLifecycleOwner()!!, observer)
|
||||
}
|
||||
dialog.show()
|
||||
}
|
||||
@ -534,10 +522,7 @@ class OrderHistoryAdapter(
|
||||
val bottomSheet = CancelOrderBottomSheet(
|
||||
orderId = orderId,
|
||||
onOrderCancelled = {
|
||||
// Handle the successful cancellation
|
||||
// Refresh the data
|
||||
viewModel.refreshOrders() // Assuming there's a method to refresh orders
|
||||
|
||||
callbacks.onOrderCancelled(orderId.toString(), true, "Order cancelled successfully")
|
||||
// Show a success message
|
||||
Toast.makeText(context, "Order cancelled successfully", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
@ -44,8 +44,8 @@ class OrderHistoryFragment : Fragment() {
|
||||
TabLayoutMediator(binding.tabLayout, binding.viewPager) { tab, position ->
|
||||
tab.text = when (position) {
|
||||
0 -> getString(R.string.all_orders)
|
||||
1 -> getString(R.string.pending_orders)
|
||||
2 -> getString(R.string.unpaid_orders)
|
||||
1 -> getString(R.string.unpaid_orders)
|
||||
2 -> getString(R.string.paid_orders)
|
||||
3 -> getString(R.string.processed_orders)
|
||||
4 -> getString(R.string.shipped_orders)
|
||||
5 -> getString(R.string.completed_orders)
|
||||
|
@ -21,7 +21,7 @@ import com.alya.ecommerce_serang.ui.order.history.detailorder.DetailOrderStatusA
|
||||
import com.alya.ecommerce_serang.utils.BaseViewModelFactory
|
||||
import com.alya.ecommerce_serang.utils.SessionManager
|
||||
|
||||
class OrderListFragment : Fragment() {
|
||||
class OrderListFragment : Fragment(), OrderHistoryAdapter.OrderActionCallbacks {
|
||||
|
||||
private var _binding: FragmentOrderListBinding? = null
|
||||
private val binding get() = _binding!!
|
||||
@ -72,6 +72,7 @@ class OrderListFragment : Fragment() {
|
||||
|
||||
setupRecyclerView()
|
||||
observeOrderList()
|
||||
observeViewModel()
|
||||
observeOrderCompletionStatus()
|
||||
loadOrders()
|
||||
}
|
||||
@ -81,7 +82,8 @@ class OrderListFragment : Fragment() {
|
||||
onOrderClickListener = { order ->
|
||||
navigateToOrderDetail(order)
|
||||
},
|
||||
viewModel = viewModel
|
||||
viewModel = viewModel,
|
||||
callbacks = this // Pass this fragment as callback
|
||||
)
|
||||
|
||||
orderAdapter.setFragmentStatus(status)
|
||||
@ -120,6 +122,40 @@ class OrderListFragment : Fragment() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun observeViewModel() {
|
||||
// Observe order completion
|
||||
viewModel.orderCompletionStatus.observe(viewLifecycleOwner) { result ->
|
||||
when (result) {
|
||||
is Result.Success -> {
|
||||
Toast.makeText(requireContext(), "Order completed successfully!", Toast.LENGTH_SHORT).show()
|
||||
loadOrders() // Refresh here
|
||||
}
|
||||
is Result.Error -> {
|
||||
Toast.makeText(requireContext(), "Failed: ${result.exception.message}", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
is Result.Loading -> {
|
||||
// Show loading if needed
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Observe cancel order status
|
||||
viewModel.cancelOrderStatus.observe(viewLifecycleOwner) { result ->
|
||||
when (result) {
|
||||
is Result.Success -> {
|
||||
Toast.makeText(requireContext(), "Order cancelled successfully!", Toast.LENGTH_SHORT).show()
|
||||
loadOrders() // Refresh here
|
||||
}
|
||||
is Result.Error -> {
|
||||
Toast.makeText(requireContext(), "Failed to cancel: ${result.exception.message}", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
is Result.Loading -> {
|
||||
// Show loading if needed
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadOrders() {
|
||||
// Simple - just call getOrderList for any status including "all"
|
||||
viewModel.getOrderList(status)
|
||||
@ -142,6 +178,30 @@ class OrderListFragment : Fragment() {
|
||||
detailOrderLauncher.launch(intent)
|
||||
}
|
||||
|
||||
|
||||
|
||||
override fun onOrderCancelled(orderId: String, success: Boolean, message: String) {
|
||||
if (success) {
|
||||
Toast.makeText(requireContext(), message, Toast.LENGTH_SHORT).show()
|
||||
loadOrders() // Refresh the list
|
||||
} else {
|
||||
Toast.makeText(requireContext(), message, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onOrderCompleted(orderId: Int, success: Boolean, message: String) {
|
||||
if (success) {
|
||||
Toast.makeText(requireContext(), message, Toast.LENGTH_SHORT).show()
|
||||
loadOrders() // Refresh the list
|
||||
} else {
|
||||
Toast.makeText(requireContext(), message, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onShowLoading(show: Boolean) {
|
||||
binding.progressBar.visibility = if (show) View.VISIBLE else View.GONE
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
super.onDestroyView()
|
||||
_binding = null
|
||||
|
@ -11,8 +11,8 @@ class OrderViewPagerAdapter(
|
||||
// Define all possible order statuses
|
||||
private val orderStatuses = listOf(
|
||||
"all", // All orders
|
||||
"pending", // Menunggu Tagihan
|
||||
"unpaid", // Belum Dibayar
|
||||
"unpaid", // Menunggu Tagihan
|
||||
"paid", // Belum Dibayar
|
||||
"processed", // Diproses
|
||||
"shipped", // Dikirim
|
||||
"completed", // Selesai
|
||||
|
@ -234,8 +234,8 @@ class DetailOrderStatusActivity : AppCompatActivity() {
|
||||
|
||||
// Set status header
|
||||
val statusText = when(status) {
|
||||
"pending" -> "Belum Bayar"
|
||||
"unpaid" -> "Belum Bayar"
|
||||
"paid" -> "Sudah Dibayar"
|
||||
"processed" -> "Diproses"
|
||||
"shipped" -> "Dikirim"
|
||||
"delivered" -> "Diterima"
|
||||
@ -248,22 +248,6 @@ class DetailOrderStatusActivity : AppCompatActivity() {
|
||||
Log.d(TAG, "adjustButtonsBasedOnStatus: Status header set to '$statusText'")
|
||||
|
||||
when (status) {
|
||||
"pending"->{
|
||||
binding.tvStatusHeader.text = "Menunggu Tagihan"
|
||||
binding.tvStatusNote.visibility = View.VISIBLE
|
||||
binding.tvStatusNote.text = "Pesanan ini harus dibayar sebelum ${formatDatePay(orders.updatedAt)}"
|
||||
|
||||
// Set buttons
|
||||
binding.btnSecondary.apply {
|
||||
visibility = View.VISIBLE
|
||||
text = "Batalkan Pesanan"
|
||||
setOnClickListener {
|
||||
Log.d(TAG, "Cancel Order button clicked")
|
||||
showCancelOrderBottomSheet(orders.orderId)
|
||||
viewModel.getOrderDetails(orders.orderId)
|
||||
}
|
||||
}
|
||||
}
|
||||
"unpaid" -> {
|
||||
Log.d(TAG, "adjustButtonsBasedOnStatus: Setting up UI for pending/unpaid order")
|
||||
|
||||
@ -295,7 +279,25 @@ class DetailOrderStatusActivity : AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
}
|
||||
"paid" -> {
|
||||
Log.d(TAG, "adjustButtonsBasedOnStatus: Setting up UI for pending/unpaid order")
|
||||
|
||||
// Show status note
|
||||
binding.tvStatusHeader.text = "Sudah Dibayar"
|
||||
binding.tvStatusNote.visibility = View.VISIBLE
|
||||
binding.tvStatusNote.text = "Menunggu pesanan dikonfirmasi penjual ${formatDatePay(orders.updatedAt)}"
|
||||
|
||||
// Set buttons
|
||||
binding.btnSecondary.apply {
|
||||
visibility = View.VISIBLE
|
||||
text = "Batalkan Pesanan"
|
||||
setOnClickListener {
|
||||
Log.d(TAG, "Cancel Order button clicked")
|
||||
showCancelOrderDialog(orders.orderId.toString())
|
||||
viewModel.getOrderDetails(orders.orderId)
|
||||
}
|
||||
}
|
||||
}
|
||||
"processed" -> {
|
||||
Log.d(TAG, "adjustButtonsBasedOnStatus: Setting up UI for processed order")
|
||||
|
||||
|
@ -31,7 +31,6 @@ 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
|
||||
import com.alya.ecommerce_serang.ui.product.storeDetail.StoreDetailActivity
|
||||
import com.alya.ecommerce_serang.utils.BaseViewModelFactory
|
||||
@ -45,7 +44,7 @@ class DetailProductActivity : AppCompatActivity() {
|
||||
private lateinit var binding: ActivityDetailProductBinding
|
||||
private lateinit var apiService: ApiService
|
||||
private lateinit var sessionManager: SessionManager
|
||||
private var productAdapter: HorizontalProductAdapter? = null
|
||||
private var productAdapter: OtherProductAdapter? = null
|
||||
private var reviewsAdapter: ReviewsAdapter? = null
|
||||
private var currentQuantity = 1
|
||||
private var isWholesaleAvailable: Boolean = false
|
||||
@ -125,7 +124,8 @@ class DetailProductActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
viewModel.otherProducts.observe(this) { products ->
|
||||
updateOtherProducts(products)
|
||||
viewModel.loadStoresForProducts(products)
|
||||
// updateOtherProducts(products)
|
||||
}
|
||||
|
||||
viewModel.reviewProduct.observe(this) { reviews ->
|
||||
@ -155,6 +155,13 @@ class DetailProductActivity : AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
viewModel.storeMap.observe(this){ storeMap ->
|
||||
val products = viewModel.otherProducts.value.orEmpty()
|
||||
if (products.isNotEmpty()) {
|
||||
updateOtherProducts(products, storeMap)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateStoreInfo(store: StoreItem?) {
|
||||
@ -178,13 +185,21 @@ class DetailProductActivity : AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateOtherProducts(products: List<ProductsItem>) {
|
||||
|
||||
private fun updateOtherProducts(products: List<ProductsItem>, storeMap: Map<Int, StoreItem>) {
|
||||
if (products.isEmpty()) {
|
||||
binding.recyclerViewOtherProducts.visibility = View.GONE
|
||||
Log.d("DetailProductActivity", "Product list is empty, hiding RecyclerView")
|
||||
binding.recyclerViewOtherProducts.visibility = View.VISIBLE
|
||||
binding.tvViewAllProducts.visibility = View.GONE
|
||||
} else {
|
||||
Log.d("DetailProductActivity", "Displaying product list in RecyclerView")
|
||||
binding.recyclerViewOtherProducts.visibility = View.VISIBLE
|
||||
binding.tvViewAllProducts.visibility = View.VISIBLE
|
||||
|
||||
productAdapter = OtherProductAdapter(products, onClick = { product ->
|
||||
handleProductClick(product)
|
||||
}, storeMap = storeMap)
|
||||
binding.recyclerViewOtherProducts.adapter = productAdapter
|
||||
productAdapter?.updateProducts(products)
|
||||
}
|
||||
}
|
||||
@ -275,10 +290,6 @@ class DetailProductActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
private fun setupRecyclerViewOtherProducts(){
|
||||
productAdapter = HorizontalProductAdapter(
|
||||
products = emptyList(),
|
||||
onClick = { productsItem -> handleProductClick(productsItem) }
|
||||
)
|
||||
|
||||
binding.recyclerViewOtherProducts.apply {
|
||||
adapter = productAdapter
|
||||
@ -349,6 +360,8 @@ class DetailProductActivity : AppCompatActivity() {
|
||||
val btnClose = view.findViewById<ImageButton>(R.id.btnCloseDialog)
|
||||
|
||||
val switchWholesale = view.findViewById<SwitchCompat>(R.id.switch_price)
|
||||
val titleWholesale = view.findViewById<TextView>(R.id.tv_active_wholesale)
|
||||
// val descWholesale = view.findViewById<TextView>(R.id.tv_desc_wholesale)
|
||||
|
||||
if (!isBuyNow) {
|
||||
btnBuyNow.setText(R.string.add_to_cart)
|
||||
@ -357,10 +370,18 @@ class DetailProductActivity : AppCompatActivity() {
|
||||
switchWholesale.isEnabled = isWholesaleAvailable
|
||||
switchWholesale.isChecked = isWholesaleSelected
|
||||
|
||||
// Set initial quantity based on current selection
|
||||
currentQuantity = if (isWholesaleSelected) minOrder else 1
|
||||
tvQuantity.text = currentQuantity.toString()
|
||||
|
||||
if (isWholesaleAvailable){
|
||||
switchWholesale.visibility = View.VISIBLE
|
||||
Toast.makeText(this, "Minimal pembelian grosir $currentQuantity produk", Toast.LENGTH_SHORT).show()
|
||||
} else {
|
||||
switchWholesale.visibility = View.GONE
|
||||
}
|
||||
// Set initial quantity based on current selection
|
||||
|
||||
|
||||
switchWholesale.setOnCheckedChangeListener { _, isChecked ->
|
||||
isWholesaleSelected = isChecked
|
||||
|
||||
|
@ -0,0 +1,94 @@
|
||||
package com.alya.ecommerce_serang.ui.product
|
||||
|
||||
import android.util.Log
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.alya.ecommerce_serang.BuildConfig.BASE_URL
|
||||
import com.alya.ecommerce_serang.R
|
||||
import com.alya.ecommerce_serang.data.api.dto.ProductsItem
|
||||
import com.alya.ecommerce_serang.data.api.response.customer.product.StoreItem
|
||||
import com.alya.ecommerce_serang.databinding.ItemProductHorizontalBinding
|
||||
import com.bumptech.glide.Glide
|
||||
|
||||
class OtherProductAdapter (
|
||||
private var products: List<ProductsItem>,
|
||||
private val onClick: (ProductsItem) -> Unit,
|
||||
private val storeMap: Map<Int, StoreItem>
|
||||
) : RecyclerView.Adapter<OtherProductAdapter.ProductViewHolder>() {
|
||||
|
||||
inner class ProductViewHolder(private val binding: ItemProductHorizontalBinding) :
|
||||
RecyclerView.ViewHolder(binding.root) {
|
||||
|
||||
fun bind(product: ProductsItem) = with(binding) {
|
||||
|
||||
val fullImageUrl = if (product.image.startsWith("/")) {
|
||||
BASE_URL + product.image.removePrefix("/") // Append base URL if the path starts with "/"
|
||||
} else {
|
||||
product.image // Use as is if it's already a full URL
|
||||
}
|
||||
|
||||
Log.d("ProductAdapter", "Loading image: $fullImageUrl")
|
||||
|
||||
tvProductName.text = product.name
|
||||
tvProductPrice.text = product.price
|
||||
rating.text = product.rating
|
||||
|
||||
// Load image using Glide
|
||||
Glide.with(itemView)
|
||||
.load(fullImageUrl)
|
||||
.placeholder(R.drawable.placeholder_image)
|
||||
.into(ivProductImage)
|
||||
|
||||
val storeName = product.storeId?.let { storeMap[it]?.storeName } ?: "Unknown Store"
|
||||
binding.tvStoreName.text = storeName
|
||||
|
||||
root.setOnClickListener { onClick(product) }
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ProductViewHolder {
|
||||
val binding = ItemProductHorizontalBinding.inflate(
|
||||
LayoutInflater.from(parent.context), parent, false
|
||||
)
|
||||
return ProductViewHolder(binding)
|
||||
}
|
||||
|
||||
override fun getItemCount() = products.size
|
||||
|
||||
override fun onBindViewHolder(holder: ProductViewHolder, position: Int) {
|
||||
holder.bind(products[position])
|
||||
}
|
||||
|
||||
fun updateProducts(newProducts: List<ProductsItem>) {
|
||||
val diffCallback = ProductDiffCallback(products, newProducts)
|
||||
val diffResult = DiffUtil.calculateDiff(diffCallback)
|
||||
products = newProducts
|
||||
diffResult.dispatchUpdatesTo(this)
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
fun updateLimitedProducts(newProducts: List<ProductsItem>) {
|
||||
val limitedProducts = newProducts.take(10)
|
||||
val diffCallback = ProductDiffCallback(products, limitedProducts)
|
||||
val diffResult = DiffUtil.calculateDiff(diffCallback)
|
||||
products = limitedProducts
|
||||
diffResult.dispatchUpdatesTo(this)
|
||||
}
|
||||
|
||||
class ProductDiffCallback(
|
||||
private val oldList: List<ProductsItem>,
|
||||
private val newList: List<ProductsItem>
|
||||
) : DiffUtil.Callback() {
|
||||
|
||||
override fun getOldListSize() = oldList.size
|
||||
override fun getNewListSize() = newList.size
|
||||
|
||||
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int) =
|
||||
oldList[oldItemPosition].id == newList[newItemPosition].id // Compare unique IDs
|
||||
|
||||
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int) =
|
||||
oldList[oldItemPosition] == newList[newItemPosition] // Compare entire object
|
||||
}
|
||||
}
|
@ -20,9 +20,15 @@ class ProductUserViewModel(private val repository: ProductRepository) : ViewMode
|
||||
private val _productDetail = MutableLiveData<Product?>()
|
||||
val productDetail: LiveData<Product?> get() = _productDetail
|
||||
|
||||
private val _productList = MutableLiveData<Result<List<ProductsItem>>>()
|
||||
val productList: LiveData<Result<List<ProductsItem>>> get() = _productList
|
||||
|
||||
private val _storeDetail = MutableLiveData<Result<StoreItem>>()
|
||||
val storeDetail : LiveData<Result<StoreItem>> get() = _storeDetail
|
||||
|
||||
private val _storeMap = MutableLiveData<Map<Int, StoreItem>>()
|
||||
val storeMap: LiveData<Map<Int, StoreItem>> get() = _storeMap
|
||||
|
||||
private val _reviewProduct = MutableLiveData<List<ReviewsItem>>()
|
||||
val reviewProduct: LiveData<List<ReviewsItem>> get() = _reviewProduct
|
||||
|
||||
@ -59,6 +65,23 @@ class ProductUserViewModel(private val repository: ProductRepository) : ViewMode
|
||||
}
|
||||
}
|
||||
|
||||
fun loadProductsList() {
|
||||
_isLoading.value = true
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
val result = repository.getAllProducts()
|
||||
_productList.value = result
|
||||
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error loading product list: ${e.message}")
|
||||
_error.value = "Failed to load product list ${e.message}"
|
||||
} finally {
|
||||
_isLoading.value = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun loadStoreDetail(storeId: Int) {
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
@ -73,6 +96,28 @@ class ProductUserViewModel(private val repository: ProductRepository) : ViewMode
|
||||
}
|
||||
}
|
||||
|
||||
fun loadStoresForProducts(products: List<ProductsItem>) {
|
||||
viewModelScope.launch {
|
||||
val map = mutableMapOf<Int, StoreItem>()
|
||||
val storeIds = products.mapNotNull { it.storeId }.toSet()
|
||||
|
||||
for (storeId in storeIds) {
|
||||
try {
|
||||
val result = repository.fetchStoreDetail(storeId)
|
||||
if (result is Result.Success) {
|
||||
map[storeId] = result.data
|
||||
} else if (result is Result.Error) {
|
||||
Log.e(TAG, "Failed to load storeId $storeId", result.exception)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Exception fetching storeId $storeId", e)
|
||||
}
|
||||
}
|
||||
|
||||
_storeMap.postValue(map)
|
||||
}
|
||||
}
|
||||
|
||||
fun loadReviews(productId: Int) {
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
|
@ -2,7 +2,6 @@ package com.alya.ecommerce_serang.ui.product.category
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.alya.ecommerce_serang.BuildConfig
|
||||
import com.alya.ecommerce_serang.R
|
||||
@ -59,14 +58,14 @@ class ProductsCategoryAdapter(
|
||||
onClick(product)
|
||||
}
|
||||
|
||||
// Optional: Show stock status
|
||||
if (product.stock > 0) {
|
||||
tvStockStatus.text = "Stock: ${product.stock}"
|
||||
tvStockStatus.setTextColor(ContextCompat.getColor(itemView.context, R.color.green))
|
||||
} else {
|
||||
tvStockStatus.text = "Out of Stock"
|
||||
tvStockStatus.setTextColor(ContextCompat.getColor(itemView.context, R.color.red))
|
||||
}
|
||||
// // Optional: Show stock status
|
||||
// if (product.stock > 0) {
|
||||
// tvStockStatus.text = "Stock: ${product.stock}"
|
||||
// tvStockStatus.setTextColor(ContextCompat.getColor(itemView.context, R.color.green))
|
||||
// } else {
|
||||
// tvStockStatus.text = "Out of Stock"
|
||||
// tvStockStatus.setTextColor(ContextCompat.getColor(itemView.context, R.color.red))
|
||||
// }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,112 @@
|
||||
package com.alya.ecommerce_serang.ui.product.listproduct
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.view.View
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.activity.viewModels
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.WindowCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.alya.ecommerce_serang.data.api.dto.CategoryItem
|
||||
import com.alya.ecommerce_serang.data.api.retrofit.ApiConfig
|
||||
import com.alya.ecommerce_serang.data.repository.ProductRepository
|
||||
import com.alya.ecommerce_serang.databinding.ActivityListCategoryBinding
|
||||
import com.alya.ecommerce_serang.ui.product.category.CategoryProductsActivity
|
||||
import com.alya.ecommerce_serang.utils.BaseViewModelFactory
|
||||
import com.alya.ecommerce_serang.utils.SessionManager
|
||||
import com.alya.ecommerce_serang.utils.viewmodel.HomeViewModel
|
||||
|
||||
class ListCategoryActivity : AppCompatActivity() {
|
||||
companion object {
|
||||
private val TAG = "ListCategoryActivity"
|
||||
}
|
||||
private lateinit var binding: ActivityListCategoryBinding
|
||||
private lateinit var sessionManager: SessionManager
|
||||
private var categoryAdapter: ListCategoryAdapter? = null
|
||||
|
||||
private val viewModel: HomeViewModel by viewModels {
|
||||
BaseViewModelFactory {
|
||||
val apiService = ApiConfig.getApiService(sessionManager)
|
||||
val repository = ProductRepository(apiService)
|
||||
HomeViewModel(repository)
|
||||
}
|
||||
}
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
binding = ActivityListCategoryBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
|
||||
sessionManager = SessionManager(this)
|
||||
|
||||
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||
enableEdgeToEdge()
|
||||
|
||||
ViewCompat.setOnApplyWindowInsetsListener(binding.root) { view, windowInsets ->
|
||||
val systemBars = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
|
||||
view.setPadding(
|
||||
systemBars.left,
|
||||
systemBars.top,
|
||||
systemBars.right,
|
||||
systemBars.bottom
|
||||
)
|
||||
windowInsets
|
||||
}
|
||||
|
||||
setupToolbar()
|
||||
setupObserver()
|
||||
viewModel.loadCategory()
|
||||
setupRecyclerView()
|
||||
|
||||
}
|
||||
|
||||
private fun setupToolbar(){
|
||||
binding.header.headerLeftIcon.setOnClickListener{
|
||||
finish()
|
||||
}
|
||||
binding.header.headerTitle.text = "Kategori Produk"
|
||||
}
|
||||
|
||||
private fun setupRecyclerView(){
|
||||
categoryAdapter = ListCategoryAdapter(
|
||||
categories = emptyList(),
|
||||
onClick = { category -> handleClickOnCategory(category) }
|
||||
)
|
||||
|
||||
binding.rvListCategories.apply {
|
||||
adapter = categoryAdapter
|
||||
layoutManager = GridLayoutManager(
|
||||
context,
|
||||
3, // 3 columns
|
||||
RecyclerView.VERTICAL,
|
||||
false
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupObserver(){
|
||||
viewModel.category.observe(this) { category ->
|
||||
Log.d("ListCategoryActivity", "Received categories: ${category.size}")
|
||||
updateCategories(category)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateCategories(category: List<CategoryItem>){
|
||||
if (category.isEmpty()) {
|
||||
binding.rvListCategories.visibility = View.GONE
|
||||
} else {
|
||||
binding.rvListCategories.visibility = View.VISIBLE
|
||||
categoryAdapter?.updateData(category)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleClickOnCategory(category: CategoryItem){
|
||||
val intent = Intent(this, CategoryProductsActivity::class.java)
|
||||
intent.putExtra(CategoryProductsActivity.EXTRA_CATEGORY, category)
|
||||
startActivity(intent)
|
||||
}
|
||||
}
|
@ -0,0 +1,55 @@
|
||||
package com.alya.ecommerce_serang.ui.product.listproduct
|
||||
|
||||
import android.util.Log
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.alya.ecommerce_serang.BuildConfig.BASE_URL
|
||||
import com.alya.ecommerce_serang.R
|
||||
import com.alya.ecommerce_serang.data.api.dto.CategoryItem
|
||||
import com.alya.ecommerce_serang.databinding.ItemCategoryHomeBinding
|
||||
import com.bumptech.glide.Glide
|
||||
|
||||
class ListCategoryAdapter(
|
||||
private var categories:List<CategoryItem>,
|
||||
private val onClick:(category: CategoryItem) -> Unit
|
||||
): RecyclerView.Adapter<ListCategoryAdapter.ViewHolder>() {
|
||||
|
||||
inner class ViewHolder(private val binding: ItemCategoryHomeBinding): RecyclerView.ViewHolder(binding.root){
|
||||
fun bind(category: CategoryItem) = with(binding) {
|
||||
Log.d("CategoriesAdapter", "Binding category: ${category.name}, Image: ${category.image}")
|
||||
|
||||
val fullImageUrl = if (category.image.startsWith("/")) {
|
||||
BASE_URL + category.image.removePrefix("/") // Append base URL if the path starts with "/"
|
||||
} else {
|
||||
category.image // Use as is if it's already a full URL
|
||||
}
|
||||
|
||||
Log.d("CategoriesAdapter", "Loading image: $fullImageUrl")
|
||||
|
||||
Glide.with(itemView.context)
|
||||
.load(fullImageUrl) // Ensure full URL
|
||||
.placeholder(R.drawable.placeholder_image)
|
||||
.into(imageCategory)
|
||||
|
||||
name.text = category.name
|
||||
|
||||
root.setOnClickListener { onClick(category) }
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ViewHolder (
|
||||
ItemCategoryHomeBinding.inflate(LayoutInflater.from(parent.context),parent,false)
|
||||
)
|
||||
|
||||
override fun getItemCount() = categories.size
|
||||
|
||||
override fun onBindViewHolder(holder: ListCategoryAdapter.ViewHolder, position: Int) {
|
||||
holder.bind(categories[position])
|
||||
}
|
||||
|
||||
fun updateData(newCategories: List<CategoryItem>) {
|
||||
categories = newCategories.toList()
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
}
|
@ -0,0 +1,133 @@
|
||||
package com.alya.ecommerce_serang.ui.product.listproduct
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.view.View
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.activity.viewModels
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.WindowCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import com.alya.ecommerce_serang.data.api.dto.ProductsItem
|
||||
import com.alya.ecommerce_serang.data.api.response.customer.product.StoreItem
|
||||
import com.alya.ecommerce_serang.data.api.retrofit.ApiConfig
|
||||
import com.alya.ecommerce_serang.data.repository.ProductRepository
|
||||
import com.alya.ecommerce_serang.data.repository.Result
|
||||
import com.alya.ecommerce_serang.databinding.ActivityListProductBinding
|
||||
import com.alya.ecommerce_serang.ui.product.DetailProductActivity
|
||||
import com.alya.ecommerce_serang.ui.product.ProductUserViewModel
|
||||
import com.alya.ecommerce_serang.utils.BaseViewModelFactory
|
||||
import com.alya.ecommerce_serang.utils.SessionManager
|
||||
|
||||
class ListProductActivity : AppCompatActivity() {
|
||||
companion object {
|
||||
private val TAG = "ListProductActivity"
|
||||
}
|
||||
private lateinit var binding: ActivityListProductBinding
|
||||
private lateinit var sessionManager: SessionManager
|
||||
private var productAdapter: ListProductAdapter? = null
|
||||
|
||||
private val viewModel: ProductUserViewModel by viewModels {
|
||||
BaseViewModelFactory {
|
||||
val apiService = ApiConfig.getApiService(sessionManager)
|
||||
val repository = ProductRepository(apiService)
|
||||
ProductUserViewModel(repository)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
binding = ActivityListProductBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
|
||||
sessionManager = SessionManager(this)
|
||||
|
||||
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||
enableEdgeToEdge()
|
||||
|
||||
ViewCompat.setOnApplyWindowInsetsListener(binding.root) { view, windowInsets ->
|
||||
val systemBars = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
|
||||
view.setPadding(
|
||||
systemBars.left,
|
||||
systemBars.top,
|
||||
systemBars.right,
|
||||
systemBars.bottom
|
||||
)
|
||||
windowInsets
|
||||
}
|
||||
|
||||
|
||||
setupObserver()
|
||||
setupRecyclerView()
|
||||
viewModel.loadProductsList()
|
||||
|
||||
}
|
||||
|
||||
private fun setupRecyclerView() {
|
||||
|
||||
binding.rvProductsList.apply {
|
||||
adapter = productAdapter
|
||||
layoutManager = GridLayoutManager(
|
||||
context,
|
||||
2,
|
||||
LinearLayoutManager.VERTICAL,
|
||||
false
|
||||
)
|
||||
}
|
||||
|
||||
Log.d(TAG, "RecyclerView setup complete with GridLayoutManager")
|
||||
}
|
||||
|
||||
private fun setupObserver(){
|
||||
|
||||
viewModel.productList.observe(this) { result ->
|
||||
when (result) {
|
||||
is Result.Success -> {
|
||||
val products = result.data
|
||||
viewModel.loadStoresForProducts(products)
|
||||
Log.d(TAG, "Product list loaded successfully: ${products.size} items")
|
||||
}
|
||||
is Result.Error -> {
|
||||
Log.e(TAG, "Failed to load products: ${result.exception.message}")
|
||||
}
|
||||
is Result.Loading -> {
|
||||
// Show loading indicator if needed
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
viewModel.storeMap.observe(this){ storeMap ->
|
||||
val products = (viewModel.productList.value as? Result.Success)?.data.orEmpty()
|
||||
if (products.isNotEmpty()) {
|
||||
updateProducts(products, storeMap)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private fun updateProducts(products: List<ProductsItem>, storeMap: Map<Int, StoreItem>) {
|
||||
if (products.isEmpty()) {
|
||||
Log.d(TAG, "Product list is empty, hiding RecyclerView")
|
||||
binding.rvProductsList.visibility = View.VISIBLE
|
||||
} else {
|
||||
Log.d(TAG, "Displaying product list in RecyclerView")
|
||||
binding.rvProductsList.visibility = View.VISIBLE // <-- Fix here
|
||||
productAdapter = ListProductAdapter(products, onClick = { product ->
|
||||
handleProductClick(product)
|
||||
}, storeMap = storeMap)
|
||||
binding.rvProductsList.adapter = productAdapter
|
||||
productAdapter?.updateProducts(products)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleProductClick(product: ProductsItem) {
|
||||
val intent = Intent(this, DetailProductActivity::class.java)
|
||||
intent.putExtra("PRODUCT_ID", product.id) // Pass product ID
|
||||
startActivity(intent)
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,85 @@
|
||||
package com.alya.ecommerce_serang.ui.product.listproduct
|
||||
|
||||
import android.util.Log
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.alya.ecommerce_serang.BuildConfig.BASE_URL
|
||||
import com.alya.ecommerce_serang.R
|
||||
import com.alya.ecommerce_serang.data.api.dto.ProductsItem
|
||||
import com.alya.ecommerce_serang.data.api.response.customer.product.StoreItem
|
||||
import com.alya.ecommerce_serang.databinding.ItemProductGridBinding
|
||||
import com.bumptech.glide.Glide
|
||||
|
||||
class ListProductAdapter(
|
||||
private var products: List<ProductsItem>,
|
||||
private val onClick: (ProductsItem) -> Unit,
|
||||
private val storeMap: Map<Int, StoreItem>
|
||||
): RecyclerView.Adapter<ListProductAdapter.ProductViewHolder>() {
|
||||
|
||||
inner class ProductViewHolder(private val binding: ItemProductGridBinding) :
|
||||
RecyclerView.ViewHolder(binding.root) {
|
||||
|
||||
fun bind(product: ProductsItem) = with(binding) {
|
||||
|
||||
tvProductName.text = product.name
|
||||
tvProductPrice.text = product.price
|
||||
rating.text = product.rating
|
||||
|
||||
val fullImageUrl = if (product.image.startsWith("/")) {
|
||||
BASE_URL + product.image.removePrefix("/") // Append base URL if the path starts with "/"
|
||||
} else {
|
||||
product.image // Use as is if it's already a full URL
|
||||
}
|
||||
Log.d("ProductAdapter", "Loading image: $fullImageUrl")
|
||||
|
||||
|
||||
// Load image using Glide
|
||||
Glide.with(itemView)
|
||||
.load(fullImageUrl)
|
||||
.placeholder(R.drawable.placeholder_image)
|
||||
.into(ivProductImage)
|
||||
|
||||
val storeName = product.storeId?.let { storeMap[it]?.storeName } ?: "Unknown Store"
|
||||
binding.tvStoreName.text = storeName
|
||||
root.setOnClickListener { onClick(product) }
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ProductViewHolder {
|
||||
val binding = ItemProductGridBinding.inflate(
|
||||
LayoutInflater.from(parent.context), parent, false
|
||||
)
|
||||
return ProductViewHolder(binding)
|
||||
}
|
||||
|
||||
override fun getItemCount() = products.size
|
||||
|
||||
override fun onBindViewHolder(holder: ProductViewHolder, position: Int) {
|
||||
holder.bind(products[position])
|
||||
}
|
||||
|
||||
fun updateProducts(newProducts: List<ProductsItem>) {
|
||||
val diffCallback = ProductDiffCallback(products, newProducts)
|
||||
val diffResult = DiffUtil.calculateDiff(diffCallback)
|
||||
products = newProducts
|
||||
diffResult.dispatchUpdatesTo(this)
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
class ProductDiffCallback(
|
||||
private val oldList: List<ProductsItem>,
|
||||
private val newList: List<ProductsItem>
|
||||
) : DiffUtil.Callback() {
|
||||
|
||||
override fun getOldListSize() = oldList.size
|
||||
override fun getNewListSize() = newList.size
|
||||
|
||||
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int) =
|
||||
oldList[oldItemPosition].id == newList[newItemPosition].id // Compare unique IDs
|
||||
|
||||
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int) =
|
||||
oldList[oldItemPosition] == newList[newItemPosition] // Compare entire object
|
||||
}
|
||||
}
|
@ -11,7 +11,7 @@ import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.WindowCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import com.alya.ecommerce_serang.BuildConfig.BASE_URL
|
||||
import com.alya.ecommerce_serang.R
|
||||
import com.alya.ecommerce_serang.data.api.dto.ProductsItem
|
||||
@ -101,7 +101,15 @@ class StoreDetailActivity : AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
viewModel.otherProducts.observe(this) { products ->
|
||||
updateOtherProducts(products)
|
||||
viewModel.loadStoresForProducts(products)
|
||||
// updateOtherProducts(products)
|
||||
}
|
||||
|
||||
viewModel.storeMap.observe(this){ storeMap ->
|
||||
val products = viewModel.otherProducts.value.orEmpty()
|
||||
if (products.isNotEmpty()) {
|
||||
updateProducts(products, storeMap)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -140,28 +148,28 @@ class StoreDetailActivity : AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateOtherProducts(products: List<ProductsItem>) {
|
||||
private fun updateProducts(products: List<ProductsItem>, storeMap: Map<Int, StoreItem>) {
|
||||
if (products.isEmpty()) {
|
||||
binding.rvProducts.visibility = View.GONE
|
||||
Log.d("StoreDetailActivity", "Product list is empty, hiding RecyclerView")
|
||||
} else {
|
||||
Log.d("StoreDetailActivity", "Displaying product list in RecyclerView")
|
||||
|
||||
binding.rvProducts.visibility = View.VISIBLE
|
||||
productAdapter = HorizontalProductAdapter(products, onClick = { product ->
|
||||
handleProductClick(product)
|
||||
}, storeMap = storeMap)
|
||||
binding.rvProducts.adapter = productAdapter
|
||||
productAdapter?.updateProducts(products)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun setupRecyclerViewOtherProducts(){
|
||||
productAdapter = HorizontalProductAdapter(
|
||||
products = emptyList(),
|
||||
onClick = { productsItem -> handleProductClick(productsItem) }
|
||||
)
|
||||
|
||||
binding.rvProducts.apply {
|
||||
adapter = productAdapter
|
||||
layoutManager = LinearLayoutManager(
|
||||
context,
|
||||
LinearLayoutManager.HORIZONTAL,
|
||||
false
|
||||
)
|
||||
layoutManager = GridLayoutManager(context, 2)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -21,6 +21,9 @@ class StoreDetailViewModel (private val repository: ProductRepository
|
||||
private val _otherProducts = MutableLiveData<List<ProductsItem>>()
|
||||
val otherProducts: LiveData<List<ProductsItem>> get() = _otherProducts
|
||||
|
||||
private val _storeMap = MutableLiveData<Map<Int, StoreItem>>()
|
||||
val storeMap: LiveData<Map<Int, StoreItem>> get() = _storeMap
|
||||
|
||||
private val _isLoading = MutableLiveData<Boolean>()
|
||||
val isLoading: LiveData<Boolean> get() = _isLoading
|
||||
|
||||
@ -84,4 +87,26 @@ class StoreDetailViewModel (private val repository: ProductRepository
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun loadStoresForProducts(products: List<ProductsItem>) {
|
||||
viewModelScope.launch {
|
||||
val map = mutableMapOf<Int, StoreItem>()
|
||||
val storeIds = products.mapNotNull { it.storeId }.toSet()
|
||||
|
||||
for (storeId in storeIds) {
|
||||
try {
|
||||
val result = repository.fetchStoreDetail(storeId)
|
||||
if (result is Result.Success) {
|
||||
map[storeId] = result.data
|
||||
} else if (result is Result.Error) {
|
||||
Log.e("ProductViewModel", "Failed to load storeId $storeId", result.exception)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e("ProductViewModel", "Exception fetching storeId $storeId", e)
|
||||
}
|
||||
}
|
||||
|
||||
_storeMap.postValue(map)
|
||||
}
|
||||
}
|
||||
}
|
@ -1,10 +1,13 @@
|
||||
package com.alya.ecommerce_serang.utils.viewmodel
|
||||
|
||||
import android.util.Log
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.alya.ecommerce_serang.data.api.dto.CategoryItem
|
||||
import com.alya.ecommerce_serang.data.api.dto.ProductsItem
|
||||
import com.alya.ecommerce_serang.data.api.response.customer.product.StoreItem
|
||||
import com.alya.ecommerce_serang.data.repository.ProductRepository
|
||||
import com.alya.ecommerce_serang.data.repository.Result
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
@ -21,6 +24,12 @@ class HomeViewModel (
|
||||
private val _categories = MutableStateFlow<List<CategoryItem>>(emptyList())
|
||||
val categories: StateFlow<List<CategoryItem>> = _categories.asStateFlow()
|
||||
|
||||
private val _storeMap = MutableStateFlow<Map<Int, StoreItem>>(emptyMap())
|
||||
val storeMap: StateFlow<Map<Int, StoreItem>> = _storeMap.asStateFlow()
|
||||
|
||||
private val _category = MutableLiveData<List<CategoryItem>>()
|
||||
val category: LiveData<List<CategoryItem>> get() = _category
|
||||
|
||||
init {
|
||||
loadProducts()
|
||||
loadCategories()
|
||||
@ -47,6 +56,38 @@ class HomeViewModel (
|
||||
}
|
||||
}
|
||||
|
||||
fun loadStoresForProducts(products: List<ProductsItem>) {
|
||||
viewModelScope.launch {
|
||||
val map = mutableMapOf<Int, StoreItem>()
|
||||
val storeIds = products.mapNotNull { it.storeId }.toSet()
|
||||
|
||||
for (storeId in storeIds) {
|
||||
try {
|
||||
val result = productRepository.fetchStoreDetail(storeId)
|
||||
if (result is Result.Success) {
|
||||
map[storeId] = result.data
|
||||
} else if (result is Result.Error) {
|
||||
Log.e("HomeViewModel", "Failed to load storeId $storeId", result.exception)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e("HomeViewModel", "Exception fetching storeId $storeId", e)
|
||||
}
|
||||
}
|
||||
|
||||
_storeMap.value = map
|
||||
}
|
||||
}
|
||||
|
||||
fun loadCategory() {
|
||||
viewModelScope.launch {
|
||||
when (val result = productRepository.getAllCategories()) {
|
||||
is Result.Success -> _category.value = result.data
|
||||
is Result.Error -> Log.e("HomeViewModel", "Failed to fetch categories", result.exception)
|
||||
is Result.Loading -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun retry() {
|
||||
loadProducts()
|
||||
|
Reference in New Issue
Block a user