Merge branch 'screen-features'

This commit is contained in:
shaulascr
2025-05-29 16:40:25 +07:00
13 changed files with 862 additions and 171 deletions

View File

@ -29,6 +29,9 @@
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.category.CategoryProductsActivity"
android:exported="false" />
<activity <activity
android:name=".ui.profile.mystore.chat.ChatListStoreActivity" android:name=".ui.profile.mystore.chat.ChatListStoreActivity"
android:exported="false" /> android:exported="false" />
@ -69,11 +72,11 @@
android:enabled="true" android:enabled="true"
android:exported="false" android:exported="false"
android:foregroundServiceType="dataSync" /> android:foregroundServiceType="dataSync" />
<activity <activity
android:name=".ui.profile.mystore.chat.ChatStoreActivity" android:name=".ui.profile.mystore.chat.ChatStoreActivity"
android:exported="false" android:exported="false"
android:windowSoftInputMode="adjustResize" /> android:windowSoftInputMode="adjustResize" />
<activity <activity
android:name=".ui.profile.mystore.profile.shipping_service.ShippingServiceActivity" android:name=".ui.profile.mystore.profile.shipping_service.ShippingServiceActivity"
android:exported="false" /> android:exported="false" />

View File

@ -288,6 +288,58 @@ class ProductRepository(private val apiService: ApiService) {
} }
} }
suspend fun getProductsByCategory(categoryId: Int): Result<List<ProductsItem>> =
withContext(Dispatchers.IO) {
try {
Log.d(TAG, "Attempting to fetch products for category: $categoryId")
val response = apiService.getAllProduct()
if (response.isSuccessful) {
val allProducts = response.body()?.products ?: emptyList()
// Filter products by category_id
val filteredProducts = allProducts.filter { product ->
product.categoryId == categoryId
}
Log.d(TAG, "Filtered products for category $categoryId: ${filteredProducts.size} products")
Result.Success(filteredProducts)
} else {
val errorBody = response.errorBody()?.string() ?: "Unknown error"
Log.e(TAG, "Failed to fetch products. Code: ${response.code()}, Error: $errorBody")
Result.Error(Exception("Failed to fetch products. Code: ${response.code()}"))
}
} catch (e: Exception) {
Log.e(TAG, "Exception while fetching products by category", e)
Result.Error(e)
}
}
// Optional: Get category by ID if needed
suspend fun getCategoryById(categoryId: Int): Result<CategoryItem?> =
withContext(Dispatchers.IO) {
try {
Log.d(TAG, "Attempting to fetch category: $categoryId")
val response = apiService.allCategory()
if (response.isSuccessful) {
val categories = response.body()?.category ?: emptyList()
val category = categories.find { it.id == categoryId }
Log.d(TAG, "Category found: ${category?.name}")
Result.Success(category)
} else {
val errorBody = response.errorBody()?.string() ?: "Unknown error"
Log.e(TAG, "Failed to fetch category. Code: ${response.code()}, Error: $errorBody")
Result.Error(Exception("Failed to fetch category. Code: ${response.code()}"))
}
} catch (e: Exception) {
Log.e(TAG, "Exception while fetching category by ID", e)
Result.Error(e)
}
}
companion object { companion object {
private const val TAG = "ProductRepository" private const val TAG = "ProductRepository"
} }

View File

@ -24,6 +24,7 @@ import com.alya.ecommerce_serang.databinding.FragmentHomeBinding
import com.alya.ecommerce_serang.ui.cart.CartActivity 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.utils.BaseViewModelFactory import com.alya.ecommerce_serang.utils.BaseViewModelFactory
import com.alya.ecommerce_serang.utils.HorizontalMarginItemDecoration import com.alya.ecommerce_serang.utils.HorizontalMarginItemDecoration
import com.alya.ecommerce_serang.utils.SessionManager import com.alya.ecommerce_serang.utils.SessionManager
@ -211,7 +212,10 @@ class HomeFragment : Fragment() {
} }
private fun handleCategoryProduct(category: CategoryItem) { private fun handleCategoryProduct(category: CategoryItem) {
// Your implementation // Navigate to CategoryProductsActivity when category is clicked
val intent = Intent(requireContext(), CategoryProductsActivity::class.java)
intent.putExtra(CategoryProductsActivity.EXTRA_CATEGORY, category)
startActivity(intent)
} }
override fun onDestroyView() { override fun onDestroyView() {

View File

@ -42,19 +42,19 @@ class SearchResultsAdapter(
} }
fun bind(product: ProductsItem) { fun bind(product: ProductsItem) {
binding.productName.text = product.name binding.tvProductName.text = product.name
binding.productPrice.text = (product.price) binding.tvProductPrice.text = (product.price)
// Load image with Glide // Load image with Glide
Glide.with(binding.root.context) Glide.with(binding.root.context)
.load(product.image) .load(product.image)
.placeholder(R.drawable.placeholder_image) .placeholder(R.drawable.placeholder_image)
// .error(R.drawable.error_image) // .error(R.drawable.error_image)
.into(binding.productImage) .into(binding.ivProductImage)
// Set store name if available // Set store name if available
product.storeId?.toString().let { product.storeId?.toString().let {
binding.storeName.text = it binding.tvStoreName.text = it
} }
} }
} }

