fix address, edit address

This commit is contained in:
shaulascr
2025-08-15 02:11:16 +07:00
parent f373035f8e
commit a6d5d10e78
11 changed files with 865 additions and 40 deletions

View File

@ -82,7 +82,8 @@ data class Product(
data class CartItemCheckoutInfo( data class CartItemCheckoutInfo(
val cartItem: CartItemsItem, val cartItem: CartItemsItem,
val isWholesale: Boolean val isWholesale: Boolean,
val wholesalePrice: Int? = null
) )

View File

@ -5,7 +5,7 @@ import com.google.gson.annotations.SerializedName
data class AddressDetailResponse( data class AddressDetailResponse(
@field:SerializedName("address") @field:SerializedName("address")
val address: Address, val address: AddressDetail,
@field:SerializedName("message") @field:SerializedName("message")
val message: String val message: String
@ -14,7 +14,7 @@ data class AddressDetailResponse(
data class AddressDetail( data class AddressDetail(
@field:SerializedName("village_id") @field:SerializedName("village_id")
val villageId: String, val villageId: String?,
@field:SerializedName("is_store_location") @field:SerializedName("is_store_location")
val isStoreLocation: Boolean, val isStoreLocation: Boolean,
@ -38,7 +38,7 @@ data class AddressDetail(
val provinceId: String, val provinceId: String,
@field:SerializedName("phone") @field:SerializedName("phone")
val phone: String, val phone: String?,
@field:SerializedName("street") @field:SerializedName("street")
val street: String, val street: String,
@ -47,7 +47,7 @@ data class AddressDetail(
val subdistrict: String, val subdistrict: String,
@field:SerializedName("recipient") @field:SerializedName("recipient")
val recipient: String, val recipient: String?,
@field:SerializedName("id") @field:SerializedName("id")
val id: Int, val id: Int,

View File

@ -63,6 +63,7 @@ import com.alya.ecommerce_serang.data.api.response.customer.product.DetailStoreP
import com.alya.ecommerce_serang.data.api.response.customer.product.ProductResponse import com.alya.ecommerce_serang.data.api.response.customer.product.ProductResponse
import com.alya.ecommerce_serang.data.api.response.customer.product.ReviewProductResponse import com.alya.ecommerce_serang.data.api.response.customer.product.ReviewProductResponse
import com.alya.ecommerce_serang.data.api.response.customer.product.StoreResponse import com.alya.ecommerce_serang.data.api.response.customer.product.StoreResponse
import com.alya.ecommerce_serang.data.api.response.customer.profile.AddressDetailResponse
import com.alya.ecommerce_serang.data.api.response.customer.profile.AddressResponse import com.alya.ecommerce_serang.data.api.response.customer.profile.AddressResponse
import com.alya.ecommerce_serang.data.api.response.customer.profile.CreateAddressResponse import com.alya.ecommerce_serang.data.api.response.customer.profile.CreateAddressResponse
import com.alya.ecommerce_serang.data.api.response.customer.profile.EditProfileResponse import com.alya.ecommerce_serang.data.api.response.customer.profile.EditProfileResponse
@ -528,4 +529,9 @@ interface ApiService {
suspend fun postResetPass( suspend fun postResetPass(
@Body request: ResetPassReq @Body request: ResetPassReq
): Response<ResetPassResponse> ): Response<ResetPassResponse>
@GET("address/detail/{id}")
suspend fun getDetailAddress(
@Path("id") addressId: Int
): Response<AddressDetailResponse>
} }

View File

@ -20,12 +20,16 @@ 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.ListProvinceResponse
import com.alya.ecommerce_serang.data.api.response.customer.order.OrderDetailResponse 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.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.DetailPaymentItem import com.alya.ecommerce_serang.data.api.response.customer.product.DetailPaymentItem
import com.alya.ecommerce_serang.data.api.response.customer.product.ProductResponse import com.alya.ecommerce_serang.data.api.response.customer.product.ProductResponse
import com.alya.ecommerce_serang.data.api.response.customer.product.StoreItem import com.alya.ecommerce_serang.data.api.response.customer.product.StoreItem
import com.alya.ecommerce_serang.data.api.response.customer.product.StoreResponse import com.alya.ecommerce_serang.data.api.response.customer.product.StoreResponse
import com.alya.ecommerce_serang.data.api.response.customer.profile.AddressDetailResponse
import com.alya.ecommerce_serang.data.api.response.customer.profile.AddressResponse import com.alya.ecommerce_serang.data.api.response.customer.profile.AddressResponse
import com.alya.ecommerce_serang.data.api.response.customer.profile.CreateAddressResponse import com.alya.ecommerce_serang.data.api.response.customer.profile.CreateAddressResponse
import com.alya.ecommerce_serang.data.api.response.customer.profile.UpdateAddressResponse
import com.alya.ecommerce_serang.data.api.response.order.AddEvidenceResponse import com.alya.ecommerce_serang.data.api.response.order.AddEvidenceResponse
import com.alya.ecommerce_serang.data.api.response.order.ComplaintResponse 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.order.CompletedOrderResponse
@ -294,6 +298,30 @@ class OrderRepository(private val apiService: ApiService) {
return if (response.isSuccessful) response.body() else null 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 getAddressDetail(addressId: Int): AddressDetailResponse? {
return try {
val response = apiService.getDetailAddress(addressId)
if (response.isSuccessful) response.body() else null
} catch (e: Exception) {
Log.e("OrderRepository", "Error getting address detail $e ")
null
}
}
suspend fun updateAddress(addressId: Int, params: Map<String, Any>): Response<UpdateAddressResponse> {
return apiService.updateAddress(addressId, params)
}
suspend fun fetchUserProfile(): Result<UserProfile?> { suspend fun fetchUserProfile(): Result<UserProfile?> {
return try { return try {
val response = apiService.getUserProfile() val response = apiService.getUserProfile()
@ -319,6 +347,7 @@ class OrderRepository(private val apiService: ApiService) {
} }
} }
suspend fun uploadPaymentProof(request: AddEvidenceMultipartRequest): Result<AddEvidenceResponse> { suspend fun uploadPaymentProof(request: AddEvidenceMultipartRequest): Result<AddEvidenceResponse> {
return try { return try {
Log.d("OrderRepository", "Uploading payment proof...") Log.d("OrderRepository", "Uploading payment proof...")

View File

@ -30,6 +30,8 @@ class CartActivity : AppCompatActivity() {
private lateinit var sessionManager: SessionManager private lateinit var sessionManager: SessionManager
private lateinit var storeAdapter: StoreAdapter private lateinit var storeAdapter: StoreAdapter
private var TAG = "Cart Activity"
private val viewModel: CartViewModel by viewModels { private val viewModel: CartViewModel by viewModels {
BaseViewModelFactory { BaseViewModelFactory {
val apiService = ApiConfig.getApiService(sessionManager) val apiService = ApiConfig.getApiService(sessionManager)
@ -135,11 +137,33 @@ class CartActivity : AppCompatActivity() {
} }
private fun startCheckoutWithWholesaleInfo(checkoutItems: List<CartItemCheckoutInfo>) { private fun startCheckoutWithWholesaleInfo(checkoutItems: List<CartItemCheckoutInfo>) {
// Extract cart item IDs and wholesale status val wholesalePriceMap = viewModel.cartItemWholesalePrice.value ?: emptyMap()
val cartItemIds = checkoutItems.map { it.cartItem.cartItemId }
val wholesaleArray = checkoutItems.map { it.isWholesale }.toBooleanArray() val updatedItems = checkoutItems.map { info ->
val wholesalePrice = wholesalePriceMap[info.cartItem.cartItemId]
val updatedCartItem = if (info.isWholesale && wholesalePrice != null) {
// Replace the price with wholesale price
info.cartItem.copy(price = wholesalePrice.toInt())
} else {
info.cartItem
}
// Debug log
Log.d(
TAG,
"cartItemId: ${updatedCartItem.cartItemId}, " +
"isWholesale: ${info.isWholesale}, " +
"wholesalePrice: $wholesalePrice, " +
"finalPrice: ${updatedCartItem.price}"
)
info.copy(cartItem = updatedCartItem)
}
val cartItemIds = updatedItems.map { it.cartItem.cartItemId }
val wholesaleArray = updatedItems.map { it.isWholesale }.toBooleanArray()
// Start checkout activity with the cart items and wholesale info
CheckoutActivity.startForCart(this, cartItemIds, wholesaleArray) CheckoutActivity.startForCart(this, cartItemIds, wholesaleArray)
} }
@ -233,7 +257,5 @@ class CartActivity : AppCompatActivity() {
val format = NumberFormat.getCurrencyInstance(Locale("id", "ID")) val format = NumberFormat.getCurrencyInstance(Locale("id", "ID"))
return format.format(amount).replace("Rp", "Rp ") return format.format(amount).replace("Rp", "Rp ")
} }
} }

View File

@ -10,6 +10,9 @@ 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.dto.UserProfile
import com.alya.ecommerce_serang.data.api.response.customer.order.CitiesItem 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.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.response.customer.profile.AddressDetail
import com.alya.ecommerce_serang.data.repository.OrderRepository import com.alya.ecommerce_serang.data.repository.OrderRepository
import com.alya.ecommerce_serang.data.repository.Result import com.alya.ecommerce_serang.data.repository.Result
import com.alya.ecommerce_serang.data.repository.UserRepository import com.alya.ecommerce_serang.data.repository.UserRepository
@ -31,6 +34,18 @@ class AddAddressViewModel(private val repository: OrderRepository, private val u
private val _citiesState = MutableLiveData<ViewState<List<CitiesItem>>>() private val _citiesState = MutableLiveData<ViewState<List<CitiesItem>>>()
val citiesState: LiveData<ViewState<List<CitiesItem>>> = _citiesState val citiesState: LiveData<ViewState<List<CitiesItem>>> = _citiesState
private val _subdistrictState = MutableLiveData<Result<List<SubdistrictsItem>>>()
val subdistrictState: LiveData<Result<List<SubdistrictsItem>>> = _subdistrictState
private val _villagesState = MutableLiveData<Result<List<VillagesItem>>>()
val villagesState: LiveData<Result<List<VillagesItem>>> = _villagesState
private val _userAddress = MutableLiveData<AddressDetail>()
val userAddress: LiveData<AddressDetail> = _userAddress
private val _editAddress = MutableLiveData<Boolean>()
val editAddress: LiveData<Boolean> get() = _editAddress
// Stored in SavedStateHandle for configuration changes // Stored in SavedStateHandle for configuration changes
var selectedProvinceId: Int? var selectedProvinceId: Int?
get() = savedStateHandle.get<Int>("selectedProvinceId") get() = savedStateHandle.get<Int>("selectedProvinceId")
@ -40,6 +55,9 @@ class AddAddressViewModel(private val repository: OrderRepository, private val u
get() = savedStateHandle.get<String>("selectedCityId") get() = savedStateHandle.get<String>("selectedCityId")
set(value) { savedStateHandle["selectedCityId"] = value } set(value) { savedStateHandle["selectedCityId"] = value }
var selectedSubdistrict: String? = null
var selectedVillages: String? = null
init { init {
// Load provinces on initialization // Load provinces on initialization
getProvinces() getProvinces()
@ -86,6 +104,8 @@ class AddAddressViewModel(private val repository: OrderRepository, private val u
} }
} }
fun getProvinces() { fun getProvinces() {
_provincesState.value = ViewState.Loading _provincesState.value = ViewState.Loading
viewModelScope.launch { viewModelScope.launch {
@ -125,24 +145,113 @@ class AddAddressViewModel(private val repository: OrderRepository, private val u
} }
} }
fun getSubdistrict(cityId: String) {
_subdistrictState.value = Result.Loading
viewModelScope.launch {
try {
selectedSubdistrict = 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}")
} ?: run {
_subdistrictState.postValue(Result.Error(Exception("Failed to load cities")))
Log.e(TAG, "City result was null for province $cityId")
}
} catch (e: Exception) {
_subdistrictState.postValue(Result.Error(Exception(e.message ?: "Error loading cities")))
Log.e(TAG, "Error fetching cities for province $cityId", e)
}
}
}
fun getVillages(subdistrictId: String) {
_villagesState.value = Result.Loading
viewModelScope.launch {
try {
selectedVillages = subdistrictId
val result = repository.getListVillages(subdistrictId)
result?.let {
_villagesState.postValue(Result.Success(it.villages))
Log.d(TAG, "Cities loaded for province $subdistrictId: ${it.villages.size}")
} ?: run {
_villagesState.postValue(Result.Error(Exception("Failed to load cities")))
Log.e(TAG, "City result was null for province $subdistrictId")
}
} catch (e: Exception) {
_villagesState.postValue(Result.Error(Exception(e.message ?: "Error loading cities")))
Log.e(TAG, "Error fetching cities for province $subdistrictId", e)
}
}
}
fun setSelectedProvinceId(id: Int) { fun setSelectedProvinceId(id: Int) {
selectedProvinceId = id selectedProvinceId = id
} }
fun updateSelectedCityId(id: String) { fun detailAddress(addressId: Int){
selectedCityId = id
}
fun loadUserProfile(){
viewModelScope.launch { viewModelScope.launch {
when (val result = userRepo.fetchUserProfile()){ try {
is Result.Success -> _userProfile.postValue(result.data) val response = repository.getAddressDetail(addressId)
is Result.Error -> _errorMessageUser.postValue(result.exception.message ?: "Unknown Error") if (response != null){
is Result.Loading -> null _userAddress.value = response.address
} else {
Log.e(TAG, "Failed load address detail")
}
} catch (e:Exception){
Log.e(TAG, "Error fetching address detail: $e", e)
} }
} }
} }
fun updateAddress(oldAddress: AddressDetail, newAddress: AddressDetail) {
val params = buildUpdateBody(oldAddress, newAddress)
if (params.isEmpty()) {
Log.d(TAG, "No changes detected")
_editAddress.value = false
return
}
viewModelScope.launch {
try {
val response = repository.updateAddress(oldAddress.id, params)
_editAddress.value = response.isSuccessful
} catch (e: Exception) {
Log.e(TAG, "Error: ${e.message}")
_editAddress.value = false
}
}
}
private fun buildUpdateBody(oldAddress: AddressDetail, newAddress: AddressDetail): Map<String, Any> {
val params = mutableMapOf<String, Any>()
fun addIfChanged(key: String, oldValue: Any?, newValue: Any?) {
if (newValue != null && newValue != oldValue) {
params[key] = newValue
}
}
addIfChanged("street", oldAddress.street, newAddress.street)
addIfChanged("province_id", oldAddress.provinceId, newAddress.provinceId)
addIfChanged("detail", oldAddress.detail, newAddress.detail)
addIfChanged("subdistrict", oldAddress.subdistrict, newAddress.subdistrict)
addIfChanged("city_id", oldAddress.cityId, newAddress.cityId)
addIfChanged("village_id", oldAddress.villageId, newAddress.villageId)
addIfChanged("postal_code", oldAddress.postalCode, newAddress.postalCode)
addIfChanged("phone", oldAddress.phone, newAddress.phone)
addIfChanged("recipient", oldAddress.recipient, newAddress.recipient)
addIfChanged("latitude", oldAddress.latitude, newAddress.latitude)
addIfChanged("longitude", oldAddress.longitude, newAddress.longitude)
addIfChanged("is_store_location", oldAddress.isStoreLocation, newAddress.isStoreLocation)
return params
}
companion object { companion object {
private const val TAG = "AddAddressViewModel" private const val TAG = "AddAddressViewModel"
} }

View File

@ -74,13 +74,17 @@ class AddressActivity : AppCompatActivity() {
} }
private fun setupRecyclerView() { private fun setupRecyclerView() {
adapter = AddressAdapter { address -> adapter = AddressAdapter(
// Select the address in the ViewModel onAddressClick = { address ->
viewModel.selectAddress(address.id) viewModel.selectAddress(address.id)
returnResultAndFinish(address.id)
// Return immediately with the selected address },
returnResultAndFinish(address.id) onEditClick = { address ->
} val intent = Intent(this, EditAddressActivity::class.java)
intent.putExtra(EditAddressActivity.EXTRA_ADDRESS_ID, address.id)
startActivity(intent)
}
)
binding.rvSellerOrder.apply { binding.rvSellerOrder.apply {
layoutManager = LinearLayoutManager(this@AddressActivity) layoutManager = LinearLayoutManager(this@AddressActivity)

View File

@ -3,6 +3,7 @@ package com.alya.ecommerce_serang.ui.order.address
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView import android.widget.TextView
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.DiffUtil
@ -13,7 +14,8 @@ import com.alya.ecommerce_serang.data.api.response.customer.profile.AddressesIte
import com.google.android.material.card.MaterialCardView import com.google.android.material.card.MaterialCardView
class AddressAdapter( class AddressAdapter(
private val onAddressClick: (AddressesItem) -> Unit private val onAddressClick: (AddressesItem) -> Unit,
private val onEditClick: (AddressesItem) -> Unit
) : ListAdapter<AddressesItem, AddressAdapter.AddressViewHolder>(DIFF_CALLBACK) { ) : ListAdapter<AddressesItem, AddressAdapter.AddressViewHolder>(DIFF_CALLBACK) {
private var selectedAddressId: Int? = null private var selectedAddressId: Int? = null
@ -47,18 +49,21 @@ class AddressAdapter(
// Pass the whole address object to provide more context // Pass the whole address object to provide more context
onAddressClick(address) onAddressClick(address)
} }
holder.editButton.setOnClickListener {
onEditClick(address)
}
} }
class AddressViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { class AddressViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
private val tvName: TextView = itemView.findViewById(R.id.tv_name_address) private val tvName: TextView = itemView.findViewById(R.id.tv_name_address)
private val tvDetail: TextView = itemView.findViewById(R.id.tv_detail_address) private val tvDetail: TextView = itemView.findViewById(R.id.tv_detail_address)
val editButton: ImageView = itemView.findViewById(R.id.iv_edit)
private val card: MaterialCardView = itemView as MaterialCardView private val card: MaterialCardView = itemView as MaterialCardView
fun bind(address: AddressesItem, isSelected: Boolean) { fun bind(address: AddressesItem, isSelected: Boolean) {
tvName.text = address.recipient tvName.text = address.recipient
tvDetail.text = "${address.street}, ${address.subdistrict}, ${address.phone}" tvDetail.text = "${address.street}, ${address.subdistrict}, ${address.phone}"
// Make selection more visible
card.strokeWidth = if (isSelected) 3 else 0 card.strokeWidth = if (isSelected) 3 else 0
card.strokeColor = if (isSelected) card.strokeColor = if (isSelected)
ContextCompat.getColor(itemView.context, R.color.blue_400) ContextCompat.getColor(itemView.context, R.color.blue_400)

View File

@ -1,21 +1,418 @@
package com.alya.ecommerce_serang.ui.order.address package com.alya.ecommerce_serang.ui.order.address
import android.os.Bundle import android.os.Bundle
import android.util.Log
import android.view.View
import android.widget.Toast
import androidx.activity.enableEdgeToEdge import androidx.activity.enableEdgeToEdge
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.ViewCompat import androidx.core.view.ViewCompat
import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat import androidx.core.view.WindowInsetsCompat
import com.alya.ecommerce_serang.R 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.response.customer.profile.AddressDetail
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.ActivityEditAddressBinding
import com.alya.ecommerce_serang.utils.SavedStateViewModelFactory
import com.alya.ecommerce_serang.utils.SessionManager
class EditAddressActivity : AppCompatActivity() { class EditAddressActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) private lateinit var binding: ActivityEditAddressBinding
enableEdgeToEdge() private lateinit var apiService: ApiService
setContentView(R.layout.activity_edit_address) private lateinit var sessionManager: SessionManager
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets ->
val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars()) private var latitude: Double? = null
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom) private var longitude: Double? = null
insets private var addressId: Int = -1
private var currentAddress: AddressDetail? = 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 var provincesList = mutableListOf<ProvincesItem>()
private var citiesList = mutableListOf<CitiesItem>()
private var subdistrictsList = mutableListOf<SubdistrictsItem>()
private var villagesList = mutableListOf<VillagesItem>()
private val viewModel: AddAddressViewModel by viewModels {
SavedStateViewModelFactory(this) { savedStateHandle ->
val apiService = ApiConfig.getApiService(sessionManager)
val orderRepository = OrderRepository(apiService)
val userRepository = UserRepository(apiService)
AddAddressViewModel(orderRepository, userRepository, savedStateHandle)
} }
} }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityEditAddressBinding.inflate(layoutInflater)
setContentView(binding.root)
sessionManager = SessionManager(this)
apiService = ApiConfig.getApiService(sessionManager)
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
}
addressId = intent.getIntExtra(EXTRA_ADDRESS_ID, -1)
if (addressId == -1) {
Toast.makeText(this, "Gagal mendapatkan alamat pengguna", Toast.LENGTH_SHORT).show()
finish()
return
}
setupToolbar()
setupAdapters()
setupObservers()
setupListeners()
// Load address detail first
Log.d(TAG, "Loading address with ID: $addressId")
viewModel.detailAddress(addressId)
}
private fun setupToolbar() {
binding.toolbar.setNavigationOnClickListener {
onBackPressedDispatcher.onBackPressed()
}
}
private fun setupAdapters() {
// Set custom adapters to AutoCompleteTextViews
binding.autoCompleteProvinsi.setAdapter(provinceAdapter)
binding.autoCompleteKabupaten.setAdapter(cityAdapter)
binding.autoCompleteKecamatan.setAdapter(subdistrictAdapter)
binding.autoCompleteDesa.setAdapter(villageAdapter)
}
private fun setupObservers() {
// Observe address detail
viewModel.userAddress.observe(this) { address ->
currentAddress = address
Log.d(TAG, "Address loaded: $address")
populateAddressData(address)
}
// Observe provinces
viewModel.provincesState.observe(this) { viewState ->
when (viewState) {
is ViewState.Loading -> {
binding.progressBar.visibility = View.VISIBLE
}
is ViewState.Success -> {
binding.progressBar.visibility = View.GONE
provincesList.clear()
provincesList.addAll(viewState.data)
provinceAdapter.updateData(viewState.data)
// Set selected province if address is loaded
currentAddress?.let { address ->
setSelectedProvince(address.provinceId.toInt())
}
}
is ViewState.Error -> {
binding.progressBar.visibility = View.GONE
Log.e(TAG, "Failed to load province ${viewState.message}")
}
}
}
// Observe cities
viewModel.citiesState.observe(this) { viewState ->
when (viewState) {
is ViewState.Loading -> {
binding.cityProgressBar.visibility = View.VISIBLE
}
is ViewState.Success -> {
binding.cityProgressBar.visibility = View.GONE
citiesList.clear()
citiesList.addAll(viewState.data)
cityAdapter.updateData(viewState.data)
// Set selected city if address is loaded
currentAddress?.let { address ->
setSelectedCity(address.cityId)
}
}
is ViewState.Error -> {
binding.cityProgressBar.visibility = View.GONE
Log.e(TAG, "Failed to load cities ${viewState.message}")
}
}
}
// Observe subdistricts
viewModel.subdistrictState.observe(this) { result ->
when (result) {
is com.alya.ecommerce_serang.data.repository.Result.Loading -> {
// You can add loading indicator for subdistrict if needed
}
is com.alya.ecommerce_serang.data.repository.Result.Success -> {
subdistrictsList.clear()
subdistrictsList.addAll(result.data)
subdistrictAdapter.updateData(result.data)
// Set selected subdistrict if address is loaded
currentAddress?.let { address ->
setSelectedSubdistrict(address.subdistrict)
}
}
is com.alya.ecommerce_serang.data.repository.Result.Error -> {
Log.e(TAG, "Failed to load subdistricy")
}
}
}
// Observe villages
viewModel.villagesState.observe(this) { result ->
when (result) {
is com.alya.ecommerce_serang.data.repository.Result.Loading -> {
// You can add loading indicator for village if needed
}
is com.alya.ecommerce_serang.data.repository.Result.Success -> {
villagesList.clear()
villagesList.addAll(result.data)
villageAdapter.updateData(result.data)
// Set selected village if address is loaded
currentAddress?.let { address ->
setSelectedVillage(address.villageId)
}
}
is Result.Error -> {
Log.e(TAG, "Failed to load villages")
}
}
}
// Observe update result
viewModel.editAddress.observe(this) { isSuccess ->
binding.submitProgressBar.visibility = View.GONE
binding.buttonSimpan.isEnabled = true
if (isSuccess) {
Log.d(TAG, "Address updated successfully")
finish()
} else {
Log.d(TAG, "Failed to update address")
}
}
}
private fun setupListeners() {
// Province selection
binding.autoCompleteProvinsi.setOnItemClickListener { _, _, position, _ ->
val provinceId = provinceAdapter.getProvinceId(position)
provinceId?.let { id ->
viewModel.getCities(id)
// Clear dependent dropdowns
clearCitySelection()
clearSubdistrictSelection()
clearVillageSelection()
}
}
// City selection
binding.autoCompleteKabupaten.setOnItemClickListener { _, _, position, _ ->
val cityId = cityAdapter.getCityId(position)
cityId?.let { id ->
viewModel.getSubdistrict(id)
// Clear dependent dropdowns
clearSubdistrictSelection()
clearVillageSelection()
}
}
// Subdistrict selection
binding.autoCompleteKecamatan.setOnItemClickListener { _, _, position, _ ->
val subdistrictId = subdistrictAdapter.getSubdistrictId(position)
subdistrictId?.let { id ->
viewModel.getVillages(id)
// Clear dependent dropdowns
clearVillageSelection()
}
}
// Village selection - auto-populate postal code
binding.autoCompleteDesa.setOnItemClickListener { _, _, position, _ ->
// val postalCode = villageAdapter.getPostalCode(position)
// postalCode?.let {
// binding.etKodePos.setText(it)
// }
}
// Save button
binding.buttonSimpan.setOnClickListener {
saveAddress()
}
}
private fun populateAddressData(address: AddressDetail) {
binding.etNamaPenerima.setText(address.recipient ?: "")
binding.etNomorHp.setText(address.phone ?: "")
binding.etDetailAlamat.setText(address.detail ?: "")
binding.etKodePos.setText(address.postalCode ?: "")
// Province will be set when provinces are loaded
// City, subdistrict, village will be set in their respective observers
}
private fun setSelectedProvince(provinceId: Int?) {
provinceId?.let { id ->
val position = provincesList.indexOfFirst { it.provinceId?.toIntOrNull() == id }
if (position >= 0) {
val provinceName = provincesList[position].province
binding.autoCompleteProvinsi.setText(provinceName, false)
viewModel.getCities(id)
}
}
}
private fun setSelectedCity(cityId: String?) {
cityId?.let { id ->
val position = citiesList.indexOfFirst { it.cityId?.toString() == id }
if (position >= 0) {
val cityName = citiesList[position].cityName
binding.autoCompleteKabupaten.setText(cityName, false)
viewModel.getSubdistrict(id)
}
}
}
private fun setSelectedSubdistrict(subdistrictId: String?) {
subdistrictId?.let { id ->
val position = subdistrictsList.indexOfFirst { it.subdistrictId?.toString() == id }
if (position >= 0) {
val subdistrictName = subdistrictsList[position].subdistrictName
binding.autoCompleteKecamatan.setText(subdistrictName, false)
viewModel.getVillages(id)
}
}
}
private fun setSelectedVillage(villageId: String?) {
villageId?.let { id ->
val position = villagesList.indexOfFirst { it.villageId?.toString() == id }
if (position >= 0) {
val villageName = villagesList[position].villageName
binding.autoCompleteDesa.setText(villageName, false)
}
}
}
//
// private fun populatePostalCodeFromVillage(villageId: String?) {
// villageId?.let { id ->
// val village = villagesList.find { it.villageId?.toString() == id }
// village?.postalCode?.let { postalCode ->
// if (binding.etKodePos.text.isNullOrEmpty()) {
// binding.etKodePos.setText(postalCode)
// }
// }
// }
// }
private fun clearCitySelection() {
binding.autoCompleteKabupaten.setText("", false)
citiesList.clear()
cityAdapter.updateData(emptyList())
}
private fun clearSubdistrictSelection() {
binding.autoCompleteKecamatan.setText("", false)
subdistrictsList.clear()
subdistrictAdapter.updateData(emptyList())
}
private fun clearVillageSelection() {
binding.autoCompleteDesa.setText("", false)
binding.etKodePos.setText("") // Clear postal code when village is cleared
villagesList.clear()
villageAdapter.updateData(emptyList())
}
private fun saveAddress() {
currentAddress?.let { oldAddress ->
val newAddress = createNewAddressFromInputs(oldAddress)
binding.submitProgressBar.visibility = View.VISIBLE
binding.buttonSimpan.isEnabled = false
viewModel.updateAddress(oldAddress, newAddress)
} ?: run {
Log.d(TAG, "Address not loaded")
Toast.makeText(this, "Gagal mendapatkan alamat", Toast.LENGTH_SHORT).show()
}
}
private fun createNewAddressFromInputs(oldAddress: AddressDetail): AddressDetail {
val selectedProvinceId = getSelectedProvinceId()
val selectedCityId = getSelectedCityId()
val selectedSubdistrictId = getSelectedSubdistrictId()
val selectedVillageId = getSelectedVillageId()
return 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
)
}
private fun getSelectedProvinceId(): Int? {
val selectedText = binding.autoCompleteProvinsi.text.toString()
val position = provincesList.indexOfFirst { it.province == selectedText }
return if (position >= 0) provinceAdapter.getProvinceId(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 getSelectedSubdistrictId(): String? {
val selectedText = binding.autoCompleteKecamatan.text.toString()
val position = subdistrictsList.indexOfFirst { it.subdistrictName == selectedText }
return if (position >= 0) subdistrictAdapter.getSubdistrictId(position) else null
}
private fun getSelectedVillageId(): String? {
val selectedText = binding.autoCompleteDesa.text.toString()
val position = villagesList.indexOfFirst { it.villageName == selectedText }
return if (position >= 0) villageAdapter.getVillageId(position) else null
}
companion object {
const val EXTRA_ADDRESS_ID = "extra_address_id"
private const val TAG = "EditAddressActivity"
}
} }

View File

@ -5,6 +5,257 @@
android:id="@+id/main" android:id="@+id/main"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
tools:context=".ui.order.address.EditAddressActivity"> android:background="@android:color/white"
tools:context=".ui.order.address.AddAddressActivity">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:navigationIcon="@drawable/ic_back_24"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:title="Ubah Alamat" />
<View
android:id="@+id/divider"
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="#E0E0E0"
app:layout_constraintTop_toBottomOf="@id/toolbar" />
<ScrollView
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintBottom_toTopOf="@id/buttonSimpan"
app:layout_constraintTop_toBottomOf="@id/divider">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Nama Penerima"
android:textColor="@android:color/black"
android:textSize="14sp" />
<EditText
android:id="@+id/etNamaPenerima"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:background="@drawable/edit_text_background"
android:hint="Isi nama penerima"
android:inputType="textPersonName"
android:padding="12dp"
android:textSize="14sp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="Nomor Hp"
android:textColor="@android:color/black"
android:textSize="14sp" />
<EditText
android:id="@+id/etNomorHp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:background="@drawable/edit_text_background"
android:hint="Isi nomor handphone aktif"
android:inputType="phone"
android:padding="12dp"
android:textSize="14sp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="Detail Alamat"
android:textColor="@android:color/black"
android:textSize="14sp" />
<EditText
android:id="@+id/etDetailAlamat"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:background="@drawable/edit_text_background"
android:gravity="top"
android:hint="Isi detail alamat (nomor rumah, lantai, dll)"
android:inputType="textMultiLine"
android:lines="3"
android:padding="12dp"
android:textSize="14sp" />
<!-- Provinsi -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="Provinsi"
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="Pilih Provinsi"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.ExposedDropdownMenu">
<AutoCompleteTextView
android:id="@+id/autoCompleteProvinsi"
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/progressBar"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_gravity="center_horizontal"
android:layout_marginBottom="8dp"
android:visibility="gone" />
<!-- Kabupaten / Kota -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="Kabupaten / Kota"
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="Pilih Kota / Kabupaten"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.ExposedDropdownMenu">
<AutoCompleteTextView
android:id="@+id/autoCompleteKabupaten"
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/cityProgressBar"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_gravity="center_horizontal"
android:layout_marginBottom="8dp"
android:visibility="gone" />
<!-- Kecamatan -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
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="Pilih Kecamatan"
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" />
</com.google.android.material.textfield.TextInputLayout>
<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"
android:hint="Pilih Desa"
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>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="Kode Pos"
android:textColor="@android:color/black"
android:textSize="14sp" />
<EditText
android:id="@+id/etKodePos"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:background="@drawable/edit_text_background"
android:hint="Isi jawaban Anda di sini"
android:inputType="number"
android:padding="12dp"
android:textSize="14sp" />
</LinearLayout>
</ScrollView>
<Button
android:id="@+id/buttonSimpan"
android:layout_width="match_parent"
android:layout_height="56dp"
android:layout_margin="16dp"
android:background="@drawable/button_address_background"
android:text="Simpan"
android:textAllCaps="false"
android:textColor="@android:color/white"
android:textSize="16sp"
app:layout_constraintBottom_toBottomOf="parent" />
<ProgressBar
android:id="@+id/submitProgressBar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="16dp"
android:visibility="gone"
app:layout_constraintTop_toBottomOf="@id/buttonSimpan"
app:layout_constraintEnd_toEndOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -36,8 +36,9 @@
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:src="@drawable/baseline_edit_24" android:src="@drawable/baseline_edit_24"
android:focusable="true"
android:clickable="true"
android:layout_marginHorizontal="16dp" android:layout_marginHorizontal="16dp"
android:visibility="gone"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"/> app:layout_constraintTop_toTopOf="parent"/>
<TextView <TextView