Merge branch 'screen-features'

# Conflicts:
#	app/src/main/AndroidManifest.xml
This commit is contained in:
shaulascr
2025-06-18 00:41:47 +07:00
39 changed files with 1225 additions and 365 deletions

View File

@ -29,6 +29,12 @@
android:theme="@style/Theme.Ecommerce_serang" android:theme="@style/Theme.Ecommerce_serang"
android:usesCleartextTraffic="true" android:usesCleartextTraffic="true"
tools:targetApi="31"> tools:targetApi="31">
<activity
android:name=".ui.product.listproduct.ListCategoryActivity"
android:exported="false" />
<activity
android:name=".ui.product.listproduct.ListProductActivity"
android:exported="false" />
<activity <activity
android:name=".ui.profile.mystore.sells.DetailSellsActivity" android:name=".ui.profile.mystore.sells.DetailSellsActivity"
android:exported="false" /> android:exported="false" />

View File

@ -12,14 +12,9 @@ import com.bumptech.glide.Glide
class HomeCategoryAdapter( class HomeCategoryAdapter(
private var categories:List<CategoryItem>, private var categories:List<CategoryItem>,
//A lambda function that will be invoked when a category item is clicked.
private val onClick:(category:CategoryItem) -> Unit private val onClick:(category:CategoryItem) -> Unit
): RecyclerView.Adapter<HomeCategoryAdapter.ViewHolder>() { ): RecyclerView.Adapter<HomeCategoryAdapter.ViewHolder>() {
/*
ViewHolder is responsible for caching and managing the view references for each item in
the RecyclerView.It binds the Category data to the corresponding views within the item layout.
*/
inner class ViewHolder(private val binding: ItemCategoryHomeBinding): RecyclerView.ViewHolder(binding.root){ inner class ViewHolder(private val binding: ItemCategoryHomeBinding): RecyclerView.ViewHolder(binding.root){
fun bind(category: CategoryItem) = with(binding) { fun bind(category: CategoryItem) = with(binding) {
Log.d("CategoriesAdapter", "Binding category: ${category.name}, Image: ${category.image}") Log.d("CategoriesAdapter", "Binding category: ${category.name}, Image: ${category.image}")
@ -59,7 +54,7 @@ class HomeCategoryAdapter(
} }
fun updateLimitedCategory(newCategories: List<CategoryItem>){ fun updateLimitedCategory(newCategories: List<CategoryItem>){
val limitedCategories = newCategories.take(10) val limitedCategories = newCategories.take(9)
updateData(limitedCategories) updateData(limitedCategories)
} }
} }

View File

@ -14,10 +14,11 @@ import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle import androidx.lifecycle.repeatOnLifecycle
import androidx.navigation.findNavController import androidx.navigation.findNavController
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.GridLayoutManager
import com.alya.ecommerce_serang.R import androidx.recyclerview.widget.RecyclerView
import com.alya.ecommerce_serang.data.api.dto.CategoryItem 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.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.api.retrofit.ApiConfig
import com.alya.ecommerce_serang.data.repository.ProductRepository import com.alya.ecommerce_serang.data.repository.ProductRepository
import com.alya.ecommerce_serang.databinding.FragmentHomeBinding import com.alya.ecommerce_serang.databinding.FragmentHomeBinding
@ -25,8 +26,9 @@ import com.alya.ecommerce_serang.ui.cart.CartActivity
import com.alya.ecommerce_serang.ui.notif.NotificationActivity import com.alya.ecommerce_serang.ui.notif.NotificationActivity
import com.alya.ecommerce_serang.ui.product.DetailProductActivity import com.alya.ecommerce_serang.ui.product.DetailProductActivity
import com.alya.ecommerce_serang.ui.product.category.CategoryProductsActivity 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.BaseViewModelFactory
import com.alya.ecommerce_serang.utils.HorizontalMarginItemDecoration
import com.alya.ecommerce_serang.utils.SessionManager import com.alya.ecommerce_serang.utils.SessionManager
import com.alya.ecommerce_serang.utils.setLightStatusBar import com.alya.ecommerce_serang.utils.setLightStatusBar
import com.alya.ecommerce_serang.utils.viewmodel.HomeUiState import com.alya.ecommerce_serang.utils.viewmodel.HomeUiState
@ -75,11 +77,6 @@ class HomeFragment : Fragment() {
} }
private fun setupRecyclerView() { private fun setupRecyclerView() {
productAdapter = HorizontalProductAdapter(
products = emptyList(),
onClick = { product -> handleProductClick(product) }
)
categoryAdapter = HomeCategoryAdapter( categoryAdapter = HomeCategoryAdapter(
categories = emptyList(), categories = emptyList(),
onClick = { category -> handleCategoryProduct(category)} onClick = { category -> handleCategoryProduct(category)}
@ -87,21 +84,28 @@ class HomeFragment : Fragment() {
binding.newProducts.apply { binding.newProducts.apply {
adapter = productAdapter adapter = productAdapter
layoutManager = LinearLayoutManager( layoutManager = GridLayoutManager(requireContext(), 2)
context,
LinearLayoutManager.HORIZONTAL,
false
)
} }
binding.categories.apply { binding.categories.apply {
adapter = categoryAdapter adapter = categoryAdapter
layoutManager = LinearLayoutManager( layoutManager = GridLayoutManager(
context, context,
LinearLayoutManager.HORIZONTAL, 3, // 3 columns
RecyclerView.VERTICAL, // vertical layout
false false
) )
} }
binding.productshowAll.setOnClickListener {
val intent = Intent(requireContext(), ListProductActivity::class.java)
startActivity(intent)
}
binding.categoryShowAll.setOnClickListener {
val intent = Intent(requireContext(), ListCategoryActivity::class.java)
startActivity(intent)
}
} }
private fun setupSearchView() { private fun setupSearchView() {
@ -155,7 +159,10 @@ class HomeFragment : Fragment() {
binding.loading.root.isVisible = false binding.loading.root.isVisible = false
binding.error.root.isVisible = false binding.error.root.isVisible = false
binding.home.isVisible = true binding.home.isVisible = true
productAdapter?.updateLimitedProducts(state.products) val products = state.products
viewModel.loadStoresForProducts(products) // << add this here
productAdapter?.updateLimitedProducts(products)
} }
is HomeUiState.Error -> { is HomeUiState.Error -> {
binding.loading.root.isVisible = false binding.loading.root.isVisible = false
@ -168,7 +175,9 @@ class HomeFragment : Fragment() {
} }
} }
} }
} }
} }
viewLifecycleOwner.lifecycleScope.launch { viewLifecycleOwner.lifecycleScope.launch {
@ -179,30 +188,56 @@ class HomeFragment : Fragment() {
} }
} }
} }
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.storeMap.collect { storeMap ->
val products = (viewModel.uiState.value as? HomeUiState.Success)?.products.orEmpty()
if (products.isNotEmpty()) {
updateProducts(products, storeMap)
}
}
}
}
}
private fun updateProducts(products: List<ProductsItem>, storeMap: Map<Int, StoreItem>) {
if (products.isEmpty()) {
Log.d("HomeFragment", "Product list is empty, hiding RecyclerView")
binding.newProducts.visibility = View.VISIBLE
} else {
Log.d("HomeFragment", "Displaying product list in RecyclerView")
binding.newProducts.visibility = View.VISIBLE // <-- Fix here
productAdapter = HorizontalProductAdapter(products, onClick = { product ->
handleProductClick(product)
}, storeMap = storeMap)
binding.newProducts.adapter = productAdapter
productAdapter?.updateProducts(products)
}
} }
private fun initUi() { private fun initUi() {
// For LightStatusBar // For LightStatusBar
setLightStatusBar() setLightStatusBar()
val banners = binding.banners // val banners = binding.banners
banners.offscreenPageLimit = 1 // banners.offscreenPageLimit = 1
//
val nextItemVisiblePx = resources.getDimension(R.dimen.viewpager_next_item_visible) // val nextItemVisiblePx = resources.getDimension(R.dimen.viewpager_next_item_visible)
val currentItemHorizontalMarginPx = // val currentItemHorizontalMarginPx =
resources.getDimension(R.dimen.viewpager_current_item_horizontal_margin) // resources.getDimension(R.dimen.viewpager_current_item_horizontal_margin)
val pageTranslationX = nextItemVisiblePx + currentItemHorizontalMarginPx // val pageTranslationX = nextItemVisiblePx + currentItemHorizontalMarginPx
//
banners.setPageTransformer { page, position -> // banners.setPageTransformer { page, position ->
page.translationX = -pageTranslationX * position // page.translationX = -pageTranslationX * position
page.scaleY = 1 - (0.25f * kotlin.math.abs(position)) // page.scaleY = 1 - (0.25f * kotlin.math.abs(position))
} // }
//
banners.addItemDecoration( // banners.addItemDecoration(
HorizontalMarginItemDecoration( // HorizontalMarginItemDecoration(
requireContext(), // requireContext(),
R.dimen.viewpager_current_item_horizontal_margin // R.dimen.viewpager_current_item_horizontal_margin
) // )
) // )
} }
private fun handleProductClick(product: ProductsItem) { private fun handleProductClick(product: ProductsItem) {

View File

@ -8,15 +8,17 @@ import androidx.recyclerview.widget.RecyclerView
import com.alya.ecommerce_serang.BuildConfig.BASE_URL import com.alya.ecommerce_serang.BuildConfig.BASE_URL
import com.alya.ecommerce_serang.R import com.alya.ecommerce_serang.R
import com.alya.ecommerce_serang.data.api.dto.ProductsItem 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 import com.bumptech.glide.Glide
class HorizontalProductAdapter( class HorizontalProductAdapter(
private var products: List<ProductsItem>, private var products: List<ProductsItem>,
private val onClick: (ProductsItem) -> Unit private val onClick: (ProductsItem) -> Unit,
private val storeMap: Map<Int, StoreItem>
) : RecyclerView.Adapter<HorizontalProductAdapter.ProductViewHolder>() { ) : RecyclerView.Adapter<HorizontalProductAdapter.ProductViewHolder>() {
inner class ProductViewHolder(private val binding: ItemProductHorizontalBinding) : inner class ProductViewHolder(private val binding: ItemProductGridBinding) :
RecyclerView.ViewHolder(binding.root) { RecyclerView.ViewHolder(binding.root) {
fun bind(product: ProductsItem) = with(binding) { fun bind(product: ProductsItem) = with(binding) {
@ -29,22 +31,25 @@ class HorizontalProductAdapter(
Log.d("ProductAdapter", "Loading image: $fullImageUrl") Log.d("ProductAdapter", "Loading image: $fullImageUrl")
itemName.text = product.name tvProductName.text = product.name
itemPrice.text = product.price tvProductPrice.text = product.price
rating.text = product.rating rating.text = product.rating
// Load image using Glide // Load image using Glide
Glide.with(itemView) Glide.with(itemView)
.load(fullImageUrl) .load(fullImageUrl)
.placeholder(R.drawable.placeholder_image) .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) } root.setOnClickListener { onClick(product) }
} }
} }
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ProductViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ProductViewHolder {
val binding = ItemProductHorizontalBinding.inflate( val binding = ItemProductGridBinding.inflate(
LayoutInflater.from(parent.context), parent, false LayoutInflater.from(parent.context), parent, false
) )
return ProductViewHolder(binding) return ProductViewHolder(binding)
@ -65,11 +70,11 @@ class HorizontalProductAdapter(
} }
fun updateLimitedProducts(newProducts: List<ProductsItem>) { fun updateLimitedProducts(newProducts: List<ProductsItem>) {
val diffCallback = ProductDiffCallback(products, newProducts) val limitedProducts = newProducts.take(10)
val limitedProducts = newProducts.take(10) //limit 10 produk val diffCallback = ProductDiffCallback(products, limitedProducts)
val diffResult = DiffUtil.calculateDiff(diffCallback) val diffResult = DiffUtil.calculateDiff(diffCallback)
products = limitedProducts
diffResult.dispatchUpdatesTo(this) diffResult.dispatchUpdatesTo(this)
updateProducts(limitedProducts)
} }
class ProductDiffCallback( class ProductDiffCallback(

View File

@ -117,9 +117,6 @@ class SearchHomeFragment : Fragment() {
} }
private fun setupSearchResultsRecyclerView() { private fun setupSearchResultsRecyclerView() {
searchResultsAdapter = SearchResultsAdapter { product ->
navigateToProductDetail(product)
}
binding.searchResultsRecyclerView.apply { binding.searchResultsRecyclerView.apply {
adapter = searchResultsAdapter adapter = searchResultsAdapter
@ -130,9 +127,26 @@ class SearchHomeFragment : Fragment() {
private fun observeData() { private fun observeData() {
viewModel.searchResults.observe(viewLifecycleOwner) { products -> viewModel.searchResults.observe(viewLifecycleOwner) { products ->
if (!products.isNullOrEmpty()){
viewModel.storeDetail(products)
}
searchResultsAdapter?.submitList(products) searchResultsAdapter?.submitList(products)
binding.noResultsText.isVisible = products.isEmpty() && !viewModel.isSearching.value!! binding.noResultsText.isVisible = products.isEmpty() && !viewModel.isSearching.value!!
binding.searchResultsRecyclerView.isVisible = products.isNotEmpty() 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 -> viewModel.isSearching.observe(viewLifecycleOwner) { isSearching ->

View File

@ -6,6 +6,7 @@ import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.alya.ecommerce_serang.data.api.dto.ProductsItem 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.ProductRepository
import com.alya.ecommerce_serang.data.repository.Result import com.alya.ecommerce_serang.data.repository.Result
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -15,6 +16,9 @@ class SearchHomeViewModel (private val productRepository: ProductRepository) : V
private val _searchResults = MutableLiveData<List<ProductsItem>>(emptyList()) private val _searchResults = MutableLiveData<List<ProductsItem>>(emptyList())
val searchResults: LiveData<List<ProductsItem>> = _searchResults val searchResults: LiveData<List<ProductsItem>> = _searchResults
private val _storeDetail = MutableLiveData<Map<Int, StoreItem>>()
val storeDetail : LiveData<Map<Int, StoreItem>> get() = _storeDetail
private val _searchHistory = MutableLiveData<List<String>>(emptyList()) private val _searchHistory = MutableLiveData<List<String>>(emptyList())
val searchHistory: LiveData<List<String>> = _searchHistory val searchHistory: LiveData<List<String>> = _searchHistory
@ -25,10 +29,10 @@ class SearchHomeViewModel (private val productRepository: ProductRepository) : V
val isSearchActive: LiveData<Boolean> = _isSearchActive val isSearchActive: LiveData<Boolean> = _isSearchActive
fun searchProducts(query: String) { fun searchProducts(query: String) {
Log.d("HomeViewModel", "searchProducts called with query: '$query'") Log.d("SearchHomeViewModel", "searchProducts called with query: '$query'")
if (query.isBlank()) { if (query.isBlank()) {
Log.d("HomeViewModel", "Query is blank, clearing results") Log.d("SearchHomeViewModel", "Query is blank, clearing results")
_searchResults.value = emptyList() _searchResults.value = emptyList()
_isSearchActive.value = false _isSearchActive.value = false
return return
@ -38,18 +42,18 @@ class SearchHomeViewModel (private val productRepository: ProductRepository) : V
_isSearchActive.value = true _isSearchActive.value = true
viewModelScope.launch { viewModelScope.launch {
Log.d("HomeViewModel", "Starting search coroutine") Log.d("SearchHomeViewModeldel", "Starting search coroutine")
when (val result = productRepository.searchProducts(query)) { when (val result = productRepository.searchProducts(query)) {
is Result.Success -> { 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) _searchResults.postValue(result.data)
// Double check the state after assignment // 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 -> { is Result.Error -> {
Log.e("HomeViewModel", "Search failed", result.exception) Log.e("SearchHomeViewModel", "Search failed", result.exception)
_searchResults.postValue(emptyList()) _searchResults.postValue(emptyList())
} }
else -> {} else -> {}
@ -58,6 +62,30 @@ class SearchHomeViewModel (private val productRepository: ProductRepository) : V
} }
} }
fun storeDetail(products: List<ProductsItem>){
viewModelScope.launch {
val map = mutableMapOf<Int, StoreItem>()
val storeIds = products.mapNotNull { it.storeId }.toSet()
for (storeId in storeIds){
try {
when (val result = productRepository.fetchStoreDetail(storeId)){
is Result.Success -> map[storeId] = result.data
is Result.Error -> Log.e("SearchHomeViewModel", "Error Loading Store")
else -> {}
}
} catch (e: Exception){
Log.e("SearchHomeViewModel", "Exception error for storeId: $storeId", e)
}
}
_storeDetail.value = map
}
}
fun loadStoreDetail(storeId: Int) {
}
fun clearSearch() { fun clearSearch() {
_isSearchActive.value = false _isSearchActive.value = false
_searchResults.value = emptyList() _searchResults.value = emptyList()
@ -68,7 +96,7 @@ class SearchHomeViewModel (private val productRepository: ProductRepository) : V
viewModelScope.launch { viewModelScope.launch {
when (val result = productRepository.getSearchHistory()) { when (val result = productRepository.getSearchHistory()) {
is Result.Success -> _searchHistory.value = result.data 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 -> {} else -> {}
} }
} }

View File

@ -6,13 +6,16 @@ import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.alya.ecommerce_serang.BuildConfig.BASE_URL
import com.alya.ecommerce_serang.R import com.alya.ecommerce_serang.R
import com.alya.ecommerce_serang.data.api.dto.ProductsItem 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.alya.ecommerce_serang.databinding.ItemProductGridBinding
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
class SearchResultsAdapter( class SearchResultsAdapter(
private val onItemClick: (ProductsItem) -> Unit private val onItemClick: (ProductsItem) -> Unit,
private val storeMap: Map<Int, StoreItem>
) : ListAdapter<ProductsItem, SearchResultsAdapter.ViewHolder>(DIFF_CALLBACK) { ) : ListAdapter<ProductsItem, SearchResultsAdapter.ViewHolder>(DIFF_CALLBACK) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
@ -43,19 +46,23 @@ class SearchResultsAdapter(
fun bind(product: ProductsItem) { fun bind(product: ProductsItem) {
binding.tvProductName.text = product.name 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 // Load image with Glide
Glide.with(binding.root.context) Glide.with(binding.root.context)
.load(product.image) .load(fullImageUrl)
.placeholder(R.drawable.placeholder_image) .placeholder(R.drawable.placeholder_image)
// .error(R.drawable.error_image) // .error(R.drawable.error_image)
.into(binding.ivProductImage) .into(binding.ivProductImage)
// Set store name if available val storeName = product.storeId?.let { storeMap[it]?.storeName } ?: "Unknown Store"
product.storeId?.toString().let { binding.tvStoreName.text = storeName
binding.tvStoreName.text = it
}
} }
} }

View File

@ -326,10 +326,10 @@ class CheckoutActivity : AppCompatActivity() {
} }
} }
// Voucher section (if implemented) // // Voucher section (if implemented)
binding.layoutVoucher?.setOnClickListener { // binding.layoutVoucher?.setOnClickListener {
Toast.makeText(this, "Voucher feature not implemented", Toast.LENGTH_SHORT).show() // Toast.makeText(this, "Voucher feature not implemented", Toast.LENGTH_SHORT).show()
} // }
} }
private val addressSelectionLauncher = registerForActivityResult( private val addressSelectionLauncher = registerForActivityResult(

View File

@ -91,7 +91,7 @@ class HistoryViewModel(private val repository: OrderRepository) : ViewModel() {
private suspend fun getAllOrdersCombined() { private suspend fun getAllOrdersCombined() {
try { try {
val allStatuses = listOf("pending", "unpaid", "processed", "shipped", "completed", "canceled") val allStatuses = listOf("unpaid", "paid", "processed", "shipped", "completed", "canceled")
val allOrders = mutableListOf<OrdersItem>() val allOrders = mutableListOf<OrdersItem>()
// Use coroutineScope to allow launching async blocks // Use coroutineScope to allow launching async blocks
@ -211,10 +211,7 @@ class HistoryViewModel(private val repository: OrderRepository) : ViewModel() {
fun refreshOrders(status: String = "all") { fun refreshOrders(status: String = "all") {
Log.d(TAG, "Refreshing orders with status: $status") Log.d(TAG, "Refreshing orders with status: $status")
// Clear current orders before fetching new ones // Don't set Loading here if you want to show current data while refreshing
_orders.value = ViewState.Loading
// Re-fetch the orders with the current status
getOrderList(status) getOrderList(status)
} }
} }

View File

@ -4,7 +4,6 @@ import android.app.Activity
import android.app.Dialog import android.app.Dialog
import android.content.ContextWrapper import android.content.ContextWrapper
import android.content.Intent import android.content.Intent
import android.graphics.Color
import android.net.Uri import android.net.Uri
import android.provider.MediaStore import android.provider.MediaStore
import android.util.Log import android.util.Log
@ -15,10 +14,10 @@ import android.view.Window
import android.widget.ArrayAdapter import android.widget.ArrayAdapter
import android.widget.AutoCompleteTextView import android.widget.AutoCompleteTextView
import android.widget.ImageView import android.widget.ImageView
import android.widget.ProgressBar
import android.widget.TextView import android.widget.TextView
import android.widget.Toast import android.widget.Toast
import androidx.fragment.app.FragmentActivity import androidx.fragment.app.FragmentActivity
import androidx.lifecycle.Observer
import androidx.lifecycle.findViewTreeLifecycleOwner import androidx.lifecycle.findViewTreeLifecycleOwner
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
@ -40,9 +39,16 @@ import java.util.TimeZone
class OrderHistoryAdapter( class OrderHistoryAdapter(
private val onOrderClickListener: (OrdersItem) -> Unit, private val onOrderClickListener: (OrdersItem) -> Unit,
private val viewModel: HistoryViewModel // Add this parameter private val viewModel: HistoryViewModel,
private val callbacks: OrderActionCallbacks
) : RecyclerView.Adapter<OrderHistoryAdapter.OrderViewHolder>() { ) : RecyclerView.Adapter<OrderHistoryAdapter.OrderViewHolder>() {
interface OrderActionCallbacks {
fun onOrderCancelled(orderId: String, success: Boolean, message: String)
fun onOrderCompleted(orderId: Int, success: Boolean, message: String)
fun onShowLoading(show: Boolean)
}
private val orders = mutableListOf<OrdersItem>() private val orders = mutableListOf<OrdersItem>()
private var fragmentStatus: String = "all" private var fragmentStatus: String = "all"
@ -140,28 +146,6 @@ class OrderHistoryAdapter(
deadlineLabel.visibility = View.GONE deadlineLabel.visibility = View.GONE
when (status) { 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" -> { "unpaid" -> {
statusOrder.apply { statusOrder.apply {
visibility = View.VISIBLE visibility = View.VISIBLE
@ -176,7 +160,6 @@ class OrderHistoryAdapter(
text = itemView.context.getString(R.string.canceled_order_btn) text = itemView.context.getString(R.string.canceled_order_btn)
setOnClickListener { setOnClickListener {
showCancelOrderBottomSheet(order.orderId) showCancelOrderBottomSheet(order.orderId)
viewModel.refreshOrders()
} }
} }
@ -198,6 +181,28 @@ class OrderHistoryAdapter(
text = formatDatePay(order.updatedAt) 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" -> { "processed" -> {
// Untuk status processed, tampilkan "Hubungi Penjual" // Untuk status processed, tampilkan "Hubungi Penjual"
statusOrder.apply { statusOrder.apply {
@ -239,11 +244,14 @@ class OrderHistoryAdapter(
visibility = View.VISIBLE visibility = View.VISIBLE
text = itemView.context.getString(R.string.claim_order) text = itemView.context.getString(R.string.claim_order)
setOnClickListener { setOnClickListener {
// Handle click event callbacks.onShowLoading(true)
// Call ViewModel
viewModel.confirmOrderCompleted(order.orderId, "completed") viewModel.confirmOrderCompleted(order.orderId, "completed")
viewModel.refreshOrders() viewModel.refreshOrders()
} }
} }
deadlineDate.apply { deadlineDate.apply {
visibility = View.VISIBLE visibility = View.VISIBLE
@ -454,52 +462,32 @@ class OrderHistoryAdapter(
} }
} }
// Show loading indicator callbacks.onShowLoading(true)
val loadingView = View(context).apply {
layoutParams = ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
)
setBackgroundColor(Color.parseColor("#80000000"))
val progressBar = ProgressBar(context).apply { // Call ViewModel method but don't observe here
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
viewModel.cancelOrderWithImage(orderId, reason, imageFile) viewModel.cancelOrderWithImage(orderId, reason, imageFile)
// Observe for success/failure // Create a one-time observer that will be removed automatically
viewModel.isSuccess.observe(itemView.findViewTreeLifecycleOwner()!!) { isSuccess -> val observer = object : Observer<Boolean> {
// Remove loading indicator override fun onChanged(isSuccess: Boolean) {
(loadingView.parent as? ViewGroup)?.removeView(loadingView) callbacks.onShowLoading(false)
if (isSuccess) { if (isSuccess) {
Toast.makeText(context, context.getString(R.string.order_canceled_successfully), Toast.LENGTH_SHORT).show() val message = viewModel.message.value ?: context.getString(R.string.order_canceled_successfully)
dialog.dismiss() callbacks.onOrderCancelled(orderId, true, message)
dialog.dismiss()
// Find the order in the list and remove it or update its status } else {
val position = orders.indexOfFirst { it.orderId.toString() == orderId } val message = viewModel.message.value ?: context.getString(R.string.failed_to_cancel_order)
if (position != -1) { callbacks.onOrderCancelled(orderId, false, message)
orders.removeAt(position)
notifyItemRemoved(position)
notifyItemRangeChanged(position, orders.size)
} }
} 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() dialog.show()
} }
@ -534,10 +522,7 @@ class OrderHistoryAdapter(
val bottomSheet = CancelOrderBottomSheet( val bottomSheet = CancelOrderBottomSheet(
orderId = orderId, orderId = orderId,
onOrderCancelled = { onOrderCancelled = {
// Handle the successful cancellation callbacks.onOrderCancelled(orderId.toString(), true, "Order cancelled successfully")
// Refresh the data
viewModel.refreshOrders() // Assuming there's a method to refresh orders
// Show a success message // Show a success message
Toast.makeText(context, "Order cancelled successfully", Toast.LENGTH_SHORT).show() Toast.makeText(context, "Order cancelled successfully", Toast.LENGTH_SHORT).show()
} }

View File

@ -44,8 +44,8 @@ class OrderHistoryFragment : Fragment() {
TabLayoutMediator(binding.tabLayout, binding.viewPager) { tab, position -> TabLayoutMediator(binding.tabLayout, binding.viewPager) { tab, position ->
tab.text = when (position) { tab.text = when (position) {
0 -> getString(R.string.all_orders) 0 -> getString(R.string.all_orders)
1 -> getString(R.string.pending_orders) 1 -> getString(R.string.unpaid_orders)
2 -> getString(R.string.unpaid_orders) 2 -> getString(R.string.paid_orders)
3 -> getString(R.string.processed_orders) 3 -> getString(R.string.processed_orders)
4 -> getString(R.string.shipped_orders) 4 -> getString(R.string.shipped_orders)
5 -> getString(R.string.completed_orders) 5 -> getString(R.string.completed_orders)

View File

@ -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.BaseViewModelFactory
import com.alya.ecommerce_serang.utils.SessionManager import com.alya.ecommerce_serang.utils.SessionManager
class OrderListFragment : Fragment() { class OrderListFragment : Fragment(), OrderHistoryAdapter.OrderActionCallbacks {
private var _binding: FragmentOrderListBinding? = null private var _binding: FragmentOrderListBinding? = null
private val binding get() = _binding!! private val binding get() = _binding!!
@ -72,6 +72,7 @@ class OrderListFragment : Fragment() {
setupRecyclerView() setupRecyclerView()
observeOrderList() observeOrderList()
observeViewModel()
observeOrderCompletionStatus() observeOrderCompletionStatus()
loadOrders() loadOrders()
} }
@ -81,7 +82,8 @@ class OrderListFragment : Fragment() {
onOrderClickListener = { order -> onOrderClickListener = { order ->
navigateToOrderDetail(order) navigateToOrderDetail(order)
}, },
viewModel = viewModel viewModel = viewModel,
callbacks = this // Pass this fragment as callback
) )
orderAdapter.setFragmentStatus(status) 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() { private fun loadOrders() {
// Simple - just call getOrderList for any status including "all" // Simple - just call getOrderList for any status including "all"
viewModel.getOrderList(status) viewModel.getOrderList(status)
@ -142,6 +178,30 @@ class OrderListFragment : Fragment() {
detailOrderLauncher.launch(intent) 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() { override fun onDestroyView() {
super.onDestroyView() super.onDestroyView()
_binding = null _binding = null

View File

@ -11,8 +11,8 @@ class OrderViewPagerAdapter(
// Define all possible order statuses // Define all possible order statuses
private val orderStatuses = listOf( private val orderStatuses = listOf(
"all", // All orders "all", // All orders
"pending", // Menunggu Tagihan "unpaid", // Menunggu Tagihan
"unpaid", // Belum Dibayar "paid", // Belum Dibayar
"processed", // Diproses "processed", // Diproses
"shipped", // Dikirim "shipped", // Dikirim
"completed", // Selesai "completed", // Selesai

View File

@ -234,8 +234,8 @@ class DetailOrderStatusActivity : AppCompatActivity() {
// Set status header // Set status header
val statusText = when(status) { val statusText = when(status) {
"pending" -> "Belum Bayar"
"unpaid" -> "Belum Bayar" "unpaid" -> "Belum Bayar"
"paid" -> "Sudah Dibayar"
"processed" -> "Diproses" "processed" -> "Diproses"
"shipped" -> "Dikirim" "shipped" -> "Dikirim"
"delivered" -> "Diterima" "delivered" -> "Diterima"
@ -248,22 +248,6 @@ class DetailOrderStatusActivity : AppCompatActivity() {
Log.d(TAG, "adjustButtonsBasedOnStatus: Status header set to '$statusText'") Log.d(TAG, "adjustButtonsBasedOnStatus: Status header set to '$statusText'")
when (status) { 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" -> { "unpaid" -> {
Log.d(TAG, "adjustButtonsBasedOnStatus: Setting up UI for pending/unpaid order") 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" -> { "processed" -> {
Log.d(TAG, "adjustButtonsBasedOnStatus: Setting up UI for processed order") Log.d(TAG, "adjustButtonsBasedOnStatus: Setting up UI for processed order")

View File

@ -31,7 +31,6 @@ import com.alya.ecommerce_serang.data.repository.Result
import com.alya.ecommerce_serang.databinding.ActivityDetailProductBinding import com.alya.ecommerce_serang.databinding.ActivityDetailProductBinding
import com.alya.ecommerce_serang.ui.cart.CartActivity import com.alya.ecommerce_serang.ui.cart.CartActivity
import com.alya.ecommerce_serang.ui.chat.ChatActivity 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.order.CheckoutActivity
import com.alya.ecommerce_serang.ui.product.storeDetail.StoreDetailActivity import com.alya.ecommerce_serang.ui.product.storeDetail.StoreDetailActivity
import com.alya.ecommerce_serang.utils.BaseViewModelFactory import com.alya.ecommerce_serang.utils.BaseViewModelFactory
@ -45,7 +44,7 @@ class DetailProductActivity : AppCompatActivity() {
private lateinit var binding: ActivityDetailProductBinding private lateinit var binding: ActivityDetailProductBinding
private lateinit var apiService: ApiService private lateinit var apiService: ApiService
private lateinit var sessionManager: SessionManager private lateinit var sessionManager: SessionManager
private var productAdapter: HorizontalProductAdapter? = null private var productAdapter: OtherProductAdapter? = null
private var reviewsAdapter: ReviewsAdapter? = null private var reviewsAdapter: ReviewsAdapter? = null
private var currentQuantity = 1 private var currentQuantity = 1
private var isWholesaleAvailable: Boolean = false private var isWholesaleAvailable: Boolean = false
@ -125,7 +124,8 @@ class DetailProductActivity : AppCompatActivity() {
} }
viewModel.otherProducts.observe(this) { products -> viewModel.otherProducts.observe(this) { products ->
updateOtherProducts(products) viewModel.loadStoresForProducts(products)
// updateOtherProducts(products)
} }
viewModel.reviewProduct.observe(this) { reviews -> 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?) { private fun updateStoreInfo(store: StoreItem?) {
@ -178,13 +185,21 @@ class DetailProductActivity : AppCompatActivity() {
} }
} }
private fun updateOtherProducts(products: List<ProductsItem>) {
private fun updateOtherProducts(products: List<ProductsItem>, storeMap: Map<Int, StoreItem>) {
if (products.isEmpty()) { 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 binding.tvViewAllProducts.visibility = View.GONE
} else { } else {
Log.d("DetailProductActivity", "Displaying product list in RecyclerView")
binding.recyclerViewOtherProducts.visibility = View.VISIBLE binding.recyclerViewOtherProducts.visibility = View.VISIBLE
binding.tvViewAllProducts.visibility = View.VISIBLE binding.tvViewAllProducts.visibility = View.VISIBLE
productAdapter = OtherProductAdapter(products, onClick = { product ->
handleProductClick(product)
}, storeMap = storeMap)
binding.recyclerViewOtherProducts.adapter = productAdapter
productAdapter?.updateProducts(products) productAdapter?.updateProducts(products)
} }
} }
@ -275,10 +290,6 @@ class DetailProductActivity : AppCompatActivity() {
} }
private fun setupRecyclerViewOtherProducts(){ private fun setupRecyclerViewOtherProducts(){
productAdapter = HorizontalProductAdapter(
products = emptyList(),
onClick = { productsItem -> handleProductClick(productsItem) }
)
binding.recyclerViewOtherProducts.apply { binding.recyclerViewOtherProducts.apply {
adapter = productAdapter adapter = productAdapter
@ -349,6 +360,8 @@ class DetailProductActivity : AppCompatActivity() {
val btnClose = view.findViewById<ImageButton>(R.id.btnCloseDialog) val btnClose = view.findViewById<ImageButton>(R.id.btnCloseDialog)
val switchWholesale = view.findViewById<SwitchCompat>(R.id.switch_price) val switchWholesale = view.findViewById<SwitchCompat>(R.id.switch_price)
val titleWholesale = view.findViewById<TextView>(R.id.tv_active_wholesale)
// val descWholesale = view.findViewById<TextView>(R.id.tv_desc_wholesale)
if (!isBuyNow) { if (!isBuyNow) {
btnBuyNow.setText(R.string.add_to_cart) btnBuyNow.setText(R.string.add_to_cart)
@ -357,10 +370,18 @@ class DetailProductActivity : AppCompatActivity() {
switchWholesale.isEnabled = isWholesaleAvailable switchWholesale.isEnabled = isWholesaleAvailable
switchWholesale.isChecked = isWholesaleSelected switchWholesale.isChecked = isWholesaleSelected
// Set initial quantity based on current selection
currentQuantity = if (isWholesaleSelected) minOrder else 1 currentQuantity = if (isWholesaleSelected) minOrder else 1
tvQuantity.text = currentQuantity.toString() 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 -> switchWholesale.setOnCheckedChangeListener { _, isChecked ->
isWholesaleSelected = isChecked isWholesaleSelected = isChecked

View File

@ -0,0 +1,94 @@
package com.alya.ecommerce_serang.ui.product
import android.util.Log
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import com.alya.ecommerce_serang.BuildConfig.BASE_URL
import com.alya.ecommerce_serang.R
import com.alya.ecommerce_serang.data.api.dto.ProductsItem
import com.alya.ecommerce_serang.data.api.response.customer.product.StoreItem
import com.alya.ecommerce_serang.databinding.ItemProductHorizontalBinding
import com.bumptech.glide.Glide
class OtherProductAdapter (
private var products: List<ProductsItem>,
private val onClick: (ProductsItem) -> Unit,
private val storeMap: Map<Int, StoreItem>
) : RecyclerView.Adapter<OtherProductAdapter.ProductViewHolder>() {
inner class ProductViewHolder(private val binding: ItemProductHorizontalBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(product: ProductsItem) = with(binding) {
val fullImageUrl = if (product.image.startsWith("/")) {
BASE_URL + product.image.removePrefix("/") // Append base URL if the path starts with "/"
} else {
product.image // Use as is if it's already a full URL
}
Log.d("ProductAdapter", "Loading image: $fullImageUrl")
tvProductName.text = product.name
tvProductPrice.text = product.price
rating.text = product.rating
// Load image using Glide
Glide.with(itemView)
.load(fullImageUrl)
.placeholder(R.drawable.placeholder_image)
.into(ivProductImage)
val storeName = product.storeId?.let { storeMap[it]?.storeName } ?: "Unknown Store"
binding.tvStoreName.text = storeName
root.setOnClickListener { onClick(product) }
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ProductViewHolder {
val binding = ItemProductHorizontalBinding.inflate(
LayoutInflater.from(parent.context), parent, false
)
return ProductViewHolder(binding)
}
override fun getItemCount() = products.size
override fun onBindViewHolder(holder: ProductViewHolder, position: Int) {
holder.bind(products[position])
}
fun updateProducts(newProducts: List<ProductsItem>) {
val diffCallback = ProductDiffCallback(products, newProducts)
val diffResult = DiffUtil.calculateDiff(diffCallback)
products = newProducts
diffResult.dispatchUpdatesTo(this)
notifyDataSetChanged()
}
fun updateLimitedProducts(newProducts: List<ProductsItem>) {
val limitedProducts = newProducts.take(10)
val diffCallback = ProductDiffCallback(products, limitedProducts)
val diffResult = DiffUtil.calculateDiff(diffCallback)
products = limitedProducts
diffResult.dispatchUpdatesTo(this)
}
class ProductDiffCallback(
private val oldList: List<ProductsItem>,
private val newList: List<ProductsItem>
) : DiffUtil.Callback() {
override fun getOldListSize() = oldList.size
override fun getNewListSize() = newList.size
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int) =
oldList[oldItemPosition].id == newList[newItemPosition].id // Compare unique IDs
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int) =
oldList[oldItemPosition] == newList[newItemPosition] // Compare entire object
}
}

View File

@ -20,9 +20,15 @@ class ProductUserViewModel(private val repository: ProductRepository) : ViewMode
private val _productDetail = MutableLiveData<Product?>() private val _productDetail = MutableLiveData<Product?>()
val productDetail: LiveData<Product?> get() = _productDetail val productDetail: LiveData<Product?> get() = _productDetail
private val _productList = MutableLiveData<Result<List<ProductsItem>>>()
val productList: LiveData<Result<List<ProductsItem>>> get() = _productList
private val _storeDetail = MutableLiveData<Result<StoreItem>>() private val _storeDetail = MutableLiveData<Result<StoreItem>>()
val storeDetail : LiveData<Result<StoreItem>> get() = _storeDetail val storeDetail : LiveData<Result<StoreItem>> get() = _storeDetail
private val _storeMap = MutableLiveData<Map<Int, StoreItem>>()
val storeMap: LiveData<Map<Int, StoreItem>> get() = _storeMap
private val _reviewProduct = MutableLiveData<List<ReviewsItem>>() private val _reviewProduct = MutableLiveData<List<ReviewsItem>>()
val reviewProduct: LiveData<List<ReviewsItem>> get() = _reviewProduct val reviewProduct: LiveData<List<ReviewsItem>> get() = _reviewProduct
@ -59,6 +65,23 @@ class ProductUserViewModel(private val repository: ProductRepository) : ViewMode
} }
} }
fun loadProductsList() {
_isLoading.value = true
viewModelScope.launch {
try {
val result = repository.getAllProducts()
_productList.value = result
} catch (e: Exception) {
Log.e(TAG, "Error loading product list: ${e.message}")
_error.value = "Failed to load product list ${e.message}"
} finally {
_isLoading.value = false
}
}
}
fun loadStoreDetail(storeId: Int) { fun loadStoreDetail(storeId: Int) {
viewModelScope.launch { viewModelScope.launch {
try { try {
@ -73,6 +96,28 @@ class ProductUserViewModel(private val repository: ProductRepository) : ViewMode
} }
} }
fun loadStoresForProducts(products: List<ProductsItem>) {
viewModelScope.launch {
val map = mutableMapOf<Int, StoreItem>()
val storeIds = products.mapNotNull { it.storeId }.toSet()
for (storeId in storeIds) {
try {
val result = repository.fetchStoreDetail(storeId)
if (result is Result.Success) {
map[storeId] = result.data
} else if (result is Result.Error) {
Log.e(TAG, "Failed to load storeId $storeId", result.exception)
}
} catch (e: Exception) {
Log.e(TAG, "Exception fetching storeId $storeId", e)
}
}
_storeMap.postValue(map)
}
}
fun loadReviews(productId: Int) { fun loadReviews(productId: Int) {
viewModelScope.launch { viewModelScope.launch {
try { try {

View File

@ -2,7 +2,6 @@ package com.alya.ecommerce_serang.ui.product.category
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.ViewGroup import android.view.ViewGroup
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.alya.ecommerce_serang.BuildConfig import com.alya.ecommerce_serang.BuildConfig
import com.alya.ecommerce_serang.R import com.alya.ecommerce_serang.R
@ -59,14 +58,14 @@ class ProductsCategoryAdapter(
onClick(product) onClick(product)
} }
// Optional: Show stock status // // Optional: Show stock status
if (product.stock > 0) { // if (product.stock > 0) {
tvStockStatus.text = "Stock: ${product.stock}" // tvStockStatus.text = "Stock: ${product.stock}"
tvStockStatus.setTextColor(ContextCompat.getColor(itemView.context, R.color.green)) // tvStockStatus.setTextColor(ContextCompat.getColor(itemView.context, R.color.green))
} else { // } else {
tvStockStatus.text = "Out of Stock" // tvStockStatus.text = "Out of Stock"
tvStockStatus.setTextColor(ContextCompat.getColor(itemView.context, R.color.red)) // tvStockStatus.setTextColor(ContextCompat.getColor(itemView.context, R.color.red))
} // }
} }
} }
} }

View File

@ -0,0 +1,112 @@
package com.alya.ecommerce_serang.ui.product.listproduct
import android.content.Intent
import android.os.Bundle
import android.util.Log
import android.view.View
import androidx.activity.enableEdgeToEdge
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.ViewCompat
import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.alya.ecommerce_serang.data.api.dto.CategoryItem
import com.alya.ecommerce_serang.data.api.retrofit.ApiConfig
import com.alya.ecommerce_serang.data.repository.ProductRepository
import com.alya.ecommerce_serang.databinding.ActivityListCategoryBinding
import com.alya.ecommerce_serang.ui.product.category.CategoryProductsActivity
import com.alya.ecommerce_serang.utils.BaseViewModelFactory
import com.alya.ecommerce_serang.utils.SessionManager
import com.alya.ecommerce_serang.utils.viewmodel.HomeViewModel
class ListCategoryActivity : AppCompatActivity() {
companion object {
private val TAG = "ListCategoryActivity"
}
private lateinit var binding: ActivityListCategoryBinding
private lateinit var sessionManager: SessionManager
private var categoryAdapter: ListCategoryAdapter? = null
private val viewModel: HomeViewModel by viewModels {
BaseViewModelFactory {
val apiService = ApiConfig.getApiService(sessionManager)
val repository = ProductRepository(apiService)
HomeViewModel(repository)
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityListCategoryBinding.inflate(layoutInflater)
setContentView(binding.root)
sessionManager = SessionManager(this)
WindowCompat.setDecorFitsSystemWindows(window, false)
enableEdgeToEdge()
ViewCompat.setOnApplyWindowInsetsListener(binding.root) { view, windowInsets ->
val systemBars = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
view.setPadding(
systemBars.left,
systemBars.top,
systemBars.right,
systemBars.bottom
)
windowInsets
}
setupToolbar()
setupObserver()
viewModel.loadCategory()
setupRecyclerView()
}
private fun setupToolbar(){
binding.header.headerLeftIcon.setOnClickListener{
finish()
}
binding.header.headerTitle.text = "Kategori Produk"
}
private fun setupRecyclerView(){
categoryAdapter = ListCategoryAdapter(
categories = emptyList(),
onClick = { category -> handleClickOnCategory(category) }
)
binding.rvListCategories.apply {
adapter = categoryAdapter
layoutManager = GridLayoutManager(
context,
3, // 3 columns
RecyclerView.VERTICAL,
false
)
}
}
private fun setupObserver(){
viewModel.category.observe(this) { category ->
Log.d("ListCategoryActivity", "Received categories: ${category.size}")
updateCategories(category)
}
}
private fun updateCategories(category: List<CategoryItem>){
if (category.isEmpty()) {
binding.rvListCategories.visibility = View.GONE
} else {
binding.rvListCategories.visibility = View.VISIBLE
categoryAdapter?.updateData(category)
}
}
private fun handleClickOnCategory(category: CategoryItem){
val intent = Intent(this, CategoryProductsActivity::class.java)
intent.putExtra(CategoryProductsActivity.EXTRA_CATEGORY, category)
startActivity(intent)
}
}

View File

@ -0,0 +1,55 @@
package com.alya.ecommerce_serang.ui.product.listproduct
import android.util.Log
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.alya.ecommerce_serang.BuildConfig.BASE_URL
import com.alya.ecommerce_serang.R
import com.alya.ecommerce_serang.data.api.dto.CategoryItem
import com.alya.ecommerce_serang.databinding.ItemCategoryHomeBinding
import com.bumptech.glide.Glide
class ListCategoryAdapter(
private var categories:List<CategoryItem>,
private val onClick:(category: CategoryItem) -> Unit
): RecyclerView.Adapter<ListCategoryAdapter.ViewHolder>() {
inner class ViewHolder(private val binding: ItemCategoryHomeBinding): RecyclerView.ViewHolder(binding.root){
fun bind(category: CategoryItem) = with(binding) {
Log.d("CategoriesAdapter", "Binding category: ${category.name}, Image: ${category.image}")
val fullImageUrl = if (category.image.startsWith("/")) {
BASE_URL + category.image.removePrefix("/") // Append base URL if the path starts with "/"
} else {
category.image // Use as is if it's already a full URL
}
Log.d("CategoriesAdapter", "Loading image: $fullImageUrl")
Glide.with(itemView.context)
.load(fullImageUrl) // Ensure full URL
.placeholder(R.drawable.placeholder_image)
.into(imageCategory)
name.text = category.name
root.setOnClickListener { onClick(category) }
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ViewHolder (
ItemCategoryHomeBinding.inflate(LayoutInflater.from(parent.context),parent,false)
)
override fun getItemCount() = categories.size
override fun onBindViewHolder(holder: ListCategoryAdapter.ViewHolder, position: Int) {
holder.bind(categories[position])
}
fun updateData(newCategories: List<CategoryItem>) {
categories = newCategories.toList()
notifyDataSetChanged()
}
}

View File

@ -0,0 +1,133 @@
package com.alya.ecommerce_serang.ui.product.listproduct
import android.content.Intent
import android.os.Bundle
import android.util.Log
import android.view.View
import androidx.activity.enableEdgeToEdge
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.ViewCompat
import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import com.alya.ecommerce_serang.data.api.dto.ProductsItem
import com.alya.ecommerce_serang.data.api.response.customer.product.StoreItem
import com.alya.ecommerce_serang.data.api.retrofit.ApiConfig
import com.alya.ecommerce_serang.data.repository.ProductRepository
import com.alya.ecommerce_serang.data.repository.Result
import com.alya.ecommerce_serang.databinding.ActivityListProductBinding
import com.alya.ecommerce_serang.ui.product.DetailProductActivity
import com.alya.ecommerce_serang.ui.product.ProductUserViewModel
import com.alya.ecommerce_serang.utils.BaseViewModelFactory
import com.alya.ecommerce_serang.utils.SessionManager
class ListProductActivity : AppCompatActivity() {
companion object {
private val TAG = "ListProductActivity"
}
private lateinit var binding: ActivityListProductBinding
private lateinit var sessionManager: SessionManager
private var productAdapter: ListProductAdapter? = null
private val viewModel: ProductUserViewModel by viewModels {
BaseViewModelFactory {
val apiService = ApiConfig.getApiService(sessionManager)
val repository = ProductRepository(apiService)
ProductUserViewModel(repository)
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityListProductBinding.inflate(layoutInflater)
setContentView(binding.root)
sessionManager = SessionManager(this)
WindowCompat.setDecorFitsSystemWindows(window, false)
enableEdgeToEdge()
ViewCompat.setOnApplyWindowInsetsListener(binding.root) { view, windowInsets ->
val systemBars = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
view.setPadding(
systemBars.left,
systemBars.top,
systemBars.right,
systemBars.bottom
)
windowInsets
}
setupObserver()
setupRecyclerView()
viewModel.loadProductsList()
}
private fun setupRecyclerView() {
binding.rvProductsList.apply {
adapter = productAdapter
layoutManager = GridLayoutManager(
context,
2,
LinearLayoutManager.VERTICAL,
false
)
}
Log.d(TAG, "RecyclerView setup complete with GridLayoutManager")
}
private fun setupObserver(){
viewModel.productList.observe(this) { result ->
when (result) {
is Result.Success -> {
val products = result.data
viewModel.loadStoresForProducts(products)
Log.d(TAG, "Product list loaded successfully: ${products.size} items")
}
is Result.Error -> {
Log.e(TAG, "Failed to load products: ${result.exception.message}")
}
is Result.Loading -> {
// Show loading indicator if needed
}
}
}
viewModel.storeMap.observe(this){ storeMap ->
val products = (viewModel.productList.value as? Result.Success)?.data.orEmpty()
if (products.isNotEmpty()) {
updateProducts(products, storeMap)
}
}
}
private fun updateProducts(products: List<ProductsItem>, storeMap: Map<Int, StoreItem>) {
if (products.isEmpty()) {
Log.d(TAG, "Product list is empty, hiding RecyclerView")
binding.rvProductsList.visibility = View.VISIBLE
} else {
Log.d(TAG, "Displaying product list in RecyclerView")
binding.rvProductsList.visibility = View.VISIBLE // <-- Fix here
productAdapter = ListProductAdapter(products, onClick = { product ->
handleProductClick(product)
}, storeMap = storeMap)
binding.rvProductsList.adapter = productAdapter
productAdapter?.updateProducts(products)
}
}
private fun handleProductClick(product: ProductsItem) {
val intent = Intent(this, DetailProductActivity::class.java)
intent.putExtra("PRODUCT_ID", product.id) // Pass product ID
startActivity(intent)
}
}

View File

@ -0,0 +1,85 @@
package com.alya.ecommerce_serang.ui.product.listproduct
import android.util.Log
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import com.alya.ecommerce_serang.BuildConfig.BASE_URL
import com.alya.ecommerce_serang.R
import com.alya.ecommerce_serang.data.api.dto.ProductsItem
import com.alya.ecommerce_serang.data.api.response.customer.product.StoreItem
import com.alya.ecommerce_serang.databinding.ItemProductGridBinding
import com.bumptech.glide.Glide
class ListProductAdapter(
private var products: List<ProductsItem>,
private val onClick: (ProductsItem) -> Unit,
private val storeMap: Map<Int, StoreItem>
): RecyclerView.Adapter<ListProductAdapter.ProductViewHolder>() {
inner class ProductViewHolder(private val binding: ItemProductGridBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(product: ProductsItem) = with(binding) {
tvProductName.text = product.name
tvProductPrice.text = product.price
rating.text = product.rating
val fullImageUrl = if (product.image.startsWith("/")) {
BASE_URL + product.image.removePrefix("/") // Append base URL if the path starts with "/"
} else {
product.image // Use as is if it's already a full URL
}
Log.d("ProductAdapter", "Loading image: $fullImageUrl")
// Load image using Glide
Glide.with(itemView)
.load(fullImageUrl)
.placeholder(R.drawable.placeholder_image)
.into(ivProductImage)
val storeName = product.storeId?.let { storeMap[it]?.storeName } ?: "Unknown Store"
binding.tvStoreName.text = storeName
root.setOnClickListener { onClick(product) }
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ProductViewHolder {
val binding = ItemProductGridBinding.inflate(
LayoutInflater.from(parent.context), parent, false
)
return ProductViewHolder(binding)
}
override fun getItemCount() = products.size
override fun onBindViewHolder(holder: ProductViewHolder, position: Int) {
holder.bind(products[position])
}
fun updateProducts(newProducts: List<ProductsItem>) {
val diffCallback = ProductDiffCallback(products, newProducts)
val diffResult = DiffUtil.calculateDiff(diffCallback)
products = newProducts
diffResult.dispatchUpdatesTo(this)
notifyDataSetChanged()
}
class ProductDiffCallback(
private val oldList: List<ProductsItem>,
private val newList: List<ProductsItem>
) : DiffUtil.Callback() {
override fun getOldListSize() = oldList.size
override fun getNewListSize() = newList.size
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int) =
oldList[oldItemPosition].id == newList[newItemPosition].id // Compare unique IDs
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int) =
oldList[oldItemPosition] == newList[newItemPosition] // Compare entire object
}
}

View File

@ -11,7 +11,7 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.ViewCompat import androidx.core.view.ViewCompat
import androidx.core.view.WindowCompat import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat 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.BuildConfig.BASE_URL
import com.alya.ecommerce_serang.R import com.alya.ecommerce_serang.R
import com.alya.ecommerce_serang.data.api.dto.ProductsItem import com.alya.ecommerce_serang.data.api.dto.ProductsItem
@ -101,7 +101,15 @@ class StoreDetailActivity : AppCompatActivity() {
} }
} }
viewModel.otherProducts.observe(this) { products -> viewModel.otherProducts.observe(this) { products ->
updateOtherProducts(products) viewModel.loadStoresForProducts(products)
// updateOtherProducts(products)
}
viewModel.storeMap.observe(this){ storeMap ->
val products = viewModel.otherProducts.value.orEmpty()
if (products.isNotEmpty()) {
updateProducts(products, storeMap)
}
} }
} }
@ -140,28 +148,28 @@ class StoreDetailActivity : AppCompatActivity() {
} }
} }
private fun updateOtherProducts(products: List<ProductsItem>) { private fun updateProducts(products: List<ProductsItem>, storeMap: Map<Int, StoreItem>) {
if (products.isEmpty()) { if (products.isEmpty()) {
binding.rvProducts.visibility = View.GONE binding.rvProducts.visibility = View.GONE
Log.d("StoreDetailActivity", "Product list is empty, hiding RecyclerView")
} else { } else {
Log.d("StoreDetailActivity", "Displaying product list in RecyclerView")
binding.rvProducts.visibility = View.VISIBLE binding.rvProducts.visibility = View.VISIBLE
productAdapter = HorizontalProductAdapter(products, onClick = { product ->
handleProductClick(product)
}, storeMap = storeMap)
binding.rvProducts.adapter = productAdapter
productAdapter?.updateProducts(products) productAdapter?.updateProducts(products)
} }
} }
private fun setupRecyclerViewOtherProducts(){ private fun setupRecyclerViewOtherProducts(){
productAdapter = HorizontalProductAdapter(
products = emptyList(),
onClick = { productsItem -> handleProductClick(productsItem) }
)
binding.rvProducts.apply { binding.rvProducts.apply {
adapter = productAdapter adapter = productAdapter
layoutManager = LinearLayoutManager( layoutManager = GridLayoutManager(context, 2)
context,
LinearLayoutManager.HORIZONTAL,
false
)
} }
} }

View File

@ -21,6 +21,9 @@ class StoreDetailViewModel (private val repository: ProductRepository
private val _otherProducts = MutableLiveData<List<ProductsItem>>() private val _otherProducts = MutableLiveData<List<ProductsItem>>()
val otherProducts: LiveData<List<ProductsItem>> get() = _otherProducts val otherProducts: LiveData<List<ProductsItem>> get() = _otherProducts
private val _storeMap = MutableLiveData<Map<Int, StoreItem>>()
val storeMap: LiveData<Map<Int, StoreItem>> get() = _storeMap
private val _isLoading = MutableLiveData<Boolean>() private val _isLoading = MutableLiveData<Boolean>()
val isLoading: LiveData<Boolean> get() = _isLoading val isLoading: LiveData<Boolean> get() = _isLoading
@ -84,4 +87,26 @@ class StoreDetailViewModel (private val repository: ProductRepository
} }
} }
} }
fun loadStoresForProducts(products: List<ProductsItem>) {
viewModelScope.launch {
val map = mutableMapOf<Int, StoreItem>()
val storeIds = products.mapNotNull { it.storeId }.toSet()
for (storeId in storeIds) {
try {
val result = repository.fetchStoreDetail(storeId)
if (result is Result.Success) {
map[storeId] = result.data
} else if (result is Result.Error) {
Log.e("ProductViewModel", "Failed to load storeId $storeId", result.exception)
}
} catch (e: Exception) {
Log.e("ProductViewModel", "Exception fetching storeId $storeId", e)
}
}
_storeMap.postValue(map)
}
}
} }

View File

@ -1,10 +1,13 @@
package com.alya.ecommerce_serang.utils.viewmodel package com.alya.ecommerce_serang.utils.viewmodel
import android.util.Log import android.util.Log
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.alya.ecommerce_serang.data.api.dto.CategoryItem 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.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.ProductRepository
import com.alya.ecommerce_serang.data.repository.Result import com.alya.ecommerce_serang.data.repository.Result
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
@ -21,6 +24,12 @@ class HomeViewModel (
private val _categories = MutableStateFlow<List<CategoryItem>>(emptyList()) private val _categories = MutableStateFlow<List<CategoryItem>>(emptyList())
val categories: StateFlow<List<CategoryItem>> = _categories.asStateFlow() val categories: StateFlow<List<CategoryItem>> = _categories.asStateFlow()
private val _storeMap = MutableStateFlow<Map<Int, StoreItem>>(emptyMap())
val storeMap: StateFlow<Map<Int, StoreItem>> = _storeMap.asStateFlow()
private val _category = MutableLiveData<List<CategoryItem>>()
val category: LiveData<List<CategoryItem>> get() = _category
init { init {
loadProducts() loadProducts()
loadCategories() loadCategories()
@ -47,6 +56,38 @@ class HomeViewModel (
} }
} }
fun loadStoresForProducts(products: List<ProductsItem>) {
viewModelScope.launch {
val map = mutableMapOf<Int, StoreItem>()
val storeIds = products.mapNotNull { it.storeId }.toSet()
for (storeId in storeIds) {
try {
val result = productRepository.fetchStoreDetail(storeId)
if (result is Result.Success) {
map[storeId] = result.data
} else if (result is Result.Error) {
Log.e("HomeViewModel", "Failed to load storeId $storeId", result.exception)
}
} catch (e: Exception) {
Log.e("HomeViewModel", "Exception fetching storeId $storeId", e)
}
}
_storeMap.value = map
}
}
fun loadCategory() {
viewModelScope.launch {
when (val result = productRepository.getAllCategories()) {
is Result.Success -> _category.value = result.data
is Result.Error -> Log.e("HomeViewModel", "Failed to fetch categories", result.exception)
is Result.Loading -> {}
}
}
}
fun retry() { fun retry() {
loadProducts() loadProducts()

View File

@ -142,35 +142,6 @@
android:layout_height="8dp" android:layout_height="8dp"
android:background="#F5F5F5" /> android:background="#F5F5F5" />
<!-- Voucher Section -->
<LinearLayout
android:id="@+id/layout_voucher"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:background="@color/white"
android:padding="16dp"
android:gravity="center_vertical">
<!-- <ImageView-->
<!-- android:layout_width="24dp"-->
<!-- android:layout_height="24dp"-->
<!-- android:src="@drawable/ic_voucher_24" />-->
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Gunakan Voucher"
android:textSize="14sp"
android:layout_marginStart="8dp" />
<ImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:src="@drawable/ic_arrow_right" />
</LinearLayout>
<View <View
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="8dp" android:layout_height="8dp"

View File

@ -487,7 +487,7 @@
android:orientation="horizontal" android:orientation="horizontal"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:itemCount="3" tools:itemCount="3"
tools:listitem="@layout/item_related_product" /> tools:listitem="@layout/item_product_horizontal" />
</LinearLayout> </LinearLayout>
</androidx.cardview.widget.CardView> </androidx.cardview.widget.CardView>

View File

@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ui.product.listproduct.ListCategoryActivity">
<include
android:id="@+id/header"
layout="@layout/header" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rvListCategories"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:paddingStart="8dp"
android:paddingEnd="8dp"
android:clipChildren="false"
android:clipToPadding="false"
app:layout_constraintTop_toBottomOf="@id/header"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
tools:listitem="@layout/item_category_home" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,41 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
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">
<include
android:id="@+id/searchContainerList"
layout="@layout/view_search_back"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintBottom_toTopOf="@id/rvProductsList"/>
<!-- <com.google.android.material.divider.MaterialDivider-->
<!-- android:id="@+id/divider_product"-->
<!-- android:layout_width="match_parent"-->
<!-- android:layout_height="wrap_content"-->
<!-- android:layout_marginTop="2dp"-->
<!-- app:layout_constraintTop_toBottomOf="@id/searchContainer"/>-->
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rvProductsList"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingHorizontal="23dp"
app:layout_constraintTop_toBottomOf="@id/searchContainerList"
app:layout_constraintBottom_toBottomOf="parent"
android:layout_marginTop="4dp"
app:spanCount="2"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
tools:listitem="@layout/item_product_grid"
app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"/>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -158,14 +158,13 @@
<androidx.recyclerview.widget.RecyclerView <androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_products" android:id="@+id/rv_products"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="0dp"
android:layout_marginTop="16dp" android:layout_marginTop="16dp"
android:layout_marginBottom="16dp" android:layout_marginBottom="16dp"
android:orientation="vertical" app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:layout_constraintTop_toBottomOf="@id/divider_product" app:layout_constraintTop_toBottomOf="@id/divider_product"
tools:itemCount="5" app:spanCount="2"
tools:listitem="@layout/item_section_horizontal" /> tools:listitem="@layout/item_product_grid" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -39,14 +39,15 @@
app:layout_constraintTop_toTopOf="parent" /> app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>
<TextView <!-- <TextView-->
android:layout_width="match_parent" <!-- android:id="@+id/tv_desc_wholesale"-->
android:layout_height="wrap_content" <!-- android:layout_width="match_parent"-->
android:text="Minimal pembelian 10 buah untuk harga grosir" <!-- android:layout_height="wrap_content"-->
android:fontFamily="@font/dmsans_mediumitalic" <!-- android:text="Minimal pembelian 10 buah untuk harga grosir"-->
android:textSize="12sp" <!-- android:fontFamily="@font/dmsans_mediumitalic"-->
android:textColor="@color/black_300" <!-- android:textSize="12sp"-->
android:layout_marginBottom="8dp"/> <!-- android:textColor="@color/black_300"-->
<!-- android:layout_marginBottom="8dp"/>-->
<androidx.constraintlayout.widget.ConstraintLayout <androidx.constraintlayout.widget.ConstraintLayout

View File

@ -15,48 +15,39 @@
android:layout_marginTop="16dp" android:layout_marginTop="16dp"
app:layout_constraintTop_toTopOf="parent" /> app:layout_constraintTop_toTopOf="parent" />
<!-- Home content in ScrollView --> <androidx.core.widget.NestedScrollView
<ScrollView
android:id="@+id/home" android:id="@+id/home"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="0dp" android:layout_height="0dp"
android:fillViewport="true"
android:overScrollMode="never"
app:layout_constraintTop_toBottomOf="@id/searchContainer" app:layout_constraintTop_toBottomOf="@id/searchContainer"
app:layout_constraintBottom_toBottomOf="parent"> app:layout_constraintBottom_toBottomOf="parent">
<!-- Your existing home content here -->
<androidx.constraintlayout.widget.ConstraintLayout <androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content"> android:layout_height="wrap_content"
android:paddingBottom="16dp">
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/banners"
android:layout_width="match_parent"
android:layout_height="132dp"
android:layout_marginTop="4dp"
android:orientation="vertical"
app:layout_constraintTop_toTopOf="parent"
android:background="@drawable/banner_default"
tools:layout_editor_absoluteX="16dp" />
<TextView <TextView
android:id="@+id/categoriesText" android:id="@+id/categoriesText"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="32dp" android:layout_marginStart="16dp"
android:layout_marginTop="16dp" android:layout_marginTop="16dp"
android:text="@string/fragment_home_categories" android:text="@string/fragment_home_categories"
android:textColor="@color/blue_500" android:textColor="@color/blue_500"
android:fontFamily="@font/dmsans_bold" android:fontFamily="@font/dmsans_bold"
android:textSize="18sp" android:textSize="18sp"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/banners" /> app:layout_constraintTop_toTopOf="parent" />
<com.google.android.material.button.MaterialButton <com.google.android.material.button.MaterialButton
android:id="@+id/showAll" android:id="@+id/categoryShowAll"
style="@style/Widget.MaterialComponents.Button.TextButton" style="@style/Widget.MaterialComponents.Button.TextButton"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginEnd="32dp" android:layout_marginEnd="16dp"
android:text="@string/show_all" android:text="@string/show_all"
android:textAllCaps="false" android:textAllCaps="false"
android:textColor="@color/blue_600" android:textColor="@color/blue_600"
@ -64,25 +55,28 @@
app:layout_constraintBaseline_toBaselineOf="@id/categoriesText" app:layout_constraintBaseline_toBaselineOf="@id/categoriesText"
app:layout_constraintEnd_toEndOf="parent" /> app:layout_constraintEnd_toEndOf="parent" />
<!-- Fix margin by adding padding inside RecyclerView -->
<androidx.recyclerview.widget.RecyclerView <androidx.recyclerview.widget.RecyclerView
android:id="@+id/categories" android:id="@+id/categories"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginHorizontal="16dp"
android:layout_marginTop="8dp" android:layout_marginTop="8dp"
android:paddingStart="4dp"
android:paddingEnd="4dp"
android:clipChildren="false" android:clipChildren="false"
android:clipToPadding="false" android:clipToPadding="false"
android:orientation="horizontal" android:overScrollMode="never"
android:paddingHorizontal="24dp" android:nestedScrollingEnabled="false"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:layout_constraintTop_toBottomOf="@id/categoriesText" app:layout_constraintTop_toBottomOf="@id/categoriesText"
tools:layout_editor_absoluteX="0dp" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
tools:listitem="@layout/item_category_home" /> tools:listitem="@layout/item_category_home" />
<TextView <TextView
android:id="@+id/new_products_text" android:id="@+id/new_products_text"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="32dp" android:layout_marginStart="16dp"
android:layout_marginTop="16dp" android:layout_marginTop="16dp"
android:text="@string/sold_product_text" android:text="@string/sold_product_text"
android:textColor="@color/blue_500" android:textColor="@color/blue_500"
@ -90,12 +84,13 @@
android:textSize="18sp" android:textSize="18sp"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/categories" /> app:layout_constraintTop_toBottomOf="@id/categories" />
<com.google.android.material.button.MaterialButton <com.google.android.material.button.MaterialButton
android:id="@+id/productshowAll" android:id="@+id/productshowAll"
style="@style/Widget.MaterialComponents.Button.TextButton" style="@style/Widget.MaterialComponents.Button.TextButton"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginEnd="32dp" android:layout_marginEnd="16dp"
android:text="@string/show_all" android:text="@string/show_all"
android:textAllCaps="false" android:textAllCaps="false"
android:textColor="@color/blue_600" android:textColor="@color/blue_600"
@ -103,19 +98,22 @@
app:layout_constraintBaseline_toBaselineOf="@id/new_products_text" app:layout_constraintBaseline_toBaselineOf="@id/new_products_text"
app:layout_constraintEnd_toEndOf="parent" /> app:layout_constraintEnd_toEndOf="parent" />
<!-- Disable internal scroll, height wraps content -->
<androidx.recyclerview.widget.RecyclerView <androidx.recyclerview.widget.RecyclerView
android:id="@+id/new_products" android:id="@+id/new_products"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="16dp" android:layout_marginTop="16dp"
android:layout_marginBottom="16dp" android:paddingHorizontal="16dp"
android:orientation="vertical" android:nestedScrollingEnabled="false"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" android:overScrollMode="never"
app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
app:spanCount="2"
app:layout_constraintTop_toBottomOf="@id/new_products_text" app:layout_constraintTop_toBottomOf="@id/new_products_text"
tools:itemCount="5" tools:listitem="@layout/item_product_grid" />
tools:listitem="@layout/item_section_horizontal" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>
</ScrollView> </androidx.core.widget.NestedScrollView>
<androidx.recyclerview.widget.RecyclerView <androidx.recyclerview.widget.RecyclerView
android:id="@+id/searchResultsRecyclerView" android:id="@+id/searchResultsRecyclerView"

View File

@ -4,6 +4,7 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
style="@style/Theme.Ecommerce_serang"
tools:context=".ui.home.SearchHomeFragment"> tools:context=".ui.home.SearchHomeFragment">
<LinearLayout <LinearLayout

View File

@ -1,36 +1,56 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="88dp" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginHorizontal="8dp" android:layout_margin="4dp"
xmlns:app="http://schemas.android.com/apk/res-auto"> xmlns:app="http://schemas.android.com/apk/res-auto">
<com.google.android.material.card.MaterialCardView <androidx.cardview.widget.CardView
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="0dp" android:layout_height="0dp"
android:id="@+id/imageLayout" android:id="@+id/imageLayout"
app:layout_constraintDimensionRatio="1:1" app:layout_constraintDimensionRatio="1:1.15"
app:cardCornerRadius="14dp"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
android:layout_marginEnd="8dp"
app:strokeWidth="1dp" app:strokeWidth="1dp"
app:cardCornerRadius="4dp"
app:strokeColor="@color/gray_1"> app:strokeColor="@color/gray_1">
<ImageView <androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent">
android:id="@+id/image_category" <ImageView
android:src="@drawable/makanan_ringan" android:layout_width="match_parent"
android:scaleType="centerCrop" /> android:layout_height="match_parent"
</com.google.android.material.card.MaterialCardView> android:id="@+id/image_category"
android:src="@drawable/makanan_ringan"
android:scaleType="centerCrop" />
<com.google.android.material.card.MaterialCardView
android:id="@+id/tv_category"
android:backgroundTint="@color/blue_500"
android:layout_width="match_parent"
android:layout_height="0dp"
style="@style/MyCardView"
android:padding="2dp"
app:strokeWidth="1dp"
app:strokeColor="@color/blue_500"
app:layout_constraintBottom_toBottomOf="parent">
<TextView
android:id="@+id/name"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginHorizontal="8dp"
android:textColor="@color/white"
android:lineSpacingExtra="0dp"
android:fontFamily="@font/dmsans_semibold"
android:textAlignment="center"
android:textSize="10sp"
android:text="@string/fragment_home_item_categories"/>
</com.google.android.material.card.MaterialCardView>
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.cardview.widget.CardView>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/name"
android:textAlignment="center"
android:text="@string/fragment_home_item_categories"
app:layout_constraintTop_toBottomOf="@id/imageLayout"
android:textSize="16sp"
android:textColor="@color/black"
android:layout_marginTop="12dp"/>
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -57,14 +57,24 @@
<!-- Stock Status --> <!-- Stock Status -->
<TextView <TextView
android:id="@+id/tv_stock_status" android:id="@+id/rating"
android:layout_width="match_parent" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="4dp" android:background="@drawable/rating_background"
android:text="Stock: 0" android:drawablePadding="4dp"
android:paddingStart="7dp"
android:paddingTop="5dp"
android:paddingEnd="11dp"
android:paddingBottom="3dp"
android:text="@string/rating"
android:textColor="@color/black"
android:fontFamily="@font/dmsans_regular"
android:textSize="12sp" android:textSize="12sp"
android:textColor="@color/black_200" android:textAlignment="center"
tools:text="Stock: 15" /> android:gravity="center"
app:drawableStartCompat="@drawable/baseline_star_24"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tv_product_price" />
<TextView <TextView
android:id="@+id/tv_store_name" android:id="@+id/tv_store_name"
@ -75,7 +85,7 @@
android:fontFamily="@font/dmsans_semibold" android:fontFamily="@font/dmsans_semibold"
android:textSize="14sp" android:textSize="14sp"
android:textColor="@color/black_200" android:textColor="@color/black_200"
tools:text="Stock: 15" /> tools:text="Toko Jaya" />
</LinearLayout> </LinearLayout>

View File

@ -1,69 +1,94 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" <androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="185dp" android:layout_width="180dp"
android:layout_height="wrap_content" android:layout_height="320dp"
android:layout_marginHorizontal="8dp" android:layout_margin="8dp"
xmlns:app="http://schemas.android.com/apk/res-auto"> app:cardCornerRadius="12dp"
app:cardElevation="4dp"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<com.google.android.material.card.MaterialCardView <LinearLayout
android:id="@+id/imageLayout" android:layout_width="match_parent"
android:layout_width="150dp" android:layout_height="wrap_content"
android:layout_height="0dp" android:orientation="vertical">
app:cardCornerRadius="14dp"
app:layout_constraintDimensionRatio="272:218"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:cardElevation="0dp"
app:strokeColor="@color/gray_1"
app:strokeWidth="1dp">
<!-- Product Image -->
<ImageView <ImageView
android:id="@+id/image_product" android:id="@+id/iv_product_image"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="160dp"
android:scaleType="centerCrop" android:scaleType="centerCrop"
android:src="@color/blue1" /> android:background="@color/light_gray"
</com.google.android.material.card.MaterialCardView> android:contentDescription="Product Image"
tools:src="@drawable/placeholder_image" />
<TextView <!-- Product Info -->
android:id="@+id/item_name" <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="17dp" android:orientation="vertical"
android:text="Banana" android:padding="12dp">
android:textColor="@color/black"
android:fontFamily="@font/dmsans_medium"
android:textSize="16sp"
app:layout_constraintTop_toBottomOf="@id/imageLayout" />
<TextView <!-- Product Name -->
android:id="@+id/item_price" <TextView
android:layout_width="match_parent" android:id="@+id/tv_product_name"
android:layout_height="wrap_content" android:layout_width="match_parent"
android:text="@string/item_price_txt" android:layout_height="wrap_content"
android:textColor="@color/black" android:text="Product Name"
android:textStyle="bold" android:textSize="14sp"
android:textSize="16sp" android:textStyle="bold"
app:layout_constraintTop_toBottomOf="@id/item_name" /> android:textColor="@color/black_500"
android:maxLines="2"
android:ellipsize="end"
tools:text="Sample Product Name" />
<TextView <!-- Product Price -->
android:id="@+id/rating" <TextView
android:layout_width="wrap_content" android:id="@+id/tv_product_price"
android:layout_height="wrap_content" android:layout_width="match_parent"
android:background="@drawable/rating_background" android:layout_height="wrap_content"
android:drawablePadding="4dp" android:layout_marginTop="4dp"
android:paddingStart="7dp" android:text="Rp 0"
android:paddingTop="5dp" android:textSize="16sp"
android:paddingEnd="11dp" android:textStyle="bold"
android:paddingBottom="3dp" android:textColor="@color/blue_500"
android:text="@string/rating" tools:text="Rp 25,000" />
android:textColor="@color/black"
android:fontFamily="@font/dmsans_regular"
android:textSize="12sp"
android:textAlignment="center"
android:gravity="center"
app:drawableStartCompat="@drawable/baseline_star_24"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/item_price" />
</androidx.constraintlayout.widget.ConstraintLayout> <!-- Stock Status -->
<TextView
android:id="@+id/rating"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/rating_background"
android:drawablePadding="4dp"
android:paddingStart="7dp"
android:paddingTop="5dp"
android:paddingEnd="11dp"
android:paddingBottom="3dp"
android:text="@string/rating"
android:textColor="@color/black"
android:fontFamily="@font/dmsans_regular"
android:textSize="12sp"
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" />
<TextView
android:id="@+id/tv_store_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:text="Toko Jaya"
android:fontFamily="@font/dmsans_semibold"
android:textSize="14sp"
android:textColor="@color/black_200"
tools:text="Toko Jaya" />
</LinearLayout>
</LinearLayout>
</androidx.cardview.widget.CardView>

View File

@ -38,9 +38,10 @@
android:clipToPadding="false" android:clipToPadding="false"
android:clipChildren="false" android:clipChildren="false"
app:layout_constraintTop_toBottomOf="@id/title" app:layout_constraintTop_toBottomOf="@id/title"
app:layout_constraintBottom_toBottomOf="parent"
android:layout_marginTop="16dp" android:layout_marginTop="16dp"
android:orientation="horizontal" android:orientation="horizontal"
tools:listitem="@layout/item_product_horizontal" tools:listitem="@layout/item_product_grid"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"/> app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"/>
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -90,7 +90,7 @@
<string name="dl_pending">Batas Tagihan</string> <string name="dl_pending">Batas Tagihan</string>
<string name="dl_unpaid">Batas Pembayaran</string> <string name="dl_unpaid">Batas Pembayaran</string>
<string name="dl_processed">Batas Pengiriman</string> <string name="dl_processed">Batas Pengiriman</string>
<string name="dal_paid">Semua Pesanan </string> <string name="dl_paid">Batas Konfirmasi Penjual</string>
<string name="dl_delivered">Semua Pesanan </string> <string name="dl_delivered">Semua Pesanan </string>
<string name="dl_completed">Semua Pesanan </string> <string name="dl_completed">Semua Pesanan </string>
<string name="dl_shipped">Tanggal Pesanan Sampai</string> <string name="dl_shipped">Tanggal Pesanan Sampai</string>

View File

@ -60,4 +60,17 @@
<item name="cornerRadius">8dp</item> <item name="cornerRadius">8dp</item>
<item name="backgroundTint">@color/white</item> <item name="backgroundTint">@color/white</item>
</style> </style>
<style name="MyCardView" parent="@style/Widget.MaterialComponents.CardView">
<item name="shapeAppearanceOverlay">@style/ShapeAppearanceOverlay.MaterialCardView.Cut</item>
</style>
<style name="ShapeAppearanceOverlay.MaterialCardView.Cut" parent="">
<item name="cornerFamily">rounded</item>
<item name="cornerSizeTopRight">24dp</item>
<item name="cornerSizeTopLeft">24dp</item>
<item name="cornerSizeBottomRight">4dp</item>
<item name="cornerSizeBottomLeft">4dp</item>
</style>
</resources> </resources>