View File

@ -2,11 +2,14 @@ package com.alya.ecommerce_serang.ui.order.detail
import android.Manifest import android.Manifest
import android.R import android.R
import android.app.Activity
import android.app.DatePickerDialog import android.app.DatePickerDialog
import android.content.Intent
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.graphics.BitmapFactory import android.graphics.BitmapFactory
import android.net.Uri import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.provider.MediaStore
import android.util.Log import android.util.Log
import android.view.View import android.view.View
import android.webkit.MimeTypeMap import android.webkit.MimeTypeMap
@ -46,6 +49,7 @@ class AddEvidencePaymentActivity : AppCompatActivity() {
private lateinit var productPrice: String private lateinit var productPrice: String
private var selectedImageUri: Uri? = null private var selectedImageUri: Uri? = null
private val viewModel: PaymentViewModel by viewModels { private val viewModel: PaymentViewModel by viewModels {
BaseViewModelFactory { BaseViewModelFactory {
val apiService = ApiConfig.getApiService(sessionManager) val apiService = ApiConfig.getApiService(sessionManager)
@ -62,55 +66,65 @@ class AddEvidencePaymentActivity : AppCompatActivity() {
"Cash on Delivery" "Cash on Delivery"
) )
private val getContent = registerForActivityResult(ActivityResultContracts.GetContent()) { uri: Uri? -> // private val getContent = registerForActivityResult(ActivityResultContracts.GetContent()) { uri: Uri? ->
uri?.let { // uri?.let {
selectedImageUri = it // selectedImageUri = it
binding.ivUploadedImage.setImageURI(selectedImageUri) // binding.ivUploadedImage.setImageURI(selectedImageUri)
binding.ivUploadedImage.visibility = View.VISIBLE // binding.ivUploadedImage.visibility = View.VISIBLE
binding.layoutUploadPlaceholder.visibility = View.GONE // binding.layoutUploadPlaceholder.visibility = View.GONE
// }
// }
private val pickImageLauncher = registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) { result ->
if (result.resultCode == Activity.RESULT_OK) {
result.data?.data?.let { uri ->
handleSelectedImage(uri)
}
} }
} }
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
binding = ActivityAddEvidencePaymentBinding.inflate(layoutInflater)
setContentView(binding.root)
sessionManager = SessionManager(this) try {
binding = ActivityAddEvidencePaymentBinding.inflate(layoutInflater)
setContentView(binding.root)
intent.extras?.let { bundle -> sessionManager = SessionManager(this)
orderId = bundle.getInt("ORDER_ID", 0)
paymentInfoId = bundle.getInt("PAYMENT_INFO_ID", 0)
productPrice = intent.getStringExtra("TOTAL_AMOUNT") ?: "Rp0"
intent.extras?.let { bundle ->
orderId = bundle.getInt("ORDER_ID", 0)
paymentInfoId = bundle.getInt("PAYMENT_INFO_ID", 0)
productPrice = intent.getStringExtra("TOTAL_AMOUNT") ?: "Rp0"
Log.d(TAG, "Intent data: OrderID=$orderId, PaymentInfoId=$paymentInfoId, Price=$productPrice")
}
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
}
Log.d(TAG, "7. About to setup toolbar - COMMENTING OUT PROBLEMATIC LINE")
// COMMENT OUT THIS LINE TEMPORARILY:
// binding.toolbar.navigationIcon.apply { finish() }
setupUI()
viewModel.getOrderDetails(orderId)
setupListeners()
setupObservers()
} catch (e: Exception) {
Log.e(TAG, "ERROR in AddEvidencePaymentActivity onCreate: ${e.message}", e)
Toast.makeText(this, "Error: ${e.message}", Toast.LENGTH_LONG).show()
} }
WindowCompat.setDecorFitsSystemWindows(window, false)
enableEdgeToEdge()
// Apply insets to your root layout
ViewCompat.setOnApplyWindowInsetsListener(binding.root) { view, windowInsets ->
val systemBars = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
view.setPadding(
systemBars.left,
systemBars.top,
systemBars.right,
systemBars.bottom
)
windowInsets
}
binding.toolbar.navigationIcon.apply {
onBackPressed()
}
setupUI()
viewModel.getOrderDetails(orderId)
setupListeners()
setupObservers()
} }
private fun setupUI() { private fun setupUI() {
@ -126,11 +140,11 @@ class AddEvidencePaymentActivity : AppCompatActivity() {
// Upload image button // Upload image button
binding.tvAddPhoto.setOnClickListener { binding.tvAddPhoto.setOnClickListener {
checkPermissionAndPickImage() checkPermissionsAndShowImagePicker()
} }
binding.frameUploadImage.setOnClickListener { binding.frameUploadImage.setOnClickListener {
checkPermissionAndPickImage() checkPermissionsAndShowImagePicker()
} }
// Date picker // Date picker
@ -158,6 +172,7 @@ class AddEvidencePaymentActivity : AppCompatActivity() {
// Submit button // Submit button
binding.btnSubmit.setOnClickListener { binding.btnSubmit.setOnClickListener {
validateAndUpload() validateAndUpload()
Log.d(TAG, "AddEvidencePaymentActivity onCreate completed")
} }
} }
@ -182,6 +197,112 @@ class AddEvidencePaymentActivity : AppCompatActivity() {
} }
} }
private fun checkPermissionsAndShowImagePicker() {
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.TIRAMISU) {
// For Android 13+ (API 33+), use READ_MEDIA_IMAGES
if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_MEDIA_IMAGES) != PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.READ_MEDIA_IMAGES), REQUEST_CODE_STORAGE_PERMISSION)
} else {
showImagePickerOptions()
}
} else {
// For older versions, use READ_EXTERNAL_STORAGE
if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE), REQUEST_CODE_STORAGE_PERMISSION)
} else {
showImagePickerOptions()
}
}
}
// Exact same approach as ChatActivity
private fun showImagePickerOptions() {
val options = arrayOf(
"Pilih dari Galeri",
"Batal"
)
androidx.appcompat.app.AlertDialog.Builder(this)
.setTitle("Pilih Bukti Pembayaran")
.setItems(options) { dialog, which ->
when (which) {
0 -> openGallery() // Gallery
1 -> dialog.dismiss() // Cancel
}
}
.show()
}
// Using the same gallery opening method as ChatActivity
private fun openGallery() {
try {
val intent = Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI)
pickImageLauncher.launch(intent)
} catch (e: Exception) {
Log.e(TAG, "Error opening gallery", e)
Toast.makeText(this, "Gagal membuka galeri", Toast.LENGTH_SHORT).show()
}
}
private fun handleSelectedImage(uri: Uri) {
try {
Log.d(TAG, "Processing selected image: $uri")
// Use the same copy-to-cache approach as ChatActivity
contentResolver.openInputStream(uri)?.use { inputStream ->
val fileName = "evidence_${System.currentTimeMillis()}.jpg"
val outputFile = File(cacheDir, fileName)
outputFile.outputStream().use { outputStream ->
inputStream.copyTo(outputStream)
}
if (outputFile.exists() && outputFile.length() > 0) {
// Check file size (max 5MB like ChatActivity)
if (outputFile.length() > 5 * 1024 * 1024) {
Log.e(TAG, "File too large: ${outputFile.length()} bytes")
Toast.makeText(this, "Gambar terlalu besar (maksimal 5MB)", Toast.LENGTH_SHORT).show()
outputFile.delete()
return
}
// Success - update UI
selectedImageUri = Uri.fromFile(outputFile)
binding.ivUploadedImage.setImageURI(selectedImageUri)
binding.ivUploadedImage.visibility = View.VISIBLE
binding.layoutUploadPlaceholder.visibility = View.GONE
Log.d(TAG, "Image processed successfully: ${outputFile.absolutePath}, size: ${outputFile.length()}")
Toast.makeText(this, "Gambar berhasil dipilih", Toast.LENGTH_SHORT).show()
} else {
Log.e(TAG, "Failed to create image file")
Toast.makeText(this, "Gagal memproses gambar", Toast.LENGTH_SHORT).show()
}
} ?: run {
Log.e(TAG, "Could not open input stream for URI: $uri")
Toast.makeText(this, "Tidak dapat mengakses gambar", Toast.LENGTH_SHORT).show()
}
} catch (e: Exception) {
Log.e(TAG, "Error handling selected image", e)
Toast.makeText(this, "Error: ${e.message}", Toast.LENGTH_SHORT).show()
}
}
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<out String>,
grantResults: IntArray
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
if (requestCode == REQUEST_CODE_STORAGE_PERMISSION) {
if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
showImagePickerOptions()
} else {
Toast.makeText(this, "Izin diperlukan untuk mengakses galeri", Toast.LENGTH_SHORT).show()
}
}
}
private fun validateAndUpload() { private fun validateAndUpload() {
// Validate all fields // Validate all fields
if (selectedImageUri == null) { if (selectedImageUri == null) {
@ -301,40 +422,6 @@ class AddEvidencePaymentActivity : AppCompatActivity() {
} }
private fun checkPermissionAndPickImage() {
val permission = if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.TIRAMISU) {
Manifest.permission.READ_MEDIA_IMAGES
} else {
Manifest.permission.READ_EXTERNAL_STORAGE
}
if (ContextCompat.checkSelfPermission(this, permission) != PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(this, arrayOf(permission), REQUEST_CODE_STORAGE_PERMISSION)
} else {
pickImage()
}
}
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<out String>,
grantResults: IntArray
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
if (requestCode == REQUEST_CODE_STORAGE_PERMISSION && grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
pickImage()
} else {
Toast.makeText(this, "Izin dibutuhkan untuk memilih gambar", Toast.LENGTH_SHORT).show()
}
}
private fun pickImage() {
getContent.launch("image/*")
}
private fun showDatePicker() { private fun showDatePicker() {
val calendar = Calendar.getInstance() val calendar = Calendar.getInstance()
val year = calendar.get(Calendar.YEAR) val year = calendar.get(Calendar.YEAR)

View File

@ -3,6 +3,7 @@ package com.alya.ecommerce_serang.ui.order.detail
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.util.Log import android.util.Log
import android.view.View
import android.widget.Toast import android.widget.Toast
import androidx.activity.enableEdgeToEdge import androidx.activity.enableEdgeToEdge
import androidx.activity.viewModels import androidx.activity.viewModels
@ -43,65 +44,88 @@ class PaymentActivity : AppCompatActivity() {
sessionManager = SessionManager(this) sessionManager = SessionManager(this)
WindowCompat.setDecorFitsSystemWindows(window, false) setupWindowInsets()
enableEdgeToEdge() // Get data from intent
// Apply insets to your root layout
ViewCompat.setOnApplyWindowInsetsListener(binding.root) { view, windowInsets ->
val systemBars = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
view.setPadding(
systemBars.left,
systemBars.top,
systemBars.right,
systemBars.bottom
)
windowInsets
}
// Mengambil data dari intent
val orderId = intent.getIntExtra("ORDER_ID", 0) val orderId = intent.getIntExtra("ORDER_ID", 0)
val paymentInfoId = intent.getIntExtra("ORDER_PAYMENT_ID", 0) val paymentInfoId = intent.getIntExtra("ORDER_PAYMENT_ID", 0)
if (orderId == 0) { if (orderId == 0) {
Toast.makeText(this, "ID pesanan tidak valid", Toast.LENGTH_SHORT).show() Toast.makeText(this, "ID pesanan tidak valid", Toast.LENGTH_SHORT).show()
finish() finish()
return
} }
// Setup toolbar // Setup observers FIRST
observeData()
// Setup UI
setupToolbar()
setupClickListeners(orderId, paymentInfoId)
// Load data LAST
Log.d(TAG, "Fetching order details for Order ID: $orderId")
viewModel.getOrderDetails(orderId)
}
private fun setupWindowInsets() {
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
}
}
private fun setupToolbar() {
binding.toolbar.setNavigationOnClickListener { binding.toolbar.setNavigationOnClickListener {
onBackPressedDispatcher
finish() finish()
} }
}
// Setup petunjuk transfer private fun setupClickListeners(orderId: Int, paymentInfoId: Int) {
// Instructions clicks
binding.layoutMBankingInstructions.setOnClickListener { binding.layoutMBankingInstructions.setOnClickListener {
// Tampilkan instruksi mBanking
showInstructions("mBanking") showInstructions("mBanking")
} }
binding.layoutATMInstructions.setOnClickListener { binding.layoutATMInstructions.setOnClickListener {
// Tampilkan instruksi ATM
showInstructions("ATM") showInstructions("ATM")
} }
// Setup button upload bukti bayar // Upload button
binding.btnUploadPaymentProof.setOnClickListener { // binding.btnUploadPaymentProof.setOnClickListener { view ->
// Intent ke activity upload bukti bayar // Log.d(TAG, "Button clicked - showing toast")
val intent = Intent(this, AddEvidencePaymentActivity::class.java) // Toast.makeText(this@PaymentActivity, "Button works! OrderID: $orderId", Toast.LENGTH_LONG).show()
intent.putExtra("ORDER_ID", orderId) // }
intent.putExtra("PAYMENT_INFO_ID", paymentInfoId) binding.btnUploadPaymentProof.apply {
intent.putExtra("TOTAL_AMOUNT", binding.tvTotalAmount.text.toString()) isEnabled = true
Log.d(TAG, "Received Order ID: $orderId, Payment Info ID: $paymentInfoId, Total Amount: ${binding.tvTotalAmount.text}") isClickable = true
startActivity(intent) setOnClickListener {
Log.d(TAG, "Button clicked!")
val intent = Intent(this@PaymentActivity, AddEvidencePaymentActivity::class.java).apply {
putExtra("ORDER_ID", orderId)
putExtra("PAYMENT_INFO_ID", paymentInfoId)
putExtra("TOTAL_AMOUNT", binding.tvTotalAmount.text.toString())
}
Log.d(TAG, "Starting AddEvidencePaymentActivity with Order ID: $orderId, Payment Info ID: $paymentInfoId")
startActivity(intent)
}
// Debug touch events
setOnTouchListener { _, event ->
Log.d(TAG, "Button touched: ${event.action}")
false
}
} }
// Observe data
observeData()
// Load data // Debug button state
Log.d(TAG, "Fetching order details for Order ID: $orderId") Log.d(TAG, "Button setup - isEnabled: ${binding.btnUploadPaymentProof.isEnabled}, isClickable: ${binding.btnUploadPaymentProof.isClickable}")
viewModel.getOrderDetails(orderId)
} }
private fun observeData() { private fun observeData() {
@ -124,13 +148,14 @@ class PaymentActivity : AppCompatActivity() {
setupPaymentDueDate(order.updatedAt) setupPaymentDueDate(order.updatedAt)
} }
// Observe loading state
viewModel.isLoading.observe(this) { isLoading -> viewModel.isLoading.observe(this) { isLoading ->
// Show loading indicator if needed Log.d(TAG, "Loading state changed: $isLoading")
// binding.progressBar.visibility = if (isLoading) View.VISIBLE else View.GONE // Fix this line:
binding.progressBar.visibility = if (isLoading) View.VISIBLE else View.GONE
Log.d(TAG, "Button enabled: ${binding.btnUploadPaymentProof.isEnabled}")
} }
// Observe error
viewModel.error.observe(this) { error -> viewModel.error.observe(this) { error ->
if (error.isNotEmpty()) { if (error.isNotEmpty()) {
Toast.makeText(this, error, Toast.LENGTH_SHORT).show() Toast.makeText(this, error, Toast.LENGTH_SHORT).show()

View File

@ -0,0 +1,188 @@
package com.alya.ecommerce_serang.ui.product.category
import android.content.Intent
import android.os.Bundle
import android.view.MenuItem
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.core.view.isVisible
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
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.CategoryItem
import com.alya.ecommerce_serang.data.api.dto.ProductsItem
import com.alya.ecommerce_serang.data.api.retrofit.ApiConfig
import com.alya.ecommerce_serang.data.repository.ProductRepository
import com.alya.ecommerce_serang.databinding.ActivityCategoryProductsBinding
import com.alya.ecommerce_serang.ui.product.DetailProductActivity
import com.alya.ecommerce_serang.utils.BaseViewModelFactory
import com.alya.ecommerce_serang.utils.SessionManager
import com.bumptech.glide.Glide
import kotlinx.coroutines.launch
class CategoryProductsActivity : AppCompatActivity() {
private lateinit var binding: ActivityCategoryProductsBinding
private lateinit var sessionManager: SessionManager
private var productsAdapter: ProductsCategoryAdapter? = null
private val viewModel: CategoryProductsViewModel by viewModels {
BaseViewModelFactory {
val apiService = ApiConfig.getApiService(sessionManager)
val productRepository = ProductRepository(apiService)
CategoryProductsViewModel(productRepository)
}
}
companion object {
const val EXTRA_CATEGORY = "extra_category"
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityCategoryProductsBinding.inflate(layoutInflater)
setContentView(binding.root)
sessionManager = SessionManager(this)
WindowCompat.setDecorFitsSystemWindows(window, false)
enableEdgeToEdge()
// Apply insets to your root layout
ViewCompat.setOnApplyWindowInsetsListener(binding.root) { view, windowInsets ->
val systemBars = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
view.setPadding(
systemBars.left,
systemBars.top,
systemBars.right,
systemBars.bottom
)
windowInsets
}
val category = intent.getParcelableExtra<CategoryItem>(EXTRA_CATEGORY)
if (category == null) {
finish()
return
}
setupUI(category)
setupRecyclerView()
observeViewModel()
// Load products for this category using category.id (not store_type_id)
viewModel.loadProductsByCategory(category.id)
}
private fun setupUI(category: CategoryItem) {
binding.apply {
// Setup toolbar
setSupportActionBar(toolbar)
supportActionBar?.apply {
setDisplayHomeAsUpEnabled(true)
// title = category.name
}
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
}
// Load category image
Glide.with(this@CategoryProductsActivity)
.load(fullImageUrl)
.placeholder(R.drawable.placeholder_image)
.error(R.drawable.placeholder_image)
.into(ivCategoryHeader)
tvCategoryTitle.text = category.name
}
}
private fun setupRecyclerView() {
productsAdapter = ProductsCategoryAdapter(
products = emptyList(),
onClick = { product -> handleProductClick(product) }
)
binding.rvProducts.apply {
layoutManager = GridLayoutManager(this@CategoryProductsActivity, 2)
adapter = productsAdapter
// addItemDecoration(GridSpacingItemDecoration(2, 16, true))
}
}
private fun observeViewModel() {
lifecycleScope.launch {
lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.uiState.collect { state ->
when (state) {
is CategoryProductsUiState.Loading -> {
binding.progressBar.isVisible = true
binding.rvProducts.isVisible = false
binding.layoutError.isVisible = false
binding.layoutEmpty.isVisible = false
}
is CategoryProductsUiState.Success -> {
binding.progressBar.isVisible = false
binding.layoutError.isVisible = false
if (state.products.isEmpty()) {
binding.rvProducts.isVisible = false
binding.layoutEmpty.isVisible = true
} else {
binding.rvProducts.isVisible = true
binding.layoutEmpty.isVisible = false
productsAdapter?.updateProducts(state.products)
}
}
is CategoryProductsUiState.Error -> {
binding.progressBar.isVisible = false
binding.rvProducts.isVisible = false
binding.layoutEmpty.isVisible = false
binding.layoutError.isVisible = true
binding.tvErrorMessage.text = state.message
binding.btnRetry.setOnClickListener {
val category = intent.getParcelableExtra<CategoryItem>(
EXTRA_CATEGORY
)
category?.let { viewModel.loadProductsByCategory(it.id) }
}
}
}
}
}
}
}
private fun handleProductClick(product: ProductsItem) {
val intent = Intent(this, DetailProductActivity::class.java)
intent.putExtra("PRODUCT_ID", product.id)
startActivity(intent)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
android.R.id.home -> {
onBackPressed()
true
}
else -> super.onOptionsItemSelected(item)
}
}
override fun onDestroy() {
super.onDestroy()
productsAdapter = null
}
}

View File

@ -0,0 +1,49 @@
package com.alya.ecommerce_serang.ui.product.category
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.alya.ecommerce_serang.data.api.dto.ProductsItem
import com.alya.ecommerce_serang.data.repository.ProductRepository
import com.alya.ecommerce_serang.data.repository.Result
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
class CategoryProductsViewModel(
private val productRepository: ProductRepository
) : ViewModel() {
private val _uiState = MutableStateFlow<CategoryProductsUiState>(CategoryProductsUiState.Loading)
val uiState: StateFlow<CategoryProductsUiState> = _uiState.asStateFlow()
fun loadProductsByCategory(categoryId: Int) {
viewModelScope.launch {
_uiState.value = CategoryProductsUiState.Loading
when (val result = productRepository.getProductsByCategory(categoryId)) {
is com.alya.ecommerce_serang.data.repository.Result.Success -> {
_uiState.value = CategoryProductsUiState.Success(result.data)
}
is com.alya.ecommerce_serang.data.repository.Result.Error -> {
_uiState.value = CategoryProductsUiState.Error(
result.exception.message ?: "Failed to load products"
)
}
is Result.Loading -> {
// Handle if needed
}
}
}
}
fun retry(categoryId: Int) {
loadProductsByCategory(categoryId)
}
}
sealed class CategoryProductsUiState {
object Loading : CategoryProductsUiState()
data class Success(val products: List<ProductsItem>) : CategoryProductsUiState()
data class Error(val message: String) : CategoryProductsUiState()
}

View File

@ -0,0 +1,73 @@
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
import com.alya.ecommerce_serang.data.api.dto.ProductsItem
import com.alya.ecommerce_serang.databinding.ItemProductGridBinding
import com.bumptech.glide.Glide
import java.text.NumberFormat
import java.util.Locale
class ProductsCategoryAdapter(
private var products: List<ProductsItem>,
private val onClick: (ProductsItem) -> Unit
) : RecyclerView.Adapter<ProductsCategoryAdapter.ProductViewHolder>() {
fun updateProducts(newProducts: List<ProductsItem>) {
products = newProducts
notifyDataSetChanged()
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ProductViewHolder {
val binding = ItemProductGridBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
return ProductViewHolder(binding)
}
override fun onBindViewHolder(holder: ProductViewHolder, position: Int) {
holder.bind(products[position])
}
override fun getItemCount(): Int = products.size
inner class ProductViewHolder(
private val binding: ItemProductGridBinding
) : RecyclerView.ViewHolder(binding.root) {
fun bind(product: ProductsItem) {
binding.apply {
tvProductName.text = product.name
val priceValue = product.price.toDoubleOrNull() ?: 0.0
tvProductPrice.text = "Rp ${NumberFormat.getNumberInstance(Locale("id", "ID")).format(priceValue.toInt())}"
// Load product image
Glide.with(itemView.context)
.load("${BuildConfig.BASE_URL}${product.image}")
.placeholder(R.drawable.placeholder_image)
.error(R.drawable.placeholder_image)
.centerCrop()
.into(ivProductImage)
// Set click listener
root.setOnClickListener {
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))
}
}
}
}
}

View File

@ -0,0 +1,178 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout 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.category.CategoryProductsActivity">
<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar">
<com.google.android.material.appbar.CollapsingToolbarLayout
android:layout_width="match_parent"
android:layout_height="200dp"
app:layout_scrollFlags="scroll|exitUntilCollapsed"
app:contentScrim="?attr/colorPrimary"
app:expandedTitleMarginStart="48dp"
app:expandedTitleMarginEnd="64dp">
<ImageView
android:id="@+id/iv_category_header"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="centerCrop"
app:layout_collapseMode="parallax"
android:contentDescription="Category Header Image" />
<View
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/blue_50" />
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:layout_collapseMode="pin"
app:popupTheme="@style/ThemeOverlay.AppCompat.Light" />
</com.google.android.material.appbar.CollapsingToolbarLayout>
</com.google.android.material.appbar.AppBarLayout>
<androidx.core.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<!-- Category Title Section -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:id="@+id/tv_category_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Category Name"
android:textSize="24sp"
android:textStyle="bold"
android:textColor="@color/black_500"
android:gravity="center" />
<View
android:layout_width="60dp"
android:layout_height="3dp"
android:layout_gravity="center"
android:layout_marginTop="8dp"
android:background="@color/blue_500" />
</LinearLayout>
<!-- Products Section -->
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp">
<!-- Loading State -->
<ProgressBar
android:id="@+id/progress_bar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginTop="32dp"
android:visibility="gone" />
<!-- Products RecyclerView -->
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_products"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="8dp"
android:clipToPadding="false"
tools:listitem="@layout/item_product_grid" />
<!-- Empty State -->
<LinearLayout
android:id="@+id/layout_empty"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:orientation="vertical"
android:padding="32dp"
android:gravity="center"
android:visibility="gone">
<ImageView
android:layout_width="120dp"
android:layout_height="120dp"
android:src="@drawable/baseline_search_24"
android:layout_marginBottom="16dp"
android:contentDescription="No Products" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="No products found in this category"
android:textSize="16sp"
android:textColor="@color/black_200"
android:gravity="center" />
</LinearLayout>
<!-- Error State -->
<LinearLayout
android:id="@+id/layout_error"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:orientation="vertical"
android:padding="32dp"
android:gravity="center"
android:visibility="gone">
<ImageView
android:layout_width="120dp"
android:layout_height="120dp"
android:src="@drawable/baseline_alarm_24"
android:layout_marginBottom="16dp"
android:contentDescription="Error" />
<TextView
android:id="@+id/tv_error_message"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Something went wrong"
android:textSize="16sp"
android:textColor="@color/black_200"
android:gravity="center"
android:layout_marginBottom="16dp" />
<Button
android:id="@+id/btn_retry"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Retry"
style="@style/Widget.MaterialComponents.Button.OutlinedButton" />
</LinearLayout>
</FrameLayout>
</LinearLayout>
</androidx.core.widget.NestedScrollView>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@ -230,7 +230,20 @@
android:background="@drawable/bg_button_filled" android:background="@drawable/bg_button_filled"
android:text="Kirim Bukti Bayar" android:text="Kirim Bukti Bayar"
android:textAllCaps="false" android:textAllCaps="false"
android:textColor="@android:color/white" /> android:textColor="@android:color/white"
android:enabled="true"
android:clickable="true"
android:focusable="true" />
</LinearLayout> </LinearLayout>
<ProgressBar
android:id="@+id/progressBar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -1,67 +1,84 @@
<?xml version="1.0" encoding="utf-8"?> <androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_margin="8dp" android:layout_margin="8dp"
app:cardCornerRadius="8dp" app:cardCornerRadius="12dp"
app:cardElevation="2dp"> app:cardElevation="4dp"
android:foreground="?android:attr/selectableItemBackground">
<androidx.constraintlayout.widget.ConstraintLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content"> android:layout_height="wrap_content"
android:orientation="vertical">
<!-- Product Image -->
<ImageView <ImageView
android:id="@+id/productImage" android:id="@+id/iv_product_image"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="0dp" android:layout_height="160dp"
android:scaleType="centerCrop" android:scaleType="centerCrop"
app:layout_constraintDimensionRatio="1:1" android:background="@color/light_gray"
app:layout_constraintTop_toTopOf="parent" android:contentDescription="Product Image"
tools:src="@drawable/placeholder_image" /> tools:src="@drawable/placeholder_image" />
<TextView <!-- Product Info -->
android:id="@+id/productName" <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="8dp" android:orientation="vertical"
android:layout_marginTop="8dp" android:padding="12dp">
android:layout_marginEnd="8dp"
android:ellipsize="end"
android:maxLines="2"
android:textSize="14sp"
app:layout_constraintTop_toBottomOf="@id/productImage"
tools:text="Product Name" />
<TextView <!-- Product Name -->
android:id="@+id/storeName" <TextView
android:layout_width="match_parent" android:id="@+id/tv_product_name"
android:layout_height="wrap_content" android:layout_width="match_parent"
android:layout_marginStart="8dp" android:layout_height="wrap_content"
android:layout_marginTop="4dp" android:text="Product Name"
android:layout_marginEnd="8dp" android:textSize="14sp"
android:ellipsize="end" android:textStyle="bold"
android:maxLines="1" android:textColor="@color/black_500"
android:textSize="12sp" android:maxLines="2"
android:fontFamily="@font/dmsans_medium" android:ellipsize="end"
app:layout_constraintTop_toBottomOf="@id/productName" tools:text="Sample Product Name" />
tools:text="Store Name" />
<TextView <!-- Product Price -->
android:id="@+id/productPrice" <TextView
android:layout_width="match_parent" android:id="@+id/tv_product_price"
android:layout_height="wrap_content" android:layout_width="match_parent"
android:layout_marginStart="8dp" android:layout_height="wrap_content"
android:layout_marginTop="4dp" android:layout_marginTop="4dp"
android:layout_marginEnd="8dp" android:text="Rp 0"
android:layout_marginBottom="8dp" android:textSize="16sp"
android:textColor="@color/blue1" android:textStyle="bold"
android:textSize="16sp" android:textColor="@color/blue_500"
android:textStyle="bold" tools:text="Rp 25,000" />
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toBottomOf="@id/storeName"
tools:text="Rp 150.000" />
</androidx.constraintlayout.widget.ConstraintLayout> <!-- Stock Status -->
</com.google.android.material.card.MaterialCardView> <TextView
android:id="@+id/tv_stock_status"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:text="Stock: 0"
android:textSize="12sp"
android:textColor="@color/black_200"
tools:text="Stock: 15" />
<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="Stock: 15" />
</LinearLayout>
</LinearLayout>
</androidx.cardview.widget.CardView>

View File

@ -42,6 +42,8 @@
<color name="soft_gray">#7D8FAB</color> <color name="soft_gray">#7D8FAB</color>
<color name="blue1">#489EC6</color> <color name="blue1">#489EC6</color>
<color name="yellow">#faf069</color> <color name="yellow">#faf069</color>
<color name="green">#A5C882</color>
<color name="red">#EC4E20</color>
<color name="bottom_navigation_icon_color_active">#489EC6</color> <color name="bottom_navigation_icon_color_active">#489EC6</color>
<color name="bottom_navigation_icon_color_inactive">#8E8E8E</color> <color name="bottom_navigation_icon_color_inactive">#8E8E8E</color>