From e2d909d21864eddb2095301ac07905a582fcf88f Mon Sep 17 00:00:00 2001 From: shaulascr Date: Thu, 29 May 2025 16:39:50 +0700 Subject: [PATCH] update payment and add evidence --- app/src/main/AndroidManifest.xml | 5 +- .../data/repository/ProductRepository.kt | 52 ++++ .../ecommerce_serang/ui/home/HomeFragment.kt | 6 +- .../ui/home/SearchResultAdapter.kt | 8 +- .../detail/AddEvidencePaymentActivity.kt | 239 ++++++++++++------ .../ui/order/detail/PaymentActivity.kt | 103 +++++--- .../category/CategoryProductsActivity.kt | 188 ++++++++++++++ .../category/CategoryProductsViewModel.kt | 49 ++++ .../category/ProductsCategoryAdapter.kt | 73 ++++++ .../res/layout/activity_category_products.xml | 178 +++++++++++++ app/src/main/res/layout/activity_payment.xml | 15 +- app/src/main/res/layout/item_product_grid.xml | 115 +++++---- app/src/main/res/values/colors.xml | 2 + 13 files changed, 862 insertions(+), 171 deletions(-) create mode 100644 app/src/main/java/com/alya/ecommerce_serang/ui/product/category/CategoryProductsActivity.kt create mode 100644 app/src/main/java/com/alya/ecommerce_serang/ui/product/category/CategoryProductsViewModel.kt create mode 100644 app/src/main/java/com/alya/ecommerce_serang/ui/product/category/ProductsCategoryAdapter.kt create mode 100644 app/src/main/res/layout/activity_category_products.xml diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index ddbfa98..cedc9c4 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -29,6 +29,9 @@ android:theme="@style/Theme.Ecommerce_serang" android:usesCleartextTraffic="true" tools:targetApi="31"> + @@ -76,11 +79,11 @@ android:enabled="true" android:exported="false" android:foregroundServiceType="dataSync" /> + - diff --git a/app/src/main/java/com/alya/ecommerce_serang/data/repository/ProductRepository.kt b/app/src/main/java/com/alya/ecommerce_serang/data/repository/ProductRepository.kt index 85aae9e..3c31d1a 100644 --- a/app/src/main/java/com/alya/ecommerce_serang/data/repository/ProductRepository.kt +++ b/app/src/main/java/com/alya/ecommerce_serang/data/repository/ProductRepository.kt @@ -286,6 +286,58 @@ class ProductRepository(private val apiService: ApiService) { } } + suspend fun getProductsByCategory(categoryId: Int): Result> = + 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 = + 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 { private const val TAG = "ProductRepository" } diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/home/HomeFragment.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/home/HomeFragment.kt index a902329..db00d1b 100644 --- a/app/src/main/java/com/alya/ecommerce_serang/ui/home/HomeFragment.kt +++ b/app/src/main/java/com/alya/ecommerce_serang/ui/home/HomeFragment.kt @@ -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.notif.NotificationActivity 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.HorizontalMarginItemDecoration import com.alya.ecommerce_serang.utils.SessionManager @@ -211,7 +212,10 @@ class HomeFragment : Fragment() { } 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() { diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/home/SearchResultAdapter.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/home/SearchResultAdapter.kt index 7a95fdc..555976f 100644 --- a/app/src/main/java/com/alya/ecommerce_serang/ui/home/SearchResultAdapter.kt +++ b/app/src/main/java/com/alya/ecommerce_serang/ui/home/SearchResultAdapter.kt @@ -42,19 +42,19 @@ class SearchResultsAdapter( } fun bind(product: ProductsItem) { - binding.productName.text = product.name - binding.productPrice.text = (product.price) + binding.tvProductName.text = product.name + binding.tvProductPrice.text = (product.price) // Load image with Glide Glide.with(binding.root.context) .load(product.image) .placeholder(R.drawable.placeholder_image) // .error(R.drawable.error_image) - .into(binding.productImage) + .into(binding.ivProductImage) // Set store name if available product.storeId?.toString().let { - binding.storeName.text = it + binding.tvStoreName.text = it } } } diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/order/detail/AddEvidencePaymentActivity.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/order/detail/AddEvidencePaymentActivity.kt index 77d4e91..fc37df7 100644 --- a/app/src/main/java/com/alya/ecommerce_serang/ui/order/detail/AddEvidencePaymentActivity.kt +++ b/app/src/main/java/com/alya/ecommerce_serang/ui/order/detail/AddEvidencePaymentActivity.kt @@ -2,11 +2,14 @@ package com.alya.ecommerce_serang.ui.order.detail import android.Manifest import android.R +import android.app.Activity import android.app.DatePickerDialog +import android.content.Intent import android.content.pm.PackageManager import android.graphics.BitmapFactory import android.net.Uri import android.os.Bundle +import android.provider.MediaStore import android.util.Log import android.view.View import android.webkit.MimeTypeMap @@ -46,6 +49,7 @@ class AddEvidencePaymentActivity : AppCompatActivity() { private lateinit var productPrice: String private var selectedImageUri: Uri? = null + private val viewModel: PaymentViewModel by viewModels { BaseViewModelFactory { val apiService = ApiConfig.getApiService(sessionManager) @@ -62,55 +66,65 @@ class AddEvidencePaymentActivity : AppCompatActivity() { "Cash on Delivery" ) - private val getContent = registerForActivityResult(ActivityResultContracts.GetContent()) { uri: Uri? -> - uri?.let { - selectedImageUri = it - binding.ivUploadedImage.setImageURI(selectedImageUri) - binding.ivUploadedImage.visibility = View.VISIBLE - binding.layoutUploadPlaceholder.visibility = View.GONE +// private val getContent = registerForActivityResult(ActivityResultContracts.GetContent()) { uri: Uri? -> +// uri?.let { +// selectedImageUri = it +// binding.ivUploadedImage.setImageURI(selectedImageUri) +// binding.ivUploadedImage.visibility = View.VISIBLE +// 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?) { 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 -> - orderId = bundle.getInt("ORDER_ID", 0) - paymentInfoId = bundle.getInt("PAYMENT_INFO_ID", 0) - productPrice = intent.getStringExtra("TOTAL_AMOUNT") ?: "Rp0" + sessionManager = SessionManager(this) + 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() { @@ -126,11 +140,11 @@ class AddEvidencePaymentActivity : AppCompatActivity() { // Upload image button binding.tvAddPhoto.setOnClickListener { - checkPermissionAndPickImage() + checkPermissionsAndShowImagePicker() } binding.frameUploadImage.setOnClickListener { - checkPermissionAndPickImage() + checkPermissionsAndShowImagePicker() } // Date picker @@ -158,6 +172,7 @@ class AddEvidencePaymentActivity : AppCompatActivity() { // Submit button binding.btnSubmit.setOnClickListener { 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, + 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() { // Validate all fields 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, - 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() { val calendar = Calendar.getInstance() val year = calendar.get(Calendar.YEAR) diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/order/detail/PaymentActivity.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/order/detail/PaymentActivity.kt index 72f824e..d2c525a 100644 --- a/app/src/main/java/com/alya/ecommerce_serang/ui/order/detail/PaymentActivity.kt +++ b/app/src/main/java/com/alya/ecommerce_serang/ui/order/detail/PaymentActivity.kt @@ -3,6 +3,7 @@ package com.alya.ecommerce_serang.ui.order.detail import android.content.Intent import android.os.Bundle import android.util.Log +import android.view.View import android.widget.Toast import androidx.activity.enableEdgeToEdge import androidx.activity.viewModels @@ -43,65 +44,88 @@ class PaymentActivity : AppCompatActivity() { sessionManager = SessionManager(this) - WindowCompat.setDecorFitsSystemWindows(window, false) + setupWindowInsets() - 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 - } - - // Mengambil data dari intent + // Get data from intent val orderId = intent.getIntExtra("ORDER_ID", 0) val paymentInfoId = intent.getIntExtra("ORDER_PAYMENT_ID", 0) if (orderId == 0) { Toast.makeText(this, "ID pesanan tidak valid", Toast.LENGTH_SHORT).show() 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 { - onBackPressedDispatcher finish() } + } - // Setup petunjuk transfer + private fun setupClickListeners(orderId: Int, paymentInfoId: Int) { + // Instructions clicks binding.layoutMBankingInstructions.setOnClickListener { - // Tampilkan instruksi mBanking showInstructions("mBanking") } binding.layoutATMInstructions.setOnClickListener { - // Tampilkan instruksi ATM showInstructions("ATM") } - // Setup button upload bukti bayar - binding.btnUploadPaymentProof.setOnClickListener { -// Intent ke activity upload bukti bayar - val intent = Intent(this, AddEvidencePaymentActivity::class.java) - intent.putExtra("ORDER_ID", orderId) - intent.putExtra("PAYMENT_INFO_ID", paymentInfoId) - intent.putExtra("TOTAL_AMOUNT", binding.tvTotalAmount.text.toString()) - Log.d(TAG, "Received Order ID: $orderId, Payment Info ID: $paymentInfoId, Total Amount: ${binding.tvTotalAmount.text}") + // Upload button +// binding.btnUploadPaymentProof.setOnClickListener { view -> +// Log.d(TAG, "Button clicked - showing toast") +// Toast.makeText(this@PaymentActivity, "Button works! OrderID: $orderId", Toast.LENGTH_LONG).show() +// } + binding.btnUploadPaymentProof.apply { + isEnabled = true + isClickable = true - 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 - Log.d(TAG, "Fetching order details for Order ID: $orderId") - viewModel.getOrderDetails(orderId) + // Debug button state + Log.d(TAG, "Button setup - isEnabled: ${binding.btnUploadPaymentProof.isEnabled}, isClickable: ${binding.btnUploadPaymentProof.isClickable}") } private fun observeData() { @@ -124,13 +148,14 @@ class PaymentActivity : AppCompatActivity() { setupPaymentDueDate(order.updatedAt) } - // Observe loading state viewModel.isLoading.observe(this) { isLoading -> - // Show loading indicator if needed - // binding.progressBar.visibility = if (isLoading) View.VISIBLE else View.GONE + Log.d(TAG, "Loading state changed: $isLoading") + // 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 -> if (error.isNotEmpty()) { Toast.makeText(this, error, Toast.LENGTH_SHORT).show() diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/product/category/CategoryProductsActivity.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/product/category/CategoryProductsActivity.kt new file mode 100644 index 0000000..fb80bbb --- /dev/null +++ b/app/src/main/java/com/alya/ecommerce_serang/ui/product/category/CategoryProductsActivity.kt @@ -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(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( + 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 + } +} \ No newline at end of file diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/product/category/CategoryProductsViewModel.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/product/category/CategoryProductsViewModel.kt new file mode 100644 index 0000000..91c78a8 --- /dev/null +++ b/app/src/main/java/com/alya/ecommerce_serang/ui/product/category/CategoryProductsViewModel.kt @@ -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.Loading) + val uiState: StateFlow = _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) : CategoryProductsUiState() + data class Error(val message: String) : CategoryProductsUiState() +} \ No newline at end of file diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/product/category/ProductsCategoryAdapter.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/product/category/ProductsCategoryAdapter.kt new file mode 100644 index 0000000..7e82b2b --- /dev/null +++ b/app/src/main/java/com/alya/ecommerce_serang/ui/product/category/ProductsCategoryAdapter.kt @@ -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, + private val onClick: (ProductsItem) -> Unit +) : RecyclerView.Adapter() { + + fun updateProducts(newProducts: List) { + 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)) + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/res/layout/activity_category_products.xml b/app/src/main/res/layout/activity_category_products.xml new file mode 100644 index 0000000..2c023dc --- /dev/null +++ b/app/src/main/res/layout/activity_category_products.xml @@ -0,0 +1,178 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +