From 6659ba428860dadf942dba60d8deda936f620c42 Mon Sep 17 00:00:00 2001 From: shaulascr Date: Tue, 17 Jun 2025 00:18:15 +0700 Subject: [PATCH 1/6] update --- app/src/main/AndroidManifest.xml | 3 + .../ui/home/HomeCategoryAdapter.kt | 6 +- .../ecommerce_serang/ui/home/HomeFragment.kt | 92 ++++++++---- .../ui/home/HorizontalProductAdapter.kt | 25 ++-- .../ui/home/SearchHomeFragment.kt | 20 ++- .../ui/home/SearchHomeViewModel.kt | 42 +++++- .../ui/home/SearchResultAdapter.kt | 21 ++- .../ui/product/DetailProductActivity.kt | 29 ++-- .../ui/product/OtherProductAdapter.kt | 94 ++++++++++++ .../ui/product/ProductUserViewModel.kt | 45 ++++++ .../category/ProductsCategoryAdapter.kt | 17 +-- .../listproduct/ListProductActivity.kt | 133 +++++++++++++++++ .../product/listproduct/ListProductAdapter.kt | 85 +++++++++++ .../storeDetail/StoreDetailActivity.kt | 24 ++- .../storeDetail/StoreDetailViewModel.kt | 25 ++++ .../utils/viewmodel/HomeViewModel.kt | 26 ++++ .../res/layout/activity_detail_product.xml | 2 +- .../main/res/layout/activity_list_product.xml | 39 +++++ app/src/main/res/layout/fragment_home.xml | 29 ++-- .../main/res/layout/fragment_search_home.xml | 1 + .../main/res/layout/item_category_home.xml | 63 +++++--- app/src/main/res/layout/item_product_grid.xml | 24 ++- .../res/layout/item_product_horizontal.xml | 141 +++++++++++------- .../res/layout/item_section_horizontal.xml | 5 +- app/src/main/res/values/styles.xml | 13 ++ 25 files changed, 816 insertions(+), 188 deletions(-) create mode 100644 app/src/main/java/com/alya/ecommerce_serang/ui/product/OtherProductAdapter.kt create mode 100644 app/src/main/java/com/alya/ecommerce_serang/ui/product/listproduct/ListProductActivity.kt create mode 100644 app/src/main/java/com/alya/ecommerce_serang/ui/product/listproduct/ListProductAdapter.kt create mode 100644 app/src/main/res/layout/activity_list_product.xml diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 1704677..5e89a06 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -29,6 +29,9 @@ android:theme="@style/Theme.Ecommerce_serang" android:usesCleartextTraffic="true" tools:targetApi="31"> + diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/home/HomeCategoryAdapter.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/home/HomeCategoryAdapter.kt index 0e9a0bd..5bdc7dc 100644 --- a/app/src/main/java/com/alya/ecommerce_serang/ui/home/HomeCategoryAdapter.kt +++ b/app/src/main/java/com/alya/ecommerce_serang/ui/home/HomeCategoryAdapter.kt @@ -16,10 +16,6 @@ class HomeCategoryAdapter( private val onClick:(category:CategoryItem) -> Unit ): RecyclerView.Adapter() { - /* - 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 +55,7 @@ class HomeCategoryAdapter( } fun updateLimitedCategory(newCategories: List){ - val limitedCategories = newCategories.take(10) + val limitedCategories = newCategories.take(9) updateData(limitedCategories) } } \ No newline at end of file diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/home/HomeFragment.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/home/HomeFragment.kt index db00d1b..2c6cc9d 100644 --- a/app/src/main/java/com/alya/ecommerce_serang/ui/home/HomeFragment.kt +++ b/app/src/main/java/com/alya/ecommerce_serang/ui/home/HomeFragment.kt @@ -14,10 +14,11 @@ import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle import androidx.navigation.findNavController +import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.LinearLayoutManager -import com.alya.ecommerce_serang.R 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,8 @@ 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.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 +76,6 @@ class HomeFragment : Fragment() { } private fun setupRecyclerView() { - productAdapter = HorizontalProductAdapter( - products = emptyList(), - onClick = { product -> handleProductClick(product) } - ) - categoryAdapter = HomeCategoryAdapter( categories = emptyList(), onClick = { category -> handleCategoryProduct(category)} @@ -87,8 +83,9 @@ class HomeFragment : Fragment() { binding.newProducts.apply { adapter = productAdapter - layoutManager = LinearLayoutManager( + layoutManager = GridLayoutManager( context, + 2, LinearLayoutManager.HORIZONTAL, false ) @@ -96,12 +93,18 @@ class HomeFragment : Fragment() { binding.categories.apply { adapter = categoryAdapter - layoutManager = LinearLayoutManager( + layoutManager = GridLayoutManager( context, + 3, LinearLayoutManager.HORIZONTAL, false ) } + + binding.productshowAll.setOnClickListener { + val intent = Intent(requireContext(), ListProductActivity::class.java) + startActivity(intent) + } } private fun setupSearchView() { @@ -155,7 +158,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 +174,9 @@ class HomeFragment : Fragment() { } } } + } + } viewLifecycleOwner.lifecycleScope.launch { @@ -179,30 +187,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, storeMap: Map) { + 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) { diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/home/HorizontalProductAdapter.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/home/HorizontalProductAdapter.kt index 7c82da7..55792f9 100644 --- a/app/src/main/java/com/alya/ecommerce_serang/ui/home/HorizontalProductAdapter.kt +++ b/app/src/main/java/com/alya/ecommerce_serang/ui/home/HorizontalProductAdapter.kt @@ -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, - private val onClick: (ProductsItem) -> Unit + private val onClick: (ProductsItem) -> Unit, + private val storeMap: Map ) : RecyclerView.Adapter() { - 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) { - 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( diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/home/SearchHomeFragment.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/home/SearchHomeFragment.kt index 94003c6..b715d2b 100644 --- a/app/src/main/java/com/alya/ecommerce_serang/ui/home/SearchHomeFragment.kt +++ b/app/src/main/java/com/alya/ecommerce_serang/ui/home/SearchHomeFragment.kt @@ -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 -> diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/home/SearchHomeViewModel.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/home/SearchHomeViewModel.kt index fec5c4e..2a62af6 100644 --- a/app/src/main/java/com/alya/ecommerce_serang/ui/home/SearchHomeViewModel.kt +++ b/app/src/main/java/com/alya/ecommerce_serang/ui/home/SearchHomeViewModel.kt @@ -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>(emptyList()) val searchResults: LiveData> = _searchResults + private val _storeDetail = MutableLiveData>() + val storeDetail : LiveData> get() = _storeDetail + private val _searchHistory = MutableLiveData>(emptyList()) val searchHistory: LiveData> = _searchHistory @@ -25,10 +29,10 @@ class SearchHomeViewModel (private val productRepository: ProductRepository) : V val isSearchActive: LiveData = _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){ + viewModelScope.launch { + val map = mutableMapOf() + + 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 -> {} } } diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/home/SearchResultAdapter.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/home/SearchResultAdapter.kt index 555976f..b46a143 100644 --- a/app/src/main/java/com/alya/ecommerce_serang/ui/home/SearchResultAdapter.kt +++ b/app/src/main/java/com/alya/ecommerce_serang/ui/home/SearchResultAdapter.kt @@ -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 ) : ListAdapter(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 } } diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/product/DetailProductActivity.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/product/DetailProductActivity.kt index 3cbe27d..81fee0e 100644 --- a/app/src/main/java/com/alya/ecommerce_serang/ui/product/DetailProductActivity.kt +++ b/app/src/main/java/com/alya/ecommerce_serang/ui/product/DetailProductActivity.kt @@ -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) { + + private fun updateOtherProducts(products: List, storeMap: Map) { 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 diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/product/OtherProductAdapter.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/product/OtherProductAdapter.kt new file mode 100644 index 0000000..76a9de9 --- /dev/null +++ b/app/src/main/java/com/alya/ecommerce_serang/ui/product/OtherProductAdapter.kt @@ -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, + private val onClick: (ProductsItem) -> Unit, + private val storeMap: Map + ) : RecyclerView.Adapter() { + + 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) { + val diffCallback = ProductDiffCallback(products, newProducts) + val diffResult = DiffUtil.calculateDiff(diffCallback) + products = newProducts + diffResult.dispatchUpdatesTo(this) + notifyDataSetChanged() + } + + fun updateLimitedProducts(newProducts: List) { + 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, + private val newList: List + ) : 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 + } + } \ No newline at end of file diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/product/ProductUserViewModel.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/product/ProductUserViewModel.kt index 2338cdb..38b3bc6 100644 --- a/app/src/main/java/com/alya/ecommerce_serang/ui/product/ProductUserViewModel.kt +++ b/app/src/main/java/com/alya/ecommerce_serang/ui/product/ProductUserViewModel.kt @@ -20,9 +20,15 @@ class ProductUserViewModel(private val repository: ProductRepository) : ViewMode private val _productDetail = MutableLiveData() val productDetail: LiveData get() = _productDetail + private val _productList = MutableLiveData>>() + val productList: LiveData>> get() = _productList + private val _storeDetail = MutableLiveData>() val storeDetail : LiveData> get() = _storeDetail + private val _storeMap = MutableLiveData>() + val storeMap: LiveData> get() = _storeMap + private val _reviewProduct = MutableLiveData>() val reviewProduct: LiveData> 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) { + viewModelScope.launch { + val map = mutableMapOf() + 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 { diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/product/category/ProductsCategoryAdapter.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/product/category/ProductsCategoryAdapter.kt index 7e82b2b..05a2ffb 100644 --- a/app/src/main/java/com/alya/ecommerce_serang/ui/product/category/ProductsCategoryAdapter.kt +++ b/app/src/main/java/com/alya/ecommerce_serang/ui/product/category/ProductsCategoryAdapter.kt @@ -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)) +// } } } } diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/product/listproduct/ListProductActivity.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/product/listproduct/ListProductActivity.kt new file mode 100644 index 0000000..6a00f2e --- /dev/null +++ b/app/src/main/java/com/alya/ecommerce_serang/ui/product/listproduct/ListProductActivity.kt @@ -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.rvProducts.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, storeMap: Map) { + if (products.isEmpty()) { + Log.d(TAG, "Product list is empty, hiding RecyclerView") + binding.rvProducts.visibility = View.VISIBLE + } else { + Log.d(TAG, "Displaying product list in RecyclerView") + binding.rvProducts.visibility = View.VISIBLE // <-- Fix here + productAdapter = ListProductAdapter(products, onClick = { product -> + handleProductClick(product) + }, storeMap = storeMap) + binding.rvProducts.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) + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/product/listproduct/ListProductAdapter.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/product/listproduct/ListProductAdapter.kt new file mode 100644 index 0000000..f0cb5f4 --- /dev/null +++ b/app/src/main/java/com/alya/ecommerce_serang/ui/product/listproduct/ListProductAdapter.kt @@ -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, + private val onClick: (ProductsItem) -> Unit, + private val storeMap: Map +): RecyclerView.Adapter() { + + 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) { + val diffCallback = ProductDiffCallback(products, newProducts) + val diffResult = DiffUtil.calculateDiff(diffCallback) + products = newProducts + diffResult.dispatchUpdatesTo(this) + notifyDataSetChanged() + } + + class ProductDiffCallback( + private val oldList: List, + private val newList: List + ) : 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 + } +} \ No newline at end of file diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/product/storeDetail/StoreDetailActivity.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/product/storeDetail/StoreDetailActivity.kt index fd00b60..50ebbe2 100644 --- a/app/src/main/java/com/alya/ecommerce_serang/ui/product/storeDetail/StoreDetailActivity.kt +++ b/app/src/main/java/com/alya/ecommerce_serang/ui/product/storeDetail/StoreDetailActivity.kt @@ -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,20 +148,24 @@ class StoreDetailActivity : AppCompatActivity() { } } - private fun updateOtherProducts(products: List) { + private fun updateProducts(products: List, storeMap: Map) { 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 diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/product/storeDetail/StoreDetailViewModel.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/product/storeDetail/StoreDetailViewModel.kt index 329a7df..201df5f 100644 --- a/app/src/main/java/com/alya/ecommerce_serang/ui/product/storeDetail/StoreDetailViewModel.kt +++ b/app/src/main/java/com/alya/ecommerce_serang/ui/product/storeDetail/StoreDetailViewModel.kt @@ -21,6 +21,9 @@ class StoreDetailViewModel (private val repository: ProductRepository private val _otherProducts = MutableLiveData>() val otherProducts: LiveData> get() = _otherProducts + private val _storeMap = MutableLiveData>() + val storeMap: LiveData> get() = _storeMap + private val _isLoading = MutableLiveData() val isLoading: LiveData get() = _isLoading @@ -84,4 +87,26 @@ class StoreDetailViewModel (private val repository: ProductRepository } } } + + fun loadStoresForProducts(products: List) { + viewModelScope.launch { + val map = mutableMapOf() + 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) + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/alya/ecommerce_serang/utils/viewmodel/HomeViewModel.kt b/app/src/main/java/com/alya/ecommerce_serang/utils/viewmodel/HomeViewModel.kt index 0f0437b..a7667cf 100644 --- a/app/src/main/java/com/alya/ecommerce_serang/utils/viewmodel/HomeViewModel.kt +++ b/app/src/main/java/com/alya/ecommerce_serang/utils/viewmodel/HomeViewModel.kt @@ -5,6 +5,7 @@ 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 +22,9 @@ class HomeViewModel ( private val _categories = MutableStateFlow>(emptyList()) val categories: StateFlow> = _categories.asStateFlow() + private val _storeMap = MutableStateFlow>(emptyMap()) + val storeMap: StateFlow> = _storeMap.asStateFlow() + init { loadProducts() loadCategories() @@ -47,6 +51,28 @@ class HomeViewModel ( } } + fun loadStoresForProducts(products: List) { + viewModelScope.launch { + val map = mutableMapOf() + 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 retry() { loadProducts() diff --git a/app/src/main/res/layout/activity_detail_product.xml b/app/src/main/res/layout/activity_detail_product.xml index 1bb20ff..187e853 100644 --- a/app/src/main/res/layout/activity_detail_product.xml +++ b/app/src/main/res/layout/activity_detail_product.xml @@ -487,7 +487,7 @@ android:orientation="horizontal" app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" tools:itemCount="3" - tools:listitem="@layout/item_related_product" /> + tools:listitem="@layout/item_product_horizontal" /> diff --git a/app/src/main/res/layout/activity_list_product.xml b/app/src/main/res/layout/activity_list_product.xml new file mode 100644 index 0000000..ebf3300 --- /dev/null +++ b/app/src/main/res/layout/activity_list_product.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_home.xml b/app/src/main/res/layout/fragment_home.xml index 858d751..3372ca4 100644 --- a/app/src/main/res/layout/fragment_home.xml +++ b/app/src/main/res/layout/fragment_home.xml @@ -28,15 +28,15 @@ android:layout_width="match_parent" android:layout_height="wrap_content"> - + + + + + + + + + + app:layout_constraintTop_toTopOf="parent" /> @@ -110,7 +113,7 @@ android:layout_marginTop="16dp" android:layout_marginBottom="16dp" android:orientation="vertical" - app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" + app:layoutManager="androidx.recyclerview.widget.GridLayoutManager" app:layout_constraintTop_toBottomOf="@id/new_products_text" tools:itemCount="5" tools:listitem="@layout/item_section_horizontal" /> diff --git a/app/src/main/res/layout/fragment_search_home.xml b/app/src/main/res/layout/fragment_search_home.xml index 69eb47b..c88d048 100644 --- a/app/src/main/res/layout/fragment_search_home.xml +++ b/app/src/main/res/layout/fragment_search_home.xml @@ -4,6 +4,7 @@ android:layout_width="match_parent" android:layout_height="match_parent" xmlns:app="http://schemas.android.com/apk/res-auto" + style="@style/Theme.Ecommerce_serang" tools:context=".ui.home.SearchHomeFragment"> - - - + android:layout_height="match_parent"> + + + + + + + - \ No newline at end of file diff --git a/app/src/main/res/layout/item_product_grid.xml b/app/src/main/res/layout/item_product_grid.xml index 3d9c567..60d0863 100644 --- a/app/src/main/res/layout/item_product_grid.xml +++ b/app/src/main/res/layout/item_product_grid.xml @@ -57,14 +57,24 @@ + android:textAlignment="center" + android:gravity="center" + app:drawableStartCompat="@drawable/baseline_star_24" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/tv_product_price" /> + tools:text="Toko Jaya" /> diff --git a/app/src/main/res/layout/item_product_horizontal.xml b/app/src/main/res/layout/item_product_horizontal.xml index 6f38ce8..2d09da3 100644 --- a/app/src/main/res/layout/item_product_horizontal.xml +++ b/app/src/main/res/layout/item_product_horizontal.xml @@ -1,69 +1,94 @@ - + android:layout_margin="8dp" + app:cardCornerRadius="12dp" + app:cardElevation="4dp" + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools"> - + + - + android:background="@color/light_gray" + android:contentDescription="Product Image" + tools:src="@drawable/placeholder_image" /> - + + - + + - + + - \ No newline at end of file + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_section_horizontal.xml b/app/src/main/res/layout/item_section_horizontal.xml index 5b8d4ed..9e1b55a 100644 --- a/app/src/main/res/layout/item_section_horizontal.xml +++ b/app/src/main/res/layout/item_section_horizontal.xml @@ -38,9 +38,10 @@ android:clipToPadding="false" android:clipChildren="false" app:layout_constraintTop_toBottomOf="@id/title" + app:layout_constraintBottom_toBottomOf="parent" android:layout_marginTop="16dp" android:orientation="horizontal" - tools:listitem="@layout/item_product_horizontal" - app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"/> + tools:listitem="@layout/item_product_grid" + app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"/> \ No newline at end of file diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index b72680e..0fc9a63 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -60,4 +60,17 @@ 8dp @color/white + + + + + \ No newline at end of file From b37848e513bb84c29334d3e17972914360cab72b Mon Sep 17 00:00:00 2001 From: shaulascr Date: Tue, 17 Jun 2025 16:20:30 +0700 Subject: [PATCH 2/6] update display product list and paid status --- .../ui/home/HomeCategoryAdapter.kt | 1 - .../ecommerce_serang/ui/home/HomeFragment.kt | 13 +- .../ui/order/history/HistoryViewModel.kt | 7 +- .../ui/order/history/OrderHistoryAdapter.kt | 125 ++++++++---------- .../ui/order/history/OrderHistoryFragment.kt | 4 +- .../ui/order/history/OrderListFragment.kt | 64 ++++++++- .../ui/order/history/OrderViewPageAdapter.kt | 4 +- .../detailorder/DetailOrderStatusActivity.kt | 36 ++--- .../storeDetail/StoreDetailActivity.kt | 8 +- .../main/res/layout/activity_store_detail.xml | 9 +- app/src/main/res/layout/fragment_home.xml | 53 ++++---- .../main/res/layout/item_category_home.xml | 5 +- .../res/layout/item_product_horizontal.xml | 2 +- app/src/main/res/values/strings.xml | 2 +- 14 files changed, 181 insertions(+), 152 deletions(-) diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/home/HomeCategoryAdapter.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/home/HomeCategoryAdapter.kt index 5bdc7dc..9a8a788 100644 --- a/app/src/main/java/com/alya/ecommerce_serang/ui/home/HomeCategoryAdapter.kt +++ b/app/src/main/java/com/alya/ecommerce_serang/ui/home/HomeCategoryAdapter.kt @@ -12,7 +12,6 @@ import com.bumptech.glide.Glide class HomeCategoryAdapter( private var categories:List, - //A lambda function that will be invoked when a category item is clicked. private val onClick:(category:CategoryItem) -> Unit ): RecyclerView.Adapter() { diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/home/HomeFragment.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/home/HomeFragment.kt index 2c6cc9d..0965a96 100644 --- a/app/src/main/java/com/alya/ecommerce_serang/ui/home/HomeFragment.kt +++ b/app/src/main/java/com/alya/ecommerce_serang/ui/home/HomeFragment.kt @@ -15,7 +15,7 @@ import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle import androidx.navigation.findNavController import androidx.recyclerview.widget.GridLayoutManager -import androidx.recyclerview.widget.LinearLayoutManager +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 @@ -83,20 +83,15 @@ class HomeFragment : Fragment() { binding.newProducts.apply { adapter = productAdapter - layoutManager = GridLayoutManager( - context, - 2, - LinearLayoutManager.HORIZONTAL, - false - ) + layoutManager = GridLayoutManager(requireContext(), 2) } binding.categories.apply { adapter = categoryAdapter layoutManager = GridLayoutManager( context, - 3, - LinearLayoutManager.HORIZONTAL, + 3, // 3 columns + RecyclerView.VERTICAL, // vertical layout false ) } diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/order/history/HistoryViewModel.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/order/history/HistoryViewModel.kt index c2df487..1a6f8e5 100644 --- a/app/src/main/java/com/alya/ecommerce_serang/ui/order/history/HistoryViewModel.kt +++ b/app/src/main/java/com/alya/ecommerce_serang/ui/order/history/HistoryViewModel.kt @@ -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() // 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) } } \ No newline at end of file diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/order/history/OrderHistoryAdapter.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/order/history/OrderHistoryAdapter.kt index a67a2d7..f395ec2 100644 --- a/app/src/main/java/com/alya/ecommerce_serang/ui/order/history/OrderHistoryAdapter.kt +++ b/app/src/main/java/com/alya/ecommerce_serang/ui/order/history/OrderHistoryAdapter.kt @@ -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() { + 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() 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 { + 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() } diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/order/history/OrderHistoryFragment.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/order/history/OrderHistoryFragment.kt index 267eb28..353a952 100644 --- a/app/src/main/java/com/alya/ecommerce_serang/ui/order/history/OrderHistoryFragment.kt +++ b/app/src/main/java/com/alya/ecommerce_serang/ui/order/history/OrderHistoryFragment.kt @@ -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) diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/order/history/OrderListFragment.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/order/history/OrderListFragment.kt index 5e43898..28931bd 100644 --- a/app/src/main/java/com/alya/ecommerce_serang/ui/order/history/OrderListFragment.kt +++ b/app/src/main/java/com/alya/ecommerce_serang/ui/order/history/OrderListFragment.kt @@ -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 diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/order/history/OrderViewPageAdapter.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/order/history/OrderViewPageAdapter.kt index 615f7a1..146100d 100644 --- a/app/src/main/java/com/alya/ecommerce_serang/ui/order/history/OrderViewPageAdapter.kt +++ b/app/src/main/java/com/alya/ecommerce_serang/ui/order/history/OrderViewPageAdapter.kt @@ -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 diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/order/history/detailorder/DetailOrderStatusActivity.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/order/history/detailorder/DetailOrderStatusActivity.kt index 4e5d6b4..d68dd35 100644 --- a/app/src/main/java/com/alya/ecommerce_serang/ui/order/history/detailorder/DetailOrderStatusActivity.kt +++ b/app/src/main/java/com/alya/ecommerce_serang/ui/order/history/detailorder/DetailOrderStatusActivity.kt @@ -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") diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/product/storeDetail/StoreDetailActivity.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/product/storeDetail/StoreDetailActivity.kt index 50ebbe2..73a64b0 100644 --- a/app/src/main/java/com/alya/ecommerce_serang/ui/product/storeDetail/StoreDetailActivity.kt +++ b/app/src/main/java/com/alya/ecommerce_serang/ui/product/storeDetail/StoreDetailActivity.kt @@ -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 @@ -169,11 +169,7 @@ class StoreDetailActivity : AppCompatActivity() { binding.rvProducts.apply { adapter = productAdapter - layoutManager = LinearLayoutManager( - context, - LinearLayoutManager.HORIZONTAL, - false - ) + layoutManager = GridLayoutManager(context, 2) } } diff --git a/app/src/main/res/layout/activity_store_detail.xml b/app/src/main/res/layout/activity_store_detail.xml index 1b2fdce..0e71ad2 100644 --- a/app/src/main/res/layout/activity_store_detail.xml +++ b/app/src/main/res/layout/activity_store_detail.xml @@ -158,14 +158,13 @@ + app:spanCount="2" + tools:listitem="@layout/item_product_grid" /> \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_home.xml b/app/src/main/res/layout/fragment_home.xml index 3372ca4..af75ae6 100644 --- a/app/src/main/res/layout/fragment_home.xml +++ b/app/src/main/res/layout/fragment_home.xml @@ -15,34 +15,25 @@ android:layout_marginTop="16dp" app:layout_constraintTop_toTopOf="parent" /> - - - - - - - - - - - - - + android:layout_height="wrap_content" + android:paddingBottom="16dp"> + - + + + tools:listitem="@layout/item_product_grid" /> + - + Batas Tagihan Batas Pembayaran Batas Pengiriman - Semua Pesanan + Batas Konfirmasi Penjual Semua Pesanan Semua Pesanan Tanggal Pesanan Sampai From 28b0d5b0826f8cc8dfdadaccc4804c9a59081036 Mon Sep 17 00:00:00 2001 From: shaulascr Date: Wed, 18 Jun 2025 00:31:52 +0700 Subject: [PATCH 3/6] update display category list --- app/src/main/AndroidManifest.xml | 3 + .../ecommerce_serang/ui/home/HomeFragment.kt | 6 + .../ui/order/CheckoutActivity.kt | 8 +- .../ui/product/DetailProductActivity.kt | 12 +- .../listproduct/ListCategoryActivity.kt | 112 ++++++++++++++++++ .../listproduct/ListCategoryAdapter.kt | 55 +++++++++ .../listproduct/ListProductActivity.kt | 8 +- .../utils/viewmodel/HomeViewModel.kt | 15 +++ app/src/main/res/layout/activity_checkout.xml | 29 ----- .../res/layout/activity_list_category.xml | 28 +++++ .../main/res/layout/activity_list_product.xml | 10 +- app/src/main/res/layout/dialog_count_buy.xml | 17 +-- app/src/main/res/layout/fragment_home.xml | 2 +- 13 files changed, 254 insertions(+), 51 deletions(-) create mode 100644 app/src/main/java/com/alya/ecommerce_serang/ui/product/listproduct/ListCategoryActivity.kt create mode 100644 app/src/main/java/com/alya/ecommerce_serang/ui/product/listproduct/ListCategoryAdapter.kt create mode 100644 app/src/main/res/layout/activity_list_category.xml diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 5e89a06..f7e9e77 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -29,6 +29,9 @@ android:theme="@style/Theme.Ecommerce_serang" android:usesCleartextTraffic="true" tools:targetApi="31"> + diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/home/HomeFragment.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/home/HomeFragment.kt index 0965a96..8f270a5 100644 --- a/app/src/main/java/com/alya/ecommerce_serang/ui/home/HomeFragment.kt +++ b/app/src/main/java/com/alya/ecommerce_serang/ui/home/HomeFragment.kt @@ -26,6 +26,7 @@ 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.SessionManager @@ -100,6 +101,11 @@ class HomeFragment : Fragment() { val intent = Intent(requireContext(), ListProductActivity::class.java) startActivity(intent) } + + binding.categoryShowAll.setOnClickListener { + val intent = Intent(requireContext(), ListCategoryActivity::class.java) + startActivity(intent) + } } private fun setupSearchView() { diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/order/CheckoutActivity.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/order/CheckoutActivity.kt index f7859e4..9f4e7e4 100644 --- a/app/src/main/java/com/alya/ecommerce_serang/ui/order/CheckoutActivity.kt +++ b/app/src/main/java/com/alya/ecommerce_serang/ui/order/CheckoutActivity.kt @@ -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( diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/product/DetailProductActivity.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/product/DetailProductActivity.kt index 81fee0e..9630784 100644 --- a/app/src/main/java/com/alya/ecommerce_serang/ui/product/DetailProductActivity.kt +++ b/app/src/main/java/com/alya/ecommerce_serang/ui/product/DetailProductActivity.kt @@ -360,6 +360,8 @@ class DetailProductActivity : AppCompatActivity() { val btnClose = view.findViewById(R.id.btnCloseDialog) val switchWholesale = view.findViewById(R.id.switch_price) + val titleWholesale = view.findViewById(R.id.tv_active_wholesale) +// val descWholesale = view.findViewById(R.id.tv_desc_wholesale) if (!isBuyNow) { btnBuyNow.setText(R.string.add_to_cart) @@ -368,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 diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/product/listproduct/ListCategoryActivity.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/product/listproduct/ListCategoryActivity.kt new file mode 100644 index 0000000..73f20fd --- /dev/null +++ b/app/src/main/java/com/alya/ecommerce_serang/ui/product/listproduct/ListCategoryActivity.kt @@ -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){ + 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) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/product/listproduct/ListCategoryAdapter.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/product/listproduct/ListCategoryAdapter.kt new file mode 100644 index 0000000..d5fa5af --- /dev/null +++ b/app/src/main/java/com/alya/ecommerce_serang/ui/product/listproduct/ListCategoryAdapter.kt @@ -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, + private val onClick:(category: CategoryItem) -> Unit +): RecyclerView.Adapter() { + + 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) { + categories = newCategories.toList() + notifyDataSetChanged() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/product/listproduct/ListProductActivity.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/product/listproduct/ListProductActivity.kt index 6a00f2e..2b74409 100644 --- a/app/src/main/java/com/alya/ecommerce_serang/ui/product/listproduct/ListProductActivity.kt +++ b/app/src/main/java/com/alya/ecommerce_serang/ui/product/listproduct/ListProductActivity.kt @@ -69,7 +69,7 @@ class ListProductActivity : AppCompatActivity() { private fun setupRecyclerView() { - binding.rvProducts.apply { + binding.rvProductsList.apply { adapter = productAdapter layoutManager = GridLayoutManager( context, @@ -112,14 +112,14 @@ class ListProductActivity : AppCompatActivity() { private fun updateProducts(products: List, storeMap: Map) { if (products.isEmpty()) { Log.d(TAG, "Product list is empty, hiding RecyclerView") - binding.rvProducts.visibility = View.VISIBLE + binding.rvProductsList.visibility = View.VISIBLE } else { Log.d(TAG, "Displaying product list in RecyclerView") - binding.rvProducts.visibility = View.VISIBLE // <-- Fix here + binding.rvProductsList.visibility = View.VISIBLE // <-- Fix here productAdapter = ListProductAdapter(products, onClick = { product -> handleProductClick(product) }, storeMap = storeMap) - binding.rvProducts.adapter = productAdapter + binding.rvProductsList.adapter = productAdapter productAdapter?.updateProducts(products) } } diff --git a/app/src/main/java/com/alya/ecommerce_serang/utils/viewmodel/HomeViewModel.kt b/app/src/main/java/com/alya/ecommerce_serang/utils/viewmodel/HomeViewModel.kt index a7667cf..b37ad1b 100644 --- a/app/src/main/java/com/alya/ecommerce_serang/utils/viewmodel/HomeViewModel.kt +++ b/app/src/main/java/com/alya/ecommerce_serang/utils/viewmodel/HomeViewModel.kt @@ -1,6 +1,8 @@ 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 @@ -25,6 +27,9 @@ class HomeViewModel ( private val _storeMap = MutableStateFlow>(emptyMap()) val storeMap: StateFlow> = _storeMap.asStateFlow() + private val _category = MutableLiveData>() + val category: LiveData> get() = _category + init { loadProducts() loadCategories() @@ -73,6 +78,16 @@ class HomeViewModel ( } } + 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() diff --git a/app/src/main/res/layout/activity_checkout.xml b/app/src/main/res/layout/activity_checkout.xml index 8c793d4..2c5a7ba 100644 --- a/app/src/main/res/layout/activity_checkout.xml +++ b/app/src/main/res/layout/activity_checkout.xml @@ -142,35 +142,6 @@ android:layout_height="8dp" android:background="#F5F5F5" /> - - - - - - - - - - - - - + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_list_product.xml b/app/src/main/res/layout/activity_list_product.xml index ebf3300..aab4e4c 100644 --- a/app/src/main/res/layout/activity_list_product.xml +++ b/app/src/main/res/layout/activity_list_product.xml @@ -8,12 +8,14 @@ tools:context=".ui.product.listproduct.ListProductActivity"> + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintBottom_toTopOf="@id/rvProductsList"/> @@ -23,11 +25,11 @@ - + + + + + + + + + Date: Wed, 18 Jun 2025 14:00:20 +0700 Subject: [PATCH 4/6] fix search --- .../ecommerce_serang/ui/home/HomeFragment.kt | 21 +++++-------------- .../ui/home/SearchHomeFragment.kt | 8 +++---- .../listproduct/ListProductActivity.kt | 13 +++++++++++- .../main/res/layout/activity_list_product.xml | 1 - .../main/res/layout/fragment_search_home.xml | 4 +++- 5 files changed, 24 insertions(+), 23 deletions(-) diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/home/HomeFragment.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/home/HomeFragment.kt index 8f270a5..dba398a 100644 --- a/app/src/main/java/com/alya/ecommerce_serang/ui/home/HomeFragment.kt +++ b/app/src/main/java/com/alya/ecommerce_serang/ui/home/HomeFragment.kt @@ -6,7 +6,6 @@ import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import android.view.inputmethod.EditorInfo import androidx.core.view.isVisible import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels @@ -110,26 +109,16 @@ class HomeFragment : Fragment() { private fun setupSearchView() { binding.searchContainer.search.apply { - // When user clicks the search box, navigate to search fragment + // Make it non-editable so it acts like a button + isFocusable = false + isFocusableInTouchMode = false + isClickable = true + setOnClickListener { findNavController().navigate( HomeFragmentDirections.actionHomeFragmentToSearchHomeFragment(null) ) } - -// Handle search action if user presses search on keyboard - setOnEditorActionListener { _, actionId, _ -> - if (actionId == EditorInfo.IME_ACTION_SEARCH) { - val query = text.toString().trim() - if (query.isNotEmpty()) { - findNavController().navigate( - HomeFragmentDirections.actionHomeFragmentToSearchHomeFragment(query) - ) - } - return@setOnEditorActionListener true - } - false - } } // Setup cart and notification buttons diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/home/SearchHomeFragment.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/home/SearchHomeFragment.kt index b715d2b..aca01ec 100644 --- a/app/src/main/java/com/alya/ecommerce_serang/ui/home/SearchHomeFragment.kt +++ b/app/src/main/java/com/alya/ecommerce_serang/ui/home/SearchHomeFragment.kt @@ -76,6 +76,7 @@ class SearchHomeFragment : Fragment() { // Setup search view binding.searchView.apply { + clearFocus() setOnQueryTextListener(object : androidx.appcompat.widget.SearchView.OnQueryTextListener { override fun onQueryTextSubmit(query: String?): Boolean { query?.let { @@ -105,13 +106,12 @@ class SearchHomeFragment : Fragment() { } }) - // Request focus and show keyboard if (args.query.isNullOrEmpty()) { requestFocus() - postDelayed({ + post { val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager - imm.showSoftInput(findFocus(), InputMethodManager.SHOW_IMPLICIT) - }, 200) + imm.showSoftInput(this, InputMethodManager.SHOW_IMPLICIT) + } } } } diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/product/listproduct/ListProductActivity.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/product/listproduct/ListProductActivity.kt index 2b74409..f6a380b 100644 --- a/app/src/main/java/com/alya/ecommerce_serang/ui/product/listproduct/ListProductActivity.kt +++ b/app/src/main/java/com/alya/ecommerce_serang/ui/product/listproduct/ListProductActivity.kt @@ -18,6 +18,7 @@ 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.cart.CartActivity import com.alya.ecommerce_serang.ui.product.DetailProductActivity import com.alya.ecommerce_serang.ui.product.ProductUserViewModel import com.alya.ecommerce_serang.utils.BaseViewModelFactory @@ -60,13 +61,23 @@ class ListProductActivity : AppCompatActivity() { windowInsets } - + setupToolbar() setupObserver() setupRecyclerView() viewModel.loadProductsList() } + private fun setupToolbar(){ + binding.searchContainerList.btnBack.setOnClickListener{ + finish() + } + binding.searchContainerList.btnCart.setOnClickListener{ + val intent = Intent(this, CartActivity::class.java) + startActivity(intent) + } + } + private fun setupRecyclerView() { binding.rvProductsList.apply { diff --git a/app/src/main/res/layout/activity_list_product.xml b/app/src/main/res/layout/activity_list_product.xml index aab4e4c..cb0fbff 100644 --- a/app/src/main/res/layout/activity_list_product.xml +++ b/app/src/main/res/layout/activity_list_product.xml @@ -4,7 +4,6 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="wrap_content" - style="@style/Theme.Ecommerce_serang" tools:context=".ui.product.listproduct.ListProductActivity"> + android:queryHint="Search products..." + app:iconifiedByDefault="false" + app:queryHint="Search products..." /> From 019b469556055e1fc8a58a016745a45b3e00b8bd Mon Sep 17 00:00:00 2001 From: shaulascr Date: Wed, 18 Jun 2025 16:43:03 +0700 Subject: [PATCH 5/6] fix price and chat date --- .../customer/order/OrderDetailResponse.kt | 2 +- .../ecommerce_serang/ui/chat/ChatActivity.kt | 11 ++- .../ecommerce_serang/ui/chat/ChatAdapter.kt | 84 +++++++++++++---- .../ecommerce_serang/ui/chat/ChatViewModel.kt | 93 ++++++++++++++++++- .../ui/home/HorizontalProductAdapter.kt | 21 ++++- .../ui/home/SearchResultAdapter.kt | 9 +- .../ui/order/PaymentMethodAdapter.kt | 6 ++ .../ui/order/ShippingAdapter.kt | 9 +- .../detail/AddEvidencePaymentActivity.kt | 38 +++++--- .../ui/order/detail/PaymentActivity.kt | 9 +- .../ui/order/detail/SpinnerCardAdapter.kt | 33 +++++++ .../detailorder/DetailOrderItemsAdapter.kt | 13 ++- .../detailorder/DetailOrderStatusActivity.kt | 45 +++++++-- .../ui/product/OtherProductAdapter.kt | 9 +- .../product/listproduct/ListProductAdapter.kt | 9 +- .../profile/mystore/chat/ChatStoreActivity.kt | 4 +- app/src/main/res/drawable/bg_date_header.xml | 6 ++ app/src/main/res/layout/item_date_header.xml | 21 +++++ .../res/layout/item_dialog_add_evidence.xml | 34 +++++++ .../res/layout/item_dialog_spinner_card.xml | 21 +++++ .../res/layout/item_order_detail_product.xml | 3 +- 21 files changed, 419 insertions(+), 61 deletions(-) create mode 100644 app/src/main/java/com/alya/ecommerce_serang/ui/order/detail/SpinnerCardAdapter.kt create mode 100644 app/src/main/res/drawable/bg_date_header.xml create mode 100644 app/src/main/res/layout/item_date_header.xml create mode 100644 app/src/main/res/layout/item_dialog_add_evidence.xml create mode 100644 app/src/main/res/layout/item_dialog_spinner_card.xml diff --git a/app/src/main/java/com/alya/ecommerce_serang/data/api/response/customer/order/OrderDetailResponse.kt b/app/src/main/java/com/alya/ecommerce_serang/data/api/response/customer/order/OrderDetailResponse.kt index 6530886..3adb6d4 100644 --- a/app/src/main/java/com/alya/ecommerce_serang/data/api/response/customer/order/OrderDetailResponse.kt +++ b/app/src/main/java/com/alya/ecommerce_serang/data/api/response/customer/order/OrderDetailResponse.kt @@ -80,7 +80,7 @@ data class Orders( val cancelReason: String? = null, @field:SerializedName("total_amount") - val totalAmount: String? = null, + val totalAmount: Int? = null, @field:SerializedName("user_id") val userId: Int, diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/chat/ChatActivity.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/chat/ChatActivity.kt index 2151030..39fc4a1 100644 --- a/app/src/main/java/com/alya/ecommerce_serang/ui/chat/ChatActivity.kt +++ b/app/src/main/java/com/alya/ecommerce_serang/ui/chat/ChatActivity.kt @@ -394,7 +394,10 @@ class ChatActivity : AppCompatActivity() { // Update messages val previousCount = chatAdapter.itemCount - chatAdapter.submitList(state.messages) { + + val displayItems = viewModel.getDisplayItems() + + chatAdapter.submitList(displayItems) { Log.d(TAG, "Messages submitted to adapter") // Only auto-scroll for new messages or initial load if (previousCount == 0 || state.messages.size > previousCount) { @@ -426,17 +429,15 @@ class ChatActivity : AppCompatActivity() { .error(R.drawable.placeholder_image) .into(binding.imgProduct) } + updateProductCardUI(state.hasProductAttachment) - binding.productContainer.visibility = View.VISIBLE + binding.productContainer.visibility = View.GONE } else { binding.productContainer.visibility = View.GONE } updateInputHint(state) - // Update product card visual feedback - updateProductCardUI(state.hasProductAttachment) - // Update attachment hint if (state.hasAttachment) { binding.editTextMessage.hint = getString(R.string.image_attached) diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/chat/ChatAdapter.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/chat/ChatAdapter.kt index 05cfbfc..8a136f0 100644 --- a/app/src/main/java/com/alya/ecommerce_serang/ui/chat/ChatAdapter.kt +++ b/app/src/main/java/com/alya/ecommerce_serang/ui/chat/ChatAdapter.kt @@ -8,6 +8,7 @@ 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.databinding.ItemDateHeaderBinding import com.alya.ecommerce_serang.databinding.ItemMessageProductReceivedBinding import com.alya.ecommerce_serang.databinding.ItemMessageProductSentBinding import com.alya.ecommerce_serang.databinding.ItemMessageReceivedBinding @@ -17,22 +18,29 @@ import com.bumptech.glide.Glide class ChatAdapter( private val onProductClick: ((ProductInfo) -> Unit)? = null -) : ListAdapter(ChatMessageDiffCallback()) { +) : ListAdapter(ChatMessageDiffCallback()) { companion object { private const val VIEW_TYPE_MESSAGE_SENT = 1 private const val VIEW_TYPE_MESSAGE_RECEIVED = 2 private const val VIEW_TYPE_PRODUCT_SENT = 3 private const val VIEW_TYPE_PRODUCT_RECEIVED = 4 + private const val VIEW_TYPE_DATE_HEADER = 5 } override fun getItemViewType(position: Int): Int { - val message = getItem(position) - return when { - message.messageType == MessageType.PRODUCT && message.isSentByMe -> VIEW_TYPE_PRODUCT_SENT - message.messageType == MessageType.PRODUCT && !message.isSentByMe -> VIEW_TYPE_PRODUCT_RECEIVED - message.isSentByMe -> VIEW_TYPE_MESSAGE_SENT - else -> VIEW_TYPE_MESSAGE_RECEIVED + val item = getItem(position) + return when (item) { + is ChatDisplayItem.DateHeaderItem -> VIEW_TYPE_DATE_HEADER + is ChatDisplayItem.MessageItem -> { + val message = item.chatUiMessage + when { + message.messageType == MessageType.PRODUCT && message.isSentByMe -> VIEW_TYPE_PRODUCT_SENT + message.messageType == MessageType.PRODUCT && !message.isSentByMe -> VIEW_TYPE_PRODUCT_RECEIVED + message.isSentByMe -> VIEW_TYPE_MESSAGE_SENT + else -> VIEW_TYPE_MESSAGE_RECEIVED + } + } } } @@ -40,6 +48,10 @@ class ChatAdapter( val inflater = LayoutInflater.from(parent.context) return when (viewType) { + VIEW_TYPE_DATE_HEADER -> { + val binding = ItemDateHeaderBinding.inflate(inflater, parent, false) + DateHeaderViewHolder(binding) + } VIEW_TYPE_MESSAGE_SENT -> { val binding = ItemMessageSentBinding.inflate(inflater, parent, false) SentMessageViewHolder(binding) @@ -61,13 +73,34 @@ class ChatAdapter( } override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { - val message = getItem(position) + val item = getItem(position) when (holder) { - is SentMessageViewHolder -> holder.bind(message) - is ReceivedMessageViewHolder -> holder.bind(message) - is SentProductViewHolder -> holder.bind(message) - is ReceivedProductViewHolder -> holder.bind(message) + is DateHeaderViewHolder -> { + if (item is ChatDisplayItem.DateHeaderItem) { + holder.bind(item) + } + } + is SentMessageViewHolder -> { + if (item is ChatDisplayItem.MessageItem) { + holder.bind(item.chatUiMessage) + } + } + is ReceivedMessageViewHolder -> { + if (item is ChatDisplayItem.MessageItem) { + holder.bind(item.chatUiMessage) + } + } + is SentProductViewHolder -> { + if (item is ChatDisplayItem.MessageItem) { + holder.bind(item.chatUiMessage) + } + } + is ReceivedProductViewHolder -> { + if (item is ChatDisplayItem.MessageItem) { + holder.bind(item.chatUiMessage) + } + } } } @@ -233,17 +266,36 @@ class ChatAdapter( } } } + + inner class DateHeaderViewHolder(private val binding: ItemDateHeaderBinding) : + RecyclerView.ViewHolder(binding.root) { + + fun bind(item: ChatDisplayItem.DateHeaderItem) { + binding.tvDate.text = item.formattedDate + } + } } /** * DiffUtil callback for optimizing RecyclerView updates */ -class ChatMessageDiffCallback : DiffUtil.ItemCallback() { - override fun areItemsTheSame(oldItem: ChatUiMessage, newItem: ChatUiMessage): Boolean { - return oldItem.id == newItem.id +class ChatMessageDiffCallback : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: ChatDisplayItem, newItem: ChatDisplayItem): Boolean { + return when { + oldItem is ChatDisplayItem.MessageItem && newItem is ChatDisplayItem.MessageItem -> + oldItem.chatUiMessage.id == newItem.chatUiMessage.id + oldItem is ChatDisplayItem.DateHeaderItem && newItem is ChatDisplayItem.DateHeaderItem -> + oldItem.date == newItem.date + else -> false + } } - override fun areContentsTheSame(oldItem: ChatUiMessage, newItem: ChatUiMessage): Boolean { + override fun areContentsTheSame(oldItem: ChatDisplayItem, newItem: ChatDisplayItem): Boolean { return oldItem == newItem } +} + +sealed class ChatDisplayItem { + data class MessageItem(val chatUiMessage: ChatUiMessage) : ChatDisplayItem() + data class DateHeaderItem(val date: String, val formattedDate: String) : ChatDisplayItem() } \ No newline at end of file diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/chat/ChatViewModel.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/chat/ChatViewModel.kt index 677bf34..3b550f5 100644 --- a/app/src/main/java/com/alya/ecommerce_serang/ui/chat/ChatViewModel.kt +++ b/app/src/main/java/com/alya/ecommerce_serang/ui/chat/ChatViewModel.kt @@ -16,6 +16,9 @@ import com.alya.ecommerce_serang.utils.SessionManager import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.launch import java.io.File +import java.text.SimpleDateFormat +import java.util.Calendar +import java.util.Date import java.util.Locale import java.util.TimeZone import javax.inject.Inject @@ -737,7 +740,8 @@ class ChatViewModel @Inject constructor( attachment = chatLine.attachment ?: "", status = chatLine.status, time = formattedTime, - isSentByMe = chatLine.senderId == currentUserId + isSentByMe = chatLine.senderId == currentUserId, + createdAt = chatLine.createdAt ) } @@ -781,7 +785,8 @@ class ChatViewModel @Inject constructor( time = formattedTime, isSentByMe = chatItem.senderId == currentUserId, messageType = messageType, - productInfo = productInfo + productInfo = productInfo, + createdAt = chatItem.createdAt ) // Fetch product info for non-current-user products @@ -923,6 +928,85 @@ class ChatViewModel @Inject constructor( Log.d(TAG, "ChatViewModel cleared - Disconnecting socket") socketService.disconnect() } + + fun getDisplayItems(): List { + return transformMessagesToDisplayItems(state.value?.messages ?: emptyList()) + } + + private fun transformMessagesToDisplayItems(messages: List): List { + if (messages.isEmpty()) return emptyList() + + val displayItems = mutableListOf() + var lastDate: String? = null + + for (message in messages) { + // Extract date from message timestamp + val messageDate = extractDateFromTimestamp(message.createdAt) // You need to implement this + + // Add date header if this is a new day + if (messageDate != lastDate) { + val formattedDate = formatDateHeader(messageDate) // You need to implement this + displayItems.add(ChatDisplayItem.DateHeaderItem(messageDate, formattedDate)) + lastDate = messageDate + } + + // Add the message + displayItems.add(ChatDisplayItem.MessageItem(message)) + } + + return displayItems + } + + private fun extractDateFromTimestamp(timestamp: String): String { + return try { + // Parse ISO 8601 format: "2025-05-27T08:36:53.946Z" + val inputFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.getDefault()) + inputFormat.timeZone = TimeZone.getTimeZone("UTC") + + val date = inputFormat.parse(timestamp) + val outputFormat = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()) + outputFormat.format(date ?: Date()) + } catch (e: Exception) { + Log.e(TAG, "Error parsing timestamp: $timestamp", e) + return timestamp.take(10) + } + } + + private fun formatDateHeader(dateString: String): String { + return try { + val dateFormat = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()) + val messageDate = dateFormat.parse(dateString) ?: return dateString + + val today = Calendar.getInstance() + val yesterday = Calendar.getInstance().apply { add(Calendar.DAY_OF_YEAR, -1) } + val messageCalendar = Calendar.getInstance().apply { time = messageDate } + + when { + isSameDay(messageCalendar, today) -> "Today" + isSameDay(messageCalendar, yesterday) -> "Yesterday" + isThisYear(messageCalendar, today) -> { + // Show "Mon, Dec 15" format for this year + SimpleDateFormat("EEE, MMM dd", Locale.getDefault()).format(messageDate) + } + else -> { + // Show "Dec 15, 2024" format for other years + SimpleDateFormat("MMM dd, yyyy", Locale.getDefault()).format(messageDate) + } + } + } catch (e: Exception) { + Log.e(TAG, "Error formatting date: $dateString", e) + dateString // Fallback to raw date + } + } + + private fun isSameDay(cal1: Calendar, cal2: Calendar): Boolean { + return cal1.get(Calendar.YEAR) == cal2.get(Calendar.YEAR) && + cal1.get(Calendar.DAY_OF_YEAR) == cal2.get(Calendar.DAY_OF_YEAR) + } + + private fun isThisYear(messageCalendar: Calendar, today: Calendar): Boolean { + return messageCalendar.get(Calendar.YEAR) == today.get(Calendar.YEAR) + } } enum class MessageType { @@ -949,9 +1033,12 @@ data class ChatUiMessage( val time: String, val isSentByMe: Boolean, val messageType: MessageType = MessageType.TEXT, - val productInfo: ProductInfo? = null + val productInfo: ProductInfo? = null, + val createdAt: String ) + + // representing UI state to screen data class ChatUiState( val messages: List = emptyList(), diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/home/HorizontalProductAdapter.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/home/HorizontalProductAdapter.kt index 55792f9..b89d7fd 100644 --- a/app/src/main/java/com/alya/ecommerce_serang/ui/home/HorizontalProductAdapter.kt +++ b/app/src/main/java/com/alya/ecommerce_serang/ui/home/HorizontalProductAdapter.kt @@ -2,6 +2,7 @@ package com.alya.ecommerce_serang.ui.home import android.util.Log import android.view.LayoutInflater +import android.view.View import android.view.ViewGroup import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView @@ -11,6 +12,8 @@ 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 +import java.text.NumberFormat +import java.util.Locale class HorizontalProductAdapter( private var products: List, @@ -32,8 +35,17 @@ class HorizontalProductAdapter( Log.d("ProductAdapter", "Loading image: $fullImageUrl") tvProductName.text = product.name - tvProductPrice.text = product.price - rating.text = product.rating + tvProductPrice.text = formatCurrency(product.price.toDouble()) + val ratingStr = product.rating + val ratingValue = ratingStr?.toFloatOrNull() + + if (ratingValue != null && ratingValue > 0f) { + binding.rating.text = String.format("%.1f", ratingValue) + binding.rating.visibility = View.VISIBLE + } else { + binding.rating.text = "Belum ada rating" + binding.rating.setCompoundDrawablesWithIntrinsicBounds(null, null, null, null) + } // Load image using Glide Glide.with(itemView) @@ -77,6 +89,11 @@ class HorizontalProductAdapter( diffResult.dispatchUpdatesTo(this) } + private fun formatCurrency(amount: Double): String { + val formatter = NumberFormat.getCurrencyInstance(Locale("in", "ID")) + return formatter.format(amount).replace(",00", "") + } + class ProductDiffCallback( private val oldList: List, private val newList: List diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/home/SearchResultAdapter.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/home/SearchResultAdapter.kt index b46a143..89d8e22 100644 --- a/app/src/main/java/com/alya/ecommerce_serang/ui/home/SearchResultAdapter.kt +++ b/app/src/main/java/com/alya/ecommerce_serang/ui/home/SearchResultAdapter.kt @@ -12,6 +12,8 @@ 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 +import java.text.NumberFormat +import java.util.Locale class SearchResultsAdapter( private val onItemClick: (ProductsItem) -> Unit, @@ -46,7 +48,7 @@ class SearchResultsAdapter( fun bind(product: ProductsItem) { binding.tvProductName.text = product.name - binding.tvProductPrice.text = product.price + binding.tvProductPrice.text = formatCurrency(product.price.toDouble()) val fullImageUrl = if (product.image.startsWith("/")) { BASE_URL + product.image.removePrefix("/") // Append base URL if the path starts with "/" @@ -71,6 +73,11 @@ class SearchResultsAdapter( super.submitList(list) } + private fun formatCurrency(amount: Double): String { + val formatter = NumberFormat.getCurrencyInstance(Locale("in", "ID")) + return formatter.format(amount).replace(",00", "") + } + companion object { private val DIFF_CALLBACK = object : DiffUtil.ItemCallback() { override fun areItemsTheSame(oldItem: ProductsItem, newItem: ProductsItem): Boolean { diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/order/PaymentMethodAdapter.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/order/PaymentMethodAdapter.kt index 86b7254..a520da2 100644 --- a/app/src/main/java/com/alya/ecommerce_serang/ui/order/PaymentMethodAdapter.kt +++ b/app/src/main/java/com/alya/ecommerce_serang/ui/order/PaymentMethodAdapter.kt @@ -48,6 +48,12 @@ class PaymentMethodAdapter( // Load payment icon if available if (!payment.qrisImage.isNullOrEmpty()) { +// val fullImageUrl = if (payment.qrisImage.startsWith("/")) { +// BASE_URL + payment.qrisImage.removePrefix("/") // Append base URL if the path starts with "/" +// } else { +// payment.qrisImage// Use as is if it's already a full URL +// } + Glide.with(ivPaymentMethod.context) .load(payment.qrisImage) .apply( diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/order/ShippingAdapter.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/order/ShippingAdapter.kt index bea432b..d311a86 100644 --- a/app/src/main/java/com/alya/ecommerce_serang/ui/order/ShippingAdapter.kt +++ b/app/src/main/java/com/alya/ecommerce_serang/ui/order/ShippingAdapter.kt @@ -6,6 +6,8 @@ import androidx.recyclerview.widget.RecyclerView import com.alya.ecommerce_serang.data.api.response.customer.order.CourierCostsItem import com.alya.ecommerce_serang.data.api.response.customer.order.ServicesItem import com.alya.ecommerce_serang.databinding.ItemShippingOrderBinding +import java.text.NumberFormat +import java.util.Locale class ShippingAdapter( private val onItemSelected: (CourierCostsItem, ServicesItem) -> Unit @@ -30,7 +32,7 @@ class ShippingAdapter( // Combine courier name and service courierNameCost.text = "${courierCostsItem.courier} - ${service.service}" estDate.text = "Estimasi ${service.etd} hari" - costPrice.text = "Rp${service.cost}" + costPrice.text = formatCurrency(service.cost.toDouble()) // Single click handler for both item and radio button val onClickAction = { @@ -90,6 +92,11 @@ class ShippingAdapter( } } + private fun formatCurrency(amount: Double): String { + val formatter = NumberFormat.getCurrencyInstance(Locale("in", "ID")) + return formatter.format(amount).replace(",00", "") + } + override fun getItemCount(): Int { return courierCostsList.sumOf { it.services.size } } diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/order/detail/AddEvidencePaymentActivity.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/order/detail/AddEvidencePaymentActivity.kt index fc37df7..1329a95 100644 --- a/app/src/main/java/com/alya/ecommerce_serang/ui/order/detail/AddEvidencePaymentActivity.kt +++ b/app/src/main/java/com/alya/ecommerce_serang/ui/order/detail/AddEvidencePaymentActivity.kt @@ -1,8 +1,8 @@ package com.alya.ecommerce_serang.ui.order.detail import android.Manifest -import android.R import android.app.Activity +import android.app.AlertDialog import android.app.DatePickerDialog import android.content.Intent import android.content.pm.PackageManager @@ -12,6 +12,7 @@ import android.os.Bundle import android.provider.MediaStore import android.util.Log import android.view.View +import android.view.ViewGroup import android.webkit.MimeTypeMap import android.widget.AdapterView import android.widget.ArrayAdapter @@ -25,6 +26,7 @@ import androidx.core.content.ContextCompat import androidx.core.view.ViewCompat import androidx.core.view.WindowCompat import androidx.core.view.WindowInsetsCompat +import com.alya.ecommerce_serang.R import com.alya.ecommerce_serang.data.api.dto.AddEvidenceMultipartRequest import com.alya.ecommerce_serang.data.api.retrofit.ApiConfig import com.alya.ecommerce_serang.data.repository.OrderRepository @@ -37,6 +39,7 @@ import okhttp3.MultipartBody import okhttp3.RequestBody.Companion.asRequestBody import okhttp3.RequestBody.Companion.toRequestBody import java.io.File +import java.text.NumberFormat import java.text.SimpleDateFormat import java.util.Calendar import java.util.Locale @@ -59,11 +62,9 @@ class AddEvidencePaymentActivity : AppCompatActivity() { } private val paymentMethods = arrayOf( - "Pilih metode pembayaran", "Transfer Bank", "E-Wallet", - "Virtual Account", - "Cash on Delivery" + "QRIS", ) // private val getContent = registerForActivityResult(ActivityResultContracts.GetContent()) { uri: Uri? -> @@ -128,11 +129,8 @@ class AddEvidencePaymentActivity : AppCompatActivity() { } private fun setupUI() { - // Set product details\ - - // Setup payment methods spinner - val adapter = ArrayAdapter(this, R.layout.simple_spinner_item, paymentMethods) - adapter.setDropDownViewResource(R.layout.simple_spinner_dropdown_item) + val paymentMethods = listOf("Transfer Bank", "COD", "QRIS") + val adapter = SpinnerCardAdapter(this, paymentMethods) binding.spinnerPaymentMethod.adapter = adapter } @@ -219,15 +217,23 @@ class AddEvidencePaymentActivity : AppCompatActivity() { private fun showImagePickerOptions() { val options = arrayOf( "Pilih dari Galeri", - "Batal" + "Kembali" ) - androidx.appcompat.app.AlertDialog.Builder(this) - .setTitle("Pilih Bukti Pembayaran") - .setItems(options) { dialog, which -> + val adapter = object : ArrayAdapter(this, R.layout.item_dialog_add_evidence, R.id.tvOption, options) { + override fun getView(position: Int, convertView: View?, parent: ViewGroup): View { + val view = super.getView(position, convertView, parent) + val divider = view.findViewById(R.id.divider) + divider.visibility = if (position == count - 1) View.GONE else View.VISIBLE + return view + } + } + + AlertDialog.Builder(this) + .setAdapter(adapter) { dialog, which -> when (which) { - 0 -> openGallery() // Gallery - 1 -> dialog.dismiss() // Cancel + 0 -> openGallery() + 1 -> dialog.dismiss() } } .show() @@ -440,6 +446,8 @@ class AddEvidencePaymentActivity : AppCompatActivity() { } + + companion object { private const val PERMISSION_REQUEST_CODE = 100 private const val TAG = "AddEvidenceActivity" diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/order/detail/PaymentActivity.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/order/detail/PaymentActivity.kt index d2c525a..c470b52 100644 --- a/app/src/main/java/com/alya/ecommerce_serang/ui/order/detail/PaymentActivity.kt +++ b/app/src/main/java/com/alya/ecommerce_serang/ui/order/detail/PaymentActivity.kt @@ -17,6 +17,7 @@ import com.alya.ecommerce_serang.data.repository.OrderRepository import com.alya.ecommerce_serang.databinding.ActivityPaymentBinding import com.alya.ecommerce_serang.utils.BaseViewModelFactory import com.alya.ecommerce_serang.utils.SessionManager +import java.text.NumberFormat import java.text.SimpleDateFormat import java.util.Calendar import java.util.Locale @@ -100,6 +101,7 @@ class PaymentActivity : AppCompatActivity() { // Log.d(TAG, "Button clicked - showing toast") // Toast.makeText(this@PaymentActivity, "Button works! OrderID: $orderId", Toast.LENGTH_LONG).show() // } + binding.btnUploadPaymentProof.apply { isEnabled = true isClickable = true @@ -134,7 +136,7 @@ class PaymentActivity : AppCompatActivity() { Log.d(TAG, "Order details received: $order") // Set total amount - binding.tvTotalAmount.text = order.totalAmount ?: "Rp0" + binding.tvTotalAmount.text = formatCurrency(order.totalAmount?.toDouble() ?: 0.00) Log.d(TAG, "Total Amount: ${order.totalAmount}") @@ -202,6 +204,11 @@ class PaymentActivity : AppCompatActivity() { } } + private fun formatCurrency(amount: Double): String { + val formatter = NumberFormat.getCurrencyInstance(Locale("in", "ID")) + return formatter.format(amount).replace(",00", "") + } + private fun showInstructions(type: String) { // Implementasi tampilkan instruksi val instructions = when (type) { diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/order/detail/SpinnerCardAdapter.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/order/detail/SpinnerCardAdapter.kt new file mode 100644 index 0000000..edd1988 --- /dev/null +++ b/app/src/main/java/com/alya/ecommerce_serang/ui/order/detail/SpinnerCardAdapter.kt @@ -0,0 +1,33 @@ +package com.alya.ecommerce_serang.ui.order.detail + +import android.content.Context +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.ArrayAdapter +import android.widget.TextView +import com.alya.ecommerce_serang.R + +class SpinnerCardAdapter( + context: Context, + private val items: List +) : ArrayAdapter(context, 0, items) { + + override fun getView(position: Int, convertView: View?, parent: ViewGroup): View { + // View shown when Spinner is collapsed + return createCardView(position, convertView, parent) + } + + override fun getDropDownView(position: Int, convertView: View?, parent: ViewGroup): View { + // View shown for dropdown items + return createCardView(position, convertView, parent) + } + + private fun createCardView(position: Int, convertView: View?, parent: ViewGroup): View { + val inflater = LayoutInflater.from(context) + val view = convertView ?: inflater.inflate(R.layout.item_dialog_spinner_card, parent, false) + val textView = view.findViewById(R.id.tvOption) + textView.text = items[position] + return view + } +} diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/order/history/detailorder/DetailOrderItemsAdapter.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/order/history/detailorder/DetailOrderItemsAdapter.kt index 58f5c43..924d4ef 100644 --- a/app/src/main/java/com/alya/ecommerce_serang/ui/order/history/detailorder/DetailOrderItemsAdapter.kt +++ b/app/src/main/java/com/alya/ecommerce_serang/ui/order/history/detailorder/DetailOrderItemsAdapter.kt @@ -6,6 +6,7 @@ import android.view.ViewGroup import android.widget.ImageView import android.widget.TextView 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.response.customer.order.OrderListItemsItem import com.bumptech.glide.Glide @@ -39,11 +40,18 @@ class DetailOrderItemsAdapter : RecyclerView.Adapter, @@ -32,7 +34,7 @@ class OtherProductAdapter ( Log.d("ProductAdapter", "Loading image: $fullImageUrl") tvProductName.text = product.name - tvProductPrice.text = product.price + tvProductPrice.text = formatCurrency(product.price.toDouble()) rating.text = product.rating // Load image using Glide @@ -77,6 +79,11 @@ class OtherProductAdapter ( diffResult.dispatchUpdatesTo(this) } + private fun formatCurrency(amount: Double): String { + val formatter = NumberFormat.getCurrencyInstance(Locale("in", "ID")) + return formatter.format(amount).replace(",00", "") + } + class ProductDiffCallback( private val oldList: List, private val newList: List diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/product/listproduct/ListProductAdapter.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/product/listproduct/ListProductAdapter.kt index f0cb5f4..25314e5 100644 --- a/app/src/main/java/com/alya/ecommerce_serang/ui/product/listproduct/ListProductAdapter.kt +++ b/app/src/main/java/com/alya/ecommerce_serang/ui/product/listproduct/ListProductAdapter.kt @@ -11,6 +11,8 @@ 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 +import java.text.NumberFormat +import java.util.Locale class ListProductAdapter( private var products: List, @@ -24,7 +26,7 @@ class ListProductAdapter( fun bind(product: ProductsItem) = with(binding) { tvProductName.text = product.name - tvProductPrice.text = product.price + tvProductPrice.text = formatCurrency(product.price.toDouble()) rating.text = product.rating val fullImageUrl = if (product.image.startsWith("/")) { @@ -68,6 +70,11 @@ class ListProductAdapter( notifyDataSetChanged() } + private fun formatCurrency(amount: Double): String { + val formatter = NumberFormat.getCurrencyInstance(Locale("in", "ID")) + return formatter.format(amount).replace(",00", "") + } + class ProductDiffCallback( private val oldList: List, private val newList: List diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/profile/mystore/chat/ChatStoreActivity.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/profile/mystore/chat/ChatStoreActivity.kt index 8dfabfc..169f1de 100644 --- a/app/src/main/java/com/alya/ecommerce_serang/ui/profile/mystore/chat/ChatStoreActivity.kt +++ b/app/src/main/java/com/alya/ecommerce_serang/ui/profile/mystore/chat/ChatStoreActivity.kt @@ -378,7 +378,9 @@ class ChatStoreActivity : AppCompatActivity() { // Update messages val previousCount = chatAdapter.itemCount - chatAdapter.submitList(state.messages) { + val displayItems = viewModel.getDisplayItems() + + chatAdapter.submitList(displayItems) { Log.d(TAG, "Messages submitted to adapter") // Only auto-scroll for new messages or initial load if (previousCount == 0 || state.messages.size > previousCount) { diff --git a/app/src/main/res/drawable/bg_date_header.xml b/app/src/main/res/drawable/bg_date_header.xml new file mode 100644 index 0000000..7539706 --- /dev/null +++ b/app/src/main/res/drawable/bg_date_header.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_date_header.xml b/app/src/main/res/layout/item_date_header.xml new file mode 100644 index 0000000..70aa5f8 --- /dev/null +++ b/app/src/main/res/layout/item_date_header.xml @@ -0,0 +1,21 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_dialog_add_evidence.xml b/app/src/main/res/layout/item_dialog_add_evidence.xml new file mode 100644 index 0000000..9756df8 --- /dev/null +++ b/app/src/main/res/layout/item_dialog_add_evidence.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + diff --git a/app/src/main/res/layout/item_dialog_spinner_card.xml b/app/src/main/res/layout/item_dialog_spinner_card.xml new file mode 100644 index 0000000..96694bd --- /dev/null +++ b/app/src/main/res/layout/item_dialog_spinner_card.xml @@ -0,0 +1,21 @@ + + + + + diff --git a/app/src/main/res/layout/item_order_detail_product.xml b/app/src/main/res/layout/item_order_detail_product.xml index f34e2a1..71f7ed1 100644 --- a/app/src/main/res/layout/item_order_detail_product.xml +++ b/app/src/main/res/layout/item_order_detail_product.xml @@ -4,7 +4,7 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="wrap_content" - android:padding="8dp"> + android:paddingHorizontal="8dp"> From 0b47d0beb8986b5d320be1cd1c61d1eafa65ce6f Mon Sep 17 00:00:00 2001 From: Gracia Hotmauli <95269134+hotmauligracia@users.noreply.github.com> Date: Mon, 23 Jun 2025 11:11:03 +0700 Subject: [PATCH 6/6] store review --- app/src/main/AndroidManifest.xml | 3 + .../data/api/dto/ReviewsItem.kt | 30 +++++ .../store/review/ProductReviewResponse.kt | 13 ++ .../data/api/retrofit/ApiService.kt | 5 + .../data/repository/ReviewRepository.kt | 50 +++++++ .../ui/profile/mystore/MyStoreActivity.kt | 21 +-- .../profile/mystore/review/ReviewActivity.kt | 74 +++++++++++ .../profile/mystore/review/ReviewAdapter.kt | 81 ++++++++++++ .../profile/mystore/review/ReviewFragment.kt | 52 ++++++-- .../mystore/review/ReviewListFragment.kt | 123 ++++++++++++++++++ .../mystore/review/ReviewViewPagerAdapter.kt | 24 ++++ .../utils/viewmodel/ReviewViewModel.kt | 68 +++++++++- app/src/main/res/layout/activity_review.xml | 85 ++++++++++++ app/src/main/res/layout/fragment_review.xml | 29 ++++- .../main/res/layout/fragment_review_list.xml | 41 ++++++ app/src/main/res/layout/fragment_sells.xml | 6 +- .../main/res/layout/fragment_sells_list.xml | 10 +- .../main/res/layout/item_store_product.xml | 8 +- .../res/layout/item_store_product_review.xml | 105 +++++++++++++++ 19 files changed, 783 insertions(+), 45 deletions(-) create mode 100644 app/src/main/java/com/alya/ecommerce_serang/data/api/dto/ReviewsItem.kt create mode 100644 app/src/main/java/com/alya/ecommerce_serang/data/api/response/store/review/ProductReviewResponse.kt create mode 100644 app/src/main/java/com/alya/ecommerce_serang/data/repository/ReviewRepository.kt create mode 100644 app/src/main/java/com/alya/ecommerce_serang/ui/profile/mystore/review/ReviewActivity.kt create mode 100644 app/src/main/java/com/alya/ecommerce_serang/ui/profile/mystore/review/ReviewAdapter.kt create mode 100644 app/src/main/java/com/alya/ecommerce_serang/ui/profile/mystore/review/ReviewListFragment.kt create mode 100644 app/src/main/java/com/alya/ecommerce_serang/ui/profile/mystore/review/ReviewViewPagerAdapter.kt create mode 100644 app/src/main/res/layout/activity_review.xml create mode 100644 app/src/main/res/layout/fragment_review_list.xml create mode 100644 app/src/main/res/layout/item_store_product_review.xml diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index f7e9e77..ddaf412 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -29,6 +29,9 @@ android:theme="@style/Theme.Ecommerce_serang" android:usesCleartextTraffic="true" tools:targetApi="31"> + diff --git a/app/src/main/java/com/alya/ecommerce_serang/data/api/dto/ReviewsItem.kt b/app/src/main/java/com/alya/ecommerce_serang/data/api/dto/ReviewsItem.kt new file mode 100644 index 0000000..44e3365 --- /dev/null +++ b/app/src/main/java/com/alya/ecommerce_serang/data/api/dto/ReviewsItem.kt @@ -0,0 +1,30 @@ +package com.alya.ecommerce_serang.data.api.dto + +import com.google.gson.annotations.SerializedName + +data class ReviewsItem( + + @field:SerializedName("order_item_id") + val orderItemId: Int? = null, + + @field:SerializedName("review_date") + val reviewDate: String? = null, + + @field:SerializedName("user_image") + val userImage: String? = null, + + @field:SerializedName("product_id") + val productId: Int? = null, + + @field:SerializedName("rating") + val rating: Int? = null, + + @field:SerializedName("review_text") + val reviewText: String? = null, + + @field:SerializedName("product_name") + val productName: String? = null, + + @field:SerializedName("username") + val username: String? = null +) \ No newline at end of file diff --git a/app/src/main/java/com/alya/ecommerce_serang/data/api/response/store/review/ProductReviewResponse.kt b/app/src/main/java/com/alya/ecommerce_serang/data/api/response/store/review/ProductReviewResponse.kt new file mode 100644 index 0000000..3847b69 --- /dev/null +++ b/app/src/main/java/com/alya/ecommerce_serang/data/api/response/store/review/ProductReviewResponse.kt @@ -0,0 +1,13 @@ +package com.alya.ecommerce_serang.data.api.response.store.review + +import com.alya.ecommerce_serang.data.api.dto.ReviewsItem +import com.google.gson.annotations.SerializedName + +data class ProductReviewResponse( + + @field:SerializedName("reviews") + val reviews: List? = null, + + @field:SerializedName("message") + val message: String? = null +) diff --git a/app/src/main/java/com/alya/ecommerce_serang/data/api/retrofit/ApiService.kt b/app/src/main/java/com/alya/ecommerce_serang/data/api/retrofit/ApiService.kt index a081e8e..1a04e32 100644 --- a/app/src/main/java/com/alya/ecommerce_serang/data/api/retrofit/ApiService.kt +++ b/app/src/main/java/com/alya/ecommerce_serang/data/api/retrofit/ApiService.kt @@ -75,6 +75,7 @@ import com.alya.ecommerce_serang.data.api.response.store.product.UpdateProductRe import com.alya.ecommerce_serang.data.api.response.store.product.ViewStoreProductsResponse import com.alya.ecommerce_serang.data.api.response.store.GenericResponse import com.alya.ecommerce_serang.data.api.response.store.profile.StoreDataResponse +import com.alya.ecommerce_serang.data.api.response.store.review.ProductReviewResponse import com.alya.ecommerce_serang.data.api.response.store.topup.BalanceTopUpResponse import com.alya.ecommerce_serang.data.api.response.store.topup.TopUpResponse import okhttp3.MultipartBody @@ -507,4 +508,8 @@ interface ApiService { @GET("mystore/notification") suspend fun getNotifStore( ): Response + + @GET("store/reviews") + suspend fun getStoreProductReview( + ): Response } \ No newline at end of file diff --git a/app/src/main/java/com/alya/ecommerce_serang/data/repository/ReviewRepository.kt b/app/src/main/java/com/alya/ecommerce_serang/data/repository/ReviewRepository.kt new file mode 100644 index 0000000..b443a3b --- /dev/null +++ b/app/src/main/java/com/alya/ecommerce_serang/data/repository/ReviewRepository.kt @@ -0,0 +1,50 @@ +package com.alya.ecommerce_serang.data.repository + +import com.alya.ecommerce_serang.data.api.dto.ProductsItem +import com.alya.ecommerce_serang.data.api.response.customer.product.ProductResponse +import com.alya.ecommerce_serang.data.api.response.store.review.ProductReviewResponse +import com.alya.ecommerce_serang.data.api.retrofit.ApiService + +class ReviewRepository(private val apiService: ApiService) { + suspend fun getReviewList(score: String): Result { + return try { + val response = apiService.getStoreProductReview() + + if (response.isSuccessful) { + val allReviews = response.body() + val filteredReviews = if (score == "all") { + allReviews + } else { + val targetScore = score.toIntOrNull() + allReviews?.copy(reviews = allReviews.reviews?.filter { + val rating = it?.rating ?: 0 + when(targetScore) { + 5 -> rating > 4 + 4 -> rating > 3 && rating <= 4 + 3 -> rating > 2 && rating <= 3 + 2 -> rating > 1 && rating <= 2 + 1 -> rating <= 1 + else -> true + } + }) + } + Result.Success(filteredReviews!!) + } else { + Result.Error(Exception("HTTP ${response.code()}: ${response.message()}")) + } + } catch (e: Exception) { + Result.Error(e) + } + } + + suspend fun getProductDetail(productId: Int): ProductResponse? { + return try { + val response = apiService.getDetailProduct(productId) + if (response.isSuccessful) { + response.body() + } else null + } catch (e: Exception) { + null + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/profile/mystore/MyStoreActivity.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/profile/mystore/MyStoreActivity.kt index a1d78b1..12a8ad7 100644 --- a/app/src/main/java/com/alya/ecommerce_serang/ui/profile/mystore/MyStoreActivity.kt +++ b/app/src/main/java/com/alya/ecommerce_serang/ui/profile/mystore/MyStoreActivity.kt @@ -18,6 +18,7 @@ import com.alya.ecommerce_serang.ui.profile.mystore.balance.BalanceActivity import com.alya.ecommerce_serang.ui.profile.mystore.chat.ChatListStoreActivity import com.alya.ecommerce_serang.ui.profile.mystore.product.ProductActivity import com.alya.ecommerce_serang.ui.profile.mystore.profile.DetailStoreProfileActivity +import com.alya.ecommerce_serang.ui.profile.mystore.review.ReviewActivity import com.alya.ecommerce_serang.ui.profile.mystore.review.ReviewFragment import com.alya.ecommerce_serang.ui.profile.mystore.sells.SellsActivity import com.alya.ecommerce_serang.utils.BaseViewModelFactory @@ -99,25 +100,21 @@ class MyStoreActivity : AppCompatActivity() { } binding.tvHistory.setOnClickListener { - val intent = Intent(this, SellsActivity::class.java) - startActivity(intent) + startActivity(Intent(this, SellsActivity::class.java)) } binding.layoutPerluTagihan.setOnClickListener { - val intent = Intent(this, SellsActivity::class.java) - startActivity(intent) + startActivity(Intent(this, SellsActivity::class.java)) //navigateToSellsFragment("pending") } binding.layoutPembayaran.setOnClickListener { - val intent = Intent(this, SellsActivity::class.java) - startActivity(intent) + startActivity(Intent(this, SellsActivity::class.java)) //navigateToSellsFragment("paid") } binding.layoutPerluDikirim.setOnClickListener { - val intent = Intent(this, SellsActivity::class.java) - startActivity(intent) + startActivity(Intent(this, SellsActivity::class.java)) //navigateToSellsFragment("processed") } @@ -126,15 +123,11 @@ class MyStoreActivity : AppCompatActivity() { } binding.layoutReview.setOnClickListener { - supportFragmentManager.beginTransaction() - .replace(android.R.id.content, ReviewFragment()) - .addToBackStack(null) - .commit() + startActivity(Intent(this, ReviewActivity::class.java)) } binding.layoutInbox.setOnClickListener { - val intent = Intent(this, ChatListStoreActivity::class.java) - startActivity(intent) + startActivity(Intent(this, ChatListStoreActivity::class.java)) } } diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/profile/mystore/review/ReviewActivity.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/profile/mystore/review/ReviewActivity.kt new file mode 100644 index 0000000..8d131ea --- /dev/null +++ b/app/src/main/java/com/alya/ecommerce_serang/ui/profile/mystore/review/ReviewActivity.kt @@ -0,0 +1,74 @@ +package com.alya.ecommerce_serang.ui.profile.mystore.review + +import android.os.Bundle +import androidx.activity.enableEdgeToEdge +import androidx.activity.viewModels +import androidx.appcompat.app.AppCompatActivity +import androidx.core.view.ViewCompat +import androidx.core.view.WindowInsetsCompat +import androidx.fragment.app.commit +import com.alya.ecommerce_serang.R +import com.alya.ecommerce_serang.data.api.retrofit.ApiConfig +import com.alya.ecommerce_serang.data.repository.ReviewRepository +import com.alya.ecommerce_serang.databinding.ActivityReviewBinding +import com.alya.ecommerce_serang.utils.BaseViewModelFactory +import com.alya.ecommerce_serang.utils.SessionManager +import com.alya.ecommerce_serang.utils.viewmodel.ReviewViewModel + +class ReviewActivity : AppCompatActivity() { + private lateinit var binding: ActivityReviewBinding + private lateinit var sessionManager: SessionManager + + private val viewModel: ReviewViewModel by viewModels { + BaseViewModelFactory { + val apiService = ApiConfig.getApiService(sessionManager) + val reviewRepository = ReviewRepository(apiService) + ReviewViewModel(reviewRepository) + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + binding = ActivityReviewBinding.inflate(layoutInflater) + setContentView(binding.root) + + sessionManager = SessionManager(this) + + ViewCompat.setOnApplyWindowInsetsListener(binding.root) { view, windowInsets -> + val systemBars = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()) + view.setPadding( + systemBars.left, + systemBars.top, + systemBars.right, + systemBars.bottom + ) + windowInsets + } + + setupHeader() + + viewModel.getReview("all") + viewModel.averageScore.observe(this) { binding.tvReviewScore.text = it } + viewModel.totalReview.observe(this) { binding.tvTotalReview.text = "$it rating" } + viewModel.totalReviewWithDesc.observe(this) { binding.tvTotalReviewWithDesc.text = "$it ulasan" } + + if (savedInstanceState == null) { + showReviewFragment() + } + } + + private fun setupHeader() { + binding.header.headerTitle.text = "Ulasan Pembeli" + + binding.header.headerLeftIcon.setOnClickListener { + onBackPressed() + finish() + } + } + + private fun showReviewFragment() { + supportFragmentManager.commit { + replace(R.id.fragment_container_reviews, ReviewFragment()) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/profile/mystore/review/ReviewAdapter.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/profile/mystore/review/ReviewAdapter.kt new file mode 100644 index 0000000..5eab57f --- /dev/null +++ b/app/src/main/java/com/alya/ecommerce_serang/ui/profile/mystore/review/ReviewAdapter.kt @@ -0,0 +1,81 @@ +package com.alya.ecommerce_serang.ui.profile.mystore.review + +import android.util.Log +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.ImageView +import android.widget.TextView +import androidx.recyclerview.widget.RecyclerView +import com.alya.ecommerce_serang.R +import com.alya.ecommerce_serang.data.api.dto.ReviewsItem +import com.alya.ecommerce_serang.utils.viewmodel.ReviewViewModel +import com.bumptech.glide.Glide +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch + +class ReviewAdapter( + private val viewModel: ReviewViewModel +): RecyclerView.Adapter() { + + private val reviews = mutableListOf() + private var fragmentScore: String = "all" + + fun setFragmentScore(score: String) { + fragmentScore = score + } + + fun submitList(newReviews: List) { + reviews.clear() + reviews.addAll(newReviews) + notifyDataSetChanged() + } + + override fun onCreateViewHolder( + parent: ViewGroup, + viewType: Int + ): ReviewAdapter.ReviewViewHolder { + val view = LayoutInflater.from(parent.context).inflate(R.layout.item_store_product_review, parent, false) + return ReviewViewHolder(view) + } + + override fun onBindViewHolder(holder: ReviewViewHolder, position: Int) { + if (position < reviews.size) { + holder.bind(reviews[position]) + } else { + Log.e("ReviewAdapter", "Position $position is out of bounds for size ${reviews.size}") + } + } + + override fun getItemCount(): Int = reviews.size + + inner class ReviewViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { + private val ivProduct: ImageView = itemView.findViewById(R.id.iv_product) + private val tvProductName: TextView = itemView.findViewById(R.id.tv_product_name) + private val tvReviewScore: TextView = itemView.findViewById(R.id.tv_review_score) + private val tvReviewDate: TextView = itemView.findViewById(R.id.tv_review_date) + private val tvUsername: TextView = itemView.findViewById(R.id.tv_username) + private val tvReviewDesc: TextView = itemView.findViewById(R.id.tv_review_desc) + private val ivMenu: ImageView = itemView.findViewById(R.id.iv_menu) + + fun bind(review: ReviewsItem) { + val actualScore = + if (fragmentScore == "all") review.rating.toString() else fragmentScore + + CoroutineScope(Dispatchers.Main).launch { + val imageUrl = viewModel.getProductImage(review.productId ?: -1) + Glide.with(itemView.context) + .load(imageUrl) + .placeholder(R.drawable.placeholder_image) + .into(ivProduct) + } + + tvProductName.text = review.productName + tvReviewScore.text = actualScore + tvReviewDate.text = review.reviewDate + tvUsername.text = review.username + tvReviewDesc.text = review.reviewText + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/profile/mystore/review/ReviewFragment.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/profile/mystore/review/ReviewFragment.kt index 45e553b..0b9e947 100644 --- a/app/src/main/java/com/alya/ecommerce_serang/ui/profile/mystore/review/ReviewFragment.kt +++ b/app/src/main/java/com/alya/ecommerce_serang/ui/profile/mystore/review/ReviewFragment.kt @@ -1,32 +1,56 @@ package com.alya.ecommerce_serang.ui.profile.mystore.review -import androidx.fragment.app.viewModels import android.os.Bundle import androidx.fragment.app.Fragment import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import com.alya.ecommerce_serang.R -import com.alya.ecommerce_serang.utils.viewmodel.ReviewViewModel +import com.alya.ecommerce_serang.databinding.FragmentReviewBinding +import com.alya.ecommerce_serang.utils.SessionManager +import com.google.android.material.tabs.TabLayoutMediator class ReviewFragment : Fragment() { - companion object { - fun newInstance() = ReviewFragment() - } + private var _binding: FragmentReviewBinding? = null + private val binding get() = _binding!! + private lateinit var sessionManager: SessionManager - private val viewModel: ReviewViewModel by viewModels() - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - - // TODO: Use the ViewModel - } + private lateinit var viewPagerAdapter: ReviewViewPagerAdapter override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View { - return inflater.inflate(R.layout.fragment_review, container, false) + _binding = FragmentReviewBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + sessionManager = SessionManager(requireContext()) + + setupViewPager() + } + + private fun setupViewPager() { + viewPagerAdapter = ReviewViewPagerAdapter(requireActivity()) + binding.viewPagerReview.adapter = viewPagerAdapter + + TabLayoutMediator(binding.tabLayoutReview, binding.viewPagerReview) { tab, position -> + tab.text = when (position) { + 0 -> "Semua" + 1 -> "5 Bintang" + 2 -> "4 Bintang" + 3 -> "3 Bintang" + 4 -> "2 Bintang" + 5 -> "1 Bintang" + else -> "Tab $position" + } + }.attach() + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null } } \ No newline at end of file diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/profile/mystore/review/ReviewListFragment.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/profile/mystore/review/ReviewListFragment.kt new file mode 100644 index 0000000..029d873 --- /dev/null +++ b/app/src/main/java/com/alya/ecommerce_serang/ui/profile/mystore/review/ReviewListFragment.kt @@ -0,0 +1,123 @@ +package com.alya.ecommerce_serang.ui.profile.mystore.review + +import android.os.Bundle +import android.util.Log +import androidx.fragment.app.Fragment +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.Toast +import androidx.fragment.app.viewModels +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.repository.ReviewRepository +import com.alya.ecommerce_serang.databinding.FragmentReviewListBinding +import com.alya.ecommerce_serang.ui.order.address.ViewState +import com.alya.ecommerce_serang.utils.BaseViewModelFactory +import com.alya.ecommerce_serang.utils.SessionManager +import com.alya.ecommerce_serang.utils.viewmodel.ProductViewModel +import com.alya.ecommerce_serang.utils.viewmodel.ReviewViewModel + +class ReviewListFragment : Fragment() { + + private var _binding: FragmentReviewListBinding? = null + private val binding get() = _binding!! + private lateinit var sessionManager: SessionManager + + private lateinit var reviewAdapter: ReviewAdapter + private val viewModel: ReviewViewModel by viewModels { + BaseViewModelFactory { + val apiService = ApiConfig.getApiService(SessionManager(requireContext())) + ReviewViewModel(ReviewRepository(apiService)) + } + } + + private var score: String = "all" + + companion object { + private const val ARG_SCORE = "score" + + fun newInstance(score: String): ReviewListFragment = ReviewListFragment().apply { + arguments = Bundle().apply { + putString(ARG_SCORE, score) + } + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + sessionManager = SessionManager(requireContext()) + score = arguments?.getString(ARG_SCORE) ?: "all" + } + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + _binding = FragmentReviewListBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + reviewAdapter = ReviewAdapter(viewModel) + binding.rvReview.apply { + layoutManager = LinearLayoutManager(requireContext()) + adapter = reviewAdapter + } + + observeReviewList() + fetchReviewByScore(score) + } + + private fun fetchReviewByScore(score: String) { + val normalizedScore = when (score) { + "all" -> "all" + else -> { + val scoreValue = score.toDoubleOrNull() ?: 0.0 + when { + scoreValue > 4.5 -> "5" + scoreValue > 3.5 -> "4" + scoreValue > 2.5 -> "3" + scoreValue > 1.5 -> "2" + else -> "1" + } + } + } + viewModel.getReview(normalizedScore) + } + + private fun observeReviewList() { + viewModel.review.observe(viewLifecycleOwner) { result -> + when (result) { + is ViewState.Success -> { + val data = result.data.orEmpty().sortedByDescending { it.reviewDate } + binding.progressBar.visibility = View.GONE + + if (data.isEmpty()) { + binding.tvEmptyState.visibility = View.VISIBLE + binding.rvReview.visibility = View.GONE + } else { + binding.tvEmptyState.visibility = View.GONE + binding.rvReview.visibility = View.VISIBLE + reviewAdapter.submitList(data) + } + } + + is ViewState.Loading -> binding.progressBar.visibility = View.VISIBLE + is ViewState.Error -> { + binding.progressBar.visibility = View.GONE + binding.tvEmptyState.visibility = View.VISIBLE + Toast.makeText(requireContext(), result.message, Toast.LENGTH_SHORT).show() + } + } + } + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } +} \ No newline at end of file diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/profile/mystore/review/ReviewViewPagerAdapter.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/profile/mystore/review/ReviewViewPagerAdapter.kt new file mode 100644 index 0000000..8973690 --- /dev/null +++ b/app/src/main/java/com/alya/ecommerce_serang/ui/profile/mystore/review/ReviewViewPagerAdapter.kt @@ -0,0 +1,24 @@ +package com.alya.ecommerce_serang.ui.profile.mystore.review + +import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentActivity +import androidx.viewpager2.adapter.FragmentStateAdapter + +class ReviewViewPagerAdapter( + fragmentActivity: FragmentActivity +) : FragmentStateAdapter(fragmentActivity) { + private val reviewScore = listOf( + "all", + "5", + "4", + "3", + "2", + "1" + ) + + override fun getItemCount(): Int = reviewScore.size + + override fun createFragment(position: Int): Fragment { + return ReviewListFragment.newInstance(reviewScore[position]) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/alya/ecommerce_serang/utils/viewmodel/ReviewViewModel.kt b/app/src/main/java/com/alya/ecommerce_serang/utils/viewmodel/ReviewViewModel.kt index 3959502..2493692 100644 --- a/app/src/main/java/com/alya/ecommerce_serang/utils/viewmodel/ReviewViewModel.kt +++ b/app/src/main/java/com/alya/ecommerce_serang/utils/viewmodel/ReviewViewModel.kt @@ -1,7 +1,71 @@ package com.alya.ecommerce_serang.utils.viewmodel +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.alya.ecommerce_serang.BuildConfig.BASE_URL +import com.alya.ecommerce_serang.data.api.dto.ReviewsItem +import com.alya.ecommerce_serang.data.repository.Result +import com.alya.ecommerce_serang.data.repository.ReviewRepository +import com.alya.ecommerce_serang.ui.order.address.ViewState +import kotlinx.coroutines.launch +import kotlin.getOrThrow -class ReviewViewModel : ViewModel() { - // TODO: Implement the ViewModel +class ReviewViewModel(private val repository: ReviewRepository) : ViewModel() { + + private val _review = MutableLiveData>>() + val review: LiveData>> = _review + + private val _averageScore = MutableLiveData() + val averageScore: LiveData = _averageScore + + private val _totalReview = MutableLiveData() + val totalReview: LiveData = _totalReview + + private val _totalReviewWithDesc = MutableLiveData() + val totalReviewWithDesc: LiveData = _totalReviewWithDesc + + private val _isLoading = MutableLiveData() + val isLoading: LiveData = _isLoading + + private val productImageCache = mutableMapOf() + + fun getReview(score: String) { + _review.value = ViewState.Loading + viewModelScope.launch { + try { + val response = repository.getReviewList(score) + if (response is Result.Success) { + val reviews = response.data.reviews?.filterNotNull().orEmpty() + _review.value = ViewState.Success(reviews) + + if (score == "all") { + val avg = if (reviews.isNotEmpty()) { + reviews.mapNotNull { it.rating }.average() + } else 0.0 + _averageScore.value = String.format("%.1f", avg) + _totalReview.value = reviews.size + _totalReviewWithDesc.value = reviews.count { !it.reviewText.isNullOrBlank() } + } + } else if (response is Result.Error) { + _review.value = ViewState.Error(response.exception.message ?: "Gagal memuat ulasan") + } + } catch (e: Exception) { + _review.value = ViewState.Error(e.message ?: "Terjadi kesalahan") + } + } + } + + suspend fun getProductImage(productId: Int): String? { + if (productImageCache.containsKey(productId)) { + return productImageCache[productId] + } + val result = repository.getProductDetail(productId) + val imageUrl = if (result?.product?.image?.startsWith("/") == true) { + BASE_URL + result.product.image.removePrefix("/") + } else result?.product?.image + productImageCache[productId] = imageUrl.toString() + return imageUrl.toString() + } } \ No newline at end of file diff --git a/app/src/main/res/layout/activity_review.xml b/app/src/main/res/layout/activity_review.xml new file mode 100644 index 0000000..40b70c0 --- /dev/null +++ b/app/src/main/res/layout/activity_review.xml @@ -0,0 +1,85 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_review.xml b/app/src/main/res/layout/fragment_review.xml index dace922..84f61b0 100644 --- a/app/src/main/res/layout/fragment_review.xml +++ b/app/src/main/res/layout/fragment_review.xml @@ -1,13 +1,32 @@ - - + android:layout_height="wrap_content" + app:tabMode="scrollable" + app:tabTextAppearance="@style/label_medium_prominent" + app:tabSelectedTextAppearance="@style/label_medium_prominent" + app:tabIndicatorColor="@color/blue_500" + app:tabSelectedTextColor="@color/blue_500" + app:tabTextColor="@color/black_300" + app:tabBackground="@color/white" + app:tabPadding="13dp" + app:layout_constraintTop_toTopOf="parent"/> - \ No newline at end of file + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_review_list.xml b/app/src/main/res/layout/fragment_review_list.xml new file mode 100644 index 0000000..19438d4 --- /dev/null +++ b/app/src/main/res/layout/fragment_review_list.xml @@ -0,0 +1,41 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_sells.xml b/app/src/main/res/layout/fragment_sells.xml index 70e8e3f..2d20d1e 100644 --- a/app/src/main/res/layout/fragment_sells.xml +++ b/app/src/main/res/layout/fragment_sells.xml @@ -9,7 +9,7 @@ tools:context=".ui.profile.mystore.sells.SellsFragment"> + app:layout_constraintTop_toBottomOf="@+id/tab_layout_sells" /> \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_sells_list.xml b/app/src/main/res/layout/fragment_sells_list.xml index c74d548..8dd8bae 100644 --- a/app/src/main/res/layout/fragment_sells_list.xml +++ b/app/src/main/res/layout/fragment_sells_list.xml @@ -7,7 +7,7 @@ tools:context=".ui.profile.mystore.sells.SellsListFragment"> + android:orientation="vertical" + android:clickable="true" + android:focusable="true"> + android:layout_marginStart="8dp" + android:clickable="true" + android:focusable="true"/> diff --git a/app/src/main/res/layout/item_store_product_review.xml b/app/src/main/res/layout/item_store_product_review.xml new file mode 100644 index 0000000..43b14c6 --- /dev/null +++ b/app/src/main/res/layout/item_store_product_review.xml @@ -0,0 +1,105 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file