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:usesCleartextTraffic="true"
tools:targetApi="31">
<activity
android:name=".ui.product.listproduct.ListCategoryActivity"
android:exported="false" />
<activity
android:name=".ui.product.listproduct.ListProductActivity"
android:exported="false" />
<activity
android:name=".ui.profile.mystore.sells.DetailSellsActivity"
android:exported="false" />

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -6,13 +6,16 @@ import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import com.alya.ecommerce_serang.BuildConfig.BASE_URL
import com.alya.ecommerce_serang.R
import com.alya.ecommerce_serang.data.api.dto.ProductsItem
import com.alya.ecommerce_serang.data.api.response.customer.product.StoreItem
import com.alya.ecommerce_serang.databinding.ItemProductGridBinding
import com.bumptech.glide.Glide
class SearchResultsAdapter(
private val onItemClick: (ProductsItem) -> Unit
private val onItemClick: (ProductsItem) -> Unit,
private val storeMap: Map<Int, StoreItem>
) : ListAdapter<ProductsItem, SearchResultsAdapter.ViewHolder>(DIFF_CALLBACK) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
@ -43,19 +46,23 @@ class SearchResultsAdapter(
fun bind(product: ProductsItem) {
binding.tvProductName.text = product.name
binding.tvProductPrice.text = (product.price)
binding.tvProductPrice.text = product.price
val fullImageUrl = if (product.image.startsWith("/")) {
BASE_URL + product.image.removePrefix("/") // Append base URL if the path starts with "/"
} else {
product.image // Use as is if it's already a full URL
}
Log.d("ProductAdapter", "Loading image: $fullImageUrl")
// Load image with Glide
Glide.with(binding.root.context)
.load(product.image)
.load(fullImageUrl)
.placeholder(R.drawable.placeholder_image)
// .error(R.drawable.error_image)
.into(binding.ivProductImage)
// Set store name if available
product.storeId?.toString().let {
binding.tvStoreName.text = it
}
val storeName = product.storeId?.let { storeMap[it]?.storeName } ?: "Unknown Store"
binding.tvStoreName.text = storeName
}
}

View File

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

View File

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

View File

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

View File

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

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.SessionManager
class OrderListFragment : Fragment() {
class OrderListFragment : Fragment(), OrderHistoryAdapter.OrderActionCallbacks {
private var _binding: FragmentOrderListBinding? = null
private val binding get() = _binding!!
@ -72,6 +72,7 @@ class OrderListFragment : Fragment() {
setupRecyclerView()
observeOrderList()
observeViewModel()
observeOrderCompletionStatus()
loadOrders()
}
@ -81,7 +82,8 @@ class OrderListFragment : Fragment() {
onOrderClickListener = { order ->
navigateToOrderDetail(order)
},
viewModel = viewModel
viewModel = viewModel,
callbacks = this // Pass this fragment as callback
)
orderAdapter.setFragmentStatus(status)
@ -120,6 +122,40 @@ class OrderListFragment : Fragment() {
}
}
private fun observeViewModel() {
// Observe order completion
viewModel.orderCompletionStatus.observe(viewLifecycleOwner) { result ->
when (result) {
is Result.Success -> {
Toast.makeText(requireContext(), "Order completed successfully!", Toast.LENGTH_SHORT).show()
loadOrders() // Refresh here
}
is Result.Error -> {
Toast.makeText(requireContext(), "Failed: ${result.exception.message}", Toast.LENGTH_SHORT).show()
}
is Result.Loading -> {
// Show loading if needed
}
}
}
// Observe cancel order status
viewModel.cancelOrderStatus.observe(viewLifecycleOwner) { result ->
when (result) {
is Result.Success -> {
Toast.makeText(requireContext(), "Order cancelled successfully!", Toast.LENGTH_SHORT).show()
loadOrders() // Refresh here
}
is Result.Error -> {
Toast.makeText(requireContext(), "Failed to cancel: ${result.exception.message}", Toast.LENGTH_SHORT).show()
}
is Result.Loading -> {
// Show loading if needed
}
}
}
}
private fun loadOrders() {
// Simple - just call getOrderList for any status including "all"
viewModel.getOrderList(status)
@ -142,6 +178,30 @@ class OrderListFragment : Fragment() {
detailOrderLauncher.launch(intent)
}
override fun onOrderCancelled(orderId: String, success: Boolean, message: String) {
if (success) {
Toast.makeText(requireContext(), message, Toast.LENGTH_SHORT).show()
loadOrders() // Refresh the list
} else {
Toast.makeText(requireContext(), message, Toast.LENGTH_SHORT).show()
}
}
override fun onOrderCompleted(orderId: Int, success: Boolean, message: String) {
if (success) {
Toast.makeText(requireContext(), message, Toast.LENGTH_SHORT).show()
loadOrders() // Refresh the list
} else {
Toast.makeText(requireContext(), message, Toast.LENGTH_SHORT).show()
}
}
override fun onShowLoading(show: Boolean) {
binding.progressBar.visibility = if (show) View.VISIBLE else View.GONE
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null

View File

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

View File

@ -234,8 +234,8 @@ class DetailOrderStatusActivity : AppCompatActivity() {
// Set status header
val statusText = when(status) {
"pending" -> "Belum Bayar"
"unpaid" -> "Belum Bayar"
"paid" -> "Sudah Dibayar"
"processed" -> "Diproses"
"shipped" -> "Dikirim"
"delivered" -> "Diterima"
@ -248,22 +248,6 @@ class DetailOrderStatusActivity : AppCompatActivity() {
Log.d(TAG, "adjustButtonsBasedOnStatus: Status header set to '$statusText'")
when (status) {
"pending"->{
binding.tvStatusHeader.text = "Menunggu Tagihan"
binding.tvStatusNote.visibility = View.VISIBLE
binding.tvStatusNote.text = "Pesanan ini harus dibayar sebelum ${formatDatePay(orders.updatedAt)}"
// Set buttons
binding.btnSecondary.apply {
visibility = View.VISIBLE
text = "Batalkan Pesanan"
setOnClickListener {
Log.d(TAG, "Cancel Order button clicked")
showCancelOrderBottomSheet(orders.orderId)
viewModel.getOrderDetails(orders.orderId)
}
}
}
"unpaid" -> {
Log.d(TAG, "adjustButtonsBasedOnStatus: Setting up UI for pending/unpaid order")
@ -295,7 +279,25 @@ class DetailOrderStatusActivity : AppCompatActivity() {
}
}
}
"paid" -> {
Log.d(TAG, "adjustButtonsBasedOnStatus: Setting up UI for pending/unpaid order")
// Show status note
binding.tvStatusHeader.text = "Sudah Dibayar"
binding.tvStatusNote.visibility = View.VISIBLE
binding.tvStatusNote.text = "Menunggu pesanan dikonfirmasi penjual ${formatDatePay(orders.updatedAt)}"
// Set buttons
binding.btnSecondary.apply {
visibility = View.VISIBLE
text = "Batalkan Pesanan"
setOnClickListener {
Log.d(TAG, "Cancel Order button clicked")
showCancelOrderDialog(orders.orderId.toString())
viewModel.getOrderDetails(orders.orderId)
}
}
}
"processed" -> {
Log.d(TAG, "adjustButtonsBasedOnStatus: Setting up UI for processed order")

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

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

View File

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

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

View File

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

View File

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

View File

@ -142,35 +142,6 @@
android:layout_height="8dp"
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
android:layout_width="match_parent"
android:layout_height="8dp"

View File

@ -487,7 +487,7 @@
android:orientation="horizontal"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:itemCount="3"
tools:listitem="@layout/item_related_product" />
tools:listitem="@layout/item_product_horizontal" />
</LinearLayout>
</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
android:id="@+id/rv_products"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_height="0dp"
android:layout_marginTop="16dp"
android:layout_marginBottom="16dp"
android:orientation="vertical"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
app:layout_constraintTop_toBottomOf="@id/divider_product"
tools:itemCount="5"
tools:listitem="@layout/item_section_horizontal" />
app:spanCount="2"
tools:listitem="@layout/item_product_grid" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

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

View File

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

View File

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

View File

@ -1,36 +1,56 @@
<?xml version="1.0" encoding="utf-8"?>
<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_marginHorizontal="8dp"
android:layout_margin="4dp"
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_height="0dp"
android:id="@+id/imageLayout"
app:layout_constraintDimensionRatio="1:1"
app:cardCornerRadius="14dp"
app:layout_constraintDimensionRatio="1:1.15"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
android:layout_marginEnd="8dp"
app:strokeWidth="1dp"
app:cardCornerRadius="4dp"
app:strokeColor="@color/gray_1">
<ImageView
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/image_category"
android:src="@drawable/makanan_ringan"
android:scaleType="centerCrop" />
</com.google.android.material.card.MaterialCardView>
android:layout_height="match_parent">
<ImageView
android:layout_width="match_parent"
android:layout_height="match_parent"
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>

View File

@ -57,14 +57,24 @@
<!-- Stock Status -->
<TextView
android:id="@+id/tv_stock_status"
android:layout_width="match_parent"
android:id="@+id/rating"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:text="Stock: 0"
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:textColor="@color/black_200"
tools:text="Stock: 15" />
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"
@ -75,7 +85,7 @@
android:fontFamily="@font/dmsans_semibold"
android:textSize="14sp"
android:textColor="@color/black_200"
tools:text="Stock: 15" />
tools:text="Toko Jaya" />
</LinearLayout>

View File

@ -1,69 +1,94 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="185dp"
android:layout_height="wrap_content"
android:layout_marginHorizontal="8dp"
xmlns:app="http://schemas.android.com/apk/res-auto">
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="180dp"
android:layout_height="320dp"
android:layout_margin="8dp"
app:cardCornerRadius="12dp"
app:cardElevation="4dp"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<com.google.android.material.card.MaterialCardView
android:id="@+id/imageLayout"
android:layout_width="150dp"
android:layout_height="0dp"
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">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<!-- Product Image -->
<ImageView
android:id="@+id/image_product"
android:id="@+id/iv_product_image"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_height="160dp"
android:scaleType="centerCrop"
android:src="@color/blue1" />
</com.google.android.material.card.MaterialCardView>
android:background="@color/light_gray"
android:contentDescription="Product Image"
tools:src="@drawable/placeholder_image" />
<TextView
android:id="@+id/item_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="17dp"
android:text="Banana"
android:textColor="@color/black"
android:fontFamily="@font/dmsans_medium"
android:textSize="16sp"
app:layout_constraintTop_toBottomOf="@id/imageLayout" />
<!-- Product Info -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="12dp">
<TextView
android:id="@+id/item_price"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/item_price_txt"
android:textColor="@color/black"
android:textStyle="bold"
android:textSize="16sp"
app:layout_constraintTop_toBottomOf="@id/item_name" />
<!-- Product Name -->
<TextView
android:id="@+id/tv_product_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Product Name"
android:textSize="14sp"
android:textStyle="bold"
android:textColor="@color/black_500"
android:maxLines="2"
android:ellipsize="end"
tools:text="Sample Product Name" />
<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/item_price" />
<!-- Product Price -->
<TextView
android:id="@+id/tv_product_price"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:text="Rp 0"
android:textSize="16sp"
android:textStyle="bold"
android:textColor="@color/blue_500"
tools:text="Rp 25,000" />
</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:clipChildren="false"
app:layout_constraintTop_toBottomOf="@id/title"
app:layout_constraintBottom_toBottomOf="parent"
android:layout_marginTop="16dp"
android:orientation="horizontal"
tools:listitem="@layout/item_product_horizontal"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"/>
tools:listitem="@layout/item_product_grid"
app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"/>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -90,7 +90,7 @@
<string name="dl_pending">Batas Tagihan</string>
<string name="dl_unpaid">Batas Pembayaran</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_completed">Semua Pesanan </string>
<string name="dl_shipped">Tanggal Pesanan Sampai</string>

View File

@ -60,4 +60,17 @@
<item name="cornerRadius">8dp</item>
<item name="backgroundTint">@color/white</item>
</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>