mirror of
https://github.com/shaulascr/ecommerce_serang.git
synced 2025-08-10 09:22:21 +00:00
Merge branch 'screen-features'
This commit is contained in:
@ -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" />
|
||||||
|
@ -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"
|
||||||
}
|
}
|
||||||
|
@ -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() {
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
|
@ -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()
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
@ -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()
|
||||||
|
}
|
@ -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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
178
app/src/main/res/layout/activity_category_products.xml
Normal file
178
app/src/main/res/layout/activity_category_products.xml
Normal 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>
|
@ -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>
|
@ -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>
|
@ -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>
|
||||||
|
Reference in New Issue
Block a user