Merge pull request #20

edit store profile & topup
This commit is contained in:
Gracia Hotmauli
2025-05-12 16:54:57 +07:00
committed by GitHub
20 changed files with 1906 additions and 128 deletions

View File

@ -29,6 +29,9 @@
android:theme="@style/Theme.Ecommerce_serang"
android:usesCleartextTraffic="true"
tools:targetApi="31">
<activity
android:name=".ui.profile.mystore.profile.EditStoreProfileActivity"
android:exported="false" />
<activity
android:name=".ui.profile.mystore.sells.shipment.DetailShipmentActivity"
android:exported="false" />
@ -40,8 +43,7 @@
android:exported="false" />
<activity
android:name=".ui.chat.ChatActivity"
android:exported="false" />
<!-- <provider -->
android:exported="false" /> <!-- <provider -->
<!-- android:name="androidx.startup.InitializationProvider" -->
<!-- android:authorities="${applicationId}.androidx-startup" -->
<!-- tools:node="remove" /> -->

View File

@ -0,0 +1,31 @@
package com.alya.ecommerce_serang.data.api.dto
import com.google.gson.annotations.SerializedName
data class City(
@SerializedName("city_id")
val cityId: String,
@SerializedName("city_name")
val cityName: String,
@SerializedName("province_id")
val provinceId: String,
@SerializedName("province")
val provinceName: String,
@SerializedName("type")
val type: String,
@SerializedName("postal_code")
val postalCode: String
)
data class CityResponse(
@SerializedName("message")
val message: String,
@SerializedName("cities")
val data: List<City>
)

View File

@ -0,0 +1,19 @@
package com.alya.ecommerce_serang.data.api.dto
import com.google.gson.annotations.SerializedName
data class Province(
@SerializedName("province_id")
val provinceId: String,
@SerializedName("province")
val provinceName: String
)
data class ProvinceResponse(
@SerializedName("message")
val message: String,
@SerializedName("provinces")
val data: List<Province>
)

View File

@ -0,0 +1,55 @@
package com.alya.ecommerce_serang.data.api.dto
import com.google.gson.annotations.SerializedName
data class StoreAddress(
@SerializedName("id")
val id: String? = null,
@SerializedName("store_id")
val storeId: String? = null,
@SerializedName("province_id")
val provinceId: String = "",
@SerializedName("province_name")
val provinceName: String = "",
@SerializedName("city_id")
val cityId: String = "",
@SerializedName("city_name")
val cityName: String = "",
@SerializedName("street")
val street: String = "",
@SerializedName("subdistrict")
val subdistrict: String = "",
@SerializedName("detail")
val detail: String? = null,
@SerializedName("postal_code")
val postalCode: String = "",
@SerializedName("latitude")
val latitude: Double? = 0.0,
@SerializedName("longitude")
val longitude: Double? = 0.0,
@SerializedName("created_at")
val createdAt: String? = null,
@SerializedName("updated_at")
val updatedAt: String? = null
)
data class StoreAddressResponse(
@SerializedName("message")
val message: String,
@SerializedName("store")
val data: StoreAddress? = null
)

View File

@ -0,0 +1,55 @@
package com.alya.ecommerce_serang.data.api.response.store.profile
import com.google.gson.annotations.SerializedName
data class StoreDataResponse(
val message: String,
val store: Store,
val shipping: List<Shipping>,
val payment: List<Payment>
)
data class Store(
@SerializedName("store_id") val storeId: Int,
@SerializedName("store_status") val storeStatus: String,
@SerializedName("store_name") val storeName: String,
@SerializedName("user_name") val userName: String,
val email: String,
@SerializedName("user_phone") val userPhone: String,
val balance: String,
val ktp: String,
val npwp: String,
val nib: String,
val persetujuan: String?,
@SerializedName("store_image") val storeImage: String,
@SerializedName("store_description") val storeDescription: String,
@SerializedName("is_on_leave") val isOnLeave: Boolean,
@SerializedName("store_type_id") val storeTypeId: Int,
@SerializedName("store_type") val storeType: String,
val id: Int,
val latitude: String,
val longitude: String,
val street: String,
val subdistrict: String,
@SerializedName("postal_code") val postalCode: String,
val detail: String,
@SerializedName("is_store_location") val isStoreLocation: Boolean,
@SerializedName("user_id") val userId: Int,
@SerializedName("city_id") val cityId: Int,
@SerializedName("province_id") val provinceId: Int,
val phone: String?,
val recipient: String?,
@SerializedName("approval_status") val approvalStatus: String,
@SerializedName("approval_reason") val approvalReason: String?
)
data class Shipping(
val courier: String
)
data class Payment(
val id: Int,
@SerializedName("bank_num") val bankNum: String,
@SerializedName("bank_name") val bankName: String,
@SerializedName("qris_image") val qrisImage: String
)

View File

@ -0,0 +1,6 @@
package com.alya.ecommerce_serang.data.api.response.store.topup
data class BalanceTopUpResponse(
val success: Boolean,
val message: String
)

View File

@ -3,6 +3,7 @@ package com.alya.ecommerce_serang.data.api.retrofit
import com.alya.ecommerce_serang.data.api.dto.AddEvidenceRequest
import com.alya.ecommerce_serang.data.api.dto.CartItem
import com.alya.ecommerce_serang.data.api.dto.CityResponse
import com.alya.ecommerce_serang.data.api.dto.CompletedOrderRequest
import com.alya.ecommerce_serang.data.api.dto.CourierCostRequest
import com.alya.ecommerce_serang.data.api.dto.CreateAddressRequest
@ -10,8 +11,10 @@ import com.alya.ecommerce_serang.data.api.dto.LoginRequest
import com.alya.ecommerce_serang.data.api.dto.OrderRequest
import com.alya.ecommerce_serang.data.api.dto.OrderRequestBuy
import com.alya.ecommerce_serang.data.api.dto.OtpRequest
import com.alya.ecommerce_serang.data.api.dto.ProvinceResponse
import com.alya.ecommerce_serang.data.api.dto.RegisterRequest
import com.alya.ecommerce_serang.data.api.dto.SearchRequest
import com.alya.ecommerce_serang.data.api.dto.StoreAddressResponse
import com.alya.ecommerce_serang.data.api.dto.UpdateCart
import com.alya.ecommerce_serang.data.api.dto.UpdateChatRequest
import com.alya.ecommerce_serang.data.api.response.auth.LoginResponse
@ -48,6 +51,8 @@ import com.alya.ecommerce_serang.data.api.response.store.product.CreateProductRe
import com.alya.ecommerce_serang.data.api.response.store.product.DeleteProductResponse
import com.alya.ecommerce_serang.data.api.response.store.product.UpdateProductResponse
import com.alya.ecommerce_serang.data.api.response.store.product.ViewStoreProductsResponse
import com.alya.ecommerce_serang.data.api.response.store.profile.StoreDataResponse
import com.alya.ecommerce_serang.data.api.response.store.topup.BalanceTopUpResponse
import okhttp3.MultipartBody
import okhttp3.RequestBody
import retrofit2.Call
@ -147,6 +152,8 @@ interface ApiService {
@GET("mystore")
suspend fun getStore (): Response<StoreResponse>
suspend fun getStoreData(): Response<StoreDataResponse>
suspend fun getStoreAddress(): Response<StoreAddressResponse>
@GET("mystore/product") // Replace with actual endpoint
suspend fun getStoreProduct(): Response<ViewStoreProductsResponse>
@ -235,6 +242,54 @@ interface ApiService {
@Part complaintimg: MultipartBody.Part
): Response<ComplaintResponse>
@Multipart
@POST("store/createtopup")
suspend fun addBalanceTopUp(
@Part topupimg: MultipartBody.Part,
@Part("amount") amount: RequestBody,
@Part("payment_info_id") paymentInfoId: RequestBody,
@Part("transaction_date") transactionDate: RequestBody,
@Part("bank_name") bankName: RequestBody,
@Part("bank_num") bankNum: RequestBody
): Response<BalanceTopUpResponse>
@PUT("mystore/edit")
suspend fun updateStoreProfile(
@Body requestBody: okhttp3.RequestBody
): Response<StoreDataResponse>
@Multipart
@PUT("mystore/edit")
suspend fun updateStoreProfileMultipart(
@Part("store_name") storeName: RequestBody,
@Part("store_status") storeStatus: RequestBody,
@Part("store_description") storeDescription: RequestBody,
@Part("is_on_leave") isOnLeave: RequestBody,
@Part("city_id") cityId: RequestBody,
@Part("province_id") provinceId: RequestBody,
@Part("street") street: RequestBody,
@Part("subdistrict") subdistrict: RequestBody,
@Part("detail") detail: RequestBody,
@Part("postal_code") postalCode: RequestBody,
@Part("latitude") latitude: RequestBody,
@Part("longitude") longitude: RequestBody,
@Part("user_phone") userPhone: RequestBody,
@Part storeimg: MultipartBody.Part?
): Response<StoreDataResponse>
@GET("provinces")
suspend fun getProvinces(): Response<ProvinceResponse>
@GET("cities/{provinceId}")
suspend fun getCities(
@Path("provinceId") provinceId: String
): Response<CityResponse>
@PUT("mystore/edit")
suspend fun updateStoreAddress(
@Body addressData: HashMap<String, Any?>
): Response<StoreAddressResponse>
@POST("search")
suspend fun saveSearchQuery(
@Body searchRequest: SearchRequest

View File

@ -0,0 +1,174 @@
package com.alya.ecommerce_serang.data.repository
import android.util.Log
import com.alya.ecommerce_serang.data.api.dto.City
import com.alya.ecommerce_serang.data.api.dto.Province
import com.alya.ecommerce_serang.data.api.dto.StoreAddress
import com.alya.ecommerce_serang.data.api.retrofit.ApiService
import com.google.gson.Gson
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.json.JSONObject
class AddressRepository(private val apiService: ApiService) {
private val TAG = "AddressRepository"
suspend fun getProvinces(): List<Province> = withContext(Dispatchers.IO) {
Log.d(TAG, "getProvinces() called")
try {
val response = apiService.getProvinces()
Log.d(TAG, "getProvinces() response: isSuccessful=${response.isSuccessful}, code=${response.code()}")
// Log the raw response body for debugging
val rawBody = response.raw().toString()
Log.d(TAG, "Raw response: $rawBody")
if (response.isSuccessful) {
val responseBody = response.body()
Log.d(TAG, "Response body: ${Gson().toJson(responseBody)}")
val provinces = responseBody?.data ?: emptyList()
Log.d(TAG, "getProvinces() success, got ${provinces.size} provinces")
return@withContext provinces
} else {
val errorBody = response.errorBody()?.string() ?: "Unknown error"
Log.e(TAG, "getProvinces() error: $errorBody")
throw Exception("API Error (${response.code()}): $errorBody")
}
} catch (e: Exception) {
Log.e(TAG, "Exception in getProvinces()", e)
throw Exception("Network error: ${e.message}")
}
}
suspend fun getCities(provinceId: String): List<City> = withContext(Dispatchers.IO) {
Log.d(TAG, "getCities() called with provinceId: $provinceId")
try {
val response = apiService.getCities(provinceId)
Log.d(TAG, "getCities() response: isSuccessful=${response.isSuccessful}, code=${response.code()}")
if (response.isSuccessful) {
val responseBody = response.body()
Log.d(TAG, "Response body: ${Gson().toJson(responseBody)}")
val cities = responseBody?.data ?: emptyList()
Log.d(TAG, "getCities() success, got ${cities.size} cities")
return@withContext cities
} else {
val errorBody = response.errorBody()?.string() ?: "Unknown error"
Log.e(TAG, "getCities() error: $errorBody")
throw Exception("API Error (${response.code()}): $errorBody")
}
} catch (e: Exception) {
Log.e(TAG, "Exception in getCities()", e)
throw Exception("Network error: ${e.message}")
}
}
suspend fun getStoreAddress(): StoreAddress? = withContext(Dispatchers.IO) {
Log.d(TAG, "getStoreAddress() called")
try {
val response = apiService.getStoreAddress()
Log.d(TAG, "getStoreAddress() response: isSuccessful=${response.isSuccessful}, code=${response.code()}")
if (response.isSuccessful) {
val responseBody = response.body()
val rawJson = Gson().toJson(responseBody)
Log.d(TAG, "Response body: $rawJson")
val address = responseBody?.data
Log.d(TAG, "getStoreAddress() success, address: $address")
// Convert numeric strings to proper types if needed
address?.let {
// Handle city_id if it's a number
if (it.cityId.isBlank() && rawJson.contains("city_id")) {
try {
val cityId = JSONObject(rawJson).getJSONObject("store").optInt("city_id", 0)
if (cityId > 0) {
it.javaClass.getDeclaredField("cityId").apply {
isAccessible = true
set(it, cityId.toString())
}
Log.d(TAG, "Updated cityId to: ${it.cityId}")
}
} catch (e: Exception) {
Log.e(TAG, "Error parsing city_id", e)
}
}
// Handle province_id if it's a number
if (it.provinceId.isBlank() && rawJson.contains("province_id")) {
try {
val provinceId = JSONObject(rawJson).getJSONObject("store").optInt("province_id", 0)
if (provinceId > 0) {
it.javaClass.getDeclaredField("provinceId").apply {
isAccessible = true
set(it, provinceId.toString())
}
Log.d(TAG, "Updated provinceId to: ${it.provinceId}")
}
} catch (e: Exception) {
Log.e(TAG, "Error parsing province_id", e)
}
}
}
return@withContext address
} else {
val errorBody = response.errorBody()?.string() ?: "Unknown error"
Log.e(TAG, "getStoreAddress() error: $errorBody")
throw Exception("API Error (${response.code()}): $errorBody")
}
} catch (e: Exception) {
Log.e(TAG, "Exception in getStoreAddress()", e)
throw Exception("Network error: ${e.message}")
}
}
suspend fun saveStoreAddress(
provinceId: String,
provinceName: String,
cityId: String,
cityName: String,
street: String,
subdistrict: String,
detail: String,
postalCode: String,
latitude: Double,
longitude: Double
): Boolean = withContext(Dispatchers.IO) {
Log.d(TAG, "saveStoreAddress() called with provinceId: $provinceId, cityId: $cityId")
try {
val addressMap = HashMap<String, Any?>()
addressMap["provinceId"] = provinceId
addressMap["provinceName"] = provinceName
addressMap["cityId"] = cityId
addressMap["cityName"] = cityName
addressMap["street"] = street
addressMap["subdistrict"] = subdistrict
addressMap["detail"] = detail
addressMap["postalCode"] = postalCode
addressMap["latitude"] = latitude
addressMap["longitude"] = longitude
Log.d(TAG, "saveStoreAddress() request data: $addressMap")
val response = apiService.updateStoreAddress(addressMap)
Log.d(TAG, "saveStoreAddress() response: isSuccessful=${response.isSuccessful}, code=${response.code()}")
if (response.isSuccessful) {
Log.d(TAG, "saveStoreAddress() success")
return@withContext true
} else {
val errorBody = response.errorBody()?.string() ?: "Unknown error"
Log.e(TAG, "saveStoreAddress() error: $errorBody")
throw Exception("API Error (${response.code()}): $errorBody")
}
} catch (e: Exception) {
Log.e(TAG, "Exception in saveStoreAddress()", e)
throw Exception("Network error: ${e.message}")
}
}
}

View File

@ -1,21 +1,36 @@
package com.alya.ecommerce_serang.ui.profile.mystore.balance
import android.content.Intent
import android.os.Bundle
import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import com.alya.ecommerce_serang.R
import com.alya.ecommerce_serang.databinding.ActivityBalanceBinding
class BalanceActivity : AppCompatActivity() {
private lateinit var binding: ActivityBalanceBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContentView(R.layout.activity_balance)
binding = ActivityBalanceBinding.inflate(layoutInflater)
setContentView(binding.root)
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets ->
val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
insets
}
setupListeners()
}
private fun setupListeners() {
binding.btnTopUp.setOnClickListener {
val intent = Intent(this, BalanceTopUpActivity::class.java)
startActivity(intent)
}
}
}

View File

@ -1,21 +1,395 @@
package com.alya.ecommerce_serang.ui.profile.mystore.balance
import android.app.DatePickerDialog
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import androidx.activity.enableEdgeToEdge
import android.provider.MediaStore
import android.view.View
import android.widget.AdapterView
import android.widget.ArrayAdapter
import android.widget.Button
import android.widget.EditText
import android.widget.ImageView
import android.widget.Spinner
import android.widget.TextView
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.lifecycle.lifecycleScope
import com.alya.ecommerce_serang.R
import com.alya.ecommerce_serang.data.api.response.store.profile.Payment
import com.alya.ecommerce_serang.data.api.retrofit.ApiConfig
import com.alya.ecommerce_serang.utils.SessionManager
import kotlinx.coroutines.launch
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.MultipartBody
import okhttp3.RequestBody.Companion.asRequestBody
import okhttp3.RequestBody.Companion.toRequestBody
import java.io.File
import java.text.SimpleDateFormat
import java.util.Calendar
import java.util.Locale
class BalanceTopUpActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContentView(R.layout.activity_balance_top_up)
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets ->
val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
insets
private lateinit var imgPreview: ImageView
private lateinit var addPhotoTextView: TextView
private lateinit var edtNominal: EditText
private lateinit var spinnerPaymentMethod: Spinner
private lateinit var edtTransactionDate: EditText
private lateinit var datePickerIcon: ImageView
private lateinit var btnSend: Button
private lateinit var sessionManager: SessionManager
private var selectedImageUri: Uri? = null
private var paymentMethods: List<Payment> = emptyList()
private var selectedPaymentId: Int = -1
private val calendar = Calendar.getInstance()
private val getImageContent = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
if (result.resultCode == RESULT_OK) {
val imageUri = result.data?.data
imageUri?.let {
selectedImageUri = it
imgPreview.setImageURI(it)
}
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_balance_top_up)
// Initialize session manager
sessionManager = SessionManager(this)
// Initialize views
imgPreview = findViewById(R.id.img_preview)
addPhotoTextView = findViewById(R.id.tv_tambah_foto)
edtNominal = findViewById(R.id.edt_nominal_topup)
spinnerPaymentMethod = findViewById(R.id.spinner_metode_bayar)
edtTransactionDate = findViewById(R.id.edt_tgl_transaksi)
datePickerIcon = findViewById(R.id.img_date_picker)
btnSend = findViewById(R.id.btn_send)
// Setup header title
val headerTitle = findViewById<TextView>(R.id.header_title)
headerTitle.text = "Isi Ulang Saldo"
// Setup back button
val backButton = findViewById<ImageView>(R.id.header_left_icon)
backButton.setOnClickListener {
finish()
}
// Setup photo selection
addPhotoTextView.setOnClickListener {
openGallery()
}
imgPreview.setOnClickListener {
openGallery()
}
// Setup date picker
setupDatePicker()
// Fetch payment methods
fetchPaymentMethods()
// Setup submit button
btnSend.setOnClickListener {
submitForm()
}
}
private fun openGallery() {
val intent = Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI)
getImageContent.launch(intent)
}
private fun setupDatePicker() {
val dateSetListener = DatePickerDialog.OnDateSetListener { _, year, month, dayOfMonth ->
calendar.set(Calendar.YEAR, year)
calendar.set(Calendar.MONTH, month)
calendar.set(Calendar.DAY_OF_MONTH, dayOfMonth)
updateDateInView()
}
edtTransactionDate.setOnClickListener {
showDatePicker(dateSetListener)
}
datePickerIcon.setOnClickListener {
showDatePicker(dateSetListener)
}
}
private fun showDatePicker(dateSetListener: DatePickerDialog.OnDateSetListener) {
DatePickerDialog(
this,
dateSetListener,
calendar.get(Calendar.YEAR),
calendar.get(Calendar.MONTH),
calendar.get(Calendar.DAY_OF_MONTH)
).show()
}
private fun updateDateInView() {
val format = "yyyy-MM-dd"
val sdf = SimpleDateFormat(format, Locale.US)
edtTransactionDate.setText(sdf.format(calendar.time))
}
private fun fetchPaymentMethods() {
lifecycleScope.launch {
try {
val response = ApiConfig.getApiService(sessionManager).getStoreData()
if (response.isSuccessful && response.body() != null) {
val storeData = response.body()!!
paymentMethods = storeData.payment
setupPaymentMethodSpinner()
} else {
Toast.makeText(
this@BalanceTopUpActivity,
"Gagal memuat metode pembayaran",
Toast.LENGTH_SHORT
).show()
}
} catch (e: Exception) {
Toast.makeText(
this@BalanceTopUpActivity,
"Error: ${e.message}",
Toast.LENGTH_SHORT
).show()
}
}
}
private fun setupPaymentMethodSpinner() {
if (paymentMethods.isEmpty()) {
Toast.makeText(
this,
"Tidak ada metode pembayaran tersedia",
Toast.LENGTH_SHORT
).show()
return
}
// Debug payment methods
for (payment in paymentMethods) {
android.util.Log.d("BalanceTopUp", "Payment Option - ID: ${payment.id}, Bank: ${payment.bankName}, Number: ${payment.bankNum}")
}
val paymentOptions = paymentMethods.map { "${it.bankName} - ${it.bankNum}" }.toTypedArray()
val adapter = ArrayAdapter(this, android.R.layout.simple_spinner_item, paymentOptions)
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
spinnerPaymentMethod.adapter = adapter
spinnerPaymentMethod.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
selectedPaymentId = paymentMethods[position].id
android.util.Log.d("BalanceTopUp", "Selected payment ID: $selectedPaymentId")
}
override fun onNothingSelected(parent: AdapterView<*>?) {
selectedPaymentId = -1
}
}
}
private fun submitForm() {
// Prevent multiple clicks
if (!btnSend.isEnabled) {
return
}
// Validate inputs
if (selectedImageUri == null) {
Toast.makeText(this, "Mohon pilih foto bukti pembayaran", Toast.LENGTH_SHORT).show()
return
}
val nominal = edtNominal.text.toString().trim()
if (nominal.isEmpty()) {
Toast.makeText(this, "Mohon isi nominal top up", Toast.LENGTH_SHORT).show()
return
}
try {
// Validate the amount is a valid number
val amountValue = nominal.replace("[^0-9]".toRegex(), "").toLong()
if (amountValue <= 0) {
Toast.makeText(this, "Nominal harus lebih dari 0", Toast.LENGTH_SHORT).show()
return
}
} catch (e: NumberFormatException) {
Toast.makeText(this, "Format nominal tidak valid", Toast.LENGTH_SHORT).show()
return
}
if (selectedPaymentId == -1) {
Toast.makeText(this, "Mohon pilih metode pembayaran", Toast.LENGTH_SHORT).show()
return
}
val transactionDate = edtTransactionDate.text.toString().trim()
if (transactionDate.isEmpty()) {
Toast.makeText(this, "Mohon pilih tanggal transaksi", Toast.LENGTH_SHORT).show()
return
}
// Show progress indicator
btnSend.text = "Mengirim..."
btnSend.isEnabled = false
// Proceed with the API call
uploadTopUpData(nominal, selectedPaymentId.toString(), transactionDate)
}
private fun uploadTopUpData(amount: String, paymentInfoId: String, transactionDate: String) {
lifecycleScope.launch {
try {
// Log the values being sent
android.util.Log.d("BalanceTopUp", "Amount: $amount")
android.util.Log.d("BalanceTopUp", "Payment ID: $paymentInfoId")
android.util.Log.d("BalanceTopUp", "Transaction Date: $transactionDate")
// Find the selected payment method to get bank name
val selectedPayment = paymentMethods.find { it.id.toString() == paymentInfoId }
if (selectedPayment == null) {
Toast.makeText(
this@BalanceTopUpActivity,
"Metode pembayaran tidak valid",
Toast.LENGTH_SHORT
).show()
return@launch
}
val bankName = selectedPayment.bankName
val bankNum = selectedPayment.bankNum
android.util.Log.d("BalanceTopUp", "Bank Name: $bankName")
android.util.Log.d("BalanceTopUp", "Bank Number: $bankNum")
// Get the actual file from URI
val file = uriToFile(selectedImageUri!!)
android.util.Log.d("BalanceTopUp", "File size: ${file.length()} bytes")
android.util.Log.d("BalanceTopUp", "File name: ${file.name}")
// Create multipart file with specific JPEG content type
val requestFile = file.asRequestBody("image/jpeg".toMediaTypeOrNull())
val imagePart = MultipartBody.Part.createFormData("topupimg", file.name, requestFile)
// Create other request bodies - ensure proper formatting
// Make sure amount has no commas, spaces or currency symbols
val cleanedAmount = amount.replace("[^0-9]".toRegex(), "")
val amountBody = cleanedAmount.toRequestBody("text/plain".toMediaTypeOrNull())
val paymentInfoIdBody = paymentInfoId.toRequestBody("text/plain".toMediaTypeOrNull())
val transactionDateBody = transactionDate.toRequestBody("text/plain".toMediaTypeOrNull())
val bankNameBody = bankName.toRequestBody("text/plain".toMediaTypeOrNull())
val bankNumBody = bankNum.toRequestBody("text/plain".toMediaTypeOrNull())
// Make the API call
val response = ApiConfig.getApiService(sessionManager).addBalanceTopUp(
imagePart,
amountBody,
paymentInfoIdBody,
transactionDateBody,
bankNameBody,
bankNumBody
)
if (response.isSuccessful) {
// Log the complete response
val responseBody = response.body()
android.util.Log.d("BalanceTopUp", "Success response: ${responseBody?.message}")
// Show the actual message from backend
val successMessage = "Top Up Berhasil"
Toast.makeText(
this@BalanceTopUpActivity,
successMessage,
Toast.LENGTH_LONG
).show()
// Show a dialog with the success message
runOnUiThread {
androidx.appcompat.app.AlertDialog.Builder(this@BalanceTopUpActivity)
.setTitle("Berhasil")
.setMessage(successMessage)
.setPositiveButton("OK") { dialog, _ ->
dialog.dismiss()
finish()
}
.show()
}
} else {
// Get more detailed error information
val errorBody = response.errorBody()?.string()
android.util.Log.e("BalanceTopUp", "Error body: $errorBody")
android.util.Log.e("BalanceTopUp", "Error code: ${response.code()}")
// Try to parse the error body to extract the message
var errorMessage = "Gagal mengirim permintaan: ${response.message() ?: "Error ${response.code()}"}"
try {
val jsonObject = org.json.JSONObject(errorBody ?: "{}")
if (jsonObject.has("message")) {
errorMessage = jsonObject.getString("message")
}
} catch (e: Exception) {
android.util.Log.e("BalanceTopUp", "Error parsing error body", e)
}
Toast.makeText(
this@BalanceTopUpActivity,
errorMessage,
Toast.LENGTH_LONG
).show()
// Show a dialog with the error message
runOnUiThread {
androidx.appcompat.app.AlertDialog.Builder(this@BalanceTopUpActivity)
.setTitle("Error Response")
.setMessage(errorMessage)
.setPositiveButton("OK") { dialog, _ ->
dialog.dismiss()
}
.show()
}
}
} catch (e: Exception) {
android.util.Log.e("BalanceTopUp", "Exception: ${e.message}", e)
Toast.makeText(
this@BalanceTopUpActivity,
"Error: ${e.message}",
Toast.LENGTH_SHORT
).show()
} finally {
// Reset button state
btnSend.text = "Kirim"
btnSend.isEnabled = true
}
}
}
private fun uriToFile(uri: Uri): File {
val inputStream = contentResolver.openInputStream(uri)
val tempFile = File.createTempFile("upload", ".jpg", cacheDir)
tempFile.deleteOnExit()
inputStream?.use { input ->
tempFile.outputStream().use { output ->
input.copyTo(output)
}
}
// Validate file isn't empty
if (tempFile.length() == 0L) {
throw IllegalStateException("File is empty")
}
return tempFile
}
}

View File

@ -1,15 +1,20 @@
package com.alya.ecommerce_serang.ui.profile.mystore.profile
import android.app.Activity
import android.content.Intent
import android.os.Bundle
import android.util.Log
import android.widget.Toast
import androidx.activity.enableEdgeToEdge
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import com.alya.ecommerce_serang.R
import com.alya.ecommerce_serang.data.api.dto.Store
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.MyStoreRepository
import com.alya.ecommerce_serang.databinding.ActivityDetailStoreProfileBinding
import com.alya.ecommerce_serang.ui.profile.mystore.profile.address.DetailStoreAddressActivity
import com.alya.ecommerce_serang.utils.viewmodel.MyStoreViewModel
import com.alya.ecommerce_serang.utils.BaseViewModelFactory
import com.alya.ecommerce_serang.utils.SessionManager
@ -39,6 +44,24 @@ class DetailStoreProfileActivity : AppCompatActivity() {
enableEdgeToEdge()
// Set up header title
binding.header.headerTitle.text = "Profil Toko"
// Set up back button
binding.header.headerLeftIcon.setOnClickListener {
onBackPressedDispatcher.onBackPressed()
}
binding.btnEditStoreProfile.setOnClickListener {
val intent = Intent(this, EditStoreProfileActivity::class.java)
startActivityForResult(intent, EDIT_PROFILE_REQUEST_CODE)
}
binding.layoutAddress.setOnClickListener {
val intent = Intent(this, DetailStoreAddressActivity::class.java)
startActivity(intent)
}
viewModel.loadMyStore()
viewModel.myStoreProfile.observe(this){ user ->
@ -50,11 +73,48 @@ class DetailStoreProfileActivity : AppCompatActivity() {
}
}
private fun updateStoreProfile(store: Store){
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (requestCode == EDIT_PROFILE_REQUEST_CODE && resultCode == Activity.RESULT_OK) {
// Refresh the profile data
Toast.makeText(this, "Profil toko berhasil diperbarui", Toast.LENGTH_SHORT).show()
viewModel.loadMyStore()
// Pass the result back to parent activity
setResult(Activity.RESULT_OK)
} else if (requestCode == ADDRESS_REQUEST_CODE && resultCode == Activity.RESULT_OK) {
// Refresh the profile data after address update
Toast.makeText(this, "Alamat toko berhasil diperbarui", Toast.LENGTH_SHORT).show()
viewModel.loadMyStore()
// Pass the result back to parent activity
setResult(Activity.RESULT_OK)
}
}
companion object {
private const val EDIT_PROFILE_REQUEST_CODE = 100
private const val ADDRESS_REQUEST_CODE = 101
}
private fun updateStoreProfile(store: Store){
// Update text fields
binding.edtNamaToko.setText(store.storeName.toString())
binding.edtJenisToko.setText(store.storeType.toString())
binding.edtDeskripsiToko.setText(store.storeDescription.toString())
// Update store image if available
if (store.storeImage != null && store.storeImage.toString().isNotEmpty() && store.storeImage.toString() != "null") {
val imageUrl = "http:/192.168.100.156:3000${store.storeImage}"
Log.d("DetailStoreProfile", "Loading image from: $imageUrl")
Glide.with(this)
.load(imageUrl)
.placeholder(R.drawable.placeholder_image)
.error(R.drawable.placeholder_image)
.into(binding.ivProfile)
} else {
Log.d("DetailStoreProfile", "No store image available")
}
}
}

View File

