diff --git a/.gitignore b/.gitignore
index aa724b7..9683136 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,5 @@
*.iml
+*.log
.gradle
/local.properties
/.idea/caches
@@ -12,4 +13,4 @@
/captures
.externalNativeBuild
.cxx
-local.properties
+/app/google-services.json
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..185a927
--- /dev/null
+++ b/README.md
@@ -0,0 +1,68 @@
+# E-Commerce Serang (Android App)
+
+A mobile e-commerce platform built with **Kotlin** (Android) and a backend in **Express**, tailored for small businesses (UMKM). Supports browsing, ordering, chatting, and tracking — with **shipping cost calculation (RajaOngkir)** and **push notifications** via Firebase Cloud Messaging.
+
+---
+
+## Overview
+
+This Android app includes:
+
+- Account registration, login, and OTP verification
+- Browsing products by category and store
+- Cart and checkout orders
+- Shipping cost estimation via RajaOngkir API
+- Checkout order, tracking, and status updates
+- Real-time buyer–seller chat
+- Store registration and product management
+- Store Balance as active status
+- Top up store balance
+- Write rating and feedback for purchased products
+- Push notifications for user activity
+
+The app communicates with a custom backend server via REST API and WebSocket.
+
+## Tech Stack
+- MVVM architecture
+- Retrofit for API communication
+- Hilt for dependency injection
+- Socket.IO client for real-time chat
+- Firebase Cloud Messaging (FCM) for notifications
+- Coroutines for async operations
+- Glide for image loading
+- ViewBinding for UI access
+- LiveData and StateFlow for reactive UI
+- RajaOngkir API integration for shipping cost
+
+## Project Structure
+ - api/retrofit/
+ - data/
+ - di/
+ - ui/
+ - auth/
+ - home/
+ - cart/
+ - order/
+ - history/
+ - review/
+ - chat/
+ - profile/
+ - store/
+ - addProduct/
+ - sells/
+ - balance/
+ - review/
+ - product/
+ - notif/
+ - utils/
+ - google-services.json
+
+## How to Run
+
+1. Clone this project and open it in Android Studio
+2. Add your `google-services.json` for Firebase (FCM)
+3. Update the API base URL in the Retrofit client
+5. Settings BASE_URL in your local.properties
+4. Build and run on an emulator or physical device
+
+
diff --git a/app/.gitignore b/app/.gitignore
index 42afabf..65d12b9 100644
--- a/app/.gitignore
+++ b/app/.gitignore
@@ -1 +1,2 @@
-/build
\ No newline at end of file
+/build
+google-services.json
\ No newline at end of file
diff --git a/app/google-services.json b/app/google-services.json
deleted file mode 100644
index f88005d..0000000
--- a/app/google-services.json
+++ /dev/null
@@ -1,29 +0,0 @@
-{
- "project_info": {
- "project_number": "284675201257",
- "project_id": "ecommerce-serang",
- "storage_bucket": "ecommerce-serang.firebasestorage.app"
- },
- "client": [
- {
- "client_info": {
- "mobilesdk_app_id": "1:284675201257:android:2755670e3dbb1b48683878",
- "android_client_info": {
- "package_name": "com.alya.ecommerce_serang"
- }
- },
- "oauth_client": [],
- "api_key": [
- {
- "current_key": "AIzaSyB-nWHsVbdV4PPIH06JZSStIVXjv9Qc4iU"
- }
- ],
- "services": {
- "appinvite_service": {
- "other_platform_oauth_client": []
- }
- }
- }
- ],
- "configuration_version": "1"
-}
\ No newline at end of file
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 54fdf3b..4455d19 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -82,11 +82,11 @@
-
+
+
+
+
+
,
@field:SerializedName("auto_completed_at")
- val autoCompletedAt: String? = null,
+ val autoCompletedAt: String,
@field:SerializedName("is_store_location")
val isStoreLocation: Boolean? = null,
diff --git a/app/src/main/java/com/alya/ecommerce_serang/data/api/response/customer/order/OrderListResponse.kt b/app/src/main/java/com/alya/ecommerce_serang/data/api/response/customer/order/OrderListResponse.kt
index dd8c43f..2ab5242 100644
--- a/app/src/main/java/com/alya/ecommerce_serang/data/api/response/customer/order/OrderListResponse.kt
+++ b/app/src/main/java/com/alya/ecommerce_serang/data/api/response/customer/order/OrderListResponse.kt
@@ -15,7 +15,7 @@ data class OrderListResponse(
data class OrderItemsItem(
@field:SerializedName("review_id")
- val reviewId: Int? = null,
+ val reviewId: Int,
@field:SerializedName("quantity")
val quantity: Int,
diff --git a/app/src/main/java/com/alya/ecommerce_serang/data/api/response/customer/order/SubdistrictResponse.kt b/app/src/main/java/com/alya/ecommerce_serang/data/api/response/customer/order/SubdistrictResponse.kt
new file mode 100644
index 0000000..56b103a
--- /dev/null
+++ b/app/src/main/java/com/alya/ecommerce_serang/data/api/response/customer/order/SubdistrictResponse.kt
@@ -0,0 +1,21 @@
+package com.alya.ecommerce_serang.data.api.response.customer.order
+
+import com.google.gson.annotations.SerializedName
+
+data class SubdistrictResponse(
+
+ @field:SerializedName("subdistricts")
+ val subdistricts: List,
+
+ @field:SerializedName("message")
+ val message: String
+)
+
+data class SubdistrictsItem(
+
+ @field:SerializedName("subdistrict_id")
+ val subdistrictId: String,
+
+ @field:SerializedName("subdistrict_name")
+ val subdistrictName: String
+)
diff --git a/app/src/main/java/com/alya/ecommerce_serang/data/api/response/customer/order/VillagesResponse.kt b/app/src/main/java/com/alya/ecommerce_serang/data/api/response/customer/order/VillagesResponse.kt
new file mode 100644
index 0000000..04da40e
--- /dev/null
+++ b/app/src/main/java/com/alya/ecommerce_serang/data/api/response/customer/order/VillagesResponse.kt
@@ -0,0 +1,24 @@
+package com.alya.ecommerce_serang.data.api.response.customer.order
+
+import com.google.gson.annotations.SerializedName
+
+data class VillagesResponse(
+
+ @field:SerializedName("villages")
+ val villages: List,
+
+ @field:SerializedName("message")
+ val message: String
+)
+
+data class VillagesItem(
+
+ @field:SerializedName("village_id")
+ val villageId: String,
+
+ @field:SerializedName("village_name")
+ val villageName: String,
+
+ @field:SerializedName("postal_code")
+ val postalCode: String
+)
diff --git a/app/src/main/java/com/alya/ecommerce_serang/data/api/response/customer/profile/AddressResponse.kt b/app/src/main/java/com/alya/ecommerce_serang/data/api/response/customer/profile/AddressResponse.kt
index 55b23f8..588ae30 100644
--- a/app/src/main/java/com/alya/ecommerce_serang/data/api/response/customer/profile/AddressResponse.kt
+++ b/app/src/main/java/com/alya/ecommerce_serang/data/api/response/customer/profile/AddressResponse.kt
@@ -4,15 +4,18 @@ import com.google.gson.annotations.SerializedName
data class AddressResponse(
- @field:SerializedName("addresses")
+ @field:SerializedName("addresses")
val addresses: List,
- @field:SerializedName("message")
+ @field:SerializedName("message")
val message: String
)
data class AddressesItem(
+ @field:SerializedName("village_id")
+ val villageId: String,
+
@field:SerializedName("is_store_location")
val isStoreLocation: Boolean,
@@ -23,7 +26,7 @@ data class AddressesItem(
val userId: Int,
@field:SerializedName("province_id")
- val provinceId: Int,
+ val provinceId: String,
@field:SerializedName("phone")
val phone: String,
@@ -50,5 +53,5 @@ data class AddressesItem(
val longitude: String,
@field:SerializedName("city_id")
- val cityId: Int
+ val cityId: String
)
diff --git a/app/src/main/java/com/alya/ecommerce_serang/data/api/retrofit/ApiConfig.kt b/app/src/main/java/com/alya/ecommerce_serang/data/api/retrofit/ApiConfig.kt
index b277cea..f53ae8a 100644
--- a/app/src/main/java/com/alya/ecommerce_serang/data/api/retrofit/ApiConfig.kt
+++ b/app/src/main/java/com/alya/ecommerce_serang/data/api/retrofit/ApiConfig.kt
@@ -15,10 +15,14 @@ class ApiConfig {
val loggingInterceptor = HttpLoggingInterceptor().apply {
level = HttpLoggingInterceptor.Level.BODY
+ //httplogginginterceptor ntuk debug dan monitoring request/response
}
val authInterceptor = AuthInterceptor(tokenManager)
+ // utk tambak token auth otomatis pada header
+ // Konfigurasi OkHttpClient
+ //Low-level HTTP client yang melakukan actual network request
val client = OkHttpClient.Builder()
.addInterceptor(loggingInterceptor)
.addInterceptor(authInterceptor)
@@ -27,13 +31,17 @@ class ApiConfig {
.writeTimeout(300, TimeUnit.SECONDS) // 5 minutes
.build()
+ // Konfigurasi Retrofit
val retrofit = Retrofit.Builder()
+ //almat domain backend
.baseUrl(BuildConfig.BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
+ //gson convertes: mengkonversi JSON ke object Kotlin dan sebaliknya
.client(client)
.build()
return retrofit.create(ApiService::class.java)
+ // retrofit : menyederhanakan HTTP Request dgn mengubah interface Kotlin di ApiService menjadi HTTP calls secara otomatis
}
fun getUnauthenticatedApiService(): ApiService {
diff --git a/app/src/main/java/com/alya/ecommerce_serang/data/api/retrofit/ApiService.kt b/app/src/main/java/com/alya/ecommerce_serang/data/api/retrofit/ApiService.kt
index 1a04e32..6a85bca 100644
--- a/app/src/main/java/com/alya/ecommerce_serang/data/api/retrofit/ApiService.kt
+++ b/app/src/main/java/com/alya/ecommerce_serang/data/api/retrofit/ApiService.kt
@@ -53,6 +53,8 @@ import com.alya.ecommerce_serang.data.api.response.customer.order.ListCityRespon
import com.alya.ecommerce_serang.data.api.response.customer.order.ListProvinceResponse
import com.alya.ecommerce_serang.data.api.response.customer.order.OrderDetailResponse
import com.alya.ecommerce_serang.data.api.response.customer.order.OrderListResponse
+import com.alya.ecommerce_serang.data.api.response.customer.order.SubdistrictResponse
+import com.alya.ecommerce_serang.data.api.response.customer.order.VillagesResponse
import com.alya.ecommerce_serang.data.api.response.customer.product.AllProductResponse
import com.alya.ecommerce_serang.data.api.response.customer.product.CategoryResponse
import com.alya.ecommerce_serang.data.api.response.customer.product.DetailStoreProductResponse
@@ -68,14 +70,14 @@ import com.alya.ecommerce_serang.data.api.response.order.ComplaintResponse
import com.alya.ecommerce_serang.data.api.response.order.CompletedOrderResponse
import com.alya.ecommerce_serang.data.api.response.product.CreateSearchResponse
import com.alya.ecommerce_serang.data.api.response.product.SearchHistoryResponse
-import com.alya.ecommerce_serang.data.api.response.store.sells.PaymentConfirmationResponse
+import com.alya.ecommerce_serang.data.api.response.store.GenericResponse
import com.alya.ecommerce_serang.data.api.response.store.product.CreateProductResponse
import com.alya.ecommerce_serang.data.api.response.store.product.DeleteProductResponse
import com.alya.ecommerce_serang.data.api.response.store.product.UpdateProductResponse
import com.alya.ecommerce_serang.data.api.response.store.product.ViewStoreProductsResponse
-import com.alya.ecommerce_serang.data.api.response.store.GenericResponse
import com.alya.ecommerce_serang.data.api.response.store.profile.StoreDataResponse
import com.alya.ecommerce_serang.data.api.response.store.review.ProductReviewResponse
+import com.alya.ecommerce_serang.data.api.response.store.sells.PaymentConfirmationResponse
import com.alya.ecommerce_serang.data.api.response.store.topup.BalanceTopUpResponse
import com.alya.ecommerce_serang.data.api.response.store.topup.TopUpResponse
import okhttp3.MultipartBody
@@ -512,4 +514,14 @@ interface ApiService {
@GET("store/reviews")
suspend fun getStoreProductReview(
): Response
+
+ @GET("subdistrict/{cityId}")
+ suspend fun getSubdistrict(
+ @Path("cityId") cityId: String
+ ): Response
+
+ @GET("villages/{subdistrictId}")
+ suspend fun getVillages(
+ @Path("subdistrictId") subdistrictId: String
+ ): Response
}
\ No newline at end of file
diff --git a/app/src/main/java/com/alya/ecommerce_serang/data/repository/MyStoreRepository.kt b/app/src/main/java/com/alya/ecommerce_serang/data/repository/MyStoreRepository.kt
index 801c8ea..0100c5e 100644
--- a/app/src/main/java/com/alya/ecommerce_serang/data/repository/MyStoreRepository.kt
+++ b/app/src/main/java/com/alya/ecommerce_serang/data/repository/MyStoreRepository.kt
@@ -1,10 +1,12 @@
package com.alya.ecommerce_serang.data.repository
import android.util.Log
+import com.alya.ecommerce_serang.data.api.dto.ProductsItem
import com.alya.ecommerce_serang.data.api.dto.Store
import com.alya.ecommerce_serang.data.api.response.auth.ListStoreTypeResponse
import com.alya.ecommerce_serang.data.api.response.customer.product.StoreResponse
import com.alya.ecommerce_serang.data.api.response.store.profile.StoreDataResponse
+import com.alya.ecommerce_serang.data.api.response.store.sells.OrderListResponse
import com.alya.ecommerce_serang.data.api.retrofit.ApiService
import okhttp3.MultipartBody
import okhttp3.RequestBody
@@ -71,4 +73,104 @@ class MyStoreRepository(private val apiService: ApiService) {
street, subdistrict, detail, postalCode, latitude, longitude, userPhone, storeType, storeimg
)
}
+
+ suspend fun getSellList(status: String): Result {
+ return try {
+ Log.d("SellsRepository", "Add Evidence : $status")
+ val response = apiService.getSellList(status)
+
+ if (response.isSuccessful) {
+ val allListSell = response.body()
+ if (allListSell != null) {
+ Log.d("SellsRepository", "Add Evidence successfully: ${allListSell.message}")
+ Result.Success(allListSell)
+ } else {
+ Log.e("SellsRepository", "Response body was null")
+ Result.Error(Exception("Empty response from server"))
+ }
+ } else {
+ val errorBody = response.errorBody()?.string() ?: "Unknown error"
+ Log.e("SellsRepository", "Error Add Evidence : $errorBody")
+ Result.Error(Exception(errorBody))
+ }
+ } catch (e: Exception) {
+ Log.e("SellsRepository", "Exception Add Evidence ", e)
+ Result.Error(e)
+ }
+ }
+
+ suspend fun getBalance(): Result {
+ return try {
+ val response = apiService.getMyStoreData()
+
+ if (response.isSuccessful) {
+ val body = response.body()
+ ?: return Result.Error(IllegalStateException("Response body is null"))
+
+ // Validate the balance field
+ val balanceRaw = body.store.balance
+ balanceRaw.toDoubleOrNull()
+ ?: return Result.Error(NumberFormatException("Invalid balance format: $balanceRaw"))
+
+ Result.Success(body)
+ } else {
+ Result.Error(
+ Exception("Failed to load balance: ${response.code()} ${response.message()}")
+ )
+ }
+ } catch (e: Exception) {
+ Log.e("MyStoreRepository", "Error fetching balance", e)
+ Result.Error(e)
+ }
+ }
+
+ suspend fun fetchMyStoreProducts(): List {
+ return try {
+ val response = apiService.getStoreProduct()
+ if (response.isSuccessful) {
+ response.body()?.products?.filterNotNull() ?: emptyList()
+ } else {
+ throw Exception("Failed to fetch store products: ${response.message()}")
+ }
+ } catch (e: Exception) {
+ Log.e("ProductRepository", "Error fetching store products", e)
+ throw e
+ }
+ }
+
+// private fun fetchBalance() {
+// showLoading(true)
+// lifecycleScope.launch {
+// try {
+// val response = ApiConfig.getApiService(sessionManager).getMyStoreData()
+// if (response.isSuccessful && response.body() != null) {
+// val storeData = response.body()!!
+// val balance = storeData.store.balance
+//
+// // Format the balance
+// try {
+// val balanceValue = balance.toDouble()
+// binding.tvBalance.text = String.format("Rp%,.0f", balanceValue)
+// } catch (e: Exception) {
+// binding.tvBalance.text = "Rp$balance"
+// }
+// } else {
+// Toast.makeText(
+// this@BalanceActivity,
+// "Gagal memuat data saldo: ${response.message()}",
+// Toast.LENGTH_SHORT
+// ).show()
+// }
+// } catch (e: Exception) {
+// Log.e(TAG, "Error fetching balance", e)
+// Toast.makeText(
+// this@BalanceActivity,
+// "Error: ${e.message}",
+// Toast.LENGTH_SHORT
+// ).show()
+// } finally {
+// showLoading(false)
+// }
+// }
+// }
}
\ No newline at end of file
diff --git a/app/src/main/java/com/alya/ecommerce_serang/data/repository/UserRepository.kt b/app/src/main/java/com/alya/ecommerce_serang/data/repository/UserRepository.kt
index f5a5c9f..df62ae7 100644
--- a/app/src/main/java/com/alya/ecommerce_serang/data/repository/UserRepository.kt
+++ b/app/src/main/java/com/alya/ecommerce_serang/data/repository/UserRepository.kt
@@ -21,6 +21,8 @@ import com.alya.ecommerce_serang.data.api.response.auth.RegisterStoreResponse
import com.alya.ecommerce_serang.data.api.response.auth.VerifRegisterResponse
import com.alya.ecommerce_serang.data.api.response.customer.order.ListCityResponse
import com.alya.ecommerce_serang.data.api.response.customer.order.ListProvinceResponse
+import com.alya.ecommerce_serang.data.api.response.customer.order.SubdistrictResponse
+import com.alya.ecommerce_serang.data.api.response.customer.order.VillagesResponse
import com.alya.ecommerce_serang.data.api.response.customer.profile.EditProfileResponse
import com.alya.ecommerce_serang.data.api.retrofit.ApiService
import com.alya.ecommerce_serang.utils.FileUtils
@@ -68,6 +70,16 @@ class UserRepository(private val apiService: ApiService) {
return if (response.isSuccessful) response.body() else null
}
+ suspend fun getListSubdistrict(cityId : String): SubdistrictResponse? {
+ val response = apiService.getSubdistrict(cityId)
+ return if (response.isSuccessful) response.body() else null
+ }
+
+ suspend fun getListVillages(subId: String): VillagesResponse? {
+ val response = apiService.getVillages(subId)
+ return if (response.isSuccessful) response.body() else null
+ }
+
suspend fun registerUser(request: RegisterRequest): RegisterResponse {
val response = apiService.register(request) // API call
@@ -87,7 +99,7 @@ class UserRepository(private val apiService: ApiService) {
longitude: String,
street: String,
subdistrict: String,
- cityId: Int,
+ cityId: String,
provinceId: Int,
postalCode: Int,
detail: String,
diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/auth/LoginActivity.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/auth/LoginActivity.kt
index 531b526..01e7307 100644
--- a/app/src/main/java/com/alya/ecommerce_serang/ui/auth/LoginActivity.kt
+++ b/app/src/main/java/com/alya/ecommerce_serang/ui/auth/LoginActivity.kt
@@ -8,9 +8,7 @@ import android.widget.Toast
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 com.alya.ecommerce_serang.data.api.dto.FcmReq
import com.alya.ecommerce_serang.data.api.retrofit.ApiConfig
import com.alya.ecommerce_serang.data.repository.Result
@@ -43,20 +41,18 @@ class LoginActivity : AppCompatActivity() {
setContentView(binding.root)
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
- }
+// ViewCompat.setOnApplyWindowInsetsListener(binding.root) { view, windowInsets ->
+// val systemBars = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
+// view.setPadding(
+// systemBars.left,
+// systemBars.top,
+// systemBars.right,
+// systemBars.bottom
+// )
+// windowInsets
+// }
// onBackPressedDispatcher.addCallback(this) {
// // Handle the back button event
@@ -105,6 +101,7 @@ class LoginActivity : AppCompatActivity() {
finish()
}
is com.alya.ecommerce_serang.data.repository.Result.Error -> {
+ Log.e("LoginActivity", "Login Failed: ${result.exception.message}")
Toast.makeText(this, "Login Failed: ${result.exception.message}", Toast.LENGTH_LONG).show()
}
is Result.Loading -> {
diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/auth/RegisterActivity.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/auth/RegisterActivity.kt
index 04b5780..8a2c0a8 100644
--- a/app/src/main/java/com/alya/ecommerce_serang/ui/auth/RegisterActivity.kt
+++ b/app/src/main/java/com/alya/ecommerce_serang/ui/auth/RegisterActivity.kt
@@ -43,24 +43,6 @@ class RegisterActivity : AppCompatActivity() {
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
- }
-
-
-
Log.d("RegisterActivity", "Token in storage: '${sessionManager.getToken()}'")
Log.d("RegisterActivity", "User ID in storage: '${sessionManager.getUserId()}'")
@@ -104,7 +86,7 @@ class RegisterActivity : AppCompatActivity() {
}
}
- // Function to navigate to the next fragment
+ // navigate step register in fragment
fun navigateToStep(step: Int, userData: RegisterRequest?) {
val fragment = when (step) {
1 -> RegisterStep1Fragment.newInstance()
diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/auth/fragments/RegisterStep1Fragment.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/auth/fragments/RegisterStep1Fragment.kt
index 7e1eb62..814a366 100644
--- a/app/src/main/java/com/alya/ecommerce_serang/ui/auth/fragments/RegisterStep1Fragment.kt
+++ b/app/src/main/java/com/alya/ecommerce_serang/ui/auth/fragments/RegisterStep1Fragment.kt
@@ -204,6 +204,13 @@ class RegisterStep1Fragment : Fragment() {
}
}
}
+
+ registerViewModel.toastMessage.observe(viewLifecycleOwner){ event ->
+ //memanggil toast check value email dan phone
+ event.getContentIfNotHandled()?.let { msg ->
+ Toast.makeText(requireContext(), msg, Toast.LENGTH_SHORT).show()
+ }
+ }
}
private fun validateAndProceed() {
diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/auth/fragments/RegisterStep2Fragment.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/auth/fragments/RegisterStep2Fragment.kt
index e7e5bb6..15e3c22 100644
--- a/app/src/main/java/com/alya/ecommerce_serang/ui/auth/fragments/RegisterStep2Fragment.kt
+++ b/app/src/main/java/com/alya/ecommerce_serang/ui/auth/fragments/RegisterStep2Fragment.kt
@@ -88,7 +88,7 @@ class RegisterStep2Fragment : Fragment() {
// Update the email sent message
userData?.let {
- binding.tvEmailSent.text = "We've sent a verification code to ${it.email}"
+ binding.tvEmailSent.text = "Kami telah mengirimkan kode OTP ke alamat email ${it.email}. Silahkan periksa email anda."
}
// Start the resend cooldown timer
@@ -119,7 +119,7 @@ class RegisterStep2Fragment : Fragment() {
Log.d(TAG, "verifyOtp called with OTP: $otp")
if (otp.isEmpty()) {
- Toast.makeText(requireContext(), "Please enter the verification code", Toast.LENGTH_SHORT).show()
+ Toast.makeText(requireContext(), "Masukkan kode OTP anda", Toast.LENGTH_SHORT).show()
return
}
@@ -153,13 +153,13 @@ class RegisterStep2Fragment : Fragment() {
}
is com.alya.ecommerce_serang.data.repository.Result.Success -> {
binding.progressBar.visibility = View.GONE
- Toast.makeText(requireContext(), "Verification code resent", Toast.LENGTH_SHORT).show()
+ Toast.makeText(requireContext(), "Kode OTP sudah dikirim", Toast.LENGTH_SHORT).show()
startResendCooldown()
}
is Result.Error -> {
Log.e(TAG, "OTP request: Error - ${result.exception.message}")
binding.progressBar.visibility = View.GONE
- Toast.makeText(requireContext(), "Failed to resend code: ${result.exception.message}", Toast.LENGTH_SHORT).show()
+ Toast.makeText(requireContext(), "Gagal mengirim kode OTP", Toast.LENGTH_SHORT).show()
}
else -> {
Log.d(TAG, "OTP request: Unknown state")
@@ -180,7 +180,7 @@ class RegisterStep2Fragment : Fragment() {
countDownTimer = object : CountDownTimer(30000, 1000) {
override fun onTick(millisUntilFinished: Long) {
timeRemaining = (millisUntilFinished / 1000).toInt()
- binding.tvTimer.text = "Resend available in 00:${String.format("%02d", timeRemaining)}"
+ binding.tvTimer.text = "Kirim ulang OTP dalam waktu 00:${String.format("%02d", timeRemaining)}"
if (timeRemaining % 5 == 0) {
Log.d(TAG, "Cooldown remaining: $timeRemaining seconds")
}
@@ -188,7 +188,7 @@ class RegisterStep2Fragment : Fragment() {
override fun onFinish() {
Log.d(TAG, "Cooldown finished, enabling resend button")
- binding.tvTimer.text = "You can now resend the code"
+ binding.tvTimer.text = "Dapat mengirim ulang kode OTP"
binding.tvResendOtp.isEnabled = true
binding.tvResendOtp.setTextColor(ContextCompat.getColor(requireContext(), R.color.blue1))
timeRemaining = 0
@@ -222,7 +222,8 @@ class RegisterStep2Fragment : Fragment() {
binding.btnVerify.isEnabled = true
// Show error message
- Toast.makeText(requireContext(), "Registration Failed: ${result.exception.message}", Toast.LENGTH_SHORT).show()
+ Log.e(TAG, "Registration Failed: ${result.exception.message}")
+ Toast.makeText(requireContext(), "Gagal melakukan regsitrasi", Toast.LENGTH_SHORT).show()
}
else -> {
Log.d(TAG, "Registration: Unknown state")
@@ -251,15 +252,10 @@ class RegisterStep2Fragment : Fragment() {
sessionManager.saveToken(accessToken)
Log.d(TAG, "Token saved to SessionManager: $accessToken")
- // Also save user ID if available in the login response
-// result.data.?.let { userId ->
-// sessionManager.saveUserId(userId)
-// }
-
- Log.d(TAG, "Login successful, token saved: $accessToken")
-
// Proceed to Step 3
Log.d(TAG, "Proceeding to Step 3 after successful login")
+
+ // call navigate register step from activity
(activity as? RegisterActivity)?.navigateToStep(3, null )
}
is Result.Error -> {
@@ -269,7 +265,7 @@ class RegisterStep2Fragment : Fragment() {
// Show error message but continue to Step 3 anyway
Log.e(TAG, "Login failed but proceeding to Step 3", result.exception)
- Toast.makeText(requireContext(), "Note: Auto-login failed, but registration was successful", Toast.LENGTH_SHORT).show()
+ Toast.makeText(requireContext(), "Berhasil membuat akun, namun belum login", Toast.LENGTH_SHORT).show()
// Proceed to Step 3
(activity as? RegisterActivity)?.navigateToStep(3, null)
diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/auth/fragments/RegisterStep3Fragment.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/auth/fragments/RegisterStep3Fragment.kt
index e73281e..9cea2de 100644
--- a/app/src/main/java/com/alya/ecommerce_serang/ui/auth/fragments/RegisterStep3Fragment.kt
+++ b/app/src/main/java/com/alya/ecommerce_serang/ui/auth/fragments/RegisterStep3Fragment.kt
@@ -23,11 +23,14 @@ import com.alya.ecommerce_serang.ui.auth.LoginActivity
import com.alya.ecommerce_serang.ui.auth.RegisterActivity
import com.alya.ecommerce_serang.ui.order.address.CityAdapter
import com.alya.ecommerce_serang.ui.order.address.ProvinceAdapter
+import com.alya.ecommerce_serang.ui.order.address.SubdsitrictAdapter
import com.alya.ecommerce_serang.ui.order.address.ViewState
+import com.alya.ecommerce_serang.ui.order.address.VillagesAdapter
import com.alya.ecommerce_serang.utils.BaseViewModelFactory
import com.alya.ecommerce_serang.utils.SessionManager
import com.alya.ecommerce_serang.utils.viewmodel.RegisterViewModel
import com.google.android.material.progressindicator.LinearProgressIndicator
+import com.google.gson.Gson
class RegisterStep3Fragment : Fragment() {
private var _binding: FragmentRegisterStep3Binding? = null
@@ -49,6 +52,8 @@ class RegisterStep3Fragment : Fragment() {
// For province and city selection
private val provinceAdapter by lazy { ProvinceAdapter(requireContext()) }
private val cityAdapter by lazy { CityAdapter(requireContext()) }
+ private val subdistrictAdapter by lazy { SubdsitrictAdapter(requireContext()) }
+ private val villagesAdapter by lazy { VillagesAdapter(requireContext()) }
companion object {
private const val TAG = "RegisterStep3Fragment"
@@ -114,7 +119,7 @@ class RegisterStep3Fragment : Fragment() {
// Observe address submission state
observeAddressSubmissionState()
- // Load provinces
+ // Load provinces from raja ongkir
Log.d(TAG, "Requesting provinces data")
registerViewModel.getProvinces()
setupProvinceObserver()
@@ -171,9 +176,10 @@ class RegisterStep3Fragment : Fragment() {
}
private fun setupAutoComplete() {
- // Same implementation as before
binding.autoCompleteProvinsi.setAdapter(provinceAdapter)
binding.autoCompleteKabupaten.setAdapter(cityAdapter)
+ binding.autoCompleteKecamatan.setAdapter(subdistrictAdapter)
+ binding.autoCompleteDesa.setAdapter(villagesAdapter)
binding.autoCompleteProvinsi.setOnClickListener {
binding.autoCompleteProvinsi.showDropDown()
@@ -188,6 +194,24 @@ class RegisterStep3Fragment : Fragment() {
}
}
+ binding.autoCompleteKecamatan.setOnClickListener {
+ if (subdistrictAdapter.count > 0) {
+ Log.d(TAG, "Subdistrict dropdown clicked, showing ${subdistrictAdapter.count} cities")
+ binding.autoCompleteKecamatan.showDropDown()
+ } else {
+ Toast.makeText(requireContext(), "Pilih kabupaten / kota terlebih dahulu", Toast.LENGTH_SHORT).show()
+ }
+ }
+
+ binding.autoCompleteDesa.setOnClickListener {
+ if (villagesAdapter.count > 0) {
+ Log.d(TAG, "Village dropdown clicked, showing ${villagesAdapter.count} cities")
+ binding.autoCompleteDesa.showDropDown()
+ } else {
+ Toast.makeText(requireContext(), "Pilih kecamatan terlebih dahulu", Toast.LENGTH_SHORT).show()
+ }
+ }
+
binding.autoCompleteProvinsi.setOnItemClickListener { _, _, position, _ ->
val provinceId = provinceAdapter.getProvinceId(position)
Log.d(TAG, "Province selected at position $position, ID: $provinceId")
@@ -206,13 +230,44 @@ class RegisterStep3Fragment : Fragment() {
cityId?.let { id ->
Log.d(TAG, "Selected city ID set to: $id")
- registerViewModel.selectedCityId = id
+ registerViewModel.updateSelectedCityId(id)
+ registerViewModel.getSubdistrict(id)
+ binding.autoCompleteKecamatan.text.clear()
}
}
+
+ binding.autoCompleteKecamatan.setOnItemClickListener{ _, _, position, _ ->
+ val subdistrictId = subdistrictAdapter.getSubdistrictId(position)
+ Log.d(TAG, "Subdistrict selected at position $position, ID: $subdistrictId")
+
+ subdistrictId?.let { id ->
+ Log.d(TAG, "Selected subdistrict ID set to: $id")
+ registerViewModel.selectedSubdistrict = id
+ registerViewModel.getVillages(id)
+ binding.autoCompleteDesa.text.clear()
+ }
+ }
+
+ binding.autoCompleteDesa.setOnItemClickListener{ _, _, position, _ ->
+ val villageId = villagesAdapter.getVillageId(position)
+// val postalCode = villagesAdapter.getPostalCode(position)
+ Log.d(TAG, "Village selected at position $position, ID: $villageId")
+
+ villageId?.let { id ->
+ Log.d(TAG, "Selected village ID set to: $id")
+ registerViewModel.selectedVillages = id
+ }
+
+// postalCode?.let { postCode ->
+// registerViewModel.selectedPostalCode = postCode
+// }
+
+// binding.etKodePos.setText(registerViewModel.selectedPostalCode ?: "")
+ }
}
private fun setupProvinceObserver() {
- // Same implementation as before
+ // pake raja ongkir
registerViewModel.provincesState.observe(viewLifecycleOwner) { state ->
when (state) {
is ViewState.Loading -> {
@@ -256,8 +311,46 @@ class RegisterStep3Fragment : Fragment() {
}
}
}
+ registerViewModel.subdistrictState.observe(viewLifecycleOwner) { state ->
+ when (state) {
+ is ViewState.Loading -> {
+ binding.progressBarKecamatan.visibility = View.VISIBLE
+ }
+ is ViewState.Success -> {
+ Log.d(TAG, "Subdistrict: Success - received ${state.data.size} kecamatan")
+ binding.progressBarKecamatan.visibility = View.GONE
+ subdistrictAdapter.updateData(state.data)
+ Log.d(TAG, "Updated subdistrict adapter with ${state.data.size} items")
+ }
+ is ViewState.Error -> {
+ Log.e(TAG, "Subdistrict: Error - ${state.message}")
+ binding.progressBarKecamatan.visibility = View.GONE
+ showError("Failed to load kecamatan: ${state.message}")
+ }
+ }
+ }
+
+ registerViewModel.villagesState.observe(viewLifecycleOwner) { state ->
+ when (state) {
+ is ViewState.Loading -> {
+ binding.progressBarDesa.visibility = View.VISIBLE
+ }
+ is ViewState.Success -> {
+ Log.d(TAG, "Village: Success - received ${state.data.size} desa")
+ binding.progressBarDesa.visibility = View.GONE
+ villagesAdapter.updateData(state.data)
+ Log.d(TAG, "Updated village adapter with ${state.data.size} items")
+ }
+ is ViewState.Error -> {
+ Log.e(TAG, "Village: Error - ${state.message}")
+ binding.progressBarDesa.visibility = View.GONE
+ showError("Failed to load desa: ${state.message}")
+ }
+ }
+ }
}
+
private fun submitAddress() {
Log.d(TAG, "submitAddress called")
if (!validateAddressForm()) {
@@ -276,13 +369,16 @@ class RegisterStep3Fragment : Fragment() {
Log.d(TAG, "Using user ID: $userId")
val street = binding.etDetailAlamat.text.toString().trim()
- val subDistrict = binding.etKecamatan.text.toString().trim()
- val postalCode = binding.etKodePos.text.toString().trim()
val recipient = binding.etNamaPenerima.text.toString().trim()
val phone = binding.etNomorHp.text.toString().trim()
+ val postalCode = binding.etKodePos.text.toString().trim()
val provinceId = registerViewModel.selectedProvinceId?.toInt() ?: 0
- val cityId = registerViewModel.selectedCityId?.toInt() ?: 0
+ val cityId = registerViewModel.selectedCityId.toString()
+ val subDistrict = registerViewModel.selectedSubdistrict.toString()
+// val postalCode = registerViewModel.selectedPostalCode.toString()
+
+ val villageId = registerViewModel.selectedVillages ?: ""
Log.d(TAG, "Address data - Street: $street, SubDistrict: $subDistrict, PostalCode: $postalCode")
Log.d(TAG, "Address data - Recipient: $recipient, Phone: $phone")
@@ -291,21 +387,25 @@ class RegisterStep3Fragment : Fragment() {
// Create address request
val addressRequest = CreateAddressRequest(
+ userId = user.id, // must match the type expected in the DB
lat = defaultLatitude,
long = defaultLongitude,
street = street,
subDistrict = subDistrict,
- cityId = cityId,
+ cityId = cityId, // ⚠️ Make sure this is Int
provId = provinceId,
postCode = postalCode,
+ idVillage = villageId, // Or provide a real ID if needed
detailAddress = street,
- userId = userId,
+ isStoreLocation = false,
recipient = recipient,
- phone = phone,
- isStoreLocation = false
+ phone = phone
)
Log.d(TAG, "Address request created: $addressRequest")
+ val gson = Gson()
+ val jsonString = gson.toJson(addressRequest)
+ Log.d(TAG, "Request JSON: $jsonString")
// Show loading
binding.progressBar.visibility = View.VISIBLE
@@ -318,13 +418,13 @@ class RegisterStep3Fragment : Fragment() {
private fun validateAddressForm(): Boolean {
val street = binding.etDetailAlamat.text.toString().trim()
- val subDistrict = binding.etKecamatan.text.toString().trim()
- val postalCode = binding.etKodePos.text.toString().trim()
val recipient = binding.etNamaPenerima.text.toString().trim()
val phone = binding.etNomorHp.text.toString().trim()
val provinceId = registerViewModel.selectedProvinceId
val cityId = registerViewModel.selectedCityId
+ val subDistrict = registerViewModel.selectedSubdistrict.toString()
+ val postalCode = registerViewModel.selectedPostalCode
Log.d(TAG, "Validating - Street: $street, SubDistrict: $subDistrict, PostalCode: $postalCode")
Log.d(TAG, "Validating - Recipient: $recipient, Phone: $phone")
@@ -409,8 +509,4 @@ class RegisterStep3Fragment : Fragment() {
ViewCompat.setWindowInsetsAnimationCallback(binding.root, null)
_binding = null
}
-//
-// // Data classes for province and city
-// data class Province(val id: String, val name: String)
-// data class City(val id: String, val name: String)
}
\ No newline at end of file
diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/cart/CartActivity.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/cart/CartActivity.kt
index 09b10cc..6a6c69d 100644
--- a/app/src/main/java/com/alya/ecommerce_serang/ui/cart/CartActivity.kt
+++ b/app/src/main/java/com/alya/ecommerce_serang/ui/cart/CartActivity.kt
@@ -1,6 +1,7 @@
package com.alya.ecommerce_serang.ui.cart
import android.os.Bundle
+import android.util.Log
import android.view.View
import android.widget.Toast
import androidx.activity.enableEdgeToEdge
@@ -145,7 +146,9 @@ class CartActivity : AppCompatActivity() {
private fun observeViewModel() {
viewModel.cartItems.observe(this) { cartItems ->
if (cartItems.isNullOrEmpty()) {
+ binding.emptyCart.visibility = View.VISIBLE
showEmptyState(true)
+
} else {
showEmptyState(false)
storeAdapter.submitList(cartItems)
@@ -153,7 +156,8 @@ class CartActivity : AppCompatActivity() {
}
viewModel.isLoading.observe(this) { isLoading ->
- // Show/hide loading indicator if needed
+ binding.progressBarCart?.visibility = if (isLoading) View.VISIBLE else View.GONE
+ Log.d("CartActivity", "Loading state: $isLoading")
}
viewModel.errorMessage.observe(this) { errorMessage ->
diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/chat/ChatActivity.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/chat/ChatActivity.kt
index 39fc4a1..cf9fa48 100644
--- a/app/src/main/java/com/alya/ecommerce_serang/ui/chat/ChatActivity.kt
+++ b/app/src/main/java/com/alya/ecommerce_serang/ui/chat/ChatActivity.kt
@@ -27,6 +27,7 @@ import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsAnimationCompat
import androidx.core.view.WindowInsetsCompat
import androidx.lifecycle.Observer
+import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
import com.alya.ecommerce_serang.BuildConfig.BASE_URL
import com.alya.ecommerce_serang.R
@@ -63,6 +64,8 @@ class ChatActivity : AppCompatActivity() {
// For image attachment
private var tempImageUri: Uri? = null
+ private var imageAttach = false
+
// Typing indicator handler
private val typingHandler = android.os.Handler(android.os.Looper.getMainLooper())
private val stopTypingRunnable = Runnable {
@@ -127,6 +130,7 @@ class ChatActivity : AppCompatActivity() {
return
}
+ // set up data toko
binding.tvStoreName.text = storeName
val fullImageUrl = when (val img = storeImg) {
is String -> {
@@ -140,7 +144,7 @@ class ChatActivity : AppCompatActivity() {
.placeholder(R.drawable.placeholder_image)
.into(binding.imgProfile)
- // Set chat parameters to ViewModel
+ // Set chat parameter to send to ViewModel with product
viewModel.setChatParameters(
storeId = storeId,
productId = productId,
@@ -157,16 +161,17 @@ class ChatActivity : AppCompatActivity() {
}
// Setup UI components
+ // rv isi chat
setupRecyclerView()
setupWindowInsets()
setupListeners()
setupTypingIndicator()
+ // observe listener from viewmodel
observeViewModel()
// If opened from ChatListFragment with a valid chatRoomId
if (chatRoomId > 0) {
- // Directly set the chatRoomId and load chat history
- viewModel._chatRoomId.value = chatRoomId
+ viewModel.setChatRoomId(chatRoomId)
}
}
@@ -269,6 +274,7 @@ class ChatActivity : AppCompatActivity() {
}
// Options button
+ binding.btnOptions.visibility = View.GONE
binding.btnOptions.setOnClickListener {
showOptionsMenu()
}
@@ -281,6 +287,7 @@ class ChatActivity : AppCompatActivity() {
// This will automatically handle product attachment if enabled
viewModel.sendMessage(message)
binding.editTextMessage.text.clear()
+ binding.layoutAttachImage.visibility = View.GONE
// Instantly scroll to show new message
binding.recyclerChat.postDelayed({
@@ -291,24 +298,33 @@ class ChatActivity : AppCompatActivity() {
// Attachment button
binding.btnAttachment.setOnClickListener {
+ this.currentFocus?.let { view ->
+ val imm = getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager
+ imm?.hideSoftInputFromWindow(view.windowToken, 0)
+ }
checkPermissionsAndShowImagePicker()
}
+ binding.btnCloseChat.setOnClickListener{
+ binding.layoutAttachImage.visibility = View.GONE
+ imageAttach = false
+ viewModel.clearSelectedImage()
+ }
+
// Product card click to enable/disable product attachment
binding.productContainer.setOnClickListener {
toggleProductAttachment()
}
}
+
private fun toggleProductAttachment() {
val currentState = viewModel.state.value
if (currentState?.hasProductAttachment == true) {
- // Disable product attachment
viewModel.disableProductAttachment()
updateProductAttachmentUI(false)
Toast.makeText(this, "Product attachment disabled", Toast.LENGTH_SHORT).show()
} else {
- // Enable product attachment
viewModel.enableProductAttachment()
updateProductAttachmentUI(true)
Toast.makeText(this, "Product will be attached to your next message", Toast.LENGTH_SHORT).show()
@@ -389,77 +405,76 @@ class ChatActivity : AppCompatActivity() {
}
})
- viewModel.state.observe(this, Observer { state ->
- Log.d(TAG, "State updated - Messages: ${state.messages.size}")
+ lifecycleScope.launchWhenStarted {
+ viewModel.state.collect() { state ->
+ Log.d(TAG, "State updated - Messages: ${state.messages.size}")
- // Update messages
- val previousCount = chatAdapter.itemCount
+ // Update messages
+ val previousCount = chatAdapter.itemCount
- val displayItems = viewModel.getDisplayItems()
+ val displayItems = viewModel.getDisplayItems()
- chatAdapter.submitList(displayItems) {
- Log.d(TAG, "Messages submitted to adapter")
- // Only auto-scroll for new messages or initial load
- if (previousCount == 0 || state.messages.size > previousCount) {
- scrollToBottomInstant()
- }
- }
-
- // Update product info
- if (!state.productName.isNullOrEmpty()) {
- binding.tvProductName.text = state.productName
- binding.tvProductPrice.text = state.productPrice
- binding.ratingBar.rating = state.productRating
- binding.tvRating.text = state.productRating.toString()
- binding.tvSellerName.text = state.storeName
- binding.tvStoreName.text = state.storeName
-
- val fullImageUrl = when (val img = state.productImageUrl) {
- is String -> {
- if (img.startsWith("/")) BASE_URL + img.substring(1) else img
+ chatAdapter.submitList(displayItems) {
+ Log.d(TAG, "Messages submitted to adapter")
+ // Only auto-scroll for new messages or initial load
+ if (previousCount == 0 || state.messages.size > previousCount) {
+ scrollToBottomInstant()
}
- else -> R.drawable.placeholder_image
}
- if (!state.productImageUrl.isNullOrEmpty()) {
- Glide.with(this@ChatActivity)
- .load(fullImageUrl)
- .centerCrop()
- .placeholder(R.drawable.placeholder_image)
- .error(R.drawable.placeholder_image)
- .into(binding.imgProduct)
+ // layout attach product
+ if (!state.productName.isNullOrEmpty()) {
+ binding.tvProductName.text = state.productName
+ binding.tvProductPrice.text = state.productPrice
+ binding.ratingBar.rating = state.productRating
+ binding.tvRating.text = state.productRating.toString()
+ binding.tvSellerName.text = state.storeName
+ binding.tvStoreName.text = state.storeName
+
+ val fullImageUrl = when (val img = state.productImageUrl) {
+ is String -> {
+ if (img.startsWith("/")) BASE_URL + img.substring(1) else img
+ }
+
+ else -> R.drawable.placeholder_image
+ }
+
+ if (!state.productImageUrl.isNullOrEmpty()) {
+ Glide.with(this@ChatActivity)
+ .load(fullImageUrl)
+ .centerCrop()
+ .placeholder(R.drawable.placeholder_image)
+ .error(R.drawable.placeholder_image)
+ .into(binding.imgProduct)
+ }
+ updateProductCardUI(state.hasProductAttachment)
+
+ binding.productContainer.visibility = View.GONE
+ } else {
+ binding.productContainer.visibility = View.GONE
}
- updateProductCardUI(state.hasProductAttachment)
- binding.productContainer.visibility = View.GONE
- } else {
- binding.productContainer.visibility = View.GONE
+ updateInputHint(state)
+
+ // Update attachment hint
+ if (state.hasAttachment) {
+ binding.layoutAttachImage.visibility = View.VISIBLE
+ } else {
+ binding.editTextMessage.hint = getString(R.string.write_message)
+ }
+
+ // Show error if any
+ state.error?.let { error ->
+ Toast.makeText(this@ChatActivity, error, Toast.LENGTH_SHORT).show()
+ viewModel.clearError()
+ }
}
-
- updateInputHint(state)
-
- // Update attachment hint
- if (state.hasAttachment) {
- binding.editTextMessage.hint = getString(R.string.image_attached)
- } else {
- binding.editTextMessage.hint = getString(R.string.write_message)
- }
-
- // Show typing indicator
- binding.tvTypingIndicator.visibility =
- if (state.isOtherUserTyping) View.VISIBLE else View.GONE
-
- // Show error if any
- state.error?.let { error ->
- Toast.makeText(this@ChatActivity, error, Toast.LENGTH_SHORT).show()
- viewModel.clearError()
- }
- })
+ }
}
private fun updateInputHint(state: ChatUiState) {
binding.editTextMessage.hint = when {
- state.hasAttachment -> getString(R.string.image_attached)
+ state.hasAttachment -> getString(R.string.write_message)
state.hasProductAttachment -> "Type your message (product will be attached)"
else -> getString(R.string.write_message)
}
@@ -480,7 +495,7 @@ class ChatActivity : AppCompatActivity() {
Toast.makeText(this, "Opening: ${productInfo.productName}", Toast.LENGTH_SHORT).show()
// You can navigate to product detail here
- navigateToProductDetail(productInfo.productId)
+ navigateToProductDetail(productInfo.productId)
}
private fun navigateToProductDetail(productId: Int) {
@@ -504,6 +519,7 @@ class ChatActivity : AppCompatActivity() {
getString(R.string.cancel)
)
+
AlertDialog.Builder(this)
.setTitle(getString(R.string.options))
.setItems(options) { dialog, which ->
@@ -578,7 +594,21 @@ class ChatActivity : AppCompatActivity() {
private fun handleSelectedImage(uri: Uri) {
try {
- Log.d(TAG, "Processing selected image: $uri")
+ Log.d(TAG, "Processing selected image: ${uri.toString()}")
+ imageAttach = true
+ binding.layoutAttachImage.visibility = View.VISIBLE
+ val fullImageUrl = when (val img = uri.toString()) {
+ is String -> {
+ if (img.startsWith("/")) BASE_URL + img.substring(1) else img
+ }
+ else -> R.drawable.placeholder_image
+ }
+
+ Glide.with(this)
+ .load(fullImageUrl)
+ .placeholder(R.drawable.placeholder_image)
+ .into(binding.ivAttach)
+ Log.d(TAG, "Display attach image: $uri")
// Always use the copy-to-cache approach for reliability
contentResolver.openInputStream(uri)?.use { inputStream ->
@@ -598,6 +628,7 @@ class ChatActivity : AppCompatActivity() {
Log.d(TAG, "Image processed successfully: ${outputFile.absolutePath}, size: ${outputFile.length()}")
viewModel.setSelectedImageFile(outputFile)
+
Toast.makeText(this, "Image selected", Toast.LENGTH_SHORT).show()
} else {
Log.e(TAG, "Failed to create image file")
@@ -681,25 +712,4 @@ class ChatActivity : AppCompatActivity() {
context.startActivity(intent)
}
}
-}
-
-//if implement typing status
-// private fun handleConnectionState(state: ConnectionState) {
-// when (state) {
-// is ConnectionState.Connected -> {
-// binding.tvConnectionStatus.visibility = View.GONE
-// }
-// is ConnectionState.Connecting -> {
-// binding.tvConnectionStatus.visibility = View.VISIBLE
-// binding.tvConnectionStatus.text = getString(R.string.connecting)
-// }
-// is ConnectionState.Disconnected -> {
-// binding.tvConnectionStatus.visibility = View.VISIBLE
-// binding.tvConnectionStatus.text = getString(R.string.disconnected_reconnecting)
-// }
-// is ConnectionState.Error -> {
-// binding.tvConnectionStatus.visibility = View.VISIBLE
-// binding.tvConnectionStatus.text = getString(R.string.connection_error, state.message)
-// }
-// }
-// }
\ No newline at end of file
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/chat/ChatAdapter.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/chat/ChatAdapter.kt
index 8a136f0..68cd079 100644
--- a/app/src/main/java/com/alya/ecommerce_serang/ui/chat/ChatAdapter.kt
+++ b/app/src/main/java/com/alya/ecommerce_serang/ui/chat/ChatAdapter.kt
@@ -209,7 +209,7 @@ class ChatAdapter(
binding.tvProductPrice.text = product.productPrice
// Load product image
- val fullImageUrl = if (product.productImage.startsWith("/")) {
+ val fullImageUrl = if (product.productImage!!.startsWith("/")) {
BASE_URL + product.productImage.substring(1)
} else {
product.productImage
@@ -246,7 +246,7 @@ class ChatAdapter(
binding.tvProductPrice.text = product.productPrice
// Load product image
- val fullImageUrl = if (product.productImage.startsWith("/")) {
+ val fullImageUrl = if (product.productImage!!.startsWith("/")) {
BASE_URL + product.productImage.substring(1)
} else {
product.productImage
diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/chat/ChatListFragment.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/chat/ChatListFragment.kt
index ed2aa87..923ad50 100644
--- a/app/src/main/java/com/alya/ecommerce_serang/ui/chat/ChatListFragment.kt
+++ b/app/src/main/java/com/alya/ecommerce_serang/ui/chat/ChatListFragment.kt
@@ -1,6 +1,7 @@
package com.alya.ecommerce_serang.ui.chat
import android.os.Bundle
+import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
@@ -55,31 +56,44 @@ class ChatListFragment : Fragment() {
viewModel.chatList.observe(viewLifecycleOwner) { result ->
when (result) {
is Result.Success -> {
- val adapter = ChatListAdapter(result.data) { chatItem ->
- // Use the ChatActivity.createIntent factory method for proper navigation
- ChatActivity.createIntent(
- context = requireActivity(),
- storeId = chatItem.storeId,
- productId = 0, // Default value since we don't have it in ChatListItem
- productName = null, // Null is acceptable as per ChatActivity
- productPrice = "",
- productImage = null,
- productRating = null,
- storeName = chatItem.storeName,
- chatRoomId = chatItem.chatRoomId,
- storeImage = chatItem.storeImage
- )
+ val data = result.data
+
+ binding.tvEmptyChat.visibility = View.GONE
+ if (data.isNotEmpty()) {
+ val adapter = ChatListAdapter(data) { chatItem ->
+ ChatActivity.createIntent(
+ context = requireActivity(),
+ storeId = chatItem.storeId,
+ productId = 0,
+ productName = null,
+ productPrice = "",
+ productImage = null,
+ productRating = null,
+ storeName = chatItem.storeName,
+ chatRoomId = chatItem.chatRoomId,
+ storeImage = chatItem.storeImage
+ )
+ }
+ binding.chatListRecyclerView.adapter = adapter
+ } else {
+ binding.tvEmptyChat.visibility = View.VISIBLE
}
- binding.chatListRecyclerView.adapter = adapter
}
is Result.Error -> {
+ binding.tvEmptyChat.visibility = View.VISIBLE
Toast.makeText(requireContext(), "Failed to load chats", Toast.LENGTH_SHORT).show()
}
Result.Loading -> {
+ binding.progressBarChat.visibility = View.VISIBLE
// Optional: show progress bar
}
}
}
+ //loading chat list
+ viewModel.isLoading.observe(viewLifecycleOwner) { isLoading ->
+ binding.progressBarChat?.visibility = if (isLoading) View.VISIBLE else View.GONE
+ Log.d(TAG, "Loading state: $isLoading")
+ }
}
@@ -89,6 +103,6 @@ class ChatListFragment : Fragment() {
}
companion object{
-
+ private var TAG = "ChatListFragment"
}
}
\ No newline at end of file
diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/chat/ChatViewModel.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/chat/ChatViewModel.kt
index 3b550f5..8a6d001 100644
--- a/app/src/main/java/com/alya/ecommerce_serang/ui/chat/ChatViewModel.kt
+++ b/app/src/main/java/com/alya/ecommerce_serang/ui/chat/ChatViewModel.kt
@@ -14,6 +14,9 @@ import com.alya.ecommerce_serang.data.repository.Result
import com.alya.ecommerce_serang.utils.Constants
import com.alya.ecommerce_serang.utils.SessionManager
import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import java.io.File
import java.text.SimpleDateFormat
@@ -23,6 +26,28 @@ import java.util.Locale
import java.util.TimeZone
import javax.inject.Inject
+/**
+ * ChatViewModel - Manages chat functionality for both buyers and store owners
+ *
+ * ARCHITECTURE OVERVIEW:
+ * - Handles real-time messaging via Socket.IO
+ * - Manages chat state using LiveData/MutableLiveData pattern
+ * - Supports multiple message types: TEXT, IMAGE, PRODUCT
+ * - Maintains separate flows for buyer and store owner chat
+ *
+ * KEY RESPONSIBILITIES:
+ * 1. Socket connection management and real-time message handling
+ * 2. Message sending/receiving with different attachment types
+ * 3. Chat history loading and message status updates
+ * 4. Product attachment functionality for commerce integration
+ * 5. User session management and authentication
+ *
+ * STATE MANAGEMENT PATTERN:
+ * - All UI state updates go through updateState() helper function
+ * - State updates are atomic and follow immutable pattern
+ * - Error states are cleared explicitly via clearError()
+ */
+
@HiltViewModel
class ChatViewModel @Inject constructor(
private val chatRepository: ChatRepository,
@@ -34,9 +59,12 @@ class ChatViewModel @Inject constructor(
// Product attachment flag
private var shouldAttachProduct = false
- // UI state using LiveData
- private val _state = MutableLiveData(ChatUiState())
- val state: LiveData = _state
+ // use state for more seamless responsive
+ private val _state = MutableStateFlow(ChatUiState())
+ val state: StateFlow = _state
+
+ private val _isLoading = MutableLiveData()
+ val isLoading: LiveData = _isLoading
val _chatRoomId = MutableLiveData(0)
val chatRoomId: LiveData = _chatRoomId
@@ -68,16 +96,21 @@ class ChatViewModel @Inject constructor(
init {
Log.d(TAG, "ChatViewModel initialized")
+ socketService.connect() // 🛠 force connection
+ setupSocketListeners() // 🛠 always listen, even before user data
initializeUser()
}
private fun initializeUser() {
+ _isLoading.value = true
viewModelScope.launch {
Log.d(TAG, "Initializing user session...")
when (val result = chatRepository.fetchUserProfile()) {
is Result.Success -> {
currentUserId = result.data?.userId
+ _isLoading.value = false
+
Log.d(TAG, "User session initialized - User ID: $currentUserId")
if (currentUserId == null || currentUserId == 0) {
@@ -85,14 +118,17 @@ class ChatViewModel @Inject constructor(
updateState { it.copy(error = "User authentication error. Please login again.") }
} else {
Log.d(TAG, "Setting up socket listeners...")
+ socketService.connect()
setupSocketListeners()
}
}
is Result.Error -> {
+ _isLoading.value = false
Log.e(TAG, "Failed to fetch user profile: ${result.exception.message}")
updateState { it.copy(error = "User authentication error. Please login again.") }
}
is Result.Loading -> {
+ _isLoading.value = true
Log.d(TAG, "Loading user profile...")
}
}
@@ -201,26 +237,116 @@ class ChatViewModel @Inject constructor(
if (connectionState is ConnectionState.Connected) {
Log.d(TAG, "Socket connected, joining room...")
- socketService.joinRoom()
+ val roomId = _chatRoomId.value
+ if (roomId != null && roomId > 0) {
+ socketService.joinRoom(roomId)
+ }
}
}
}
+// viewModelScope.launch {
+// socketService.newMessages.collect { chatLine ->
+// chatLine?.let {
+// Log.d(TAG, "NEW message received in ViewModel: ${it.message}")
+// val updatedMessages = _state.value.messages.toMutableList()
+// updatedMessages.add(convertChatLineToUiMessage(it))
+// updateState { it.copy(messages = updatedMessages) }
+//
+// if (it.senderId != currentUserId) {
+// updateMessageStatus(it.id, Constants.STATUS_READ)
+// }
+// }
+// }
+// }
viewModelScope.launch {
socketService.newMessages.collect { chatLine ->
- chatLine?.let {
- Log.d(TAG, "New message received via socket - ID: ${it.id}, SenderID: ${it.senderId}")
- val currentMessages = _state.value?.messages ?: listOf()
- val updatedMessages = currentMessages.toMutableList().apply {
- add(convertChatLineToUiMessage(it))
+ Log.d("ChatViewModel", "Collected new message from SocketIOService: ${chatLine.message}")
+ chatLine?.let { incomingChatLine ->
+ // 1. First update: Add the message to the list (potentially without full product info)
+ _state.update { currentState ->
+ val existingMessageIndex =
+ currentState.messages.indexOfFirst { it.id == incomingChatLine.id }
+ val messagesAfterInitialUpdate = if (existingMessageIndex != -1) {
+ // If message exists (e.g., status update), just update it
+ val updatedList = currentState.messages.toMutableList()
+ updatedList[existingMessageIndex] = mapChatLineToUiMessage(
+ incomingChatLine,
+ updatedList[existingMessageIndex].productInfo
+ ) // Preserve existing productInfo if any
+ updatedList
+ } else {
+ // New message, add it
+ (currentState.messages + mapChatLineToUiMessage(incomingChatLine)).distinctBy { msg -> msg.id }
+ }
+ // Sort after any update/addition
+ currentState.copy(messages = messagesAfterInitialUpdate.sortedBy { msg ->
+ SimpleDateFormat(
+ "yyyy-MM-dd HH:mm:ss",
+ Locale.getDefault()
+ ).parse(msg.createdAt)?.time
+ })
}
- updateState { it.copy(messages = updatedMessages) }
- if (it.senderId != currentUserId) {
- Log.d(TAG, "Marking message as read: ${it.id}")
- updateMessageStatus(it.id, Constants.STATUS_READ)
+ // 2. If it's a product message and needs details, fetch them
+ if (incomingChatLine.productId != 0) { // Check if it's a product message
+ viewModelScope.launch {
+ Log.d(
+ TAG,
+ "Fetching product detail for ID: ${incomingChatLine.productId}"
+ )
+
+ // Call your repository function directly
+ val productResponse =
+ chatRepository.fetchProductDetail(incomingChatLine.productId)
+
+ if (productResponse != null && productResponse.product != null) {
+ val fetchedProduct =
+ productResponse.product // Access the nested product object
+ Log.d(
+ TAG,
+ "Successfully fetched product: ${fetchedProduct.productName}"
+ )
+
+ // Create a complete ProductInfo object
+ val fullProductInfo = ProductInfo(
+ productId = fetchedProduct.productId,
+ productName = fetchedProduct.productName, // Use productName from fetched data
+ productPrice = fetchedProduct.price, // Use productPrice from fetched data
+ productImage = fetchedProduct.image, // Use productImage from fetched data
+ productRating = fetchedProduct.rating.toFloat(),
+ storeName = fetchedProduct.productName // Use storeName from fetched data
+ )
+
+ // --- PHASE 3: Second UI update (fill in full product info) ---
+ _state.update { currentState ->
+ val updatedMessages = currentState.messages.map { msg ->
+ if (msg.id == incomingChatLine.id) {
+ // Found the message, update its productInfo with full details
+ msg.copy(productInfo = fullProductInfo)
+ } else {
+ msg
+ }
+ }
+ currentState.copy(messages = updatedMessages)
+ }
+ } else {
+ Log.e(
+ TAG,
+ "Failed to fetch product detail for ID ${incomingChatLine.productId} or product data is null."
+ )
+ // Optionally, update message status to indicate error in product loading
+ }
+ }
}
+
}
+
+// // Your existing logic for clearing typing status etc.
+// if (incomingChatLine.isTyping == false && incomingChatLine.from?.id != sessionManager.getUserId()?.toIntOrNull()) {
+// _state.update { it.copy(isOtherUserTyping = false) }
+// }
+
}
}
@@ -241,10 +367,10 @@ class ChatViewModel @Inject constructor(
if (roomId <= 0) {
Log.e(TAG, "Cannot join room: Invalid room ID")
return
+ } else if (roomId > 0){
+ Log.d(TAG, "Joining socket room: $roomId")
+ socketService.joinRoom(roomId)
}
-
- Log.d(TAG, "Joining socket room: $roomId")
- socketService.joinRoom()
}
fun sendTypingStatus(isTyping: Boolean) {
@@ -313,10 +439,13 @@ class ChatViewModel @Inject constructor(
}
fun getChatList() {
+ _isLoading.value = true
Log.d(TAG, "Getting chat list...")
viewModelScope.launch {
- _chatList.value = Result.Loading
+// _chatList.value = Result.Loading
_chatList.value = chatRepository.getListChat()
+ _isLoading.value = false
+
}
}
@@ -695,7 +824,7 @@ class ChatViewModel @Inject constructor(
}
}
- //update message status
+ //update message status
fun updateMessageStatus(messageId: Int, status: String) {
Log.d(TAG, "Updating message status - ID: $messageId, Status: $status")
@@ -723,13 +852,26 @@ class ChatViewModel @Inject constructor(
}
}
- //set image attachment
+ //set image attachment
fun setSelectedImageFile(file: File?) {
selectedImageFile = file
updateState { it.copy(hasAttachment = file != null) }
Log.d(TAG, "Image attachment ${if (file != null) "selected: ${file.name}" else "cleared"}")
}
+ fun clearSelectedImage() {
+ Log.d(TAG, "Clearing selected image attachment")
+
+ selectedImageFile?.let { file ->
+ Log.d(TAG, "Clearing image file: ${file.name}")
+ }
+
+ selectedImageFile = null
+ updateState { it.copy(hasAttachment = false) }
+
+ Log.d(TAG, "Image attachment cleared successfully")
+ }
+
// convert form chatLine api to UI chat messages
private fun convertChatLineToUiMessage(chatLine: ChatLine): ChatUiMessage {
val formattedTime = formatTimestamp(chatLine.createdAt)
@@ -745,7 +887,7 @@ class ChatViewModel @Inject constructor(
)
}
- // convert chat history item to ui
+ // convert chat history item to ui
private fun convertChatLineToUiMessageHistory(chatItem: ChatItem): ChatUiMessage {
val formattedTime = formatTimestamp(chatItem.createdAt)
@@ -886,7 +1028,7 @@ class ChatViewModel @Inject constructor(
}
}
- //format price
+ //format price
private fun formatPrice(price: String): String {
return if (price.startsWith("Rp")) price else "Rp$price"
}
@@ -912,9 +1054,7 @@ class ChatViewModel @Inject constructor(
// helper function to update live data
private fun updateState(update: (ChatUiState) -> ChatUiState) {
- _state.value?.let {
- _state.value = update(it)
- }
+ _state.value = update(_state.value)
}
//clear any error messages
@@ -1007,6 +1147,73 @@ class ChatViewModel @Inject constructor(
private fun isThisYear(messageCalendar: Calendar, today: Calendar): Boolean {
return messageCalendar.get(Calendar.YEAR) == today.get(Calendar.YEAR)
}
+
+ fun setChatRoomId(roomId: Int) {
+ _chatRoomId.value = roomId
+ joinSocketRoom(roomId)
+ loadChatHistory(roomId)
+ }
+
+ private fun convertToUiMessage(chatLine: ChatLine): ChatUiMessage {
+
+ val formattedTime = formatTimestamp(chatLine.createdAt)
+ return ChatUiMessage(
+ id = chatLine.id,
+ message = chatLine.message,
+ attachment = chatLine.attachment,
+ status = chatLine.status,
+ time = formattedTime, // or format from createdAt if needed
+ isSentByMe = chatLine.senderId == currentUserId,
+ messageType = MessageType.TEXT, // or detect from chatLine if needed
+ productInfo = null, // optional, if applicable
+ createdAt = chatLine.createdAt
+ )
+ }
+
+ private fun mapChatLineToUiMessage(chatLine: ChatLine, fetchedProductInfo: ProductInfo? = null): ChatUiMessage {
+ val isSentByMe = chatLine.senderId == sessionManager.getUserId()?.toIntOrNull() // Using senderId now
+ val formattedTime = try {
+ val inputFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.getDefault()).apply {
+ timeZone = TimeZone.getTimeZone("UTC")
+ }
+ val outputFormat = SimpleDateFormat("HH:mm", Locale.getDefault())
+ val date = inputFormat.parse(chatLine.createdAt)
+ date?.let { outputFormat.format(it) } ?: ""
+ } catch (e: Exception) {
+ Log.e("ChatViewModel", "Error parsing date: ${chatLine.createdAt}", e)
+ ""
+ }
+
+ // Determine message type based on what ChatLine provides
+ val messageType = when {
+ chatLine.attachment?.isNotEmpty() == true -> MessageType.IMAGE
+ chatLine.productId != 0 -> MessageType.PRODUCT // If productId is non-zero, it's a product message
+ else -> MessageType.TEXT
+ }
+
+ // Initialize productInfo: if fetchedProductInfo is provided, use it.
+ // Otherwise, if ChatLine has a productId, create a ProductInfo with just the ID.
+ // If no productId, it's null.
+ val productInfo = fetchedProductInfo ?: if (chatLine.productId != 0) {
+ // Create a placeholder ProductInfo with just the ID for initial display
+ // The full details will be fetched later
+ ProductInfo(productId = chatLine.productId)
+ } else {
+ null
+ }
+
+ return ChatUiMessage(
+ id = chatLine.id,
+ message = chatLine.message,
+ attachment = chatLine.attachment,
+ status = chatLine.status,
+ time = formattedTime,
+ isSentByMe = isSentByMe,
+ messageType = messageType,
+ productInfo = productInfo, // Use the determined productInfo
+ createdAt = chatLine.createdAt
+ )
+ }
}
enum class MessageType {
@@ -1016,12 +1223,12 @@ enum class MessageType {
}
data class ProductInfo(
- val productId: Int,
- val productName: String,
- val productPrice: String,
- val productImage: String,
- val productRating: Float,
- val storeName: String
+ val productId: Int, // Keep productId here
+ val productName: String? = null, // Make nullable
+ val productPrice: String? = null, // Make nullable
+ val productImage: String? = null, // Make nullable
+ val productRating: Float = 0f, // Default value
+ val storeName: String? = null
)
// representing chat messages to UI
@@ -1037,8 +1244,6 @@ data class ChatUiMessage(
val createdAt: String
)
-
-
// representing UI state to screen
data class ChatUiState(
val messages: List = emptyList(),
@@ -1056,4 +1261,8 @@ data class ChatUiState(
val productImageUrl: String = "",
val productRating: Float = 0f,
val storeName: String = ""
-)
\ No newline at end of file
+)
+
+//data class ChatUiState(
+// val messages: List = emptyList()
+//)
diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/chat/SocketIOService.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/chat/SocketIOService.kt
index 1ac378f..bb088c0 100644
--- a/app/src/main/java/com/alya/ecommerce_serang/ui/chat/SocketIOService.kt
+++ b/app/src/main/java/com/alya/ecommerce_serang/ui/chat/SocketIOService.kt
@@ -10,14 +10,24 @@ import com.alya.ecommerce_serang.utils.SessionManager
import com.google.gson.Gson
import io.socket.client.IO
import io.socket.client.Socket
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.SupervisorJob
+import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.launch
import org.json.JSONObject
import java.net.URISyntaxException
+import javax.inject.Inject
+import javax.inject.Singleton
-class SocketIOService(
+@Singleton
+class SocketIOService @Inject constructor(
private val sessionManager: SessionManager
) {
+ private val serviceScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
private val TAG = "SocketIOService"
// Socket.IO client
@@ -30,8 +40,8 @@ class SocketIOService(
private val _connectionState = MutableStateFlow(ConnectionState.Disconnected())
val connectionState: StateFlow = _connectionState
- private val _newMessages = MutableStateFlow(null)
- val newMessages: StateFlow = _newMessages
+ private val _newMessages = MutableSharedFlow(extraBufferCapacity = 1) // Using extraBufferCapacity for a non-suspending emit
+ val newMessages: SharedFlow = _newMessages
private val _typingStatus = MutableStateFlow(null)
val typingStatus: StateFlow = _typingStatus
@@ -85,63 +95,95 @@ class SocketIOService(
* Sets up Socket.IO event listeners
*/
private fun setupSocketListeners() {
- socket?.let { socket ->
- // Connection events
- socket.on(Socket.EVENT_CONNECT) {
- Log.d(TAG, "Socket.IO connected")
- isConnected = true
- _connectionState.value = ConnectionState.Connected
- _connectionStateLiveData.postValue(ConnectionState.Connected)
- }
- socket.on(Socket.EVENT_DISCONNECT) {
- Log.d(TAG, "Socket.IO disconnected")
- isConnected = false
- _connectionState.value = ConnectionState.Disconnected("Disconnected from server")
- _connectionStateLiveData.postValue(ConnectionState.Disconnected("Disconnected from server"))
- }
+ socket?.on(Constants.EVENT_NEW_MESSAGE) { args -> // Use the event name your server emits
+ Log.d(TAG, "Raw event received on ${Constants.EVENT_NEW_MESSAGE}: ${args.firstOrNull()}") // Check raw args
- socket.on(Socket.EVENT_CONNECT_ERROR) { args ->
- val error = if (args.isNotEmpty() && args[0] != null) args[0].toString() else "Unknown error"
- Log.e(TAG, "Socket.IO connection error: $error")
- isConnected = false
- _connectionState.value = ConnectionState.Error("Connection error: $error")
- _connectionStateLiveData.postValue(ConnectionState.Error("Connection error: $error"))
- }
-
- // Chat events
- socket.on(Constants.EVENT_NEW_MESSAGE) { args ->
+ if (args.isNotEmpty()) {
try {
- if (args.isNotEmpty() && args[0] != null) {
- val messageJson = args[0].toString()
- Log.d(TAG, "Received new message: $messageJson")
- val chatLine = Gson().fromJson(messageJson, ChatLine::class.java)
- _newMessages.value = chatLine
- _newMessagesLiveData.postValue(chatLine)
+ val messageJson = args[0].toString()
+ val chatLine = Gson().fromJson(messageJson, ChatLine::class.java)
+ Log.d(TAG, "Successfully parsed ChatLine: ${chatLine.message}")
+ Log.d(TAG, "Emitting new message to _newMessages SharedFlow...") // New log
+
+ // Use the serviceScope to launch a coroutine for emit()
+ serviceScope.launch {
+ _newMessages.emit(chatLine) // This ensures every message is processed
+ Log.d(TAG, "New message emitted to SharedFlow.") // New log after emit
+
}
} catch (e: Exception) {
- Log.e(TAG, "Error parsing new message event", e)
- }
- }
-
- socket.on(Constants.EVENT_TYPING) { args ->
- try {
- if (args.isNotEmpty() && args[0] != null) {
- val typingData = args[0] as JSONObject
- val userId = typingData.getInt("userId")
- val roomId = typingData.getInt("roomId")
- val isTyping = typingData.getBoolean("isTyping")
-
- Log.d(TAG, "Received typing status: User $userId in room $roomId is typing: $isTyping")
- val status = TypingStatus(userId, roomId, isTyping)
- _typingStatus.value = status
- _typingStatusLiveData.postValue(status)
- }
- } catch (e: Exception) {
- Log.e(TAG, "Error parsing typing event", e)
+ Log.e(TAG, "Error parsing or emitting new message: ${e.message}", e)
}
+ } else {
+ Log.w(TAG, "Received empty args for ${Constants.EVENT_NEW_MESSAGE}")
}
}
+// socket?.on(Constants.EVENT_NEW_MESSAGE) { args ->
+// if (args.isNotEmpty()) {
+// val messageJson = args[0].toString()
+// val chatLine = Gson().fromJson(messageJson, ChatLine::class.java)
+// Log.d("SocketIOService", "Message received: ${chatLine.message}")
+// _newMessages.value = chatLine
+// }
+// }
+// socket?.let { socket ->
+// // Connection events
+// socket.on(Socket.EVENT_CONNECT) {
+// Log.d(TAG, "Socket.IO connected")
+// isConnected = true
+// _connectionState.value = ConnectionState.Connected
+// _connectionStateLiveData.postValue(ConnectionState.Connected)
+// }
+//
+// socket.on(Socket.EVENT_DISCONNECT) {
+// Log.d(TAG, "Socket.IO disconnected")
+// isConnected = false
+// _connectionState.value = ConnectionState.Disconnected("Disconnected from server")
+// _connectionStateLiveData.postValue(ConnectionState.Disconnected("Disconnected from server"))
+// }
+//
+// socket.on(Socket.EVENT_CONNECT_ERROR) { args ->
+// val error = if (args.isNotEmpty() && args[0] != null) args[0].toString() else "Unknown error"
+// Log.e(TAG, "Socket.IO connection error: $error")
+// isConnected = false
+// _connectionState.value = ConnectionState.Error("Connection error: $error")
+// _connectionStateLiveData.postValue(ConnectionState.Error("Connection error: $error"))
+// }
+//
+// // Chat events
+// socket.on(Constants.EVENT_NEW_MESSAGE) { args ->
+// try {
+// if (args.isNotEmpty() && args[0] != null) {
+// val messageJson = args[0].toString()
+// Log.d(TAG, "Received new message: $messageJson")
+// val chatLine = Gson().fromJson(messageJson, ChatLine::class.java)
+// _newMessages.value = chatLine
+// _newMessagesLiveData.postValue(chatLine)
+// }
+// } catch (e: Exception) {
+// Log.e(TAG, "Error parsing new message event", e)
+// }
+// }
+//
+// socket.on(Constants.EVENT_TYPING) { args ->
+// try {
+// if (args.isNotEmpty() && args[0] != null) {
+// val typingData = args[0] as JSONObject
+// val userId = typingData.getInt("userId")
+// val roomId = typingData.getInt("roomId")
+// val isTyping = typingData.getBoolean("isTyping")
+//
+// Log.d(TAG, "Received typing status: User $userId in room $roomId is typing: $isTyping")
+// val status = TypingStatus(userId, roomId, isTyping)
+// _typingStatus.value = status
+// _typingStatusLiveData.postValue(status)
+// }
+// } catch (e: Exception) {
+// Log.e(TAG, "Error parsing typing event", e)
+// }
+// }
+// }
}
/**
@@ -159,22 +201,29 @@ class SocketIOService(
/**
* Joins a specific chat room
*/
- fun joinRoom() {
+ fun joinRoom(roomId: Int) {
+// if (!isConnected) {
+// connect()
+// return
+// }
+//
+// // Get user ID from SessionManager
+// val userId = sessionManager.getUserId()
+// if (userId.isNullOrEmpty()) {
+// Log.e(TAG, "Cannot join room: User ID is null or empty")
+// return
+// }
+//
+// // Join the room using the current user's ID
+// socket?.emit("joinRoom", roomId) // ✅
+// Log.d(TAG, "Joined room ID: $roomId")
+// Log.d(TAG, "Joined room for user: $userId")
if (!isConnected) {
connect()
- return
}
- // Get user ID from SessionManager
- val userId = sessionManager.getUserId()
- if (userId.isNullOrEmpty()) {
- Log.e(TAG, "Cannot join room: User ID is null or empty")
- return
- }
-
- // Join the room using the current user's ID
- socket?.emit("joinRoom", userId)
- Log.d(TAG, "Joined room for user: $userId")
+ socket?.emit("joinRoom", roomId)
+ Log.d(TAG, "Joined room ID: $roomId")
}
/**
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 dba398a..8bbfbf4 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
@@ -208,25 +208,6 @@ class HomeFragment : Fragment() {
private fun initUi() {
// For LightStatusBar
setLightStatusBar()
-// val banners = binding.banners
-// banners.offscreenPageLimit = 1
-//
-// val nextItemVisiblePx = resources.getDimension(R.dimen.viewpager_next_item_visible)
-// val currentItemHorizontalMarginPx =
-// resources.getDimension(R.dimen.viewpager_current_item_horizontal_margin)
-// val pageTranslationX = nextItemVisiblePx + currentItemHorizontalMarginPx
-//
-// banners.setPageTransformer { page, position ->
-// page.translationX = -pageTranslationX * position
-// page.scaleY = 1 - (0.25f * kotlin.math.abs(position))
-// }
-//
-// banners.addItemDecoration(
-// HorizontalMarginItemDecoration(
-// requireContext(),
-// R.dimen.viewpager_current_item_horizontal_margin
-// )
-// )
}
private fun handleProductClick(product: ProductsItem) {
@@ -248,8 +229,4 @@ class HomeFragment : Fragment() {
categoryAdapter = null
_binding = null
}
-
-// private fun showLoading(isLoading: Boolean) {
-// binding.progressBar.isVisible = isLoading
-// }
}
\ No newline at end of file
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 89d8e22..6baa2b0 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
@@ -2,6 +2,7 @@ package com.alya.ecommerce_serang.ui.home
import android.util.Log
import android.view.LayoutInflater
+import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
@@ -65,6 +66,16 @@ class SearchResultsAdapter(
val storeName = product.storeId?.let { storeMap[it]?.storeName } ?: "Unknown Store"
binding.tvStoreName.text = storeName
+ val ratingStr = product.rating
+ val ratingValue = ratingStr?.toFloatOrNull()
+
+ if (ratingValue != null && ratingValue > 0f) {
+ binding.rating.text = String.format("%.1f", ratingValue)
+ binding.rating.visibility = View.VISIBLE
+ } else {
+ binding.rating.text = "Belum ada rating"
+ binding.rating.setCompoundDrawablesWithIntrinsicBounds(null, null, null, null)
+ }
}
}
diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/order/CheckoutActivity.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/order/CheckoutActivity.kt
index 9f4e7e4..79608f9 100644
--- a/app/src/main/java/com/alya/ecommerce_serang/ui/order/CheckoutActivity.kt
+++ b/app/src/main/java/com/alya/ecommerce_serang/ui/order/CheckoutActivity.kt
@@ -4,6 +4,7 @@ import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.util.Log
+import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import android.widget.Toast
@@ -110,11 +111,6 @@ class CheckoutActivity : AppCompatActivity() {
finish()
}
}
-
-// viewModel.getPaymentMethods { paymentMethods ->
-// // Logging is just for debugging
-// Log.d("CheckoutActivity", "Loaded ${paymentMethods.size} payment methods")
-// }
}
private fun setupToolbar() {
@@ -165,7 +161,7 @@ class CheckoutActivity : AppCompatActivity() {
// Observe loading state
viewModel.isLoading.observe(this) { isLoading ->
binding.btnPay.isEnabled = !isLoading
- // Show/hide loading indicator if you have one
+
}
// Observe error messages
@@ -273,10 +269,14 @@ class CheckoutActivity : AppCompatActivity() {
private fun updateShippingUI(shipName: String, shipService: String, shipEtd: String, shipPrice: Int) {
if (shipName.isNotEmpty() && shipService.isNotEmpty()) {
// Display shipping name and service in one line
+ binding.cardShipment.visibility = View.VISIBLE
+
binding.tvCourierName.text = "$shipName $shipService"
binding.tvDeliveryEstimate.text = "$shipEtd hari kerja"
binding.tvShippingPrice.text = formatCurrency(shipPrice.toDouble())
binding.rbJne.isChecked = true
+ } else {
+ binding.cardShipment.visibility = View.GONE
}
}
diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/order/CheckoutViewModel.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/order/CheckoutViewModel.kt
index 694fa0f..38a0b28 100644
--- a/app/src/main/java/com/alya/ecommerce_serang/ui/order/CheckoutViewModel.kt
+++ b/app/src/main/java/com/alya/ecommerce_serang/ui/order/CheckoutViewModel.kt
@@ -64,7 +64,7 @@ class CheckoutViewModel(private val repository: OrderRepository) : ViewModel() {
shipPrice = 0, // Will be set when user selects shipping
shipName = "",
shipService = "",
- isNego = false, // Default value
+ isNego = false, // Default value
productId = productId,
quantity = quantity,
shipEtd = "",
diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/order/address/AddAddressActivity.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/order/address/AddAddressActivity.kt
index 9d9c4a3..cd36fe7 100644
--- a/app/src/main/java/com/alya/ecommerce_serang/ui/order/address/AddAddressActivity.kt
+++ b/app/src/main/java/com/alya/ecommerce_serang/ui/order/address/AddAddressActivity.kt
@@ -289,7 +289,7 @@ class AddAddressActivity : AppCompatActivity() {
val isStoreLocation = false
val provinceId = viewModel.selectedProvinceId
- val cityId = viewModel.selectedCityId
+ val cityId = viewModel.selectedCityId.toString()
Log.d(TAG, "Form data: street=$street, subDistrict=$subDistrict, postalCode=$postalCode, " +
"recipient=$recipient, phone=$phone, userId=$userId, provinceId=$provinceId, cityId=$cityId, " +
@@ -333,18 +333,19 @@ class AddAddressActivity : AppCompatActivity() {
// Create request with all fields
val request = CreateAddressRequest(
+ userId = userId,
lat = latitude!!, // Safe to use !! as we've checked above
long = longitude!!,
street = street,
subDistrict = subDistrict,
- cityId = cityId,
+ cityId = cityId, // ⚠️ Make sure this is Int
provId = provinceId,
postCode = postalCode,
+ idVillage = "", // Or provide a real ID if needed
detailAddress = street,
- userId = userId,
+ isStoreLocation = false,
recipient = recipient,
- phone = phone,
- isStoreLocation = isStoreLocation
+ phone = phone
)
Log.d(TAG, "Form validation successful, submitting address: $request")
diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/order/address/AddAddressViewModel.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/order/address/AddAddressViewModel.kt
index 7fcb067..0a0676e 100644
--- a/app/src/main/java/com/alya/ecommerce_serang/ui/order/address/AddAddressViewModel.kt
+++ b/app/src/main/java/com/alya/ecommerce_serang/ui/order/address/AddAddressViewModel.kt
@@ -36,8 +36,8 @@ class AddAddressViewModel(private val repository: OrderRepository, private val u
get() = savedStateHandle.get("selectedProvinceId")
set(value) { savedStateHandle["selectedProvinceId"] = value }
- var selectedCityId: Int?
- get() = savedStateHandle.get("selectedCityId")
+ var selectedCityId: String?
+ get() = savedStateHandle.get("selectedCityId")
set(value) { savedStateHandle["selectedCityId"] = value }
init {
@@ -129,7 +129,7 @@ class AddAddressViewModel(private val repository: OrderRepository, private val u
selectedProvinceId = id
}
- fun setSelectedCityId(id: Int) {
+ fun updateSelectedCityId(id: String) {
selectedCityId = id
}
diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/order/address/ProvinceAdapter.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/order/address/ProvinceAdapter.kt
index 06bbeee..85f674b 100644
--- a/app/src/main/java/com/alya/ecommerce_serang/ui/order/address/ProvinceAdapter.kt
+++ b/app/src/main/java/com/alya/ecommerce_serang/ui/order/address/ProvinceAdapter.kt
@@ -5,6 +5,8 @@ import android.util.Log
import android.widget.ArrayAdapter
import com.alya.ecommerce_serang.data.api.response.customer.order.CitiesItem
import com.alya.ecommerce_serang.data.api.response.customer.order.ProvincesItem
+import com.alya.ecommerce_serang.data.api.response.customer.order.SubdistrictsItem
+import com.alya.ecommerce_serang.data.api.response.customer.order.VillagesItem
// UI adapters and helpers
class ProvinceAdapter(
@@ -12,6 +14,7 @@ class ProvinceAdapter(
resource: Int = android.R.layout.simple_dropdown_item_1line
) : ArrayAdapter(context, resource, ArrayList()) {
+ //call from endpoint
private val provinces = ArrayList()
fun updateData(newProvinces: List) {
@@ -46,7 +49,52 @@ class CityAdapter(
notifyDataSetChanged()
}
- fun getCityId(position: Int): Int? {
- return cities.getOrNull(position)?.cityId?.toIntOrNull()
+ fun getCityId(position: Int): String? {
+ return cities.getOrNull(position)?.cityId?.toString()
+ }
+}
+
+class SubdsitrictAdapter(
+ context: Context,
+ resource: Int = android.R.layout.simple_dropdown_item_1line
+) : ArrayAdapter(context, resource, ArrayList()) {
+
+ private val cities = ArrayList()
+
+ fun updateData(newCities: List) {
+ cities.clear()
+ cities.addAll(newCities)
+
+ clear()
+ addAll(cities.map { it.subdistrictName })
+ notifyDataSetChanged()
+ }
+
+ fun getSubdistrictId(position: Int): String? {
+ return cities.getOrNull(position)?.subdistrictId?.toString()
+ }
+}
+
+class VillagesAdapter(
+ context: Context,
+ resource: Int = android.R.layout.simple_dropdown_item_1line
+) : ArrayAdapter(context, resource, ArrayList()) {
+
+ private val villages = ArrayList()
+
+ fun updateData(newCities: List) {
+ villages.clear()
+ villages.addAll(newCities)
+
+ clear()
+ addAll(villages.map { it.villageName })
+ notifyDataSetChanged()
+ }
+
+ fun getVillageId(position: Int): String? {
+ return villages.getOrNull(position)?.villageId?.toString()
+ }
+ fun getPostalCode(position: Int): String?{
+ return villages.getOrNull(position)?.postalCode
}
}
\ No newline at end of file
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 1329a95..24a940a 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
@@ -39,7 +39,6 @@ import okhttp3.MultipartBody
import okhttp3.RequestBody.Companion.asRequestBody
import okhttp3.RequestBody.Companion.toRequestBody
import java.io.File
-import java.text.NumberFormat
import java.text.SimpleDateFormat
import java.util.Calendar
import java.util.Locale
@@ -63,7 +62,6 @@ class AddEvidencePaymentActivity : AppCompatActivity() {
private val paymentMethods = arrayOf(
"Transfer Bank",
- "E-Wallet",
"QRIS",
)
@@ -129,7 +127,7 @@ class AddEvidencePaymentActivity : AppCompatActivity() {
}
private fun setupUI() {
- val paymentMethods = listOf("Transfer Bank", "COD", "QRIS")
+ val paymentMethods = listOf("Transfer Bank", "QRIS")
val adapter = SpinnerCardAdapter(this, paymentMethods)
binding.spinnerPaymentMethod.adapter = adapter
}
@@ -320,11 +318,12 @@ class AddEvidencePaymentActivity : AppCompatActivity() {
Toast.makeText(this, "Silahkan pilih metode pembayaran", Toast.LENGTH_SHORT).show()
return
}
+ binding.etAccountNumber.visibility = View.GONE
- if (binding.etAccountNumber.text.toString().trim().isEmpty()) {
- Toast.makeText(this, "Silahkan isi nomor rekening/HP", Toast.LENGTH_SHORT).show()
- return
- }
+// if (binding.etAccountNumber.text.toString().trim().isEmpty()) {
+// Toast.makeText(this, "Silahkan isi nomor rekening/HP", Toast.LENGTH_SHORT).show()
+// return
+// }
if (binding.tvPaymentDate.text.toString() == "Pilih tanggal") {
Toast.makeText(this, "Silahkan pilih tanggal pembayaran", Toast.LENGTH_SHORT).show()
diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/order/history/HistoryViewModel.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/order/history/HistoryViewModel.kt
index 1a6f8e5..a2a18af 100644
--- a/app/src/main/java/com/alya/ecommerce_serang/ui/order/history/HistoryViewModel.kt
+++ b/app/src/main/java/com/alya/ecommerce_serang/ui/order/history/HistoryViewModel.kt
@@ -10,6 +10,7 @@ import com.alya.ecommerce_serang.data.api.dto.CompletedOrderRequest
import com.alya.ecommerce_serang.data.api.dto.OrdersItem
import com.alya.ecommerce_serang.data.api.response.customer.order.CancelOrderResponse
import com.alya.ecommerce_serang.data.api.response.customer.order.OrderListItemsItem
+import com.alya.ecommerce_serang.data.api.response.customer.order.OrderListResponse
import com.alya.ecommerce_serang.data.api.response.customer.order.Orders
import com.alya.ecommerce_serang.data.api.response.order.CompletedOrderResponse
import com.alya.ecommerce_serang.data.repository.OrderRepository
@@ -18,6 +19,13 @@ import com.alya.ecommerce_serang.ui.order.address.ViewState
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.flow.flow
+import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import java.io.File
import java.text.SimpleDateFormat
@@ -29,8 +37,8 @@ class HistoryViewModel(private val repository: OrderRepository) : ViewModel() {
private const val TAG = "HistoryViewModel"
}
- private val _orders = MutableLiveData>>()
- val orders: LiveData>> = _orders
+// private val _orders = MutableLiveData>>()
+// val orders: LiveData>> = _orders
private val _orderCompletionStatus = MutableLiveData>()
val orderCompletionStatus: LiveData> = _orderCompletionStatus
@@ -59,81 +67,156 @@ class HistoryViewModel(private val repository: OrderRepository) : ViewModel() {
private val _error = MutableLiveData()
val error: LiveData get() = _error
- fun getOrderList(status: String) {
- _orders.value = ViewState.Loading
- viewModelScope.launch {
- try {
- if (status == "all") {
- // Get all orders by combining all statuses
- getAllOrdersCombined()
- } else {
- // Get orders for specific status
- when (val result = repository.getOrderList(status)) {
- is Result.Success -> {
- _orders.value = ViewState.Success(result.data.orders)
- Log.d(TAG, "Orders loaded successfully: ${result.data.orders.size} items")
+ private val _selectedStatus = MutableStateFlow("all")
+ val selectedStatus: StateFlow = _selectedStatus.asStateFlow()
+
+ val orders: StateFlow>> =
+ _selectedStatus
+ .flatMapLatest { status ->
+ flow>> {
+ Log.d(TAG, "⏳ Loading orders for status = $status")
+ emit(ViewState.Loading)
+
+ val viewState =
+ if (status == "all") {
+ getAllOrdersCombined().also {
+ Log.d(TAG, "✅ Combined orders size = ${(it as? ViewState.Success)?.data?.size}")
+ }
+ } else {
+ when (val r = repository.getOrderList(status)) {
+
+ is Result.Loading -> {
+ Log.d(TAG, " repository.getOrderList($status) → Loading")
+ ViewState.Loading
+ }
+
+ is Result.Success -> {
+ Log.d(TAG, "✅ repository.getOrderList($status) success, size = ${r.data.orders.size}")
+ // Tag each order so the fragment’s filter works
+ val tagged = r.data.orders.onEach { it.displayStatus = status }
+ ViewState.Success(tagged)
+ }
+
+ is Result.Error -> {
+ Log.e(TAG, "❌ repository.getOrderList($status) error = ${r.exception.message}")
+ ViewState.Error(r.exception.message ?: "Unknown error")
+ }
+ }
}
- is Result.Error -> {
- _orders.value = ViewState.Error(result.exception.message ?: "Unknown error occurred")
- Log.e(TAG, "Error loading orders", result.exception)
- }
- is Result.Loading -> {
- // Keep loading state
- }
- }
+
+ emit(viewState)
}
- } catch (e: Exception) {
- _orders.value = ViewState.Error("An unexpected error occurred: ${e.message}")
- Log.e(TAG, "Exception in getOrderList", e)
}
- }
- }
+ .stateIn(
+ viewModelScope,
+ SharingStarted.WhileSubscribed(5_000),
+ ViewState.Loading // ② initial value, still fine
+ )
- private suspend fun getAllOrdersCombined() {
- try {
- val allStatuses = listOf("unpaid", "paid", "processed", "shipped", "completed", "canceled")
- val allOrders = mutableListOf()
- // Use coroutineScope to allow launching async blocks
- coroutineScope {
- val deferreds = allStatuses.map { status ->
+// fun getOrderList(status: String) {
+// _orders.value = ViewState.Loading
+// viewModelScope.launch {
+// try {
+// if (status == "all") {
+// // Get all orders by combining all statuses
+// getAllOrdersCombined()
+// } else {
+// // Get orders for specific status
+// when (val result = repository.getOrderList(status)) {
+// is Result.Success -> {
+// _orders.value = ViewState.Success(result.data.orders)
+// Log.d(TAG, "Orders loaded successfully: ${result.data.orders.size} items")
+// }
+// is Result.Error -> {
+// _orders.value = ViewState.Error(result.exception.message ?: "Unknown error occurred")
+// Log.e(TAG, "Error loading orders", result.exception)
+// }
+// is Result.Loading -> {
+// // Keep loading state
+// }
+// }
+// }
+// } catch (e: Exception) {
+// _orders.value = ViewState.Error("An unexpected error occurred: ${e.message}")
+// Log.e(TAG, "Exception in getOrderList", e)
+// }
+// }
+// }
+
+// private suspend fun getAllOrdersCombined() {
+// try {
+// val allStatuses = listOf("unpaid", "paid", "processed", "shipped", "completed", "canceled")
+// val allOrders = mutableListOf()
+//
+// // Use coroutineScope to allow launching async blocks
+// coroutineScope {
+// val deferreds = allStatuses.map { status ->
+// async {
+// when (val result = repository.getOrderList(status)) {
+// is Result.Success -> {
+// // Tag each order with the status it was fetched from
+// result.data.orders.onEach { it.displayStatus = status }
+// }
+// is Result.Error -> {
+// Log.e(TAG, "Error loading orders for status $status", result.exception)
+// emptyList()
+// }
+// is Result.Loading -> emptyList()
+// }
+// }
+// }
+//
+// // Await all results and combine
+// deferreds.awaitAll().forEach { orders ->
+// allOrders.addAll(orders)
+// }
+// }
+//
+// // Sort orders
+// val sortedOrders = allOrders.sortedByDescending { order ->
+// try {
+// SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.getDefault()).parse(order.createdAt)
+// } catch (e: Exception) {
+// null
+// }
+// }
+//
+// _orders.value = ViewState.Success(sortedOrders)
+// Log.d(TAG, "All orders loaded successfully: ${sortedOrders.size} items")
+//
+// } catch (e: Exception) {
+// _orders.value = ViewState.Error("An unexpected error occurred: ${e.message}")
+// Log.e(TAG, "Exception in getAllOrdersCombined", e)
+// }
+// }
+ private suspend fun getAllOrdersCombined(): ViewState> = try {
+ val statuses = listOf("unpaid", "paid", "processed", "shipped", "completed", "canceled")
+
+ val all = coroutineScope {
+ statuses
+ .map { status ->
async {
- when (val result = repository.getOrderList(status)) {
- is Result.Success -> {
- // Tag each order with the status it was fetched from
- result.data.orders.onEach { it.displayStatus = status }
- }
- is Result.Error -> {
- Log.e(TAG, "Error loading orders for status $status", result.exception)
- emptyList()
- }
- is Result.Loading -> emptyList()
+ when (val r = repository.getOrderList(status)) {
+ is Result.Success -> r.data.orders.onEach { it.displayStatus = status }
+ else -> emptyList()
}
}
}
-
- // Await all results and combine
- deferreds.awaitAll().forEach { orders ->
- allOrders.addAll(orders)
- }
- }
-
- // Sort orders
- val sortedOrders = allOrders.sortedByDescending { order ->
- try {
- SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.getDefault()).parse(order.createdAt)
- } catch (e: Exception) {
- null
- }
- }
-
- _orders.value = ViewState.Success(sortedOrders)
- Log.d(TAG, "All orders loaded successfully: ${sortedOrders.size} items")
-
- } catch (e: Exception) {
- _orders.value = ViewState.Error("An unexpected error occurred: ${e.message}")
- Log.e(TAG, "Exception in getAllOrdersCombined", e)
+ .awaitAll()
+ .flatten()
}
+
+ val sorted = all.sortedByDescending { order ->
+ try {
+ SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.getDefault())
+ .parse(order.createdAt)
+ } catch (_: Exception) { null }
+ }
+
+ ViewState.Success(sorted)
+ } catch (e: Exception) {
+ ViewState.Error("Failed to load orders: ${e.message}")
}
fun confirmOrderCompleted(orderId: Int, status: String) {
@@ -209,9 +292,52 @@ class HistoryViewModel(private val repository: OrderRepository) : ViewModel() {
}
}
- fun refreshOrders(status: String = "all") {
- Log.d(TAG, "Refreshing orders with status: $status")
- // Don't set Loading here if you want to show current data while refreshing
- getOrderList(status)
+// fun refreshOrders(status: String = "all") {
+// Log.d(TAG, "Refreshing orders with status: $status")
+// // Don't set Loading here if you want to show current data while refreshing
+// getOrderList(status)
+// }
+
+ fun updateStatus(status: String, forceRefresh: Boolean = false) {
+ Log.d(TAG, "↪️ updateStatus(status = $status, forceRefresh = $forceRefresh)")
+
+ // No‑op guard (optional): skip if user re‑selects same tab and no refresh asked
+ if (_selectedStatus.value == status && !forceRefresh) {
+ Log.d(TAG, "🔸 Status unchanged & forceRefresh = false → skip update")
+ return
+ }
+
+ _selectedStatus.value = status
+ Log.d(TAG, "✅ _selectedStatus set to \"$status\"")
+
+ if (forceRefresh) {
+ Log.d(TAG, "🔄 forceRefresh = true → launching refresh()")
+ viewModelScope.launch { refresh(status) }
+ }
}
+
+ private suspend fun refresh(status: String) {
+ Log.d(TAG, "⏳ refresh(\"$status\") started")
+
+ try {
+ if (status == "all") {
+ Log.d(TAG, "🌐 Calling getAllOrdersCombined()")
+ getAllOrdersCombined() // network → cache
+ } else {
+ Log.d(TAG, "🌐 repository.getOrderList(\"$status\")")
+ repository.getOrderList(status) // network → cache
+ }
+ Log.d(TAG, "✅ refresh(\"$status\") completed (repository updated)")
+ // Flow that watches DB/cache will emit automatically
+ } catch (e: Exception) {
+ Log.e(TAG, "❌ refresh(\"$status\") failed: ${e.message}", e)
+ }
+ }
+
+ private fun Result.toViewState(): ViewState> =
+ when (this) {
+ is Result.Success -> ViewState.Success(data.orders)
+ is Result.Error -> ViewState.Error(exception.message ?: "Unknown error")
+ is Result.Loading -> ViewState.Loading // should rarely reach UI
+ }
}
\ No newline at end of file
diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/order/history/OrderHistoryAdapter.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/order/history/OrderHistoryAdapter.kt
index f395ec2..34ba43a 100644
--- a/app/src/main/java/com/alya/ecommerce_serang/ui/order/history/OrderHistoryAdapter.kt
+++ b/app/src/main/java/com/alya/ecommerce_serang/ui/order/history/OrderHistoryAdapter.kt
@@ -195,7 +195,7 @@ class OrderHistoryAdapter(
text = itemView.context.getString(R.string.canceled_order_btn)
setOnClickListener {
showCancelOrderDialog(order.orderId.toString())
- viewModel.refreshOrders()
+// viewModel.refreshOrders()
}
}
// deadlineDate.apply {
@@ -213,14 +213,15 @@ class OrderHistoryAdapter(
visibility = View.VISIBLE
text = itemView.context.getString(R.string.dl_processed)
}
- btnLeft.apply {
- visibility = View.VISIBLE
- text = itemView.context.getString(R.string.canceled_order_btn)
- setOnClickListener {
- showCancelOrderDialog(order.orderId.toString())
- viewModel.refreshOrders()
- }
- }
+ // gabisa complaint
+// btnLeft.apply {
+// visibility = View.VISIBLE
+// text = itemView.context.getString(R.string.canceled_order_btn)
+// setOnClickListener {
+// showCancelOrderDialog(order.orderId.toString())
+// viewModel.refreshOrders()
+// }
+// }
}
"shipped" -> {
// Untuk status shipped, tampilkan "Lacak Pengiriman" dan "Terima Barang"
@@ -237,7 +238,7 @@ class OrderHistoryAdapter(
text = itemView.context.getString(R.string.claim_complaint)
setOnClickListener {
showCancelOrderDialog(order.orderId.toString())
- viewModel.refreshOrders()
+// viewModel.refreshOrders()
}
}
btnRight.apply {
@@ -248,7 +249,7 @@ class OrderHistoryAdapter(
// Call ViewModel
viewModel.confirmOrderCompleted(order.orderId, "completed")
- viewModel.refreshOrders()
+// viewModel.refreshOrders()
}
@@ -268,13 +269,21 @@ class OrderHistoryAdapter(
text = itemView.context.getString(R.string.dl_shipped)
}
btnRight.apply {
- visibility = View.VISIBLE
- text = itemView.context.getString(R.string.add_review)
- setOnClickListener {
- addReviewProduct(order)
- viewModel.refreshOrders()
- // Handle click event
+ val checkReview = order.orderItems[0].reviewId
+ if (checkReview > 0){
+ visibility = View.VISIBLE
+ text = itemView.context.getString(R.string.add_review)
+ setOnClickListener {
+
+ addReviewProduct(order)
+// viewModel.refreshOrders()
+ // Handle click event
+ }
+ } else {
+ visibility = View.GONE
}
+
+
}
deadlineDate.apply {
visibility = View.VISIBLE
@@ -518,7 +527,7 @@ class OrderHistoryAdapter(
}
}
- // Create and show the bottom sheet using the obtained FragmentManager
+ // cancel sebelum bayar
val bottomSheet = CancelOrderBottomSheet(
orderId = orderId,
onOrderCancelled = {
@@ -531,6 +540,7 @@ class OrderHistoryAdapter(
bottomSheet.show(fragmentActivity.supportFragmentManager, CancelOrderBottomSheet.TAG)
}
+ // tambah review / ulasan
private fun addReviewProduct(order: OrdersItem) {
// Use ViewModel to fetch order details
viewModel.getOrderDetails(order.orderId)
@@ -550,7 +560,7 @@ class OrderHistoryAdapter(
}
}
- // Observe the order details result
+ // Observe order items
viewModel.orderItems.observe(itemView.findViewTreeLifecycleOwner()!!) { orderItems ->
if (orderItems != null && orderItems.isNotEmpty()) {
// For single item review
diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/order/history/OrderHistoryFragment.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/order/history/OrderHistoryFragment.kt
index 353a952..0d77c05 100644
--- a/app/src/main/java/com/alya/ecommerce_serang/ui/order/history/OrderHistoryFragment.kt
+++ b/app/src/main/java/com/alya/ecommerce_serang/ui/order/history/OrderHistoryFragment.kt
@@ -5,8 +5,13 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
+import androidx.fragment.app.activityViewModels
+import androidx.viewpager2.widget.ViewPager2
import com.alya.ecommerce_serang.R
+import com.alya.ecommerce_serang.data.api.retrofit.ApiConfig
+import com.alya.ecommerce_serang.data.repository.OrderRepository
import com.alya.ecommerce_serang.databinding.FragmentOrderHistoryBinding
+import com.alya.ecommerce_serang.utils.BaseViewModelFactory
import com.alya.ecommerce_serang.utils.SessionManager
import com.google.android.material.tabs.TabLayoutMediator
@@ -16,6 +21,12 @@ class OrderHistoryFragment : Fragment() {
private val binding get() = _binding!!
private lateinit var sessionManager: SessionManager
+ private val historyVm: HistoryViewModel by activityViewModels {
+ BaseViewModelFactory {
+ val api = ApiConfig.getApiService(SessionManager(requireContext()))
+ HistoryViewModel(OrderRepository(api))
+ }
+ }
private lateinit var viewPagerAdapter: OrderViewPagerAdapter
@@ -33,6 +44,8 @@ class OrderHistoryFragment : Fragment() {
sessionManager = SessionManager(requireContext())
setupViewPager()
+
+
}
private fun setupViewPager() {
@@ -53,6 +66,16 @@ class OrderHistoryFragment : Fragment() {
else -> "Tab $position"
}
}.attach()
+
+ binding.viewPager.registerOnPageChangeCallback(
+ object : ViewPager2.OnPageChangeCallback() {
+ override fun onPageSelected(position: Int) {
+ val status = viewPagerAdapter.orderStatuses[position]
+ /* setStatus() is the API we added earlier; TRUE → always re‑query */
+ historyVm.updateStatus(status, forceRefresh = true)
+ }
+ }
+ )
}
override fun onDestroyView() {
diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/order/history/OrderListFragment.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/order/history/OrderListFragment.kt
index 28931bd..82a9f3f 100644
--- a/app/src/main/java/com/alya/ecommerce_serang/ui/order/history/OrderListFragment.kt
+++ b/app/src/main/java/com/alya/ecommerce_serang/ui/order/history/OrderListFragment.kt
@@ -8,8 +8,10 @@ import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
+import androidx.core.view.isVisible
import androidx.fragment.app.Fragment
-import androidx.fragment.app.viewModels
+import androidx.fragment.app.activityViewModels
+import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
import com.alya.ecommerce_serang.data.api.dto.OrdersItem
import com.alya.ecommerce_serang.data.api.retrofit.ApiConfig
@@ -27,17 +29,26 @@ class OrderListFragment : Fragment(), OrderHistoryAdapter.OrderActionCallbacks {
private val binding get() = _binding!!
private lateinit var sessionManager: SessionManager
- private val viewModel: HistoryViewModel by viewModels {
+ private val viewModel: HistoryViewModel by activityViewModels {
BaseViewModelFactory {
- val apiService = ApiConfig.getApiService(sessionManager)
- val orderRepository = OrderRepository(apiService)
- HistoryViewModel(orderRepository)
+ val api = ApiConfig.getApiService(SessionManager(requireContext()))
+ HistoryViewModel(OrderRepository(api))
}
}
+
private lateinit var orderAdapter: OrderHistoryAdapter
private var status: String = "all"
+ private val detailOrderLauncher = registerForActivityResult(
+ ActivityResultContracts.StartActivityForResult()
+ ) { result ->
+ if (result.resultCode == Activity.RESULT_OK) {
+ /* force‑refresh the current tab */
+ viewModel.updateStatus(status, forceRefresh = true)
+ }
+ }
+
companion object {
private const val ARG_STATUS = "status"
@@ -73,8 +84,8 @@ class OrderListFragment : Fragment(), OrderHistoryAdapter.OrderActionCallbacks {
setupRecyclerView()
observeOrderList()
observeViewModel()
- observeOrderCompletionStatus()
- loadOrders()
+// observeOrderCompletionStatus()
+// loadOrders()
}
private fun setupRecyclerView() {
@@ -96,27 +107,50 @@ class OrderListFragment : Fragment(), OrderHistoryAdapter.OrderActionCallbacks {
private fun observeOrderList() {
// Now we only need to observe one LiveData for all cases
- viewModel.orders.observe(viewLifecycleOwner) { result ->
- when (result) {
- is ViewState.Success -> {
- binding.progressBar.visibility = View.GONE
-
- if (result.data.isNullOrEmpty()) {
- binding.tvEmptyState.visibility = View.VISIBLE
- binding.rvOrders.visibility = View.GONE
- } else {
- binding.tvEmptyState.visibility = View.GONE
- binding.rvOrders.visibility = View.VISIBLE
- orderAdapter.submitList(result.data)
+// viewModel.orders.observe(viewLifecycleOwner) { result ->
+// when (result) {
+// is ViewState.Success -> {
+// binding.progressBar.visibility = View.GONE
+//
+// if (result.data.isNullOrEmpty()) {
+// binding.tvEmptyState.visibility = View.VISIBLE
+// binding.rvOrders.visibility = View.GONE
+// } else {
+// binding.tvEmptyState.visibility = View.GONE
+// binding.rvOrders.visibility = View.VISIBLE
+// orderAdapter.submitList(result.data)
+// }
+// }
+// is ViewState.Error -> {
+// binding.progressBar.visibility = View.GONE
+// binding.tvEmptyState.visibility = View.VISIBLE
+// Toast.makeText(requireContext(), result.message, Toast.LENGTH_SHORT).show()
+// }
+// is ViewState.Loading -> {
+// binding.progressBar.visibility = View.VISIBLE
+// }
+// }
+// }
+ viewLifecycleOwner.lifecycleScope.launchWhenStarted {
+ viewModel.orders.collect { state ->
+ when (state) {
+ is ViewState.Loading -> {
+ binding.progressBar.isVisible = true
+ }
+ is ViewState.Error -> {
+ binding.progressBar.isVisible = false
+ binding.tvEmptyState.isVisible = true
+ binding.rvOrders.isVisible = false
+ Toast.makeText(requireContext(), state.message, Toast.LENGTH_SHORT).show()
+ }
+ is ViewState.Success -> {
+ binding.progressBar.isVisible = false
+ val list = state.data
+ .filter { status == "all" || it.displayStatus == status }
+ binding.tvEmptyState.isVisible = list.isEmpty()
+ binding.rvOrders.isVisible = list.isNotEmpty()
+ orderAdapter.submitList(list)
}
- }
- is ViewState.Error -> {
- binding.progressBar.visibility = View.GONE
- binding.tvEmptyState.visibility = View.VISIBLE
- Toast.makeText(requireContext(), result.message, Toast.LENGTH_SHORT).show()
- }
- is ViewState.Loading -> {
- binding.progressBar.visibility = View.VISIBLE
}
}
}
@@ -124,51 +158,78 @@ class OrderListFragment : Fragment(), OrderHistoryAdapter.OrderActionCallbacks {
private fun observeViewModel() {
// Observe order completion
+// viewModel.orderCompletionStatus.observe(viewLifecycleOwner) { result ->
+// when (result) {
+// is Result.Success -> {
+// Toast.makeText(requireContext(), "Order completed successfully!", Toast.LENGTH_SHORT).show()
+//// loadOrders() // Refresh here
+// }
+// is Result.Error -> {
+// Toast.makeText(requireContext(), "Failed: ${result.exception.message}", Toast.LENGTH_SHORT).show()
+// }
+// is Result.Loading -> {
+// // Show loading if needed
+// }
+// }
+// }
+//
+// // Observe cancel order status
+// viewModel.cancelOrderStatus.observe(viewLifecycleOwner) { result ->
+// when (result) {
+// is Result.Success -> {
+// Toast.makeText(requireContext(), "Order cancelled successfully!", Toast.LENGTH_SHORT).show()
+// loadOrders() // Refresh here
+// }
+// is Result.Error -> {
+// Toast.makeText(requireContext(), "Failed to cancel: ${result.exception.message}", Toast.LENGTH_SHORT).show()
+// }
+// is Result.Loading -> {
+// // Show loading if needed
+// }
+// }
+// }
viewModel.orderCompletionStatus.observe(viewLifecycleOwner) { result ->
when (result) {
is Result.Success -> {
- Toast.makeText(requireContext(), "Order completed successfully!", Toast.LENGTH_SHORT).show()
- loadOrders() // Refresh here
- }
- is Result.Error -> {
- Toast.makeText(requireContext(), "Failed: ${result.exception.message}", Toast.LENGTH_SHORT).show()
- }
- is Result.Loading -> {
- // Show loading if needed
+ Toast.makeText(requireContext(),
+ "Order completed!", Toast.LENGTH_SHORT).show()
+ viewModel.updateStatus(status, forceRefresh = true)
}
+ is Result.Error ->
+ Toast.makeText(requireContext(),
+ "Failed: ${result.exception.message}", Toast.LENGTH_SHORT).show()
+ else -> { /* Loading → no UI change */ }
}
}
- // Observe cancel order status
viewModel.cancelOrderStatus.observe(viewLifecycleOwner) { result ->
when (result) {
is Result.Success -> {
- Toast.makeText(requireContext(), "Order cancelled successfully!", Toast.LENGTH_SHORT).show()
- loadOrders() // Refresh here
- }
- is Result.Error -> {
- Toast.makeText(requireContext(), "Failed to cancel: ${result.exception.message}", Toast.LENGTH_SHORT).show()
- }
- is Result.Loading -> {
- // Show loading if needed
+ Toast.makeText(requireContext(),
+ "Order cancelled!", Toast.LENGTH_SHORT).show()
+ viewModel.updateStatus(status, forceRefresh = true)
}
+ is Result.Error ->
+ Toast.makeText(requireContext(),
+ "Failed: ${result.exception.message}", Toast.LENGTH_SHORT).show()
+ else -> { /* Loading */ }
}
}
}
- private fun loadOrders() {
- // Simple - just call getOrderList for any status including "all"
- viewModel.getOrderList(status)
- }
+// private fun loadOrders() {
+// // Simple - just call getOrderList for any status including "all"
+// viewModel.getOrderList(status)
+// }
- private val detailOrderLauncher = registerForActivityResult(
- ActivityResultContracts.StartActivityForResult()
- ) { result ->
- if (result.resultCode == Activity.RESULT_OK) {
- // Refresh order list when returning with OK result
- loadOrders()
- }
- }
+// private val detailOrderLauncher = registerForActivityResult(
+// ActivityResultContracts.StartActivityForResult()
+// ) { result ->
+// if (result.resultCode == Activity.RESULT_OK) {
+// // Refresh order list when returning with OK result
+//// loadOrders()
+// }
+// }
private fun navigateToOrderDetail(order: OrdersItem) {
val intent = Intent(requireContext(), DetailOrderStatusActivity::class.java).apply {
@@ -183,7 +244,9 @@ class OrderListFragment : Fragment(), OrderHistoryAdapter.OrderActionCallbacks {
override fun onOrderCancelled(orderId: String, success: Boolean, message: String) {
if (success) {
Toast.makeText(requireContext(), message, Toast.LENGTH_SHORT).show()
- loadOrders() // Refresh the list
+// loadOrders() // Refresh the list
+ if (success) viewModel.updateStatus(status, forceRefresh = true)
+
} else {
Toast.makeText(requireContext(), message, Toast.LENGTH_SHORT).show()
}
@@ -192,7 +255,8 @@ class OrderListFragment : Fragment(), OrderHistoryAdapter.OrderActionCallbacks {
override fun onOrderCompleted(orderId: Int, success: Boolean, message: String) {
if (success) {
Toast.makeText(requireContext(), message, Toast.LENGTH_SHORT).show()
- loadOrders() // Refresh the list
+// loadOrders() // Refresh the list
+ if (success) viewModel.updateStatus(status, forceRefresh = true)
} else {
Toast.makeText(requireContext(), message, Toast.LENGTH_SHORT).show()
}
@@ -207,20 +271,20 @@ class OrderListFragment : Fragment(), OrderHistoryAdapter.OrderActionCallbacks {
_binding = null
}
- private fun observeOrderCompletionStatus() {
- viewModel.orderCompletionStatus.observe(viewLifecycleOwner) { result ->
- when (result) {
- is Result.Loading -> {
- // Handle loading state if needed
- }
- is Result.Success -> {
- Toast.makeText(requireContext(), "Order completed successfully!", Toast.LENGTH_SHORT).show()
- loadOrders()
- }
- is Result.Error -> {
- Toast.makeText(requireContext(), "Failed to complete order: ${result.exception.message}", Toast.LENGTH_SHORT).show()
- }
- }
- }
- }
+// private fun observeOrderCompletionStatus() {
+// viewModel.orderCompletionStatus.observe(viewLifecycleOwner) { result ->
+// when (result) {
+// is Result.Loading -> {
+// // Handle loading state if needed
+// }
+// is Result.Success -> {
+// Toast.makeText(requireContext(), "Order completed successfully!", Toast.LENGTH_SHORT).show()
+//// loadOrders()
+// }
+// is Result.Error -> {
+// Toast.makeText(requireContext(), "Failed to complete order: ${result.exception.message}", Toast.LENGTH_SHORT).show()
+// }
+// }
+// }
+// }
}
\ No newline at end of file
diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/order/history/OrderViewPageAdapter.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/order/history/OrderViewPageAdapter.kt
index 146100d..e2e000a 100644
--- a/app/src/main/java/com/alya/ecommerce_serang/ui/order/history/OrderViewPageAdapter.kt
+++ b/app/src/main/java/com/alya/ecommerce_serang/ui/order/history/OrderViewPageAdapter.kt
@@ -9,7 +9,7 @@ class OrderViewPagerAdapter(
) : FragmentStateAdapter(fragmentActivity) {
// Define all possible order statuses
- private val orderStatuses = listOf(
+ val orderStatuses = listOf(
"all", // All orders
"unpaid", // Menunggu Tagihan
"paid", // Belum Dibayar
diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/order/history/cancelorder/CancelOrderBottomSheet.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/order/history/cancelorder/CancelOrderBottomSheet.kt
index 99f25de..8fd0376 100644
--- a/app/src/main/java/com/alya/ecommerce_serang/ui/order/history/cancelorder/CancelOrderBottomSheet.kt
+++ b/app/src/main/java/com/alya/ecommerce_serang/ui/order/history/cancelorder/CancelOrderBottomSheet.kt
@@ -55,7 +55,7 @@ class CancelOrderBottomSheet(
val btnConfirm = view.findViewById