Merge remote-tracking branch 'origin/master'

This commit is contained in:
Gracia Hotmauli
2025-08-22 04:14:04 +07:00
38 changed files with 534 additions and 172 deletions

View File

@ -124,4 +124,7 @@ dependencies {
implementation(platform("com.google.firebase:firebase-bom:33.13.0"))
implementation("com.google.firebase:firebase-analytics")
implementation("com.google.firebase:firebase-messaging-ktx")
//Splash screen
implementation("androidx.core:core-splashscreen:1.0.0")
}

View File

@ -23,13 +23,13 @@ data class AddressDetail(
val latitude: String,
@field:SerializedName("province_name")
val provinceName: String,
val provinceName: String?,
@field:SerializedName("subdistrict_id")
val subdistrictId: String,
val subdistrictId: String?,
@field:SerializedName("city_name")
val cityName: String,
val cityName: String?,
@field:SerializedName("user_id")
val userId: Int,

View File

@ -530,7 +530,7 @@ interface ApiService {
@Body request: ResetPassReq
): Response<ResetPassResponse>
@GET("address/detail/{id}")
@GET("profile/address/detail/{id}")
suspend fun getDetailAddress(
@Path("id") addressId: Int
): Response<AddressDetailResponse>

View File

@ -72,7 +72,7 @@ class LoginActivity : AppCompatActivity() {
val password = binding.etLoginPassword.text.toString()
if (email.isEmpty() || password.isEmpty()) {
Toast.makeText(this, "Please fill in all fields", Toast.LENGTH_SHORT).show()
Toast.makeText(this, "Mohon masukkan email atau password dengan benar", Toast.LENGTH_SHORT).show()
} else {
loginViewModel.login(email, password)
}
@ -100,14 +100,14 @@ class LoginActivity : AppCompatActivity() {
retrieveFCMToken()
// sessionManager.saveUserId(response.userId)
Toast.makeText(this, "Login Successful", Toast.LENGTH_SHORT).show()
Toast.makeText(this, "Berhasil masuk", Toast.LENGTH_SHORT).show()
startActivity(Intent(this, MainActivity::class.java))
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()
Toast.makeText(this, "Gagal masuk", Toast.LENGTH_LONG).show()
}
is Result.Loading -> {
// Show loading state

View File

@ -29,7 +29,7 @@ class OtpBottomSheetDialog(
onRegister(updatedUserData) // Send full data to ViewModel
dismiss() // Close dialog
} else {
Toast.makeText(requireContext(), "Please enter OTP", Toast.LENGTH_SHORT).show()
Toast.makeText(requireContext(), "Silahkan masukkan kode OTP", Toast.LENGTH_SHORT).show()
}
}
return view

View File

@ -86,7 +86,6 @@ class RegisterActivity : AppCompatActivity() {
}
}
// navigate step register in fragment
fun navigateToStep(step: Int, userData: RegisterRequest?) {
val fragment = when (step) {
1 -> RegisterStep1Fragment.newInstance()

View File

@ -111,7 +111,7 @@ class ResetPassActivity : AppCompatActivity() {
}
private fun handleError(errorMessage: String) {
Toast.makeText(this, "Error: $errorMessage", Toast.LENGTH_LONG).show()
Log.e(TAG, "Error: $errorMessage")
// Optionally show error dialog
AlertDialog.Builder(this)

View File

@ -155,19 +155,20 @@ class RegisterStep1Fragment : Fragment() {
"email" -> {
isEmailValid = isValid
if (!isValid) {
Toast.makeText(requireContext(), "Email is already registered", Toast.LENGTH_SHORT).show()
Toast.makeText(requireContext(), "Email sudah digunakan. Gunakan email lainnya.", Toast.LENGTH_SHORT).show()
}
}
"phone" -> {
isPhoneValid = isValid
if (!isValid) {
Toast.makeText(requireContext(), "Phone number is already registered", Toast.LENGTH_SHORT).show()
Toast.makeText(requireContext(), "Nomor handphone sudah digunakan. Gunakan nomor lainnya. ", Toast.LENGTH_SHORT).show()
}
}
}
}
is com.alya.ecommerce_serang.data.repository.Result.Error -> {
Toast.makeText(requireContext(), "Validation failed: ${result.exception.message}", Toast.LENGTH_SHORT).show()
Toast.makeText(requireContext(), "Gagal melakukan validasi", Toast.LENGTH_SHORT).show()
Log.e(TAG, "Validation failed: ${result.exception.message}")
}
}
}
@ -200,7 +201,8 @@ class RegisterStep1Fragment : Fragment() {
is Result.Error -> {
binding.progressBar.visibility = View.GONE
binding.btnNext.isEnabled = true
Toast.makeText(requireContext(), "OTP Request Failed: ${result.exception.message}", Toast.LENGTH_SHORT).show()
Log.e(TAG, "OTP Request Failed: ${result.exception.message}")
Toast.makeText(requireContext(), "Gagal mendapatkan OTP. Kirim ulang OTP", Toast.LENGTH_SHORT).show()
}
}
}
@ -229,13 +231,13 @@ class RegisterStep1Fragment : Fragment() {
// Check if all fields are filled
if (email.isEmpty() || password.isEmpty() || confirmPassword.isEmpty() || phone.isEmpty() ||
username.isEmpty() || fullName.isEmpty() || birthDate.isEmpty()) {
Toast.makeText(requireContext(), "Please fill all required fields", Toast.LENGTH_SHORT).show()
Toast.makeText(requireContext(), "Silahkan lengkapi seluruh isian", Toast.LENGTH_SHORT).show()
return
}
// Check if passwords match
if (password != confirmPassword) {
Toast.makeText(requireContext(), "Passwords do not match", Toast.LENGTH_SHORT).show()
Toast.makeText(requireContext(), "Konfirmasi kata sandi tidak sesua. Periksa kembali", Toast.LENGTH_SHORT).show()
return
}
@ -253,7 +255,7 @@ class RegisterStep1Fragment : Fragment() {
if (isEmailValid && isPhoneValid) {
requestOtp(email)
} else {
Toast.makeText(requireContext(), "Please fix validation errors before proceeding", Toast.LENGTH_SHORT).show()
Toast.makeText(requireContext(), "Silahkan perbaiki data yang dimasukkan", Toast.LENGTH_SHORT).show()
}
}

View File

@ -1,5 +1,6 @@
package com.alya.ecommerce_serang.ui.auth.fragments
import android.content.Context
import android.os.Build
import android.os.Bundle
import android.os.CountDownTimer
@ -13,6 +14,7 @@ import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import com.alya.ecommerce_serang.R
import com.alya.ecommerce_serang.data.api.dto.FcmReq
import com.alya.ecommerce_serang.data.api.dto.RegisterRequest
import com.alya.ecommerce_serang.data.api.retrofit.ApiConfig
import com.alya.ecommerce_serang.data.repository.OrderRepository
@ -24,6 +26,7 @@ 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.firebase.messaging.FirebaseMessaging
class RegisterStep2Fragment : Fragment() {
private var _binding: FragmentRegisterStep2Binding? = null
@ -109,6 +112,10 @@ class RegisterStep2Fragment : Fragment() {
}
}
binding.btnBack.setOnClickListener {
parentFragmentManager.popBackStack()
}
observeRegistrationState()
observeLoginState()
Log.d(TAG, "Registration and login state observers set up")
@ -129,11 +136,6 @@ class RegisterStep2Fragment : Fragment() {
Log.d(TAG, "Updating user data with OTP: $otp")
registerViewModel.updateUserData(updatedUserData)
// For demo purposes, we're just proceeding to Step 3
// In a real app, you would verify the OTP with the server first
// registerViewModel.setStep(3)
// (activity as? RegisterActivity)?.navigateToStep(3, updatedUserData)
registerViewModel.registerUser(updatedUserData)
} ?: Log.e(TAG, "userData is null, cannot proceed with verification")
}
@ -250,6 +252,8 @@ class RegisterStep2Fragment : Fragment() {
// Save the token in fragment
val accessToken = result.data.accessToken
sessionManager.saveToken(accessToken)
retrieveFCMToken()
Log.d(TAG, "Token saved to SessionManager: $accessToken")
// Proceed to Step 3
@ -279,6 +283,37 @@ class RegisterStep2Fragment : Fragment() {
}
}
private fun retrieveFCMToken() {
FirebaseMessaging.getInstance().token
.addOnCompleteListener { task ->
if (!task.isSuccessful) {
Log.e(TAG, "Failed to get FCM token", task.exception)
return@addOnCompleteListener
}
val token = task.result
// tokenTes = token
Log.d(TAG, "FCM token retrieved: $token")
// Save token locally
val sharedPreferences = requireContext().getSharedPreferences("FCM_PREFS", Context.MODE_PRIVATE)
sharedPreferences.edit().putString("FCM_TOKEN", token).apply()
// Send to your server
sendTokenToServer(token)
}
}
private fun sendTokenToServer(token: String) {
Log.d(TAG, "Would send token to server: $token")
val tokenFcm=FcmReq(
fcmToken = token
)
registerViewModel.sendFcm(tokenFcm)
Log.d(TAG, "Sent token fcm: $token")
}
override fun onDestroyView() {
super.onDestroyView()
countDownTimer?.cancel()

View File

@ -104,11 +104,20 @@ class RegisterStep3Fragment : Fragment() {
// Set up button listeners
binding.btnPrevious.setOnClickListener {
// Go back to the previous step
parentFragmentManager.popBackStack()
// parentFragmentManager.popBackStack()
// (activity as? RegisterActivity)?.navigateToStep(2, null)
// (activity as? RegisterActivity)?.goBackToPreviousStep()
// Option 2: Direct navigation to step 1
val step2Fragment = RegisterStep2Fragment()
parentFragmentManager.beginTransaction()
.replace(R.id.fragment_container, step2Fragment)
.commit()
}
binding.btnRegister.setOnClickListener {
submitAddress()
sessionManager.clearAll()
}
// If user skips address entry
@ -503,7 +512,7 @@ class RegisterStep3Fragment : Fragment() {
private fun showRegistrationSuccess() {
// Now we can show the success message for the overall registration process
Toast.makeText(requireContext(), "Registration completed successfully!", Toast.LENGTH_LONG).show()
Toast.makeText(requireContext(), "Berhasil mendaftarkan akun", Toast.LENGTH_LONG).show()
// Navigate to login screen
startActivity(Intent(requireContext(), LoginActivity::class.java))
@ -521,4 +530,5 @@ class RegisterStep3Fragment : Fragment() {
ViewCompat.setWindowInsetsAnimationCallback(binding.root, null)
_binding = null
}
}

View File

@ -41,11 +41,16 @@ class CartActivity : AppCompatActivity() {
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
sessionManager = SessionManager(this)
apiService = ApiConfig.getApiService(sessionManager)
binding = ActivityCartBinding.inflate(layoutInflater)
setContentView(binding.root)
sessionManager = SessionManager(this)
apiService = ApiConfig.getApiService(sessionManager)
if (!sessionManager.isLoggedIn()){
binding.emptyCart.text = "Silahkan masuk terlebih dahulu"
}
WindowCompat.setDecorFitsSystemWindows(window, false)
@ -118,7 +123,7 @@ class CartActivity : AppCompatActivity() {
// Start checkout with the prepared items
startCheckoutWithWholesaleInfo(selectedItems)
} else {
Toast.makeText(this, "Please select items from a single store only", Toast.LENGTH_SHORT).show()
Toast.makeText(this, "Pilih produk yang sama dengan toko", Toast.LENGTH_SHORT).show()
}
}
} else {
@ -151,7 +156,7 @@ class CartActivity : AppCompatActivity() {
// Debug log
Log.d(
TAG,
TAG,
"cartItemId: ${updatedCartItem.cartItemId}, " +
"isWholesale: ${info.isWholesale}, " +
"wholesalePrice: $wholesalePrice, " +
@ -164,7 +169,17 @@ class CartActivity : AppCompatActivity() {
val cartItemIds = updatedItems.map { it.cartItem.cartItemId }
val wholesaleArray = updatedItems.map { it.isWholesale }.toBooleanArray()
CheckoutActivity.startForCart(this, cartItemIds, wholesaleArray)
// FIX: Pass wholesale prices as IntArray
val wholesalePricesArray = updatedItems.map { info ->
if (info.isWholesale) {
val wholesalePrice = wholesalePriceMap[info.cartItem.cartItemId]
wholesalePrice?.toInt() ?: info.cartItem.price
} else {
info.cartItem.price
}
}.toIntArray()
CheckoutActivity.startForCart(this, cartItemIds, wholesaleArray, wholesalePricesArray)
}
private fun observeViewModel() {

View File

@ -124,7 +124,7 @@ class ChatActivity : AppCompatActivity() {
if (token.isEmpty()) {
// User not logged in, redirect to login
Toast.makeText(this, "Please login first", Toast.LENGTH_SHORT).show()
Toast.makeText(this, "Silahkan masuk terlebih dahulu", Toast.LENGTH_SHORT).show()
startActivity(Intent(this, LoginActivity::class.java))
finish()
return
@ -506,7 +506,7 @@ class ChatActivity : AppCompatActivity() {
}
startActivity(intent)
} catch (e: Exception) {
Toast.makeText(this, "Cannot open product details", Toast.LENGTH_SHORT).show()
Toast.makeText(this, "Gagal memuat produk", Toast.LENGTH_SHORT).show()
Log.e(TAG, "Error navigating to product detail", e)
}
}
@ -622,7 +622,7 @@ class ChatActivity : AppCompatActivity() {
if (outputFile.exists() && outputFile.length() > 0) {
if (outputFile.length() > 5 * 1024 * 1024) {
Log.e(TAG, "File too large: ${outputFile.length()} bytes")
Toast.makeText(this, "Image too large (max 5MB)", Toast.LENGTH_SHORT).show()
Toast.makeText(this, "Gambar terlalu besar. Maksimal 1MB", Toast.LENGTH_SHORT).show()
return
}

View File

@ -5,7 +5,6 @@ import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import com.alya.ecommerce_serang.data.api.retrofit.ApiConfig
@ -80,8 +79,10 @@ class ChatListFragment : Fragment() {
}
}
is Result.Error -> {
binding.tvEmptyChat.visibility = View.VISIBLE
Toast.makeText(requireContext(), "Failed to load chats", Toast.LENGTH_SHORT).show()
// binding.tvEmptyChat.visibility = View.VISIBLE
binding.progressBarChat.visibility = View.VISIBLE
// Toast.makeText(requireContext(), "Failed to load chats", Toast.LENGTH_SHORT).show()
Log.e(TAG, "Failed to load chats")
}
Result.Loading -> {
binding.progressBarChat.visibility = View.VISIBLE

View File

@ -78,4 +78,4 @@ import com.google.firebase.messaging.RemoteMessage
val notificationId = System.currentTimeMillis().toInt()
notificationManager.notify(notificationId, notificationBuilder.build())
}
}
}

View File

@ -79,9 +79,6 @@ class CheckoutActivity : AppCompatActivity() {
// Determine if this is Buy Now or Cart checkout
val isBuyNow = intent.hasExtra(EXTRA_PRODUCT_ID) && !intent.hasExtra(EXTRA_CART_ITEM_IDS)
val isWholesaleNow = intent.getBooleanExtra(EXTRA_ISWHOLESALE, false)
val wholesalePricesArray = intent.getIntArrayExtra(EXTRA_CART_ITEM_WHOLESALE_PRICES)
if (isBuyNow) {
// Process Buy Now flow
@ -99,8 +96,7 @@ class CheckoutActivity : AppCompatActivity() {
// Process Cart checkout flow
val cartItemIds = intent.getIntArrayExtra(EXTRA_CART_ITEM_IDS)?.toList() ?: emptyList()
val isWholesaleArray = intent.getBooleanArrayExtra(EXTRA_CART_ITEM_WHOLESALE)
val wholesalePricesArray = intent.getIntArrayExtra(EXTRA_CART_ITEM_WHOLESALE_PRICES)
if (cartItemIds.isNotEmpty()) {
// Build map of cartItemId -> isWholesale
@ -108,17 +104,18 @@ class CheckoutActivity : AppCompatActivity() {
cartItemIds.mapIndexed { index, id ->
id to isWholesaleArray[index]
}.toMap()
} else {
emptyMap()
}
// Build wholesalePriceMap
val wholesalePriceMap = cartItemIds.mapIndexed { index, id ->
id to (wholesalePricesArray?.get(index) ?: 0)
}.toMap()
// Build wholesalePriceMap - FIX: Map cartItemId to wholesale price
val wholesalePriceMap = if (wholesalePricesArray != null && wholesalePricesArray.size == cartItemIds.size) {
cartItemIds.mapIndexed { index, id ->
id to wholesalePricesArray[index]
}.toMap()
} else {
emptyMap()
}
viewModel.initializeFromCart(
cartItemIds,
@ -199,7 +196,7 @@ class CheckoutActivity : AppCompatActivity() {
// Observe order creation
viewModel.orderCreated.observe(this) { created ->
if (created) {
Toast.makeText(this, "Order successfully created!", Toast.LENGTH_SHORT).show()
Toast.makeText(this, "Berhasil membuat pesanan", Toast.LENGTH_SHORT).show()
setResult(RESULT_OK)
finish()
}
@ -209,10 +206,12 @@ class CheckoutActivity : AppCompatActivity() {
private fun setupPaymentMethodsRecyclerView(paymentMethods: List<DetailPaymentItem>) {
if (paymentMethods.isEmpty()) {
Log.e("CheckoutActivity", "Payment methods list is empty")
Toast.makeText(this, "No payment methods available", Toast.LENGTH_SHORT).show()
Toast.makeText(this, "Tidak ditemukan metode pembayaran", Toast.LENGTH_SHORT).show()
binding.tvEmptyPayment.visibility = View.VISIBLE
return
}
binding.tvEmptyPayment.visibility = View.GONE
// Debug logging
Log.d("CheckoutActivity", "Setting up payment methods: ${paymentMethods.size} methods available")
@ -316,7 +315,7 @@ class CheckoutActivity : AppCompatActivity() {
binding.layoutShippingMethod.setOnClickListener {
val addressId = viewModel.addressDetails.value?.id ?: 0
if (addressId <= 0) {
Toast.makeText(this, "Please select delivery address first", Toast.LENGTH_SHORT).show()
Toast.makeText(this, "Silahkan pilih metode pengiriman dahulu", Toast.LENGTH_SHORT).show()
return@setOnClickListener
}
@ -366,7 +365,7 @@ class CheckoutActivity : AppCompatActivity() {
viewModel.setSelectedAddress(addressId)
// You might want to show a toast or some UI feedback
Toast.makeText(this, "Address selected successfully", Toast.LENGTH_SHORT).show()
Toast.makeText(this, "Berhasil memilih alamat", Toast.LENGTH_SHORT).show()
}
}
}

View File

@ -96,7 +96,7 @@ class CheckoutViewModel(private val repository: OrderRepository) : ViewModel() {
fun initializeFromCart(
cartItemIds: List<Int>,
isWholesaleMap: Map<Int, Boolean> = emptyMap(),
wholesalePriceMap: Map<Int, Int> = emptyMap() // new
wholesalePriceMap: Map<Int, Int> = emptyMap()
) {
viewModelScope.launch {
_isLoading.value = true
@ -111,13 +111,21 @@ class CheckoutViewModel(private val repository: OrderRepository) : ViewModel() {
for (store in cartResult.data) {
val storeItems = store.cartItems.filter { it.cartItemId in cartItemIds }
if (storeItems.isNotEmpty()) {
// ✅ Override price with wholesale price if exists
// ✅ Apply wholesale prices - Replace item prices with wholesale prices
val updatedItems = storeItems.map { item ->
val wholesalePrice = wholesalePriceMap[item.cartItemId]
if (wholesalePrice != null) {
item.copy(price = wholesalePrice.toInt())
} else item
val isWholesale = isWholesaleMap[item.cartItemId] ?: false
// Use wholesale price if item is wholesale and price exists
if (isWholesale && wholesalePrice != null) {
Log.d(TAG, "Applying wholesale price for item ${item.cartItemId}: ${item.price} -> $wholesalePrice")
item.copy(price = wholesalePrice)
} else {
Log.d(TAG, "Using regular price for item ${item.cartItemId}: ${item.price}")
item
}
}
matchingItems.addAll(updatedItems)
storeData = store
break
@ -143,16 +151,17 @@ class CheckoutViewModel(private val repository: OrderRepository) : ViewModel() {
sellerName = storeData.storeName,
sellerId = storeData.storeId,
isBuyNow = false,
cartItems = matchingItems,
cartItems = matchingItems, // These now have updated wholesale prices
cartItemWholesaleMap = isWholesaleMap
)
Log.d(TAG, "CheckoutData initialized: ${_checkoutData.value}")
Log.d(TAG, "Matching cart items: ${matchingItems.size}")
Log.d(TAG, "Cart items: ${matchingItems.map { it.productName to it.price }}")
Log.d(TAG, "IsWholesaleMap passed: $isWholesaleMap")
Log.d(TAG, "WholesalePriceMap passed: $wholesalePriceMap")
Log.d(TAG, "CheckoutData initialized with ${matchingItems.size} items")
matchingItems.forEachIndexed { index, item ->
val isWholesale = isWholesaleMap[item.cartItemId] ?: false
Log.d(TAG, "Item $index: ${item.productName}, Price: ${item.price}, IsWholesale: $isWholesale")
}
// Calculate totals with updated prices
calculateSubtotal()
calculateTotal()
} else {
@ -163,11 +172,11 @@ class CheckoutViewModel(private val repository: OrderRepository) : ViewModel() {
}
} catch (e: Exception) {
_errorMessage.value = "Error: ${e.message}"
Log.e(TAG, "Error in initializeFromCart", e)
} finally {
_isLoading.value = false
}
}
}
fun getPaymentMethods() {
@ -418,8 +427,6 @@ class CheckoutViewModel(private val repository: OrderRepository) : ViewModel() {
}
}
companion object {
private const val TAG = "CheckoutViewModel"
}

View File

@ -67,7 +67,7 @@ class ShippingActivity : AppCompatActivity() {
// Validate required information
if (addressId <= 0 || productId <= 0) {
Log.e(TAG, "Missing required shipping information: addressId=$addressId, productId=$productId")
Toast.makeText(this, "Missing required shipping information", Toast.LENGTH_SHORT).show()
Toast.makeText(this, "Gagal memuat pengiriman", Toast.LENGTH_SHORT).show()
finish()
return
}

View File

@ -34,6 +34,8 @@ class SingleCartItemAdapter(private val cartItem: CartItemsItem) :
// Load placeholder image
Glide.with(ivProduct.context)
.load(R.drawable.placeholder_image)
.placeholder(R.drawable.placeholder_image)
.error(R.drawable.placeholder_image)
.into(ivProduct)
}
}

View File

@ -22,12 +22,14 @@ import androidx.core.view.ViewCompat
import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat
import com.alya.ecommerce_serang.data.api.dto.CreateAddressRequest
import com.alya.ecommerce_serang.data.api.dto.UserProfile
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
import com.alya.ecommerce_serang.data.api.retrofit.ApiConfig
import com.alya.ecommerce_serang.data.api.retrofit.ApiService
import com.alya.ecommerce_serang.data.repository.OrderRepository
import com.alya.ecommerce_serang.data.repository.Result
import com.alya.ecommerce_serang.data.repository.UserRepository
import com.alya.ecommerce_serang.databinding.ActivityAddAddressBinding
import com.alya.ecommerce_serang.utils.SavedStateViewModelFactory
@ -37,8 +39,8 @@ class AddAddressActivity : AppCompatActivity() {
private lateinit var binding: ActivityAddAddressBinding
private lateinit var apiService: ApiService
private lateinit var sessionManager: SessionManager
private var profileUser: Int = 1
private lateinit var locationManager: LocationManager
private var profileUserId: Int? = null
private var isRequestingLocation = false
@ -46,6 +48,8 @@ class AddAddressActivity : AppCompatActivity() {
private var longitude: Double? = null
private val provinceAdapter by lazy { ProvinceAdapter(this) }
private val cityAdapter by lazy { CityAdapter(this) }
private val subdistrictAdapter by lazy { SubdsitrictAdapter(this)}
private val villageAdapter by lazy { VillagesAdapter(this)}
private val viewModel: AddAddressViewModel by viewModels {
SavedStateViewModelFactory(this) { savedStateHandle ->
@ -80,11 +84,15 @@ class AddAddressActivity : AppCompatActivity() {
)
windowInsets
}
viewModel.loadUserProfile()
// Get user profile from session manager
// profileUser =UserProfile.
viewModel.userProfile.observe(this){ user ->
user?.let { updateProfile(it) }
viewModel.userProfile.observe(this) { user ->
if (user != null) {
profileUserId = user.userId
Log.d(TAG, "Fetched userId = $profileUserId") // ✅ debug log
} else {
Log.e(TAG, "Error get profile")
}
}
setupToolbar()
@ -94,16 +102,10 @@ class AddAddressActivity : AppCompatActivity() {
setupButtonListeners()
setupObservers()
// Force trigger province loading to ensure it happens
viewModel.getProvinces()
}
private fun updateProfile(userProfile: UserProfile){
profileUser = userProfile.userId
}
// UI setup methods
private fun setupToolbar() {
binding.toolbar.setNavigationOnClickListener {
@ -116,6 +118,8 @@ class AddAddressActivity : AppCompatActivity() {
// Set adapters
binding.autoCompleteProvinsi.setAdapter(provinceAdapter)
binding.autoCompleteKabupaten.setAdapter(cityAdapter)
binding.autoCompleteKecamatan.setAdapter(subdistrictAdapter)
binding.autoCompleteDesa.setAdapter(villageAdapter)
// Make dropdown appear on click (not just when typing)
binding.autoCompleteProvinsi.setOnClickListener {
@ -134,6 +138,26 @@ class AddAddressActivity : AppCompatActivity() {
}
}
binding.autoCompleteKecamatan.setOnClickListener{
if (subdistrictAdapter.count > 0){
Log.d(TAG, "Subdistrict clicked, dropdown with ${subdistrictAdapter.count} items")
binding.autoCompleteKecamatan.showDropDown()
} else {
Log.d(TAG, "No kecamatan available")
Toast.makeText(this, "Pilih Kabupaten / Kota terlebih dahulu", Toast.LENGTH_SHORT).show()
}
}
binding.autoCompleteDesa.setOnClickListener{
if (villageAdapter.count > 0){
Log.d(TAG, "Village clicked, dropdown with ${villageAdapter.count} items")
binding.autoCompleteDesa.showDropDown()
} else {
Log.d(TAG, "No desa available")
Toast.makeText(this, "Pilih Kecamatan terlebih dahulu", Toast.LENGTH_SHORT).show()
}
}
// Set listeners for selection
binding.autoCompleteProvinsi.setOnItemClickListener { _, _, position, _ ->
val provinceId = provinceAdapter.getProvinceId(position)
@ -152,9 +176,41 @@ class AddAddressActivity : AppCompatActivity() {
cityId?.let { id ->
Log.d(TAG, "Setting selectedCityId=$id")
viewModel.getSubdistrict(cityId)
viewModel.selectedCityId = id
binding.autoCompleteKecamatan.text.clear()
} ?: Log.e(TAG, "Could not get cityId for position $position")
}
binding.autoCompleteKecamatan.setOnItemClickListener { _, _, position, _ ->
val subdistrictId = subdistrictAdapter.getSubdistrictId(position)
val subdistrictName = subdistrictAdapter.getSubdistrictName(position)
Log.d(TAG, "Subdistrict selected at position $position, subsId=$subdistrictId")
subdistrictId?.let { id ->
Log.d(TAG, "Setting subdistrict id=$id")
viewModel.getVillages(subdistrictId)
viewModel.selectedSubdistrictId = id
binding.autoCompleteDesa.text.clear()
} ?: Log.e(TAG, "Could not get subsId for position $position")
subdistrictName?.let { name ->
Log.d(TAG, "Setting subdistrict=$name")
viewModel.selectedSubdistrict = name
} ?: Log.e(TAG, "COuldnt get subs name for position ${position}")
}
binding.autoCompleteDesa.setOnItemClickListener { _, _, position, _ ->
val villageId = villageAdapter.getVillageId(position)
Log.d(TAG, "Village selected at position $position, villageId=$villageId")
villageId?.let { id ->
Log.d(TAG, "Setting village=$id")
viewModel.selectedVillages = id
} ?: Log.e(TAG, "Could not get villageId for position $position")
}
}
private fun setupButtonListeners() {
@ -178,6 +234,16 @@ class AddAddressActivity : AppCompatActivity() {
handleCityState(state)
}
viewModel.subdistrictState.observe(this) {state ->
Log.d(TAG, "Received subdistrictId update: $state")
handleSubdistrictState(state)
}
viewModel.villagesState.observe(this) {state ->
Log.d(TAG, "Received subdistrictId update: $state")
handleVillageState(state)
}
// Observe address submission
viewModel.addressSubmissionState.observe(this) { state ->
Log.d(TAG, "Received addressSubmissionState update: $state")
@ -202,7 +268,7 @@ class AddAddressActivity : AppCompatActivity() {
}
is ViewState.Error -> {
// Hide loading indicator
showError("Failed to load provinces: ${state.message}")
// showError("Failed to load provinces: ${state.message}")
Log.e("AddAddressActivity", "Province error: ${state.message}")
}
}
@ -221,12 +287,50 @@ class AddAddressActivity : AppCompatActivity() {
}
is ViewState.Error -> {
binding.cityProgressBar.visibility = View.GONE
showError("Failed to load cities: ${state.message}")
// showError("Failed to load cities: ${state.message}")
Log.e("AddAddressActivity", "City error: ${state.message}")
}
}
}
private fun handleSubdistrictState(state: com.alya.ecommerce_serang.data.repository.Result<List<SubdistrictsItem>>) {
when (state) {
is Result.Loading -> {
Log.d(TAG, "Loading subdistrict...")
binding.subdistrictProgressBar.visibility = View.VISIBLE
}
is Result.Success -> {
Log.d(TAG, "Subdistrict loaded: ${state.data.size}")
binding.subdistrictProgressBar.visibility = View.GONE
subdistrictAdapter.updateData(state.data)
}
is Result.Error -> {
binding.subdistrictProgressBar.visibility = View.GONE
// showError("Failed to load subs: ${state.message}")
Log.e(TAG, "Subdistrict error: ${state}")
}
}
}
private fun handleVillageState(state: Result<List<VillagesItem>>) {
when (state) {
is Result.Loading -> {
Log.d(TAG, "Loading villages...")
binding.villageProgressBar.visibility = View.VISIBLE
}
is Result.Success -> {
Log.d(TAG, "Villages loaded: ${state.data.size}")
binding.villageProgressBar.visibility = View.GONE
villageAdapter.updateData(state.data)
}
is Result.Error -> {
binding.villageProgressBar.visibility = View.GONE
// showError("Failed to load subs: ${state.message}")
Log.e(TAG, "Village error: ${state}")
}
}
}
private fun handleAddressSubmissionState(state: ViewState<String>) {
when (state) {
is ViewState.Loading -> {
@ -276,23 +380,20 @@ class AddAddressActivity : AppCompatActivity() {
}
val street = binding.etDetailAlamat.text.toString().trim()
val subDistrict = binding.etKecamatan.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 userId = try {
profileUser
} catch (e: Exception) {
Log.w(TAG, "Error getting userId, using default", e)
1 // Default userId for testing
}
val userId = profileUserId
val isStoreLocation = false
val provinceId = viewModel.selectedProvinceId
val cityId = viewModel.selectedCityId.toString()
val subDistrict = viewModel.selectedSubdistrict.toString()
val villageId = viewModel.selectedVillages.toString()
Log.d(TAG, "Form data: street=$street, subDistrict=$subDistrict, postalCode=$postalCode, " +
"recipient=$recipient, phone=$phone, userId=$userId, provinceId=$provinceId, cityId=$cityId, " +
"recipient=$recipient, phone=$phone, userId=$userId, provinceId=$provinceId, cityId=$cityId, subdistrict=$subDistrict, villageId=$villageId " +
"lat=$latitude, long=$longitude")
// Validate required fields
@ -333,7 +434,7 @@ class AddAddressActivity : AppCompatActivity() {
// Create request with all fields
val request = CreateAddressRequest(
userId = userId,
userId = userId!!,
lat = latitude!!, // Safe to use !! as we've checked above
long = longitude!!,
street = street,
@ -341,7 +442,7 @@ class AddAddressActivity : AppCompatActivity() {
cityId = cityId, // ⚠️ Make sure this is Int
provId = provinceId,
postCode = postalCode,
idVillage = "", // Or provide a real ID if needed
idVillage = villageId, // Or provide a real ID if needed
detailAddress = street,
isStoreLocation = false,
recipient = recipient,
@ -389,8 +490,8 @@ class AddAddressActivity : AppCompatActivity() {
binding.locationProgressBar.visibility = View.GONE
binding.tvLocationStatus.text = "Provider lokasi tidak tersedia"
isRequestingLocation = false
Toast.makeText(this, "Provider lokasi tidak tersedia", Toast.LENGTH_SHORT).show()
showEnableLocationDialog()
// Toast.makeText(this, "Provider lokasi tidak tersedia", Toast.LENGTH_SHORT).show()
// showEnableLocationDialog()
return
}
@ -417,7 +518,7 @@ class AddAddressActivity : AppCompatActivity() {
latitude = -6.200000
longitude = 106.816666
isRequestingLocation = false
Toast.makeText(this, "Timeout lokasi, menggunakan lokasi default", Toast.LENGTH_SHORT).show()
// Toast.makeText(this, "Timeout lokasi, menggunakan lokasi default", Toast.LENGTH_SHORT).show()
}
}, 60000) // 15 seconds timeout
@ -431,7 +532,7 @@ class AddAddressActivity : AppCompatActivity() {
binding.locationProgressBar.visibility = View.GONE
binding.tvLocationStatus.text = "Lokasi terdeteksi: ${lastLocation.latitude}, ${lastLocation.longitude}"
isRequestingLocation = false
Toast.makeText(this, "Lokasi terdeteksi", Toast.LENGTH_SHORT).show()
// Toast.makeText(this, "Lokasi terdeteksi", Toast.LENGTH_SHORT).show()
return
} else {
Log.d(TAG, "No last known location, requesting updates")
@ -449,7 +550,7 @@ class AddAddressActivity : AppCompatActivity() {
binding.locationProgressBar.visibility = View.GONE
binding.tvLocationStatus.text = "Lokasi terdeteksi: ${location.latitude}, ${location.longitude}"
isRequestingLocation = false
Toast.makeText(this@AddAddressActivity, "Lokasi terdeteksi", Toast.LENGTH_SHORT).show()
// Toast.makeText(this@AddAddressActivity, "Lokasi terdeteksi", Toast.LENGTH_SHORT).show()
// Remove location updates after receiving a location
try {
@ -472,7 +573,7 @@ class AddAddressActivity : AppCompatActivity() {
binding.locationProgressBar.visibility = View.GONE
binding.tvLocationStatus.text = "Provider lokasi dimatikan"
isRequestingLocation = false
Toast.makeText(this@AddAddressActivity, "Provider $provider dimatikan", Toast.LENGTH_SHORT).show()
// Toast.makeText(this@AddAddressActivity, "Provider $provider dimatikan", Toast.LENGTH_SHORT).show()
}
}
@ -491,7 +592,7 @@ class AddAddressActivity : AppCompatActivity() {
binding.locationProgressBar.visibility = View.GONE
binding.tvLocationStatus.text = "Error: ${e.message}"
isRequestingLocation = false
Toast.makeText(this, "Error mendapatkan lokasi: ${e.message}", Toast.LENGTH_SHORT).show()
// Toast.makeText(this, "Error mendapatkan lokasi: ${e.message}", Toast.LENGTH_SHORT).show()
// Set default location
latitude = -6.200000
@ -519,12 +620,12 @@ class AddAddressActivity : AppCompatActivity() {
// Add button to reload location (add this button to your layout)
binding.btnReloadLocation.setOnClickListener {
Log.d(TAG, "Reload location button clicked")
Toast.makeText(this, "Memuat ulang lokasi...", Toast.LENGTH_SHORT).show()
// Toast.makeText(this, "Memuat ulang lokasi...", Toast.LENGTH_SHORT).show()
requestLocation()
}
}
companion object {
private const val TAG = "AddAddressViewModel"
private const val TAG = "AddAddressActivity"
}
}

View File

@ -56,6 +56,7 @@ class AddAddressViewModel(private val repository: OrderRepository, private val u
set(value) { savedStateHandle["selectedCityId"] = value }
var selectedSubdistrict: String? = null
var selectedSubdistrictId: String? = null
var selectedVillages: String? = null
init {
@ -104,8 +105,6 @@ class AddAddressViewModel(private val repository: OrderRepository, private val u
}
}
fun getProvinces() {
_provincesState.value = ViewState.Loading
viewModelScope.launch {
@ -150,18 +149,18 @@ class AddAddressViewModel(private val repository: OrderRepository, private val u
viewModelScope.launch {
try {
selectedSubdistrict = cityId
selectedSubdistrictId = cityId
val result = repository.getListSubdistrict(cityId)
result?.let {
_subdistrictState.postValue(Result.Success(it.subdistricts))
Log.d(TAG, "Cities loaded for province $cityId: ${it.subdistricts.size}")
Log.d(TAG, "Subdistrict loaded for city $cityId: ${it.subdistricts.size}")
} ?: run {
_subdistrictState.postValue(Result.Error(Exception("Failed to load cities")))
Log.e(TAG, "City result was null for province $cityId")
Log.e(TAG, "Subdistrict result was null for city $cityId")
}
} catch (e: Exception) {
_subdistrictState.postValue(Result.Error(Exception(e.message ?: "Error loading cities")))
Log.e(TAG, "Error fetching cities for province $cityId", e)
Log.e(TAG, "Error fetching subdistrict for city $cityId", e)
}
}
}
@ -175,14 +174,14 @@ class AddAddressViewModel(private val repository: OrderRepository, private val u
val result = repository.getListVillages(subdistrictId)
result?.let {
_villagesState.postValue(Result.Success(it.villages))
Log.d(TAG, "Cities loaded for province $subdistrictId: ${it.villages.size}")
Log.d(TAG, "Villages loaded for subdistrict $subdistrictId: ${it.villages.size}")
} ?: run {
_villagesState.postValue(Result.Error(Exception("Failed to load cities")))
Log.e(TAG, "City result was null for province $subdistrictId")
Log.e(TAG, "Village result was null for subdistrict $subdistrictId")
}
} catch (e: Exception) {
_villagesState.postValue(Result.Error(Exception(e.message ?: "Error loading cities")))
Log.e(TAG, "Error fetching cities for province $subdistrictId", e)
Log.e(TAG, "Error fetching villages for subdistrict $subdistrictId", e)
}
}
}
@ -248,10 +247,34 @@ class AddAddressViewModel(private val repository: OrderRepository, private val u
addIfChanged("latitude", oldAddress.latitude, newAddress.latitude)
addIfChanged("longitude", oldAddress.longitude, newAddress.longitude)
addIfChanged("is_store_location", oldAddress.isStoreLocation, newAddress.isStoreLocation)
addIfChanged("village_name", oldAddress.villageName, newAddress.villageName)
addIfChanged("subdsitrict_id", oldAddress.subdistrictId, newAddress.subdistrictId)
addIfChanged("id", oldAddress.id, newAddress.id)
addIfChanged("user_id", oldAddress.userId, newAddress.userId)
addIfChanged("city_name", oldAddress.cityName, newAddress.cityName)
addIfChanged("province_name", oldAddress.provinceName, newAddress.provinceName)
return params
}
fun loadUserProfile() {
viewModelScope.launch {
when (val result = repository.fetchUserProfile()) {
is Result.Success -> {
result.data?.let {
_userProfile.postValue(it) // send UserProfile to LiveData
} ?: _errorMessageUser.postValue("User data not found")
}
is Result.Error -> {
_errorMessageUser.postValue(result.exception.message ?: "Unknown error")
}
is Result.Loading -> {
null
}
}
}
}
companion object {
private const val TAG = "AddAddressViewModel"
}

View File

@ -371,19 +371,35 @@ class EditAddressActivity : AppCompatActivity() {
private fun createNewAddressFromInputs(oldAddress: AddressDetail): AddressDetail {
val selectedProvinceId = getSelectedProvinceId()
val selectedCityId = getSelectedCityId()
val selectedSubdistrictId = getSelectedSubdistrictId()
val selectedSubdistrictName = getSelectedSubdistrictName()
val selectedVillageId = getSelectedVillageId()
val selectedSubdistrictId = getSelectedSubdistrictId()
val selectedVillageName = getSelectedVillageName()
val selectedProvinceName = getSelectedProvinceName()
return oldAddress.copy(
Log.d(TAG, "Old subdistrict: ${oldAddress.subdistrict}")
Log.d(TAG, "Selected subdistrictId: $selectedSubdistrictName")
val newAddress = oldAddress.copy(
recipient = binding.etNamaPenerima.text.toString().trim(),
phone = binding.etNomorHp.text.toString().trim(),
detail = binding.etDetailAlamat.text.toString().trim(),
postalCode = binding.etKodePos.text.toString().trim(),
provinceId = selectedProvinceId.toString(),
cityId = selectedCityId.toString(),
subdistrict = selectedSubdistrictId.toString(),
villageId = selectedVillageId
provinceId = selectedProvinceId?.toString() ?: oldAddress.provinceId,
cityId = selectedCityId ?: oldAddress.cityId,
subdistrict = selectedSubdistrictName ?: oldAddress.subdistrict,
villageId = selectedVillageId,
subdistrictId = selectedSubdistrictId ?: oldAddress.subdistrictId,
villageName = selectedVillageName ?: oldAddress.villageName,
provinceName = selectedProvinceName ?: oldAddress.provinceName
)
// 🔎 Debug logs
Log.d(TAG, "New subdistrict: ${newAddress.subdistrict}")
Log.d(TAG, "New village name: ${newAddress.villageName}")
return newAddress
}
private fun getSelectedProvinceId(): Int? {
@ -392,12 +408,30 @@ class EditAddressActivity : AppCompatActivity() {
return if (position >= 0) provinceAdapter.getProvinceId(position) else null
}
private fun getSelectedProvinceName(): String? {
val selectedText = binding.autoCompleteProvinsi.text.toString()
val position = provincesList.indexOfFirst { it.province == selectedText }
return if (position >= 0) provinceAdapter.getProvinceName(position) else null
}
private fun getSelectedCityId(): String? {
val selectedText = binding.autoCompleteKabupaten.text.toString()
val position = citiesList.indexOfFirst { it.cityName == selectedText }
return if (position >= 0) cityAdapter.getCityId(position) else null
}
private fun getSelectedCityName(): String? {
val selectedText = binding.autoCompleteKabupaten.text.toString()
val position = citiesList.indexOfFirst { it.cityName == selectedText }
return if (position >= 0) cityAdapter.getCityName(position) else null
}
private fun getSelectedSubdistrictName(): String? {
val selectedText = binding.autoCompleteKecamatan.text.toString()
val position = subdistrictsList.indexOfFirst { it.subdistrictName == selectedText }
return if (position >= 0) subdistrictAdapter.getSubdistrictName(position) else null
}
private fun getSelectedSubdistrictId(): String? {
val selectedText = binding.autoCompleteKecamatan.text.toString()
val position = subdistrictsList.indexOfFirst { it.subdistrictName == selectedText }
@ -410,6 +444,12 @@ class EditAddressActivity : AppCompatActivity() {
return if (position >= 0) villageAdapter.getVillageId(position) else null
}
private fun getSelectedVillageName(): String? {
val selectedText = binding.autoCompleteDesa.text.toString()
val position = villagesList.indexOfFirst { it.villageName == selectedText }
return if (position >= 0) villageAdapter.getVillageName(position) else null
}
companion object {
const val EXTRA_ADDRESS_ID = "extra_address_id"
private const val TAG = "EditAddressActivity"

View File

@ -32,6 +32,10 @@ class ProvinceAdapter(
fun getProvinceId(position: Int): Int? {
return provinces.getOrNull(position)?.provinceId?.toIntOrNull()
}
fun getProvinceName(position: Int): String? {
return provinces.getOrNull(position)?.province?.toString()
}
}
class CityAdapter(
@ -53,6 +57,10 @@ class CityAdapter(
fun getCityId(position: Int): String? {
return cities.getOrNull(position)?.cityId?.toString()
}
fun getCityName(position: Int): String? {
return cities.getOrNull(position)?.cityName?.toString()
}
}
class SubdsitrictAdapter(
@ -99,6 +107,10 @@ class VillagesAdapter(
fun getVillageId(position: Int): String? {
return villages.getOrNull(position)?.villageId?.toString()
}
fun getVillageName(position: Int): String? {
return villages.getOrNull(position)?.villageName.toString()
}
fun getPostalCode(position: Int): String?{
return villages.getOrNull(position)?.postalCode
}

View File

@ -122,7 +122,6 @@ class AddEvidencePaymentActivity : AppCompatActivity() {
} catch (e: Exception) {
Log.e(TAG, "ERROR in AddEvidencePaymentActivity onCreate: ${e.message}", e)
Toast.makeText(this, "Error: ${e.message}", Toast.LENGTH_LONG).show()
}
}
@ -288,7 +287,7 @@ class AddEvidencePaymentActivity : AppCompatActivity() {
}
} catch (e: Exception) {
Log.e(TAG, "Error handling selected image", e)
Toast.makeText(this, "Error: ${e.message}", Toast.LENGTH_SHORT).show()
Toast.makeText(this, "Terjadi kendala", Toast.LENGTH_SHORT).show()
}
}
@ -367,7 +366,7 @@ class AddEvidencePaymentActivity : AppCompatActivity() {
viewModel.uploadPaymentProof(request)
} catch (e: Exception) {
Log.e(TAG, "Error creating upload request: ${e.message}", e)
Toast.makeText(this, "Error preparing upload: ${e.message}", Toast.LENGTH_SHORT).show()
Toast.makeText(this, "Gagal mengunggah foto", Toast.LENGTH_SHORT).show()
}
}
}

View File

@ -160,7 +160,8 @@ class PaymentActivity : AppCompatActivity() {
viewModel.error.observe(this) { error ->
if (error.isNotEmpty()) {
Toast.makeText(this, error, Toast.LENGTH_SHORT).show()
Toast.makeText(this, "Gagal melakukan pembayaran", Toast.LENGTH_SHORT).show()
Log.e(TAG, "Failed payment: $error")
}
}
}

View File

@ -517,14 +517,14 @@ class OrderHistoryAdapter(
} else {
// Log error and show a Toast instead if we can't get a FragmentManager
Log.e("OrderHistoryAdapter", "Cannot show bottom sheet: Context is not a FragmentActivity")
Toast.makeText(context, "Cannot show cancel order dialog", Toast.LENGTH_SHORT).show()
Toast.makeText(context, "Terjadi kendala", Toast.LENGTH_SHORT).show()
return
}
}
else -> {
// Log error and show a Toast instead if we can't get a FragmentManager
Log.e("OrderHistoryAdapter", "Cannot show bottom sheet: Context is not a FragmentActivity")
Toast.makeText(context, "Cannot show cancel order dialog", Toast.LENGTH_SHORT).show()
Toast.makeText(context, "Terjadi kendala", Toast.LENGTH_SHORT).show()
return
}
}
@ -535,7 +535,7 @@ class OrderHistoryAdapter(
onOrderCancelled = {
callbacks.onOrderCancelled(orderId.toString(), true, "Order cancelled successfully")
// Show a success message
Toast.makeText(context, "Order cancelled successfully", Toast.LENGTH_SHORT).show()
Toast.makeText(context, "Pesanan berhasil dibatalkan", Toast.LENGTH_SHORT).show()
}
)

View File

@ -67,7 +67,7 @@ class CancelOrderBottomSheet(
btnConfirm.setOnClickListener {
if (selectedReason == null) {
Toast.makeText(context, "Please select a reason", Toast.LENGTH_SHORT).show()
Toast.makeText(context, "Pilih alasan pembatalan", Toast.LENGTH_SHORT).show()
return@setOnClickListener
}

View File

@ -90,7 +90,7 @@ class CreateReviewActivity : AppCompatActivity() {
)
})
} catch (e: Exception) {
Toast.makeText(this, "Error loading review items", Toast.LENGTH_SHORT).show()
Toast.makeText(this, "Gagal memuat ulasan", Toast.LENGTH_SHORT).show()
finish()
}
} else {
@ -110,7 +110,7 @@ class CreateReviewActivity : AppCompatActivity() {
)
)
} else {
Toast.makeText(this, "No items to review", Toast.LENGTH_SHORT).show()
Toast.makeText(this, "Tidak ada produk untuk direview", Toast.LENGTH_SHORT).show()
finish()
}
}

View File

@ -106,7 +106,8 @@ class DetailProfileActivity : AppCompatActivity() {
}
editProfileLauncher.launch(intent)
} ?: run {
Toast.makeText(this, "Profile data is not available", Toast.LENGTH_SHORT).show()
Toast.makeText(this, "Akun tidak ditemukan", Toast.LENGTH_SHORT).show()
Log.e("DetailProfileActivity", "Profile data is not available")
}
}
}

View File

@ -20,10 +20,10 @@ import com.alya.ecommerce_serang.data.repository.MyStoreRepository
import com.alya.ecommerce_serang.data.repository.UserRepository
import com.alya.ecommerce_serang.databinding.FragmentProfileBinding
import com.alya.ecommerce_serang.ui.auth.LoginActivity
import com.alya.ecommerce_serang.ui.profile.mystore.RegisterStoreActivity
import com.alya.ecommerce_serang.ui.order.address.AddressActivity
import com.alya.ecommerce_serang.ui.order.history.HistoryActivity
import com.alya.ecommerce_serang.ui.profile.mystore.MyStoreActivity
import com.alya.ecommerce_serang.ui.profile.mystore.RegisterStoreActivity
import com.alya.ecommerce_serang.ui.profile.mystore.StoreOnReviewActivity
import com.alya.ecommerce_serang.ui.profile.mystore.StoreSuspendedActivity
import com.alya.ecommerce_serang.utils.BaseViewModelFactory
@ -58,7 +58,6 @@ class ProfileFragment : Fragment() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
sessionManager = SessionManager(requireContext())
}
override fun onCreateView(
@ -72,6 +71,30 @@ class ProfileFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
sessionManager = SessionManager(requireContext())
if (!sessionManager.isLoggedIn()) {
// Redirect to LoginActivity
binding.tvName.text = "Selamat Datang"
binding.tvUsername.text = "Silahkan masuk"
binding.btnDetailProfile.text = "Masuk"
binding.btnDetailProfile.setOnClickListener {
val intent = Intent(requireContext(), LoginActivity::class.java)
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
startActivity(intent)
// ✅ Finish the host activity so user cant go back
requireActivity().finish()
}
binding.containerBukaToko.visibility = View.GONE
binding.cardPesanan.visibility = View.GONE
binding.tvPengaturanAkun.visibility = View.GONE
binding.containerSettings.visibility = View.GONE
binding.cardAbout.visibility = View.GONE
binding.cardLogout.visibility = View.GONE
}
observeUserProfile()
observeStoreStatus()
@ -140,7 +163,8 @@ class ProfileFragment : Fragment() {
user?.let { updateUI(it) }
}
viewModel.errorMessage.observe(viewLifecycleOwner) { errorMessage ->
Toast.makeText(requireContext(), errorMessage, Toast.LENGTH_SHORT).show()
// Toast.makeText(requireContext(), errorMessage, Toast.LENGTH_SHORT).show()
Log.e("Profile Fragment", "Failed to load profile: $errorMessage")
}
}
@ -196,6 +220,8 @@ class ProfileFragment : Fragment() {
sessionManager.clearAll()
val intent = Intent(requireContext(), LoginActivity::class.java)
startActivity(intent)
requireActivity().finish()
} catch (e: Exception) {
Toast.makeText(
requireContext(),

View File

@ -1,10 +1,12 @@
package com.alya.ecommerce_serang.ui.profile.editprofile
import android.Manifest
import android.app.Activity
import android.app.DatePickerDialog
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.provider.MediaStore
import android.util.Log
@ -159,17 +161,14 @@ class EditProfileCustActivity : AppCompatActivity() {
}
private fun openImagePicker() {
// Check for permission first
if (ContextCompat.checkSelfPermission(
this,
android.Manifest.permission.READ_EXTERNAL_STORAGE
) != PackageManager.PERMISSION_GRANTED
) {
ActivityCompat.requestPermissions(
this,
arrayOf(android.Manifest.permission.READ_EXTERNAL_STORAGE),
REQUEST_STORAGE_PERMISSION
)
val permission = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
Manifest.permission.READ_MEDIA_IMAGES
} else {
Manifest.permission.READ_EXTERNAL_STORAGE
}
if (ContextCompat.checkSelfPermission(this, permission) != PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(this, arrayOf(permission), REQUEST_STORAGE_PERMISSION)
} else {
launchImagePicker()
}

View File

@ -446,7 +446,7 @@ class RegisterStoreActivity : AppCompatActivity() {
viewModel.errorMessage.observe(this) { errorMsg ->
if (errorMsg.isNotEmpty()) {
Log.e(TAG, "setupStoreTypesObserver: Error loading store types: $errorMsg")
Toast.makeText(this, "Error loading store types: $errorMsg", Toast.LENGTH_SHORT).show()
// Toast.makeText(this, "Error loading store types: $errorMsg", Toast.LENGTH_SHORT).show()
}
}

View File

@ -17,6 +17,7 @@ import com.alya.ecommerce_serang.ui.profile.mystore.sells.shipment.DetailShipmen
import com.alya.ecommerce_serang.utils.viewmodel.SellsViewModel
import com.bumptech.glide.Glide
import com.google.gson.Gson
import java.text.NumberFormat
import java.text.SimpleDateFormat
import java.util.Calendar
import java.util.Locale
@ -96,7 +97,7 @@ class SellsAdapter(
val product = order.orderItems?.firstOrNull()
tvSellsProductName.text = product?.productName
tvSellsProductQty.text = "x${product?.quantity}"
tvSellsProductPrice.text = formatPrice(product?.price.toString())
tvSellsProductPrice.text = product?.price?.let { formatPrice(it.toInt()) }
val fullImageUrl = when (val img = product?.productImage) {
is String -> {
@ -169,7 +170,7 @@ class SellsAdapter(
val product = order.orderItems?.firstOrNull()
tvSellsProductName.text = product?.productName
tvSellsProductQty.text = "x${product?.quantity}"
tvSellsProductPrice.text = formatPrice(product?.price.toString())
tvSellsProductPrice.text = product?.price?.let { formatPrice(it.toInt()) }
val fullImageUrl = when (val img = product?.productImage) {
is String -> {
@ -185,7 +186,7 @@ class SellsAdapter(
.into(ivSellsProduct)
tvSellsQty.text = "${order.orderItems?.size} produk"
tvSellsPrice.text = formatPrice(order.totalAmount.toString())
tvSellsPrice.text = order.totalAmount?.let { formatPrice(it.toInt()) }
}
"paid" -> {
layoutOrders.visibility = View.GONE
@ -308,10 +309,9 @@ class SellsAdapter(
}
}
private fun formatPrice(price: String): String {
val priceDouble = price.toDoubleOrNull() ?: 0.0
val formattedPrice = String.format(Locale("id", "ID"), "Rp%,.0f", priceDouble)
return formattedPrice
private fun formatPrice(amount: Int): String {
val formatter = NumberFormat.getCurrencyInstance(Locale("in", "ID"))
return formatter.format(amount.toLong()).replace(",00", "")
}
}
}

View File

@ -7,9 +7,11 @@ import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.alya.ecommerce_serang.data.api.dto.CreateAddressRequest
import com.alya.ecommerce_serang.data.api.dto.FcmReq
import com.alya.ecommerce_serang.data.api.dto.RegisterRequest
import com.alya.ecommerce_serang.data.api.dto.ResetPassReq
import com.alya.ecommerce_serang.data.api.dto.VerifRegisReq
import com.alya.ecommerce_serang.data.api.response.auth.FcmTokenResponse
import com.alya.ecommerce_serang.data.api.response.auth.LoginResponse
import com.alya.ecommerce_serang.data.api.response.auth.OtpResponse
import com.alya.ecommerce_serang.data.api.response.auth.RegisterResponse
@ -388,6 +390,34 @@ class RegisterViewModel(private val repository: UserRepository, private val orde
}
fun sendFcm(token: FcmReq) {
viewModelScope.launch {
_otpState.value = Result.Loading // Indicating API call in progress
try {
// Call the repository function to request OTP
val authenticatedApiService = getAuthenticatedApiService()
val authenticatedOrderRepo = UserRepository(authenticatedApiService)
val response: FcmTokenResponse = authenticatedOrderRepo.sendFcm(token)
// Log and store success message
Log.d("LoginViewModel", "OTP Response: ${response.message}")
_message.value = response.message ?: "berhasil" // Store the message for UI feedback
// Update state to indicate success
_otpState.value = Result.Success(Unit)
} catch (exception: Exception) {
// Handle any errors and update state
_otpState.value = Result.Error(exception)
_message.value = exception.localizedMessage ?: "Failed to request OTP"
// Log the error for debugging
Log.e("LoginViewModel", "OTP request failed for: $token", exception)
}
}
}
companion object {
private const val TAG = "RegisterViewModel"
}

View File

@ -168,25 +168,66 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="Kecamatan / Desa"
android:text="Kecamatan"
android:textColor="@android:color/black"
android:textSize="14sp" />
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:hint="Isi Kecamatan / Desa"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/etKecamatan"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.ExposedDropdownMenu">
<AutoCompleteTextView
android:id="@+id/autoCompleteKecamatan"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="none"
android:focusable="false"
android:clickable="true"
android:padding="12dp"
android:textSize="14sp"
android:inputType="textCapWords" />
android:textSize="14sp" />
</com.google.android.material.textfield.TextInputLayout>
<ProgressBar
android:id="@+id/subdistrictProgressBar"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_gravity="center_horizontal"
android:layout_marginBottom="8dp"
android:visibility="gone" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="Desa"
android:textColor="@android:color/black"
android:textSize="14sp" />
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.ExposedDropdownMenu">
<AutoCompleteTextView
android:id="@+id/autoCompleteDesa"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="none"
android:focusable="false"
android:clickable="true"
android:padding="12dp"
android:textSize="14sp" />
</com.google.android.material.textfield.TextInputLayout>
<ProgressBar
android:id="@+id/villageProgressBar"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_gravity="center_horizontal"
android:layout_marginBottom="8dp"
android:visibility="gone" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"

View File

@ -257,6 +257,15 @@
android:textSize="14sp"
android:layout_marginBottom="8dp" />
<TextView
android:id="@+id/tvEmptyPayment"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="gone"
android:textSize="12sp"
android:text="Pilih alamat terlebih dahulu"
android:layout_marginBottom="8dp"/>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_payment_info"
android:layout_width="match_parent"

View File

@ -50,8 +50,9 @@
android:id="@+id/et_otp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Enter OTP"
android:hint="Masukkan OTP"
android:inputType="number"
android:textSize="16dp"
android:textAlignment="center"
android:maxLength="6" />
</com.google.android.material.textfield.TextInputLayout>
@ -61,9 +62,20 @@
android:id="@+id/btn_verify"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Verify"
android:text="Kirim kode OTP"
style="@style/button.large.active.medium"
app:cornerRadius="8dp" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_back"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:visibility="gone"
style="@style/Widget.Material3.Button.OutlinedButton.Icon"
android:text="Kembali"
app:cornerRadius="8dp"/>
<!-- Resend OTP -->
<LinearLayout
android:layout_width="match_parent"
@ -81,7 +93,7 @@
android:id="@+id/tv_resend_otp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Resend"
android:text="Kirim Ulang"
android:textColor="@color/blue1"
android:textStyle="bold" />
</LinearLayout>

View File

@ -233,6 +233,12 @@
android:padding="12dp"
android:textSize="14sp" />
<CheckBox
android:id="@+id/checkbox_approve"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Saya telah membaca dan menyetujui Syarat dan Ketentuan aplikasi" />
<!-- Navigation Button (Previous) -->
<Button
android:id="@+id/btn_previous"
@ -240,24 +246,14 @@
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:background="@drawable/bg_button_outline"
android:visibility="gone"
android:text="Kembali"
android:textAllCaps="false"
android:textColor="@color/blue1"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"/>
style="@style/Widget.Material3.Button.OutlinedButton.Icon"/>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<CheckBox
android:id="@+id/checkbox_approve"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Saya telah membaca dan menyetujui Syarat dan Ketentuan aplikasi" />
</LinearLayout>
</ScrollView>
<!-- Register Button -->

View File

@ -38,7 +38,6 @@
android:src="@drawable/baseline_edit_24"
android:focusable="true"
android:clickable="true"
android:visibility="gone"
android:layout_marginHorizontal="16dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"/>