@ -0,0 +1,304 @@
package com.alya.ecommerce_serang.ui.profile.mystore.profile
import android.app.Activity
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.provider.MediaStore
import android.util.Log
import android.view.View
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import com.alya.ecommerce_serang.R
import com.alya.ecommerce_serang.data.api.dto.Store
import com.alya.ecommerce_serang.data.api.retrofit.ApiConfig
import com.alya.ecommerce_serang.databinding.ActivityEditStoreProfileBinding
import com.alya.ecommerce_serang.utils.SessionManager
import com.bumptech.glide.Glide
import kotlinx.coroutines.launch
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.MultipartBody
import okhttp3.RequestBody.Companion.asRequestBody
import okhttp3.RequestBody.Companion.toRequestBody
import java.io.File
import java.io.FileOutputStream
class EditStoreProfileActivity : AppCompatActivity() {
private lateinit var binding: ActivityEditStoreProfileBinding
private lateinit var sessionManager: SessionManager
private var storeImageUri: Uri? = null
private lateinit var currentStore: Store
private val pickImage = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
if (result.resultCode == Activity.RESULT_OK) {
result.data?.data?.let { uri ->
storeImageUri = uri
Log.d("EditStoreProfile", "Image selected: $uri")
// Set the image to the ImageView for immediate preview
try {
binding.ivStoreImage.setImageURI(null) // Clear any previous image
binding.ivStoreImage.setImageURI(uri)
// Alternative way using Glide for more reliable preview
Glide.with(this)
.load(uri)
.placeholder(R.drawable.placeholder_image)
.error(R.drawable.placeholder_image)
.into(binding.ivStoreImage)
Toast.makeText(this, "Gambar berhasil dipilih", Toast.LENGTH_SHORT).show()
} catch (e: Exception) {
Log.e("EditStoreProfile", "Error displaying image preview", e)
Toast.makeText(this, "Gagal menampilkan preview gambar", Toast.LENGTH_SHORT).show()
}
}
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityEditStoreProfileBinding.inflate(layoutInflater)
setContentView(binding.root)
sessionManager = SessionManager(this)
// Set up header
binding.header.headerTitle.text = "Edit Profil Toko"
binding.header.headerLeftIcon.setOnClickListener { finish() }
loadStoreData()
setupClickListeners()
}
private fun loadStoreData() {
binding.progressBar.visibility = View.VISIBLE
lifecycleScope.launch {
try {
val response = ApiConfig.getApiService(sessionManager).getStore()
binding.progressBar.visibility = View.GONE
if (response.isSuccessful && response.body() != null) {
currentStore = response.body()!!.store
populateFields(currentStore)
} else {
showError("Gagal memuat profil toko")
}
} catch (e: Exception) {
binding.progressBar.visibility = View.GONE
showError("Terjadi kesalahan: ${e.message}")
}
}
}
private fun populateFields(store: Store) {
// Load store image
if (store.storeImage != null && store.storeImage.toString().isNotEmpty() && store.storeImage.toString() != "null") {
Glide.with(this)
.load(store.storeImage.toString())
.placeholder(R.drawable.placeholder_image)
.error(R.drawable.placeholder_image)
.into(binding.ivStoreImage)
}
// Set other fields
binding.edtStoreName.setText(store.storeName)
binding.edtDescription.setText(store.storeDescription)
binding.edtUserPhone.setText(store.userPhone)
// Set is on leave
binding.switchIsOnLeave.isChecked = store.isOnLeave
}
private fun setupClickListeners() {
binding.btnSelectStoreImage.setOnClickListener {
val intent = Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI)
pickImage.launch(intent)
}
binding.btnSave.setOnClickListener {
saveStoreProfile()
}
}
private fun saveStoreProfile() {
val storeName = binding.edtStoreName.text.toString()
val storeDescription = binding.edtDescription.text.toString()
val userPhone = binding.edtUserPhone.text.toString()
val storeStatus = currentStore.storeStatus // Keep the current status
val isOnLeave = binding.switchIsOnLeave.isChecked
if (storeName.isEmpty() || userPhone.isEmpty()) {
showError("Nama toko dan nomor telepon harus diisi")
return
}
binding.progressBar.visibility = View.VISIBLE
binding.btnSave.isEnabled = false
// Show progress indicator on the image if we're uploading one
if (storeImageUri != null) {
binding.progressImage.visibility = View.VISIBLE
}
lifecycleScope.launch {
try {
Log.d("EditStoreProfile", "Starting profile update process")
// Create multipart request for image if selected
var storeImagePart: MultipartBody.Part? = null
if (storeImageUri != null) {
try {
val storeImageFile = uriToFile(storeImageUri!!)
Log.d("EditStoreProfile", "Image file created: ${storeImageFile.name}, size: ${storeImageFile.length()}")
// Get the MIME type
val mimeType = contentResolver.getType(storeImageUri!!) ?: "image/jpeg"
Log.d("EditStoreProfile", "MIME type: $mimeType")
val storeImageRequestBody = storeImageFile.asRequestBody(mimeType.toMediaTypeOrNull())
storeImagePart = MultipartBody.Part.createFormData("storeimg", storeImageFile.name, storeImageRequestBody)
Log.d("EditStoreProfile", "Image part created with name: storeimg, filename: ${storeImageFile.name}")
} catch (e: Exception) {
Log.e("EditStoreProfile", "Error creating image part", e)
runOnUiThread {
Toast.makeText(this@EditStoreProfileActivity, "Error preparing image: ${e.message}", Toast.LENGTH_SHORT).show()
}
}
}
// Create text parts
val nameRequestBody = storeName.toRequestBody("text/plain".toMediaTypeOrNull())
val descriptionRequestBody = storeDescription.toRequestBody("text/plain".toMediaTypeOrNull())
val userPhoneRequestBody = userPhone.toRequestBody("text/plain".toMediaTypeOrNull())
val statusRequestBody = storeStatus.toRequestBody("text/plain".toMediaTypeOrNull())
val onLeaveRequestBody = isOnLeave.toString().toRequestBody("text/plain".toMediaTypeOrNull())
// Log request parameters
Log.d("EditStoreProfile", "Request parameters: " +
"\nstore_name: $storeName" +
"\nstore_status: $storeStatus" +
"\nstore_description: $storeDescription" +
"\nis_on_leave: $isOnLeave" +
"\nuser_phone: $userPhone" +
"\nimage: ${storeImageUri != null}")
// Log all parts for debugging
Log.d("EditStoreProfile", "Request parts:" +
"\nstoreName: $nameRequestBody" +
"\nstoreStatus: $statusRequestBody" +
"\nstoreDescription: $descriptionRequestBody" +
"\nisOnLeave: $onLeaveRequestBody" +
"\nuserPhone: $userPhoneRequestBody" +
"\nstoreimg: ${storeImagePart != null}")
val response = ApiConfig.getApiService(sessionManager).updateStoreProfileMultipart(
storeName = nameRequestBody,
storeStatus = statusRequestBody,
storeDescription = descriptionRequestBody,
isOnLeave = onLeaveRequestBody,
cityId = currentStore.cityId.toString().toRequestBody("text/plain".toMediaTypeOrNull()),
provinceId = currentStore.provinceId.toString().toRequestBody("text/plain".toMediaTypeOrNull()),
street = currentStore.street.toRequestBody("text/plain".toMediaTypeOrNull()),
subdistrict = currentStore.subdistrict.toRequestBody("text/plain".toMediaTypeOrNull()),
detail = currentStore.detail.toRequestBody("text/plain".toMediaTypeOrNull()),
postalCode = currentStore.postalCode.toRequestBody("text/plain".toMediaTypeOrNull()),
latitude = currentStore.latitude.toRequestBody("text/plain".toMediaTypeOrNull()),
longitude = currentStore.longitude.toRequestBody("text/plain".toMediaTypeOrNull()),
userPhone = userPhoneRequestBody,
storeimg = storeImagePart
)
Log.d("EditStoreProfile", "Response received: isSuccessful=${response.isSuccessful}, code=${response.code()}")
runOnUiThread {
binding.progressBar.visibility = View.GONE
binding.progressImage.visibility = View.GONE
binding.btnSave.isEnabled = true
if (response.isSuccessful) {
Log.d("EditStoreProfile", "Response body: ${response.body()?.toString()}")
// Try to log the updated store image URL
response.body()?.let { responseBody ->
val updatedStoreImage = responseBody.store?.storeImage
Log.d("EditStoreProfile", "Updated store image URL: $updatedStoreImage")
}
showSuccess("Profil toko berhasil diperbarui")
setResult(Activity.RESULT_OK)
finish()
} else {
val errorBodyString = response.errorBody()?.string() ?: "Error body is null"
Log.e("EditStoreProfile", "Full error response: $errorBodyString")
Log.e("EditStoreProfile", "Response headers: ${response.headers()}")
showError("Gagal memperbarui profil toko (${response.code()})")
}
}
} catch (e: Exception) {
Log.e("EditStoreProfile", "Exception during API call", e)
runOnUiThread {
binding.progressBar.visibility = View.GONE
binding.progressImage.visibility = View.GONE
binding.btnSave.isEnabled = true
showError("Error: ${e.message}")
}
}
}
}
private fun uriToFile(uri: Uri): File {
val contentResolver = applicationContext.contentResolver
val fileExtension = getFileExtension(contentResolver, uri)
val timeStamp = System.currentTimeMillis()
val fileName = "IMG_${timeStamp}.$fileExtension"
val tempFile = File(cacheDir, fileName)
Log.d("EditStoreProfile", "Creating temp file: ${tempFile.absolutePath}")
try {
contentResolver.openInputStream(uri)?.use { inputStream ->
FileOutputStream(tempFile).use { outputStream ->
val buffer = ByteArray(4 * 1024) // 4k buffer
var bytesRead: Int
while (inputStream.read(buffer).also { bytesRead = it } != -1) {
outputStream.write(buffer, 0, bytesRead)
}
outputStream.flush()
}
}
Log.d("EditStoreProfile", "File created successfully: ${tempFile.name}, size: ${tempFile.length()} bytes")
return tempFile
} catch (e: Exception) {
Log.e("EditStoreProfile", "Error creating file from URI", e)
throw e
}
}
private fun getFileExtension(contentResolver: android.content.ContentResolver, uri: Uri): String {
val mimeType = contentResolver.getType(uri)
return if (mimeType != null) {
val mime = android.webkit.MimeTypeMap.getSingleton()
mime.getExtensionFromMimeType(mimeType) ?: "jpg"
} else {
// If mime type is null, try to get from URI path
val path = uri.path
if (path != null) {
val extension = android.webkit.MimeTypeMap.getFileExtensionFromUrl(path)
if (!extension.isNullOrEmpty()) {
extension
} else "jpg"
} else "jpg"
}
}
private fun showSuccess(message: String) {
Toast.makeText(this, message, Toast.LENGTH_LONG).show()
}
private fun showError(message: String) {
Toast.makeText(this, message, Toast.LENGTH_LONG).show()
}
}

