diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 20052e6..fa26332 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -29,6 +29,9 @@ android:theme="@style/Theme.Ecommerce_serang" android:usesCleartextTraffic="true" tools:targetApi="31"> + @@ -40,8 +43,7 @@ android:exported="false" /> - + android:exported="false" /> diff --git a/app/src/main/java/com/alya/ecommerce_serang/data/api/dto/City.kt b/app/src/main/java/com/alya/ecommerce_serang/data/api/dto/City.kt new file mode 100644 index 0000000..f8160a2 --- /dev/null +++ b/app/src/main/java/com/alya/ecommerce_serang/data/api/dto/City.kt @@ -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 +) \ No newline at end of file diff --git a/app/src/main/java/com/alya/ecommerce_serang/data/api/dto/Province.kt b/app/src/main/java/com/alya/ecommerce_serang/data/api/dto/Province.kt new file mode 100644 index 0000000..056d517 --- /dev/null +++ b/app/src/main/java/com/alya/ecommerce_serang/data/api/dto/Province.kt @@ -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 +) \ No newline at end of file diff --git a/app/src/main/java/com/alya/ecommerce_serang/data/api/dto/StoreAddress.kt b/app/src/main/java/com/alya/ecommerce_serang/data/api/dto/StoreAddress.kt new file mode 100644 index 0000000..f403fc2 --- /dev/null +++ b/app/src/main/java/com/alya/ecommerce_serang/data/api/dto/StoreAddress.kt @@ -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 +) \ No newline at end of file diff --git a/app/src/main/java/com/alya/ecommerce_serang/data/api/response/store/profile/StoreDataResponse.kt b/app/src/main/java/com/alya/ecommerce_serang/data/api/response/store/profile/StoreDataResponse.kt new file mode 100644 index 0000000..711e8be --- /dev/null +++ b/app/src/main/java/com/alya/ecommerce_serang/data/api/response/store/profile/StoreDataResponse.kt @@ -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, + val payment: List +) + +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 +) diff --git a/app/src/main/java/com/alya/ecommerce_serang/data/api/response/store/topup/BalanceTopUpResponse.kt b/app/src/main/java/com/alya/ecommerce_serang/data/api/response/store/topup/BalanceTopUpResponse.kt new file mode 100644 index 0000000..a6965ce --- /dev/null +++ b/app/src/main/java/com/alya/ecommerce_serang/data/api/response/store/topup/BalanceTopUpResponse.kt @@ -0,0 +1,6 @@ +package com.alya.ecommerce_serang.data.api.response.store.topup + +data class BalanceTopUpResponse( + val success: Boolean, + val message: String +) diff --git a/app/src/main/java/com/alya/ecommerce_serang/data/api/retrofit/ApiService.kt b/app/src/main/java/com/alya/ecommerce_serang/data/api/retrofit/ApiService.kt index 62a4ab0..b86b350 100644 --- a/app/src/main/java/com/alya/ecommerce_serang/data/api/retrofit/ApiService.kt +++ b/app/src/main/java/com/alya/ecommerce_serang/data/api/retrofit/ApiService.kt @@ -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 + suspend fun getStoreData(): Response + suspend fun getStoreAddress(): Response @GET("mystore/product") // Replace with actual endpoint suspend fun getStoreProduct(): Response @@ -235,6 +242,54 @@ interface ApiService { @Part complaintimg: MultipartBody.Part ): Response + @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 + + @PUT("mystore/edit") + suspend fun updateStoreProfile( + @Body requestBody: okhttp3.RequestBody + ): Response + + @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 + + @GET("provinces") + suspend fun getProvinces(): Response + + @GET("cities/{provinceId}") + suspend fun getCities( + @Path("provinceId") provinceId: String + ): Response + + @PUT("mystore/edit") + suspend fun updateStoreAddress( + @Body addressData: HashMap + ): Response + @POST("search") suspend fun saveSearchQuery( @Body searchRequest: SearchRequest diff --git a/app/src/main/java/com/alya/ecommerce_serang/data/repository/AddressRepository.kt b/app/src/main/java/com/alya/ecommerce_serang/data/repository/AddressRepository.kt new file mode 100644 index 0000000..8659ef7 --- /dev/null +++ b/app/src/main/java/com/alya/ecommerce_serang/data/repository/AddressRepository.kt @@ -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 = 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 = 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() + 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}") + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/profile/mystore/balance/BalanceActivity.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/profile/mystore/balance/BalanceActivity.kt index 4192943..24e9618 100644 --- a/app/src/main/java/com/alya/ecommerce_serang/ui/profile/mystore/balance/BalanceActivity.kt +++ b/app/src/main/java/com/alya/ecommerce_serang/ui/profile/mystore/balance/BalanceActivity.kt @@ -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) + } } } \ No newline at end of file diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/profile/mystore/balance/BalanceTopUpActivity.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/profile/mystore/balance/BalanceTopUpActivity.kt index 12c437a..5ef8b31 100644 --- a/app/src/main/java/com/alya/ecommerce_serang/ui/profile/mystore/balance/BalanceTopUpActivity.kt +++ b/app/src/main/java/com/alya/ecommerce_serang/ui/profile/mystore/balance/BalanceTopUpActivity.kt @@ -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 = 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(R.id.header_title) + headerTitle.text = "Isi Ulang Saldo" + + // Setup back button + val backButton = findViewById(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 + } } \ No newline at end of file diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/profile/mystore/profile/DetailStoreProfileActivity.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/profile/mystore/profile/DetailStoreProfileActivity.kt index 8e098d3..3cfb95f 100644 --- a/app/src/main/java/com/alya/ecommerce_serang/ui/profile/mystore/profile/DetailStoreProfileActivity.kt +++ b/app/src/main/java/com/alya/ecommerce_serang/ui/profile/mystore/profile/DetailStoreProfileActivity.kt @@ -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") + } } } \ No newline at end of file diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/profile/mystore/profile/EditStoreProfileActivity.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/profile/mystore/profile/EditStoreProfileActivity.kt new file mode 100644 index 0000000..2d31f3b --- /dev/null +++ b/app/src/main/java/com/alya/ecommerce_serang/ui/profile/mystore/profile/EditStoreProfileActivity.kt @@ -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() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/profile/mystore/profile/address/DetailStoreAddressActivity.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/profile/mystore/profile/address/DetailStoreAddressActivity.kt index 94f258c..897e6c8 100644 --- a/app/src/main/java/com/alya/ecommerce_serang/ui/profile/mystore/profile/address/DetailStoreAddressActivity.kt +++ b/app/src/main/java/com/alya/ecommerce_serang/ui/profile/mystore/profile/address/DetailStoreAddressActivity.kt @@ -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 = emptyList() + private var cities: List = 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 + ) } } } \ No newline at end of file diff --git a/app/src/main/java/com/alya/ecommerce_serang/utils/viewmodel/AddressViewModel.kt b/app/src/main/java/com/alya/ecommerce_serang/utils/viewmodel/AddressViewModel.kt new file mode 100644 index 0000000..9f3d94a --- /dev/null +++ b/app/src/main/java/com/alya/ecommerce_serang/utils/viewmodel/AddressViewModel.kt @@ -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>() + val provinces: LiveData> = _provinces + + private val _cities = MutableLiveData>() + val cities: LiveData> = _cities + + private val _storeAddress = MutableLiveData() + val storeAddress: LiveData = _storeAddress + + private val _isLoading = MutableLiveData() + val isLoading: LiveData = _isLoading + + private val _errorMessage = MutableLiveData() + val errorMessage: LiveData = _errorMessage + + private val _saveSuccess = MutableLiveData() + val saveSuccess: LiveData = _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 + } + } + } +} \ No newline at end of file diff --git a/app/src/main/res/layout/activity_balance_top_up.xml b/app/src/main/res/layout/activity_balance_top_up.xml index 913dc1a..133e8f4 100644 --- a/app/src/main/res/layout/activity_balance_top_up.xml +++ b/app/src/main/res/layout/activity_balance_top_up.xml @@ -322,7 +322,8 @@