View File

@ -1,21 +1,312 @@
package com.alya.ecommerce_serang.ui.profile.mystore.profile.address
import android.app.Activity
import android.app.AlertDialog
import android.os.Bundle
import androidx.activity.enableEdgeToEdge
import android.util.Log
import android.view.View
import android.widget.AdapterView
import android.widget.ArrayAdapter
import android.widget.Toast
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import com.alya.ecommerce_serang.R
import com.alya.ecommerce_serang.BuildConfig
import com.alya.ecommerce_serang.data.api.dto.City
import com.alya.ecommerce_serang.data.api.dto.Province
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.AddressRepository
import com.alya.ecommerce_serang.databinding.ActivityDetailStoreAddressBinding
import com.alya.ecommerce_serang.utils.viewmodel.AddressViewModel
import com.alya.ecommerce_serang.utils.BaseViewModelFactory
import com.alya.ecommerce_serang.utils.SessionManager
import com.google.android.material.snackbar.Snackbar
class DetailStoreAddressActivity : AppCompatActivity() {
private lateinit var binding: ActivityDetailStoreAddressBinding
private lateinit var apiService: ApiService
private lateinit var sessionManager: SessionManager
private var selectedProvinceId: String? = null
private var provinces: List<Province> = emptyList()
private var cities: List<City> = emptyList()
private val TAG = "StoreAddressActivity"
private val viewModel: AddressViewModel by viewModels {
BaseViewModelFactory {
val apiService = ApiConfig.getApiService(sessionManager)
val addressRepository = AddressRepository(apiService)
AddressViewModel(addressRepository)
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContentView(R.layout.activity_detail_store_address)
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets ->
val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
insets
binding = ActivityDetailStoreAddressBinding.inflate(layoutInflater)
setContentView(binding.root)
sessionManager = SessionManager(this)
apiService = ApiConfig.getApiService(sessionManager)
// Log the base URL
Log.d(TAG, "BASE_URL: ${BuildConfig.BASE_URL}")
// Add error text view
binding.tvError.visibility = View.GONE
// Set up header title
binding.header.headerTitle.text = "Alamat Toko"
// Set up back button
binding.header.headerLeftIcon.setOnClickListener {
onBackPressedDispatcher.onBackPressed()
}
setupSpinners()
setupObservers()
setupSaveButton()
// Add retry button
binding.btnRetry.setOnClickListener {
binding.tvError.visibility = View.GONE
binding.progressBar.visibility = View.VISIBLE
Log.d(TAG, "Retrying to fetch provinces...")
viewModel.fetchProvinces()
}
// Show loading spinners initially
showProvinceLoading(true)
// Load existing address data first
Log.d(TAG, "Fetching store address...")
viewModel.fetchStoreAddress()
// Load provinces data
Log.d(TAG, "Fetching provinces...")
viewModel.fetchProvinces()
}
private fun setupSpinners() {
// Province spinner listener
binding.spinnerProvince.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
Log.d(TAG, "Province selected at position: $position")
if (position > 0 && provinces.isNotEmpty()) {
selectedProvinceId = provinces[position - 1].provinceId
Log.d(TAG, "Selected province ID: $selectedProvinceId")
selectedProvinceId?.let {
Log.d(TAG, "Fetching cities for province ID: $it")
showCityLoading(true)
viewModel.fetchCities(it)
}
}
}
override fun onNothingSelected(p0: AdapterView<*>?) {
// Do nothing
}
}
}
private fun setupObservers() {
// Observe provinces data
viewModel.provinces.observe(this) { provinceList ->
Log.d(TAG, "Received provinces: ${provinceList.size}")
showProvinceLoading(false)
if (provinceList.isEmpty()) {
showError("No provinces available")
return@observe
}
provinces = provinceList
val provinceNames = mutableListOf("Pilih Provinsi")
provinceNames.addAll(provinceList.map { it.provinceName })
Log.d(TAG, "Province names: $provinceNames")
val adapter = ArrayAdapter(
this,
android.R.layout.simple_spinner_item,
provinceNames
)
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
binding.spinnerProvince.adapter = adapter
}
// Observe cities data
viewModel.cities.observe(this) { cityList ->
Log.d(TAG, "Received cities: ${cityList.size}")
showCityLoading(false)
cities = cityList
val cityNames = mutableListOf("Pilih Kota/Kabupaten")
cityNames.addAll(cityList.map { it.cityName })
Log.d(TAG, "City names: $cityNames")
val adapter = ArrayAdapter(
this,
android.R.layout.simple_spinner_item,
cityNames
)
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
binding.spinnerCity.adapter = adapter
// If we have a stored city_id, select it
viewModel.storeAddress.value?.let { address ->
if (address.cityId.isNotEmpty()) {
val cityIndex = cities.indexOfFirst { city ->
city.cityId == address.cityId
}
Log.d(TAG, "City index for ID ${address.cityId}: $cityIndex")
if (cityIndex != -1) {
binding.spinnerCity.setSelection(cityIndex + 1) // +1 because of "Pilih Kota/Kabupaten"
}
}
}
}
// Observe store address data
viewModel.storeAddress.observe(this) { address ->
Log.d(TAG, "Received store address: $address")
address?.let {
// Set the fields
binding.edtStreet.setText(address.street)
binding.edtSubdistrict.setText(address.subdistrict)
binding.edtDetailAddress.setText(address.detail ?: "")
binding.edtPostalCode.setText(address.postalCode)
// Handle latitude and longitude
val lat = if (address.latitude == null || address.latitude.toString() == "NaN") 0.0 else address.latitude
val lng = if (address.longitude == null || address.longitude.toString() == "NaN") 0.0 else address.longitude
// Set selected province ID to trigger city loading
if (address.provinceId.isNotEmpty()) {
selectedProvinceId = address.provinceId
// Find province index and select it after provinces are loaded
if (provinces.isNotEmpty()) {
val provinceIndex = provinces.indexOfFirst { province ->
province.provinceId == address.provinceId
}
Log.d(TAG, "Province index for ID ${address.provinceId}: $provinceIndex")
if (provinceIndex != -1) {
binding.spinnerProvince.setSelection(provinceIndex + 1) // +1 because of "Pilih Provinsi"
// Now fetch cities for this province
showCityLoading(true)
viewModel.fetchCities(address.provinceId)
}
}
}
}
}
// Observe loading state
viewModel.isLoading.observe(this) { isLoading ->
binding.progressBar.visibility = if (isLoading) View.VISIBLE else View.GONE
}
// Observe error messages
viewModel.errorMessage.observe(this) { errorMsg ->
Log.e(TAG, "Error: $errorMsg")
showError(errorMsg)
}
// Observe save success
viewModel.saveSuccess.observe(this) { success ->
if (success) {
Toast.makeText(this, "Alamat berhasil disimpan", Toast.LENGTH_SHORT).show()
setResult(Activity.RESULT_OK)
finish()
}
}
}
private fun showProvinceLoading(isLoading: Boolean) {
binding.provinceProgressBar.visibility = if (isLoading) View.VISIBLE else View.GONE
binding.spinnerProvince.visibility = if (isLoading) View.GONE else View.VISIBLE
}
private fun showCityLoading(isLoading: Boolean) {
binding.cityProgressBar.visibility = if (isLoading) View.VISIBLE else View.GONE
binding.spinnerCity.visibility = if (isLoading) View.GONE else View.VISIBLE
}
private fun showError(message: String) {
binding.progressBar.visibility = View.GONE
binding.tvError.visibility = View.VISIBLE
binding.tvError.text = "Error: $message\nURL: ${BuildConfig.BASE_URL}/provinces"
binding.btnRetry.visibility = View.VISIBLE
// Also show in a dialog for immediate attention
AlertDialog.Builder(this)
.setTitle("Error")
.setMessage("$message\n\nAPI URL: ${BuildConfig.BASE_URL}/provinces")
.setPositiveButton("Retry") { _, _ ->
binding.tvError.visibility = View.GONE
binding.progressBar.visibility = View.VISIBLE
viewModel.fetchProvinces()
}
.setNegativeButton("Cancel") { dialog, _ ->
dialog.dismiss()
}
.show()
// Also show a snackbar
Snackbar.make(binding.root, "Error: $message", Snackbar.LENGTH_LONG)
.setAction("Retry") {
binding.tvError.visibility = View.GONE
binding.progressBar.visibility = View.VISIBLE
viewModel.fetchProvinces()
}
.show()
}
private fun setupSaveButton() {
binding.btnSaveAddress.setOnClickListener {
val street = binding.edtStreet.text.toString()
val subdistrict = binding.edtSubdistrict.text.toString()
val detail = binding.edtDetailAddress.text.toString()
val postalCode = binding.edtPostalCode.text.toString()
val latitudeStr = TODO()
val longitudeStr = TODO()
// Validate required fields
if (selectedProvinceId == null || binding.spinnerCity.selectedItemPosition <= 0 ||
street.isEmpty() || subdistrict.isEmpty() || postalCode.isEmpty()) {
Toast.makeText(this, "Mohon lengkapi data yang wajib diisi", Toast.LENGTH_SHORT).show()
return@setOnClickListener
}
// Get selected city
val cityPosition = binding.spinnerCity.selectedItemPosition
if (cityPosition <= 0 || cities.isEmpty() || cityPosition > cities.size) {
Toast.makeText(this, "Mohon pilih kota/kabupaten", Toast.LENGTH_SHORT).show()
return@setOnClickListener
}
val selectedCity = cities[cityPosition - 1]
// Parse coordinates
val latitude = latitudeStr.toDoubleOrNull() ?: 0.0
val longitude = longitudeStr.toDoubleOrNull() ?: 0.0
// Save address
viewModel.saveStoreAddress(
provinceId = selectedProvinceId!!,
provinceName = provinces.find { it.provinceId == selectedProvinceId }?.provinceName ?: "",
cityId = selectedCity.cityId,
cityName = selectedCity.cityName,
street = street,
subdistrict = subdistrict,
detail = detail,
postalCode = postalCode,
latitude = latitude,
longitude = longitude
)
}
}
}

View File

@ -0,0 +1,129 @@
package com.alya.ecommerce_serang.utils.viewmodel
import android.util.Log
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.alya.ecommerce_serang.data.api.dto.City
import com.alya.ecommerce_serang.data.api.dto.Province
import com.alya.ecommerce_serang.data.api.dto.StoreAddress
import com.alya.ecommerce_serang.data.repository.AddressRepository
import kotlinx.coroutines.launch
class AddressViewModel(private val addressRepository: AddressRepository) : ViewModel() {
private val TAG = "AddressViewModel"
private val _provinces = MutableLiveData<List<Province>>()
val provinces: LiveData<List<Province>> = _provinces
private val _cities = MutableLiveData<List<City>>()
val cities: LiveData<List<City>> = _cities
private val _storeAddress = MutableLiveData<StoreAddress?>()
val storeAddress: LiveData<StoreAddress?> = _storeAddress
private val _isLoading = MutableLiveData<Boolean>()
val isLoading: LiveData<Boolean> = _isLoading
private val _errorMessage = MutableLiveData<String>()
val errorMessage: LiveData<String> = _errorMessage
private val _saveSuccess = MutableLiveData<Boolean>()
val saveSuccess: LiveData<Boolean> = _saveSuccess
fun fetchProvinces() {
Log.d(TAG, "fetchProvinces() called")
_isLoading.value = true
viewModelScope.launch {
try {
Log.d(TAG, "Calling addressRepository.getProvinces()")
val response = addressRepository.getProvinces()
Log.d(TAG, "Received provinces response: ${response.size} provinces")
_provinces.value = response
_isLoading.value = false
} catch (e: Exception) {
Log.e(TAG, "Error fetching provinces", e)
_errorMessage.value = "Failed to load provinces: ${e.message}"
_isLoading.value = false
}
}
}
fun fetchCities(provinceId: String) {
Log.d(TAG, "fetchCities() called with provinceId: $provinceId")
_isLoading.value = true
viewModelScope.launch {
try {
Log.d(TAG, "Calling addressRepository.getCities()")
val response = addressRepository.getCities(provinceId)
Log.d(TAG, "Received cities response: ${response.size} cities")
_cities.value = response
_isLoading.value = false
} catch (e: Exception) {
Log.e(TAG, "Error fetching cities", e)
_errorMessage.value = "Failed to load cities: ${e.message}"
_isLoading.value = false
}
}
}
fun fetchStoreAddress() {
Log.d(TAG, "fetchStoreAddress() called")
_isLoading.value = true
viewModelScope.launch {
try {
Log.d(TAG, "Calling addressRepository.getStoreAddress()")
val response = addressRepository.getStoreAddress()
Log.d(TAG, "Received store address response: $response")
_storeAddress.value = response
_isLoading.value = false
} catch (e: Exception) {
Log.e(TAG, "Error fetching store address", e)
_errorMessage.value = "Failed to load store address: ${e.message}"
_isLoading.value = false
}
}
}
fun saveStoreAddress(
provinceId: String,
provinceName: String,
cityId: String,
cityName: String,
street: String,
subdistrict: String,
detail: String,
postalCode: String,
latitude: Double,
longitude: Double
) {
Log.d(TAG, "saveStoreAddress() called with provinceId: $provinceId, cityId: $cityId")
_isLoading.value = true
viewModelScope.launch {
try {
Log.d(TAG, "Calling addressRepository.saveStoreAddress()")
val success = addressRepository.saveStoreAddress(
provinceId = provinceId,
provinceName = provinceName,
cityId = cityId,
cityName = cityName,
street = street,
subdistrict = subdistrict,
detail = detail,
postalCode = postalCode,
latitude = latitude,
longitude = longitude
)
Log.d(TAG, "Save store address result: $success")
_saveSuccess.value = success
_isLoading.value = false
} catch (e: Exception) {
Log.e(TAG, "Error saving store address", e)
_errorMessage.value = "Failed to save address: ${e.message}"
_isLoading.value = false
}
}
}
}

View File

@ -322,7 +322,8 @@
<Button
android:id="@+id/btn_send"
android:text="Kirim"
style="@style/button.large.disabled.long"/>
style="@style/button.large.disabled.long"
android:layout_marginBottom="16dp"/>
</LinearLayout>

View File

@ -26,7 +26,33 @@
android:paddingHorizontal="@dimen/horizontal_safe_area"
android:layout_marginTop="19dp">
<!-- Nama Lokasi -->
<!-- Error display -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:id="@+id/tv_error"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="@color/red_required"
style="@style/body_medium"
android:padding="12dp"
android:background="#FFEEEE"
android:visibility="gone"
android:text="Error message goes here" />
<Button
android:id="@+id/btn_retry"
style="@style/button.small.active.short"
android:text="Retry"
android:layout_gravity="center"
android:layout_marginTop="8dp"
android:visibility="gone"/>
</LinearLayout>
<!-- Provinsi -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
@ -36,20 +62,95 @@
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Nama Lokasi"
android:text="Provinsi"
style="@style/body_medium"
android:layout_marginEnd="4dp"/>
<EditText
android:id="@+id/edt_nama_lokasi"
<!-- Spinner Dropdown dengan Chevron -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/bg_text_field_disabled"
android:text="Alamat Toko"
android:padding="8dp"
style="@style/body_small"
android:layout_marginTop="10dp"
android:enabled="false"/>
android:orientation="horizontal"
android:background="@drawable/bg_text_field"
android:gravity="center_vertical"
android:layout_marginTop="10dp">
<Spinner
android:id="@+id/spinner_province"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:padding="8dp"
style="@style/body_small"
android:background="@null"/>
<ProgressBar
android:id="@+id/province_progress_bar"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_gravity="center"
android:visibility="gone"/>
<!-- Chevron Down Icon -->
<ImageView
android:layout_width="16dp"
android:layout_height="16dp"
android:src="@drawable/ic_down"
android:layout_marginEnd="8dp"
android:contentDescription="Chevron Down" />
</LinearLayout>
</LinearLayout>
<!-- Kota/Kabupaten -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_marginBottom="24dp">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Kota atau Kabupaten"
style="@style/body_medium"
android:layout_marginEnd="4dp"/>
<!-- Spinner Dropdown dengan Chevron -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:background="@drawable/bg_text_field"
android:gravity="center_vertical"
android:layout_marginTop="10dp">
<Spinner
android:id="@+id/spinner_city"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:padding="8dp"
style="@style/body_small"
android:background="@null"/>
<ProgressBar
android:id="@+id/city_progress_bar"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_gravity="center"
android:visibility="gone"/>
<!-- Chevron Down Icon -->
<ImageView
android:layout_width="16dp"
android:layout_height="16dp"
android:src="@drawable/ic_down"
android:layout_marginEnd="8dp"
android:contentDescription="Chevron Down" />
</LinearLayout>
</LinearLayout>
@ -103,94 +204,6 @@
</LinearLayout>
<!-- Kota/Kabupaten -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_marginBottom="24dp">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Kota atau Kabupaten"
style="@style/body_medium"
android:layout_marginEnd="4dp"/>
<!-- Spinner Dropdown dengan Chevron -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:background="@drawable/bg_text_field"
android:gravity="center_vertical"
android:layout_marginTop="10dp">
<Spinner
android:id="@+id/spinner_city"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:padding="8dp"
style="@style/body_small"
android:background="@null"/>
<!-- Chevron Down Icon -->
<ImageView
android:layout_width="16dp"
android:layout_height="16dp"
android:src="@drawable/ic_down"
android:layout_marginEnd="8dp"
android:contentDescription="Chevron Down" />
</LinearLayout>
</LinearLayout>
<!-- Provinsi -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_marginBottom="24dp">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Provinsi"
style="@style/body_medium"
android:layout_marginEnd="4dp"/>
<!-- Spinner Dropdown dengan Chevron -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:background="@drawable/bg_text_field"
android:gravity="center_vertical"
android:layout_marginTop="10dp">
<Spinner
android:id="@+id/spinner_province"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:padding="8dp"
style="@style/body_small"
android:background="@null"/>
<!-- Chevron Down Icon -->
<ImageView
android:layout_width="16dp"
android:layout_height="16dp"
android:src="@drawable/ic_down"
android:layout_marginEnd="8dp"
android:contentDescription="Chevron Down" />
</LinearLayout>
</LinearLayout>
<!-- Kode Pos -->
<LinearLayout
android:layout_width="match_parent"
@ -276,7 +289,15 @@
android:id="@+id/btn_save_address"
android:text="Simpan Perubahan"
style="@style/button.large.disabled.long"
android:enabled="false"/>
android:enabled="false"
android:layout_marginBottom="16dp"/>
<ProgressBar
android:id="@+id/progress_bar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:visibility="gone"/>
</LinearLayout>

View File

@ -847,7 +847,8 @@
android:id="@+id/btn_save_product"
android:text="Simpan Produk"
style="@style/button.large.disabled.long"
android:enabled="false"/>
android:enabled="false"
android:layout_marginBottom="16dp"/>
</LinearLayout>

View File

@ -8,7 +8,9 @@
android:orientation="vertical"
tools:context=".ui.profile.mystore.profile.DetailStoreProfileActivity">
<include layout="@layout/header" />
<include
android:id="@+id/header"
layout="@layout/header" />
<ScrollView
android:layout_width="match_parent"

View File

@ -0,0 +1,183 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".ui.profile.mystore.profile.EditStoreProfileActivity">
<include
android:id="@+id/header"
layout="@layout/header" />
<ScrollView
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingVertical="@dimen/vertical_safe_area"
android:paddingHorizontal="@dimen/horizontal_safe_area">
<!-- Store Image -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_marginBottom="16dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Foto Toko"
style="@style/body_medium"
android:layout_marginBottom="8dp"/>
<ImageView
android:id="@+id/iv_store_image"
android:layout_width="120dp"
android:layout_height="120dp"
android:src="@drawable/placeholder_image"
android:scaleType="centerCrop"
android:layout_gravity="center_horizontal"
android:background="@drawable/bg_text_field"/>
<ProgressBar
android:id="@+id/progress_image"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_gravity="center"
android:visibility="gone"
android:layout_marginTop="-80dp"
android:layout_marginBottom="40dp"/>
<Button
android:id="@+id/btn_select_store_image"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Pilih Foto"
android:layout_gravity="center_horizontal"
android:layout_marginTop="8dp"/>
</LinearLayout>
<!-- Store Name -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_marginBottom="16dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Nama Toko *"
style="@style/body_medium"
android:layout_marginBottom="4dp"/>
<EditText
android:id="@+id/edt_store_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/bg_text_field"
android:padding="12dp"
android:hint="Masukkan nama toko"
android:inputType="text"
style="@style/body_small"/>
</LinearLayout>
<!-- Store Description -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_marginBottom="16dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Deskripsi Toko"
style="@style/body_medium"
android:layout_marginBottom="4dp"/>
<EditText
android:id="@+id/edt_description"
android:layout_width="match_parent"
android:layout_height="120dp"
android:background="@drawable/bg_text_field"
android:padding="12dp"
android:hint="Masukkan deskripsi toko"
android:inputType="textMultiLine"
android:gravity="top"
style="@style/body_small"/>
</LinearLayout>
<!-- Is On Leave -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginBottom="16dp">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Toko Sedang Cuti"
style="@style/body_medium"/>
<Switch
android:id="@+id/switch_is_on_leave"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
</LinearLayout>
<!-- User Phone -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_marginBottom="16dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Nomor Telepon *"
style="@style/body_medium"
android:layout_marginBottom="4dp"/>
<EditText
android:id="@+id/edt_user_phone"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/bg_text_field"
android:padding="12dp"
android:hint="Masukkan nomor telepon"
android:inputType="phone"
style="@style/body_small"/>
</LinearLayout>
<!-- Save Button -->
<Button
android:id="@+id/btn_save"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Simpan"
android:layout_marginTop="24dp"
android:layout_marginBottom="16dp"/>
<!-- Progress Bar -->
<ProgressBar
android:id="@+id/progress_bar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:visibility="gone"/>
</LinearLayout>
</ScrollView>
</LinearLayout>

View File

@ -1,5 +1,5 @@
[versions]
agp = "8.5.2"
agp = "8.10.0"
glide = "4.16.0"
hiltAndroid = "2.48" # Updated from 2.44 for better compatibility
hiltLifecycleViewmodel = "1.0.0-alpha03"