fix register store and product

This commit is contained in:
Gracia Hotmauli
2025-08-26 03:26:58 +07:00
parent cef4bfa2b2
commit 9273e01324
8 changed files with 717 additions and 404 deletions

View File

@ -0,0 +1,21 @@
package com.alya.ecommerce_serang.data.api.dto
import com.google.gson.annotations.SerializedName
import java.io.File
data class PaymentUpdate(
@field:SerializedName("id")
val id: Int? = null,
@field:SerializedName("bank_name")
val bankName: String,
@field:SerializedName("bank_num")
val bankNum: String,
@field:SerializedName("account_name")
val accountName: String,
@field:SerializedName("qris_image")
val qrisImage: File? = null
)

View File

@ -1,7 +1,6 @@
package com.alya.ecommerce_serang.data.api.retrofit 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.AddPaymentInfoResponse import com.alya.ecommerce_serang.data.api.dto.AddPaymentInfoResponse
import com.alya.ecommerce_serang.data.api.dto.CancelOrderReq import com.alya.ecommerce_serang.data.api.dto.CancelOrderReq
import com.alya.ecommerce_serang.data.api.dto.CartItem import com.alya.ecommerce_serang.data.api.dto.CartItem
@ -17,7 +16,6 @@ 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.OrderRequest
import com.alya.ecommerce_serang.data.api.dto.OrderRequestBuy 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.OtpRequest
import com.alya.ecommerce_serang.data.api.dto.PaymentConfirmRequest
import com.alya.ecommerce_serang.data.api.dto.ProvinceResponse 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.RegisterRequest
import com.alya.ecommerce_serang.data.api.dto.ResetPassReq import com.alya.ecommerce_serang.data.api.dto.ResetPassReq
@ -29,7 +27,6 @@ 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.dto.UpdateChatRequest
import com.alya.ecommerce_serang.data.api.dto.VerifRegisReq import com.alya.ecommerce_serang.data.api.dto.VerifRegisReq
import com.alya.ecommerce_serang.data.api.response.auth.ChangePassResponse import com.alya.ecommerce_serang.data.api.response.auth.ChangePassResponse
import com.alya.ecommerce_serang.data.api.response.auth.CheckStoreResponse
import com.alya.ecommerce_serang.data.api.response.auth.FcmTokenResponse import com.alya.ecommerce_serang.data.api.response.auth.FcmTokenResponse
import com.alya.ecommerce_serang.data.api.response.auth.HasStoreResponse import com.alya.ecommerce_serang.data.api.response.auth.HasStoreResponse
import com.alya.ecommerce_serang.data.api.response.auth.ListNotifResponse import com.alya.ecommerce_serang.data.api.response.auth.ListNotifResponse
@ -83,12 +80,10 @@ import com.alya.ecommerce_serang.data.api.response.store.product.UpdateProductRe
import com.alya.ecommerce_serang.data.api.response.store.product.ViewStoreProductsResponse 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.profile.StoreDataResponse
import com.alya.ecommerce_serang.data.api.response.store.review.ProductReviewResponse import com.alya.ecommerce_serang.data.api.response.store.review.ProductReviewResponse
import com.alya.ecommerce_serang.data.api.response.store.sells.PaymentConfirmationResponse
import com.alya.ecommerce_serang.data.api.response.store.topup.BalanceTopUpResponse import com.alya.ecommerce_serang.data.api.response.store.topup.BalanceTopUpResponse
import com.alya.ecommerce_serang.data.api.response.store.topup.TopUpResponse import com.alya.ecommerce_serang.data.api.response.store.topup.TopUpResponse
import okhttp3.MultipartBody import okhttp3.MultipartBody
import okhttp3.RequestBody import okhttp3.RequestBody
import retrofit2.Call
import retrofit2.Response import retrofit2.Response
import retrofit2.http.Body import retrofit2.http.Body
import retrofit2.http.DELETE import retrofit2.http.DELETE
@ -99,7 +94,6 @@ import retrofit2.http.PUT
import retrofit2.http.Part import retrofit2.http.Part
import retrofit2.http.PartMap import retrofit2.http.PartMap
import retrofit2.http.Path import retrofit2.http.Path
import retrofit2.http.Query
interface ApiService { interface ApiService {
@POST("registeruser") @POST("registeruser")
@ -112,9 +106,6 @@ interface ApiService {
@Body verifRegisReq: VerifRegisReq @Body verifRegisReq: VerifRegisReq
):VerifRegisterResponse ):VerifRegisterResponse
@GET("checkstore")
suspend fun checkStore (): Response<CheckStoreResponse>
@Multipart @Multipart
@POST("registerstore") @POST("registerstore")
suspend fun registerStore( suspend fun registerStore(
@ -204,11 +195,6 @@ interface ApiService {
@Path("id") orderId: Int @Path("id") orderId: Int
): Response<OrderDetailResponse> ): Response<OrderDetailResponse>
@POST("order/addevidence")
suspend fun addEvidence(
@Body request : AddEvidenceRequest,
): Response<AddEvidenceResponse>
@Multipart @Multipart
@POST("order/addevidence") @POST("order/addevidence")
suspend fun addEvidenceMultipart( suspend fun addEvidenceMultipart(
@ -256,15 +242,9 @@ interface ApiService {
@GET("mystore") @GET("mystore")
suspend fun getMyStoreData(): Response<com.alya.ecommerce_serang.data.api.response.store.StoreResponse> suspend fun getMyStoreData(): Response<com.alya.ecommerce_serang.data.api.response.store.StoreResponse>
@GET("mystore")
suspend fun getStoreAddress(): Response<StoreAddressResponse>
@GET("mystore/product") // Replace with actual endpoint @GET("mystore/product") // Replace with actual endpoint
suspend fun getStoreProduct(): Response<ViewStoreProductsResponse> suspend fun getStoreProduct(): Response<ViewStoreProductsResponse>
@GET("category")
fun getCategories(): Call<CategoryResponse>
@Multipart @Multipart
@POST("store/createproduct") @POST("store/createproduct")
suspend fun addProduct( suspend fun addProduct(
@ -374,9 +354,6 @@ interface ApiService {
@GET("store/topup") @GET("store/topup")
suspend fun getTopUpHistory(): Response<TopUpResponse> suspend fun getTopUpHistory(): Response<TopUpResponse>
@GET("store/topup")
suspend fun getFilteredTopUpHistory(@Query("date") date: String): Response<TopUpResponse>
@Multipart @Multipart
@POST("store/createtopup") @POST("store/createtopup")
suspend fun addBalanceTopUp( suspend fun addBalanceTopUp(
@ -388,11 +365,6 @@ interface ApiService {
@Part("bank_num") bankNum: RequestBody @Part("bank_num") bankNum: RequestBody
): Response<BalanceTopUpResponse> ): Response<BalanceTopUpResponse>
@PUT("store/payment/update")
suspend fun paymentConfirmation(
@Body confirmPaymentReq : PaymentConfirmRequest
): Response<PaymentConfirmationResponse>
@Multipart @Multipart
@PUT("mystore/edit") @PUT("mystore/edit")
suspend fun updateStoreProfileMultipart( suspend fun updateStoreProfileMultipart(
@ -404,13 +376,26 @@ interface ApiService {
): Response<StoreDataResponse> ): Response<StoreDataResponse>
@Multipart @Multipart
@POST("mystore/payment/add") @PUT("mystore/edit")
suspend fun addPaymentInfo( suspend fun updateStoreApprovalMultipart(
@Part("bank_name") bankName: RequestBody, @Part("store_name") storeName: RequestBody,
@Part("bank_num") bankNum: RequestBody, @Part("store_description") storeDescription: RequestBody,
@Part("account_name") accountName: RequestBody, @Part("store_type_id") storeTypeId: RequestBody,
@Part qris: MultipartBody.Part? @Part("latitude") storeLatitude: RequestBody,
): Response<GenericResponse> @Part("longitude") storeLongitude: RequestBody,
@Part("province_id") storeProvince: RequestBody,
@Part("city_id") storeCity: RequestBody,
@Part("subdistrict") storeSubdistrict: RequestBody,
@Part("village_id") storeVillage: RequestBody,
@Part("street") storeStreet: RequestBody,
@Part("postal_code") storePostalCode: RequestBody,
@Part("detail") storeAddressDetail: RequestBody,
@Part("user_phone") storeUserPhone: RequestBody,
@Part storeimg: MultipartBody.Part?,
@Part ktp: MultipartBody.Part?,
@Part npwp: MultipartBody.Part?,
@Part nib: MultipartBody.Part?
): Response<StoreDataResponse>
@Multipart @Multipart
@POST("mystore/payment/add") @POST("mystore/payment/add")
@ -421,6 +406,16 @@ interface ApiService {
@Part qris: MultipartBody.Part? @Part qris: MultipartBody.Part?
): Response<AddPaymentInfoResponse> ): Response<AddPaymentInfoResponse>
@Multipart
@PUT("mystore/payment/edit")
suspend fun updatePaymentInfo(
@Part("payment_info_id") paymentInfoId: RequestBody,
@Part("account_name") accountName: RequestBody,
@Part("bank_name") bankName: RequestBody,
@Part("bank_num") bankNum: RequestBody,
@Part qris: MultipartBody.Part? = null
): Response<GenericResponse>
@DELETE("mystore/payment/delete/{id}") @DELETE("mystore/payment/delete/{id}")
suspend fun deletePaymentInfo( suspend fun deletePaymentInfo(
@Path("id") paymentMethodId: Int @Path("id") paymentMethodId: Int
@ -464,15 +459,6 @@ interface ApiService {
@GET("search") @GET("search")
suspend fun getSearchHistory(): Response<SearchHistoryResponse> suspend fun getSearchHistory(): Response<SearchHistoryResponse>
@Multipart
@POST("sendchat")
suspend fun sendChatLine(
@Part("store_id") storeId: RequestBody,
@Part("message") message: RequestBody,
@Part("product_id") productId: RequestBody?,
@Part chatimg: MultipartBody.Part?
): Response<SendChatResponse>
@Multipart @Multipart
@POST("store/sendchat") @POST("store/sendchat")
suspend fun sendChatMessageStore( suspend fun sendChatMessageStore(

View File

@ -1,17 +1,23 @@
package com.alya.ecommerce_serang.data.repository package com.alya.ecommerce_serang.data.repository
import android.util.Log import android.util.Log
import com.alya.ecommerce_serang.data.api.dto.PaymentUpdate
import com.alya.ecommerce_serang.data.api.dto.ProductsItem import com.alya.ecommerce_serang.data.api.dto.ProductsItem
import com.alya.ecommerce_serang.data.api.dto.ShippingServiceRequest
import com.alya.ecommerce_serang.data.api.dto.Store import com.alya.ecommerce_serang.data.api.dto.Store
import com.alya.ecommerce_serang.data.api.response.auth.ListStoreTypeResponse import com.alya.ecommerce_serang.data.api.response.auth.ListStoreTypeResponse
import com.alya.ecommerce_serang.data.api.response.store.StoreResponse import com.alya.ecommerce_serang.data.api.response.store.StoreResponse
import com.alya.ecommerce_serang.data.api.response.store.profile.StoreDataResponse import com.alya.ecommerce_serang.data.api.response.store.profile.StoreDataResponse
import com.alya.ecommerce_serang.data.api.response.store.sells.OrderListResponse import com.alya.ecommerce_serang.data.api.response.store.sells.OrderListResponse
import com.alya.ecommerce_serang.data.api.retrofit.ApiService import com.alya.ecommerce_serang.data.api.retrofit.ApiService
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.MultipartBody import okhttp3.MultipartBody
import okhttp3.RequestBody import okhttp3.RequestBody
import okhttp3.RequestBody.Companion.asRequestBody
import okhttp3.RequestBody.Companion.toRequestBody
import retrofit2.HttpException import retrofit2.HttpException
import retrofit2.Response import retrofit2.Response
import java.io.File
import java.io.IOException import java.io.IOException
class MyStoreRepository(private val apiService: ApiService) { class MyStoreRepository(private val apiService: ApiService) {
@ -139,6 +145,186 @@ class MyStoreRepository(private val apiService: ApiService) {
} }
} }
suspend fun updateStoreApproval(
storeName: RequestBody,
description: RequestBody,
storeType: RequestBody,
latitude: RequestBody,
longitude: RequestBody,
storeProvince: RequestBody,
storeCity: RequestBody,
storeSubdistrict: RequestBody,
storeVillage: RequestBody,
storeStreet: RequestBody,
storePostalCode: RequestBody,
storeAddressDetail: RequestBody,
userPhone: RequestBody,
paymentsToUpdate: List<PaymentUpdate> = emptyList(),
paymentIdToDelete: List<Int> = emptyList(),
storeCourier: List<String>? = null,
storeImage: MultipartBody.Part?,
ktpImage: MultipartBody.Part?,
npwpDocument: MultipartBody.Part?,
nibDocument: MultipartBody.Part?
): Response<StoreDataResponse>? {
return try {
Log.d(TAG, "Updating store profile & address for approval...")
val profileResp = apiService.updateStoreApprovalMultipart(
storeName = storeName,
storeDescription = description,
storeTypeId = storeType,
storeLatitude = latitude,
storeLongitude = longitude,
storeProvince = storeProvince,
storeCity = storeCity,
storeSubdistrict = storeSubdistrict,
storeVillage = storeVillage,
storeStreet = storeStreet,
storePostalCode = storePostalCode,
storeAddressDetail = storeAddressDetail,
storeUserPhone = userPhone,
storeimg = storeImage,
ktp = ktpImage,
npwp = npwpDocument,
nib = nibDocument
)
if (!profileResp.isSuccessful) {
Log.e(TAG, "Profile update failed: ${profileResp.code()} ${profileResp.errorBody()?.string()}")
return profileResp // short-circuit; let caller inspect the failure
}
// 2) Payments: delete, then upsert (safer if youre changing accounts)
if (paymentIdToDelete.isNotEmpty() || paymentsToUpdate.isNotEmpty()) {
Log.d(TAG, "Synchronizing payments: delete=${paymentIdToDelete.size}, upsert=${paymentsToUpdate.size}")
}
// 2a) Delete payments
paymentIdToDelete.forEach { id ->
runCatching {
apiService.deletePaymentInfo(id)
}.onSuccess {
if (!it.isSuccessful) {
Log.e(TAG, "Delete payment $id failed: ${it.code()} ${it.errorBody()?.string()}")
} else {
Log.d(TAG, "Deleted payment $id")
}
}.onFailure { e ->
Log.e(TAG, "Delete payment $id exception", e)
}
}
// 2b) Upsert payments (add if id==null, else update)
paymentsToUpdate.forEach { item ->
runCatching {
// --- CHANGE HERE if your PaymentUpdate field names differ ---
val id = item.id // Int? (null => add)
val bankName = item.bankName // String
val bankNum = item.bankNum // String
val accountName = item.accountName // String
val qrisImage = item.qrisImage // File? (Optional)
// -----------------------------------------------------------
if (id == null) {
// ADD
val resp = apiService.addPaymentInfoDirect(
bankName = bankName.toPlain(),
bankNum = bankNum.toPlain(),
accountName = accountName.toPlain(),
qris = createQrisPartOrNull(qrisImage)
)
if (!resp.isSuccessful) {
Log.e(TAG, "Add payment failed: ${resp.code()} ${resp.errorBody()?.string()}")
} else {
Log.d(TAG, "Added payment: $bankName/$bankNum")
}
} else {
// UPDATE
val resp = apiService.updatePaymentInfo(
paymentInfoId = id.toString().toPlain(),
accountName = accountName.toPlain(),
bankName = bankName.toPlain(),
bankNum = bankNum.toPlain(),
qris = createQrisPartOrNull(qrisImage)
)
if (!resp.isSuccessful) {
Log.e(TAG, "Update payment $id failed: ${resp.code()} ${resp.errorBody()?.string()}")
} else {
Log.d(TAG, "Updated payment $id: $bankName/$bankNum")
}
}
}.onFailure { e ->
Log.e(TAG, "Upsert payment exception", e)
}
}
// 3) Shipping: sync to desiredCouriers (if provided)
storeCourier?.let { desired ->
try {
val current = apiService.getStoreData().let { resp ->
if (resp.isSuccessful) {
resp.body()?.shipping?.mapNotNull { it.courier } ?: emptyList()
} else {
Log.e(TAG, "Failed to read current shipping: ${resp.code()} ${resp.errorBody()?.string()}")
emptyList()
}
}
val desiredSet = desired.toSet()
val currentSet = current.toSet()
val toAdd = (desiredSet - currentSet).toList()
val toDel = (currentSet - desiredSet).toList()
if (toAdd.isNotEmpty()) {
val addResp = apiService.addShippingService(ShippingServiceRequest(couriers = toAdd))
if (!addResp.isSuccessful) {
Log.e(TAG, "Add couriers failed: ${addResp.code()} ${addResp.errorBody()?.string()}")
} else {
Log.d(TAG, "Added couriers: $toAdd")
}
}
if (toDel.isNotEmpty()) {
val delResp = apiService.deleteShippingService(ShippingServiceRequest(couriers = toDel))
if (!delResp.isSuccessful) {
Log.e(TAG, "Delete couriers failed: ${delResp.code()} ${delResp.errorBody()?.string()}")
} else {
Log.d(TAG, "Deleted couriers: $toDel")
}
}
} catch (e: Exception) {
Log.e(TAG, "Sync shipping exception", e)
}
}
// Return the profile response (already successful here)
profileResp
} catch (e: Exception) {
Log.e(TAG, "Error updating store approval flow", e)
null
}
}
private fun String.toPlain(): RequestBody =
this.toRequestBody("text/plain".toMediaTypeOrNull())
private fun createQrisPartOrNull(file: File?): MultipartBody.Part? =
file?.let {
val mime = when (it.extension.lowercase()) {
"jpg", "jpeg" -> "image/jpeg"
"png" -> "image/png"
else -> "application/octet-stream"
}.toMediaTypeOrNull()
MultipartBody.Part.createFormData(
"qris",
it.name,
it.asRequestBody(mime)
)
}
companion object { companion object {
private var TAG = "MyStoreRepository" private var TAG = "MyStoreRepository"
} }

View File

@ -47,6 +47,7 @@ import okhttp3.RequestBody
import okhttp3.RequestBody.Companion.toRequestBody import okhttp3.RequestBody.Companion.toRequestBody
import java.io.File import java.io.File
import androidx.core.net.toUri import androidx.core.net.toUri
import com.alya.ecommerce_serang.data.api.dto.PaymentUpdate
class RegisterStoreActivity : AppCompatActivity() { class RegisterStoreActivity : AppCompatActivity() {
@ -58,6 +59,21 @@ class RegisterStoreActivity : AppCompatActivity() {
private lateinit var subdistrictAdapter: SubdsitrictAdapter private lateinit var subdistrictAdapter: SubdsitrictAdapter
private lateinit var bankAdapter: BankAdapter private lateinit var bankAdapter: BankAdapter
// pending values (filled from myStoreProfile once)
private var wantedProvinceId: Int? = null
private var wantedCityId: String? = null
private var wantedSubdistrictId: String? = null
private var wantedBankName: String? = null
// one-shot guards so we don't re-apply repeatedly
private var provinceApplied = false
private var cityApplied = false
private var subdistrictApplied = false
private var bankApplied = false
// avoid clearing/overriding while restoring
private var isRestoringSelections = false
// Request codes for file picking // Request codes for file picking
private val PICK_STORE_IMAGE_REQUEST = 1001 private val PICK_STORE_IMAGE_REQUEST = 1001
private val PICK_KTP_REQUEST = 1002 private val PICK_KTP_REQUEST = 1002
@ -123,6 +139,10 @@ class RegisterStoreActivity : AppCompatActivity() {
setupSpinners() // Location spinners setupSpinners() // Location spinners
Log.d(TAG, "onCreate: Spinners setup completed") Log.d(TAG, "onCreate: Spinners setup completed")
binding.checkboxApprove.setOnCheckedChangeListener { _, _ ->
validateRequiredFields()
}
// Setup observers // Setup observers
setupStoreTypesObserver() // Store type observer setupStoreTypesObserver() // Store type observer
setupObservers() setupObservers()
@ -209,12 +229,27 @@ class RegisterStoreActivity : AppCompatActivity() {
// Prefill spinner for store types // Prefill spinner for store types
preselectStoreType(store.storeTypeId) preselectStoreType(store.storeTypeId)
// Prefill province, city, and subdistrict // Cache what we want to select later (after data arrives)
preselectProvinceCitySubdistrict( wantedProvinceId = store.provinceId
provinceId = store.provinceId, wantedCityId = store.cityId
cityId = store.cityId, wantedSubdistrictId = store.subdistrict
subdistrictId = store.subdistrict wantedBankName = storeResponse.payment.firstOrNull()?.bankName
)
// Cache what we want to select later (after data arrives)
wantedProvinceId = store.provinceId
wantedCityId = store.cityId
wantedSubdistrictId = store.subdistrict
wantedBankName = storeResponse.payment.firstOrNull()?.bankName
// Mark restoring flow on
isRestoringSelections = true
// Try to apply immediately (if adapters already have data), otherwise
// observers below will apply when data is ready.
tryApplyProvince()
tryApplyCity()
tryApplySubdistrict()
tryApplyBank()
validateRequiredFields() validateRequiredFields()
} }
@ -229,64 +264,78 @@ class RegisterStoreActivity : AppCompatActivity() {
else Toast.makeText(this, "Harap lengkapi semua field yang wajib diisi", Toast.LENGTH_SHORT).show() else Toast.makeText(this, "Harap lengkapi semua field yang wajib diisi", Toast.LENGTH_SHORT).show()
} }
} }
validateRequiredFields()
} }
private fun preselectStoreType(storeTypeId: Int) { private fun preselectStoreType(storeTypeId: Int) {
// The adapter is created in setupStoreTypeSpinner(...) // The adapter is created in setupStoreTypeSpinner(...)
val adapter = binding.spinnerStoreType.adapter val adapter = binding.spinnerStoreType.adapter ?: return
if (adapter != null) { for (i in 0 until adapter.count) {
val count = adapter.count val item = adapter.getItem(i) as? StoreTypesItem
for (i in 0 until count) { if (item?.id == storeTypeId) {
val item = adapter.getItem(i) as? StoreTypesItem binding.spinnerStoreType.setSelection(i, false)
if (item?.id == storeTypeId) { viewModel.storeTypeId.value = storeTypeId
binding.spinnerStoreType.setSelection(i, false) validateRequiredFields()
break break
}
} }
} }
} }
private fun preselectProvinceCitySubdistrict( private fun tryApplyProvince() {
provinceId: Int, if (provinceApplied) return
cityId: String, val target = wantedProvinceId ?: return
subdistrictId: String val count = provinceAdapter.count
) { for (i in 0 until count) {
// Province first (this will trigger cities fetch) if (provinceAdapter.getProvinceId(i) == target) {
val provCount = provinceAdapter.count
for (i in 0 until provCount) {
if (provinceAdapter.getProvinceId(i) == provinceId) {
binding.spinnerProvince.setSelection(i, false) binding.spinnerProvince.setSelection(i, false)
break provinceApplied = true
maybeFinishRestoring()
return
} }
} }
}
// When cities arrive, select the city, then load subdistricts private fun tryApplyCity() {
viewModel.citiesState.observe(this) { state -> if (cityApplied) return
if (state is Result.Success) { val target = wantedCityId ?: return
val cityCount = cityAdapter.count val count = cityAdapter.count
for (i in 0 until cityCount) { for (i in 0 until count) {
if (cityAdapter.getCityId(i) == cityId) { if (cityAdapter.getCityId(i) == target) {
binding.spinnerCity.setSelection(i, false) binding.spinnerCity.setSelection(i, false)
break cityApplied = true
} maybeFinishRestoring()
} return
} }
} }
}
// When subdistricts arrive, select the subdistrict private fun tryApplySubdistrict() {
viewModel.subdistrictState.observe(this) { state -> if (subdistrictApplied) return
if (state is Result.Success) { val target = wantedSubdistrictId ?: return
val subCount = subdistrictAdapter.count val count = subdistrictAdapter.count
for (i in 0 until subCount) { for (i in 0 until count) {
if (subdistrictAdapter.getSubdistrictId(i) == subdistrictId) { if (subdistrictAdapter.getSubdistrictId(i) == target) {
binding.spinnerSubdistrict.setSelection(i, false) binding.spinnerSubdistrict.setSelection(i, false)
break subdistrictApplied = true
} maybeFinishRestoring()
} return
} }
} }
} }
private fun tryApplyBank() {
if (bankApplied) return
val targetName = wantedBankName ?: return
val pos = bankAdapter.findPositionByName(targetName)
if (pos >= 0) {
binding.spinnerBankName.setSelection(pos, false)
viewModel.bankName.value = targetName
viewModel.selectedBankName = targetName
validateRequiredFields()
bankApplied = true
}
}
private fun setupHeader() { private fun setupHeader() {
binding.header.main.background = ContextCompat.getColor(this, R.color.blue_500).toDrawable() binding.header.main.background = ContextCompat.getColor(this, R.color.blue_500).toDrawable()
binding.header.headerTitle.visibility = View.GONE binding.header.headerTitle.visibility = View.GONE
@ -301,31 +350,42 @@ class RegisterStoreActivity : AppCompatActivity() {
} }
private fun validateRequiredFields() { private fun validateRequiredFields() {
val isFormValid = !viewModel.storeName.value.isNullOrBlank() && val bankName = viewModel.bankName.value?.trim().orEmpty()
!viewModel.street.value.isNullOrBlank() && val bankSelected = bankName.isNotEmpty() && !bankName.equals("Pilih Bank", ignoreCase = true)
(viewModel.postalCode.value ?: 0) > 0 &&
!viewModel.subdistrict.value.isNullOrBlank() &&
!viewModel.bankName.value.isNullOrBlank() &&
(viewModel.bankNumber.value ?: 0) > 0 &&
(viewModel.provinceId.value ?: 0) > 0 &&
!viewModel.cityId.value.isNullOrBlank() &&
(viewModel.storeTypeId.value ?: 0) > 0 &&
viewModel.ktpUri != null &&
viewModel.nibUri != null &&
viewModel.npwpUri != null &&
viewModel.selectedCouriers.isNotEmpty() &&
!viewModel.accountName.value.isNullOrBlank()
binding.btnRegister.isEnabled = true val provinceSelected = viewModel.provinceId.value != null
if (isFormValid) { val citySelected = !viewModel.cityId.value.isNullOrBlank()
binding.btnRegister.setBackgroundResource(R.drawable.bg_button_active) val subdistrictSelected = !viewModel.subdistrict.value.isNullOrBlank()
binding.btnRegister.setTextColor(ContextCompat.getColor(this, R.color.white))
binding.btnRegister.isEnabled = true val currentStoreType = binding.spinnerStoreType.selectedItem as? StoreTypesItem
} else { val storeTypeSelected = when {
binding.btnRegister.setBackgroundResource(R.drawable.bg_button_disabled) currentStoreType != null -> currentStoreType.id != 0 &&
binding.btnRegister.setTextColor(ContextCompat.getColor(this, R.color.black_300)) !currentStoreType.name.equals("Pilih Jenis UMKM", true)
binding.btnRegister.isEnabled = false else -> (viewModel.storeTypeId.value ?: -1) > 0
} }
val isFormValid =
!viewModel.storeName.value.isNullOrBlank() &&
!viewModel.street.value.isNullOrBlank() &&
(viewModel.postalCode.value ?: 0) > 0 &&
provinceSelected && citySelected && subdistrictSelected &&
storeTypeSelected &&
bankSelected &&
(viewModel.bankNumber.value ?: 0) > 0 &&
viewModel.ktpUri != null &&
viewModel.nibUri != null &&
viewModel.npwpUri != null &&
viewModel.selectedCouriers.isNotEmpty() &&
!viewModel.accountName.value.isNullOrBlank() &&
binding.checkboxApprove.isChecked
binding.btnRegister.isEnabled = isFormValid
binding.btnRegister.setBackgroundResource(
if (isFormValid) R.drawable.bg_button_active else R.drawable.bg_button_disabled
)
binding.btnRegister.setTextColor(
ContextCompat.getColor(this, if (isFormValid) R.color.white else R.color.black_300)
)
} }
private fun setupObservers() { private fun setupObservers() {
@ -340,12 +400,10 @@ class RegisterStoreActivity : AppCompatActivity() {
binding.spinnerProvince.isEnabled = false binding.spinnerProvince.isEnabled = false
} }
is Result.Success -> { is Result.Success -> {
Log.d(TAG, "setupObservers: Provinces loaded successfully: ${state.data.size} provinces")
binding.provinceProgressBar.visibility = View.GONE binding.provinceProgressBar.visibility = View.GONE
binding.spinnerProvince.isEnabled = true binding.spinnerProvince.isEnabled = true
// Update adapter with data
provinceAdapter.updateData(state.data) provinceAdapter.updateData(state.data)
tryApplyProvince()
} }
is Result.Error -> { is Result.Error -> {
Log.e(TAG, "setupObservers: Error loading provinces: ${state.exception.message}") Log.e(TAG, "setupObservers: Error loading provinces: ${state.exception.message}")
@ -364,12 +422,10 @@ class RegisterStoreActivity : AppCompatActivity() {
binding.spinnerCity.isEnabled = false binding.spinnerCity.isEnabled = false
} }
is Result.Success -> { is Result.Success -> {
Log.d(TAG, "setupObservers: Cities loaded successfully: ${state.data.size} cities")
binding.cityProgressBar.visibility = View.GONE binding.cityProgressBar.visibility = View.GONE
binding.spinnerCity.isEnabled = true binding.spinnerCity.isEnabled = true
// Update adapter with data
cityAdapter.updateData(state.data) cityAdapter.updateData(state.data)
tryApplyCity()
} }
is Result.Error -> { is Result.Error -> {
Log.e(TAG, "setupObservers: Error loading cities: ${state.exception.message}") Log.e(TAG, "setupObservers: Error loading cities: ${state.exception.message}")
@ -387,11 +443,20 @@ class RegisterStoreActivity : AppCompatActivity() {
binding.spinnerSubdistrict.isEnabled = false binding.spinnerSubdistrict.isEnabled = false
} }
is Result.Success -> { is Result.Success -> {
Log.d(TAG, "setupobservers: Subdistrict loaded successfullti: ${state.data.size} subdistrict")
binding.subdistrictProgressBar.visibility = View.GONE binding.subdistrictProgressBar.visibility = View.GONE
binding.spinnerSubdistrict.isEnabled = true binding.spinnerSubdistrict.isEnabled = true
subdistrictAdapter.updateData(state.data) subdistrictAdapter.updateData(state.data)
// If youre not restoring a specific subdistrict, select the first real item
if (!isRestoringSelections && state.data.isNotEmpty()) {
binding.spinnerSubdistrict.setSelection(0, false)
val id0 = subdistrictAdapter.getSubdistrictId(0)
viewModel.subdistrict.value = id0 ?: ""
viewModel.selectedSubdistrict = id0
validateRequiredFields()
}
tryApplySubdistrict()
} }
is Result.Error -> { is Result.Error -> {
Log.e(TAG, "setupObservers: Error loading subdistrict: ${state.exception.message}") Log.e(TAG, "setupObservers: Error loading subdistrict: ${state.exception.message}")
@ -466,6 +531,7 @@ class RegisterStoreActivity : AppCompatActivity() {
// Setup spinner with API data // Setup spinner with API data
setupStoreTypeSpinner(displayList) setupStoreTypeSpinner(displayList)
tryApplyBank()
} else { } else {
Log.w(TAG, "setupStoreTypesObserver: Received empty store types list") Log.w(TAG, "setupStoreTypesObserver: Received empty store types list")
} }
@ -510,17 +576,10 @@ class RegisterStoreActivity : AppCompatActivity() {
// Set item selection listener // Set item selection listener
binding.spinnerStoreType.onItemSelectedListener = object : AdapterView.OnItemSelectedListener { binding.spinnerStoreType.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) { override fun onItemSelected(parent: AdapterView<*>?, view: View?, pos: Int, id: Long) {
val selectedItem = adapter.getItem(position) val item = (binding.spinnerStoreType.adapter.getItem(pos) as? StoreTypesItem)
Log.d(TAG, "Store type selected: position=$position, item=${selectedItem?.name}, id=${selectedItem?.id}") if (item != null && item.id > 0) viewModel.storeTypeId.value = item.id
validateRequiredFields()
if (selectedItem != null && selectedItem.id > 0) {
// Store the actual ID from the API, not just position
viewModel.storeTypeId.value = selectedItem.id
Log.d(TAG, "Set storeTypeId to ${selectedItem.id}")
} else {
Log.d(TAG, "Default or null store type selected, not setting storeTypeId")
}
} }
override fun onNothingSelected(parent: AdapterView<*>?) { override fun onNothingSelected(parent: AdapterView<*>?) {
@ -539,22 +598,12 @@ class RegisterStoreActivity : AppCompatActivity() {
// Setup province spinner // Setup province spinner
binding.spinnerProvince.adapter = provinceAdapter binding.spinnerProvince.adapter = provinceAdapter
binding.spinnerProvince.onItemSelectedListener = object : AdapterView.OnItemSelectedListener { binding.spinnerProvince.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) { override fun onItemSelected(parent: AdapterView<*>?, view: View?, pos: Int, id: Long) {
Log.d(TAG, "Province selected at position: $position") provinceAdapter.getProvinceId(pos)?.let {
val provinceId = provinceAdapter.getProvinceId(position) viewModel.provinceId.value = it
if (provinceId != null) { viewModel.getCities(it)
Log.d(TAG, "Setting province ID: $provinceId")
viewModel.provinceId.value = provinceId
Log.d(TAG, "Fetching cities for province ID: $provinceId")
viewModel.getCities(provinceId)
// Reset city selection when province changes
Log.d(TAG, "Clearing city adapter for new province selection")
cityAdapter.clear()
binding.spinnerCity.setSelection(0)
} else {
Log.e(TAG, "Invalid province ID for position: $position")
} }
validateRequiredFields()
} }
override fun onNothingSelected(parent: AdapterView<*>?) { override fun onNothingSelected(parent: AdapterView<*>?) {
@ -565,21 +614,13 @@ class RegisterStoreActivity : AppCompatActivity() {
// Setup city spinner // Setup city spinner
binding.spinnerCity.adapter = cityAdapter binding.spinnerCity.adapter = cityAdapter
binding.spinnerCity.onItemSelectedListener = object : AdapterView.OnItemSelectedListener { binding.spinnerCity.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) { override fun onItemSelected(p: AdapterView<*>?, v: View?, pos: Int, id: Long) {
Log.d(TAG, "City selected at position: $position") cityAdapter.getCityId(pos)?.let {
val cityId = cityAdapter.getCityId(position) viewModel.cityId.value = it
if (cityId != null) { viewModel.getSubdistrict(it)
Log.d(TAG, "Setting city ID: $cityId") viewModel.selectedCityId = it
viewModel.cityId.value = cityId
Log.d(TAG, "Fetching subdistrict for city ID: $cityId")
viewModel.getSubdistrict(cityId)
subdistrictAdapter.clear()
binding.spinnerSubdistrict.setSelection(0)
viewModel.selectedCityId = cityId
} else {
Log.e(TAG, "Invalid city ID for position: $position")
} }
validateRequiredFields()
} }
override fun onNothingSelected(parent: AdapterView<*>?) { override fun onNothingSelected(parent: AdapterView<*>?) {
@ -590,16 +631,11 @@ class RegisterStoreActivity : AppCompatActivity() {
//Setup Subdistrict spinner //Setup Subdistrict spinner
binding.spinnerSubdistrict.adapter = subdistrictAdapter binding.spinnerSubdistrict.adapter = subdistrictAdapter
binding.spinnerSubdistrict.onItemSelectedListener = object : AdapterView.OnItemSelectedListener { binding.spinnerSubdistrict.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) { override fun onItemSelected(p: AdapterView<*>?, v: View?, pos: Int, id: Long) {
Log.d(TAG, "Subdistrict selected at position: $position") val selectedId = subdistrictAdapter.getSubdistrictId(pos)
val subdistrictId = subdistrictAdapter.getSubdistrictId(position) viewModel.subdistrict.value = selectedId ?: "" // empty => not selected
if (subdistrictId != null) { viewModel.selectedSubdistrict = selectedId
Log.d(TAG, "Setting subdistrict ID: $subdistrictId") validateRequiredFields()
viewModel.subdistrict.value = subdistrictId
viewModel.selectedSubdistrict = subdistrictId
} else {
Log.e(TAG, "Invalid subdistrict ID for position: $position")
}
} }
override fun onNothingSelected(parent: AdapterView<*>?) { override fun onNothingSelected(parent: AdapterView<*>?) {
@ -609,63 +645,31 @@ class RegisterStoreActivity : AppCompatActivity() {
binding.spinnerBankName.adapter = bankAdapter binding.spinnerBankName.adapter = bankAdapter
binding.spinnerBankName.onItemSelectedListener = object : AdapterView.OnItemSelectedListener { binding.spinnerBankName.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected( override fun onItemSelected(p: AdapterView<*>?, v: View?, pos: Int, id: Long) {
parent: AdapterView<*>?, val bankName = bankAdapter.getBankName(pos)
view: View?, viewModel.bankName.value = bankName
position: Int, viewModel.selectedBankName = bankName
id: Long validateRequiredFields()
) {
Log.d(TAG, "Bank selected at position: $position")
val bankName = bankAdapter.getBankName(position)
if (bankName != null) {
Log.d(TAG, "Setting bank name: $bankName")
viewModel.bankName.value = bankName
viewModel.selectedBankName = bankName
// Optional: Log the selected bank details
val selectedBank = bankAdapter.getBankItem(position)
selectedBank?.let {
Log.d(TAG, "Selected bank: ${it.bankName} (Code: ${it.bankCode})")
}
// Hide progress bar if it was showing
binding.bankNameProgressBar.visibility = View.GONE
} else {
Log.e(TAG, "Invalid bank name for position: $position")
}
}
override fun onNothingSelected(parent: AdapterView<*>?) {
Log.d(TAG, "No bank selected")
viewModel.selectedBankName = null
} }
override fun onNothingSelected(parent: AdapterView<*>?) { /* no-op */ }
} }
tryApplyBank()
// Add initial hints to the spinners // // Add initial hints to the spinners
if (provinceAdapter.isEmpty) { // if (provinceAdapter.isEmpty) provinceAdapter.add("Pilih Provinsi")
Log.d(TAG, "Adding default province hint") // if (cityAdapter.isEmpty) cityAdapter.add("Pilih Kabupaten/Kota")
provinceAdapter.add("Pilih Provinsi") // if (subdistrictAdapter.isEmpty) subdistrictAdapter.add("Pilih Kecamatan")
} // if (bankAdapter.isEmpty) bankAdapter.add("Pilih Bank")
if (cityAdapter.isEmpty) {
Log.d(TAG, "Adding default city hint")
cityAdapter.add("Pilih Kabupaten/Kota")
}
if (subdistrictAdapter.isEmpty) {
Log.d(TAG, "Adding default kecamatan hint")
subdistrictAdapter.add("Pilih Kecamatan")
}
if (bankAdapter.isEmpty) {
Log.d(TAG, "Adding default bank hint")
bankAdapter.add("Pilih Bank")
}
Log.d(TAG, "setupSpinners: Province and city spinners setup completed") Log.d(TAG, "setupSpinners: Province and city spinners setup completed")
} }
private fun maybeFinishRestoring() {
if (provinceApplied && cityApplied && subdistrictApplied) {
isRestoringSelections = false
}
}
private fun setupDocumentUploads() { private fun setupDocumentUploads() {
Log.d(TAG, "setupDocumentUploads: Setting up document upload buttons") Log.d(TAG, "setupDocumentUploads: Setting up document upload buttons")
@ -805,6 +809,7 @@ class RegisterStoreActivity : AppCompatActivity() {
override fun afterTextChanged(s: Editable?) { override fun afterTextChanged(s: Editable?) {
viewModel.storeDescription.value = s.toString() viewModel.storeDescription.value = s.toString()
Log.d(TAG, "Store description updated: ${s.toString().take(20)}${if ((s?.length ?: 0) > 20) "..." else ""}") Log.d(TAG, "Store description updated: ${s.toString().take(20)}${if ((s?.length ?: 0) > 20) "..." else ""}")
validateRequiredFields()
} }
}) })
@ -822,14 +827,8 @@ class RegisterStoreActivity : AppCompatActivity() {
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {} override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {} override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
override fun afterTextChanged(s: Editable?) { override fun afterTextChanged(s: Editable?) {
try { viewModel.postalCode.value = s.toString().toIntOrNull() ?: 0
viewModel.postalCode.value = s.toString().toInt() validateRequiredFields()
Log.d(TAG, "Postal code updated: ${s.toString()}")
} catch (e: NumberFormatException) {
// Handle invalid input
Log.e(TAG, "Invalid postal code input: ${s.toString()}, error: $e")
validateRequiredFields()
}
} }
}) })
@ -839,6 +838,7 @@ class RegisterStoreActivity : AppCompatActivity() {
override fun afterTextChanged(s: Editable?) { override fun afterTextChanged(s: Editable?) {
viewModel.addressDetail.value = s.toString() viewModel.addressDetail.value = s.toString()
Log.d(TAG, "Address detail updated: ${s.toString()}") Log.d(TAG, "Address detail updated: ${s.toString()}")
validateRequiredFields()
} }
}) })
@ -993,30 +993,41 @@ class RegisterStoreActivity : AppCompatActivity() {
} }
private fun doUpdateStoreProfile() { private fun doUpdateStoreProfile() {
val nameBody: RequestBody = (viewModel.storeName.value ?: "") // --- Text parts ---
.toRequestBody("text/plain".toMediaTypeOrNull()) val nameBody: RequestBody = (viewModel.storeName.value ?: "").toRequestBody("text/plain".toMediaTypeOrNull())
val typeBody: RequestBody = ((viewModel.storeTypeId.value ?: 0).toString()) val typeBody: RequestBody = ((viewModel.storeTypeId.value ?: 0).toString()).toRequestBody("text/plain".toMediaTypeOrNull())
.toRequestBody("text/plain".toMediaTypeOrNull()) val descBody: RequestBody = (viewModel.storeDescription.value ?: "").toRequestBody("text/plain".toMediaTypeOrNull())
val descBody: RequestBody = (viewModel.storeDescription.value ?: "") // NOTE: is_on_leave is NOT part of approval multipart; keep separate if needed
.toRequestBody("text/plain".toMediaTypeOrNull())
val onLeaveBody: RequestBody = "false"
.toRequestBody("text/plain".toMediaTypeOrNull())
// --- Build Multipart for store image (optional) --- val latBody: RequestBody = (viewModel.latitude.value ?: "").toRequestBody("text/plain".toMediaTypeOrNull())
// Prefer compressing images to keep payload small; fall back to raw copy if needed. val longBody: RequestBody = (viewModel.longitude.value ?: "").toRequestBody("text/plain".toMediaTypeOrNull())
val provBody: RequestBody = ((viewModel.provinceId.value ?: 0).toString()).toRequestBody("text/plain".toMediaTypeOrNull())
val cityBody: RequestBody = (viewModel.cityId.value ?: "").toRequestBody("text/plain".toMediaTypeOrNull())
val subdistrictBody: RequestBody = (viewModel.selectedSubdistrict ?: viewModel.subdistrict.value ?: "").toRequestBody("text/plain".toMediaTypeOrNull())
// If you don't have village picker yet, send empty string or reuse subdistrict
val villageBody: RequestBody = "".toRequestBody("text/plain".toMediaTypeOrNull())
val streetBody: RequestBody = (viewModel.street.value ?: "").toRequestBody("text/plain".toMediaTypeOrNull())
val postalBody: RequestBody = ((viewModel.postalCode.value ?: 0).toString()).toRequestBody("text/plain".toMediaTypeOrNull())
val detailBody: RequestBody = (viewModel.addressDetail.value ?: "").toRequestBody("text/plain".toMediaTypeOrNull())
// You can read user phone from current store profile when reapply
val currentPhone = myStoreViewModel.myStoreProfile.value?.store?.userPhone ?: ""
val userPhoneBody: RequestBody = currentPhone.toRequestBody("text/plain".toMediaTypeOrNull())
// --- Multipart images/docs (safe compress/copy) ---
val storeImgPart: MultipartBody.Part? = viewModel.storeImageUri?.let { uri -> val storeImgPart: MultipartBody.Part? = viewModel.storeImageUri?.let { uri ->
try { try {
// (A) Optional safety check: only allow jpg/png/webp
val allowed = Regex("^(jpg|jpeg|png|webp)$", RegexOption.IGNORE_CASE) val allowed = Regex("^(jpg|jpeg|png|webp)$", RegexOption.IGNORE_CASE)
if (!ImageUtils.isAllowedFileType(this, uri, allowed)) { if (!ImageUtils.isAllowedFileType(this, uri, allowed)) {
Toast.makeText(this, "Format gambar tidak didukung", Toast.LENGTH_SHORT).show() Toast.makeText(this, "Format gambar tidak didukung", Toast.LENGTH_SHORT).show()
null null
} else { } else {
// (B) Compress for upload (ke cacheDir), then build multipart
val compressed: File = ImageUtils.compressImage( val compressed: File = ImageUtils.compressImage(
context = this, context = this,
uri = uri, uri = uri,
filename = "storeimg", // prefix filename = "storeimg",
maxWidth = 1024, maxWidth = 1024,
maxHeight = 1024, maxHeight = 1024,
quality = 80 quality = 80
@ -1024,18 +1035,76 @@ class RegisterStoreActivity : AppCompatActivity() {
FileUtils.createMultipartFromFile("storeimg", compressed) FileUtils.createMultipartFromFile("storeimg", compressed)
} }
} catch (e: Exception) { } catch (e: Exception) {
// If compression fails, try raw copy as fallback
val rawFile = FileUtils.createTempFileFromUri(this, uri) val rawFile = FileUtils.createTempFileFromUri(this, uri)
rawFile?.let { FileUtils.createMultipartFromFile("storeimg", it) } rawFile?.let { FileUtils.createMultipartFromFile("storeimg", it) }
} }
} }
myStoreViewModel.updateStoreProfile( val ktpPart: MultipartBody.Part? = viewModel.ktpUri?.let { uri ->
val file = FileUtils.createTempFileFromUri(this, uri)
file?.let { FileUtils.createMultipartFromFile("ktp", it) }
}
val npwpPart: MultipartBody.Part? = viewModel.npwpUri?.let { uri ->
val file = FileUtils.createTempFileFromUri(this, uri)
file?.let { FileUtils.createMultipartFromFile("npwp", it) }
}
val nibPart: MultipartBody.Part? = viewModel.nibUri?.let { uri ->
val file = FileUtils.createTempFileFromUri(this, uri)
file?.let { FileUtils.createMultipartFromFile("nib", it) }
}
// --- Couriers desired (sync to exactly this set) ---
val desiredCouriers = viewModel.selectedCouriers.toList()
// --- (Optional) Payment upsert from UI fields ---
// If you want to send the bank from the form during re-apply:
val paymentsToUpsert = buildList {
val bankName = viewModel.bankName.value
val bankNum = viewModel.bankNumber.value?.toString()
val accName = viewModel.accountName.value
if (!bankName.isNullOrBlank() && !bankNum.isNullOrBlank() && !accName.isNullOrBlank()) {
// If you want to update the first existing payment instead of adding new:
val existingId = myStoreViewModel.payment.value?.firstOrNull()?.id
add(
PaymentUpdate(
id = existingId, // null => add; id!=null => update
bankName = bankName,
bankNum = bankNum,
accountName = accName,
qrisImage = null // attach File if you have new QRIS to upload
)
)
}
}
// --- Delete list (empty if none) ---
val paymentIdToDelete = emptyList<Int>()
// --- Fire the update ---
myStoreViewModel.updateStoreApproval(
storeName = nameBody, storeName = nameBody,
storeType = typeBody,
description = descBody, description = descBody,
isOnLeave = onLeaveBody, storeType = typeBody,
storeImage = storeImgPart latitude = latBody,
longitude = longBody,
storeProvince = provBody,
storeCity = cityBody,
storeSubdistrict = subdistrictBody,
storeVillage = villageBody,
storeStreet = streetBody,
storePostalCode = postalBody,
storeAddressDetail = detailBody,
userPhone = userPhoneBody,
paymentsToUpdate = paymentsToUpsert,
paymentIdToDelete = paymentIdToDelete,
storeCourier = desiredCouriers,
storeImage = storeImgPart,
ktpImage = ktpPart,
npwpDocument = npwpPart,
nibDocument = nibPart
) )
myStoreViewModel.updateStoreProfileResult.observe(this) { myStoreViewModel.updateStoreProfileResult.observe(this) {
@ -1049,6 +1118,7 @@ class RegisterStoreActivity : AppCompatActivity() {
} }
} }
companion object { companion object {
private const val TAG = "RegisterStoreActivity" private const val TAG = "RegisterStoreActivity"
} }

View File

@ -49,6 +49,9 @@ class DetailStoreProductActivity : AppCompatActivity() {
private var productId: Int? = null private var productId: Int? = null
private var hasImage: Boolean = false private var hasImage: Boolean = false
private var isEditing = false
private var hasExistingImage = false
private val viewModel: ProductViewModel by viewModels { private val viewModel: ProductViewModel by viewModels {
BaseViewModelFactory { BaseViewModelFactory {
sessionManager = SessionManager(this) sessionManager = SessionManager(this)
@ -98,7 +101,7 @@ class DetailStoreProductActivity : AppCompatActivity() {
binding = ActivityDetailStoreProductBinding.inflate(layoutInflater) binding = ActivityDetailStoreProductBinding.inflate(layoutInflater)
setContentView(binding.root) setContentView(binding.root)
val isEditing = intent.getBooleanExtra("is_editing", false) isEditing = intent.getBooleanExtra("is_editing", false)
productId = intent.getIntExtra("product_id", -1) productId = intent.getIntExtra("product_id", -1)
binding.headerStoreProduct.headerTitle.text = if (isEditing) "Ubah Produk" else "Tambah Produk" binding.headerStoreProduct.headerTitle.text = if (isEditing) "Ubah Produk" else "Tambah Produk"
@ -179,6 +182,7 @@ class DetailStoreProductActivity : AppCompatActivity() {
binding.btnRemoveFoto.setOnClickListener { binding.btnRemoveFoto.setOnClickListener {
imageUri = null imageUri = null
hasImage = false hasImage = false
hasExistingImage = false
binding.switcherFotoProduk.showPrevious() binding.switcherFotoProduk.showPrevious()
validateForm() validateForm()
} }
@ -236,7 +240,9 @@ class DetailStoreProductActivity : AppCompatActivity() {
} else product.image } else product.image
Glide.with(this).load(imageUrl).into(binding.ivPreviewFoto) Glide.with(this).load(imageUrl).into(binding.ivPreviewFoto)
binding.switcherFotoProduk.showNext() binding.switcherFotoProduk.showNext()
hasImage = true hasImage = true
hasExistingImage = true
// SPPIRT // SPPIRT
product.sppirt?.let { product.sppirt?.let {
@ -331,10 +337,18 @@ class DetailStoreProductActivity : AppCompatActivity() {
val duration = binding.edtDurasi.text.toString().trim() val duration = binding.edtDurasi.text.toString().trim()
val wholesaleMinItem = binding.edtMinPesanGrosir.text.toString().trim() val wholesaleMinItem = binding.edtMinPesanGrosir.text.toString().trim()
val wholesalePrice = binding.edtHargaGrosir.text.toString().trim() val wholesalePrice = binding.edtHargaGrosir.text.toString().trim()
val category = binding.spinnerKategoriProduk.selectedItemPosition != -1 val categorySelected = binding.spinnerKategoriProduk.selectedItemPosition != -1
val isPreOrderChecked = binding.switchIsPreOrder.isChecked val isPreOrderChecked = binding.switchIsPreOrder.isChecked
val isWholesaleChecked = binding.switchIsWholesale.isChecked val isWholesaleChecked = binding.switchIsWholesale.isChecked
val hasRequiredImage = if (isEditing) {
// In edit mode: allow existing server image OR newly picked image
hasImage || hasExistingImage
} else {
// In create mode: must have a picked image
hasImage
}
val valid = name.isNotEmpty() && val valid = name.isNotEmpty() &&
description.isNotEmpty() && description.isNotEmpty() &&
price.isNotEmpty() && price.isNotEmpty() &&
@ -343,8 +357,8 @@ class DetailStoreProductActivity : AppCompatActivity() {
weight.isNotEmpty() && weight.isNotEmpty() &&
(!isPreOrderChecked || duration.isNotEmpty()) && (!isPreOrderChecked || duration.isNotEmpty()) &&
(!isWholesaleChecked || (wholesaleMinItem.isNotEmpty() && wholesalePrice.isNotEmpty())) && (!isWholesaleChecked || (wholesaleMinItem.isNotEmpty() && wholesalePrice.isNotEmpty())) &&
category && categorySelected &&
hasImage hasRequiredImage
binding.btnSaveProduct.isEnabled = valid binding.btnSaveProduct.isEnabled = valid
binding.btnSaveProduct.setTextColor( binding.btnSaveProduct.setTextColor(

View File

@ -6,6 +6,7 @@ import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.map import androidx.lifecycle.map
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.alya.ecommerce_serang.data.api.dto.PaymentUpdate
import com.alya.ecommerce_serang.data.api.dto.ProductsItem import com.alya.ecommerce_serang.data.api.dto.ProductsItem
import com.alya.ecommerce_serang.data.api.dto.Store import com.alya.ecommerce_serang.data.api.dto.Store
import com.alya.ecommerce_serang.data.api.response.auth.StoreTypesItem import com.alya.ecommerce_serang.data.api.response.auth.StoreTypesItem
@ -19,6 +20,7 @@ import kotlinx.coroutines.launch
import okhttp3.MediaType.Companion.toMediaTypeOrNull import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.MultipartBody import okhttp3.MultipartBody
import okhttp3.RequestBody import okhttp3.RequestBody
import okhttp3.RequestBody.Companion.toRequestBody
import java.text.NumberFormat import java.text.NumberFormat
import java.util.Locale import java.util.Locale
@ -199,6 +201,80 @@ class MyStoreViewModel(private val repository: MyStoreRepository): ViewModel() {
} }
} }
private fun String.toRequestBody(): RequestBody = private fun String.toPlain(): RequestBody =
RequestBody.create("text/plain".toMediaTypeOrNull(), this) this.toRequestBody("text/plain".toMediaTypeOrNull())
fun updateStoreApproval(
storeName: RequestBody,
description: RequestBody,
storeType: RequestBody,
latitude: RequestBody,
longitude: RequestBody,
storeProvince: RequestBody,
storeCity: RequestBody,
storeSubdistrict: RequestBody,
storeVillage: RequestBody,
storeStreet: RequestBody,
storePostalCode: RequestBody,
storeAddressDetail: RequestBody,
userPhone: RequestBody,
paymentsToUpdate: List<PaymentUpdate> = emptyList(),
paymentIdToDelete: List<Int> = emptyList(),
storeCourier: List<String>? = null,
storeImage: MultipartBody.Part?,
ktpImage: MultipartBody.Part?,
npwpDocument: MultipartBody.Part?,
nibDocument: MultipartBody.Part?
) {
viewModelScope.launch {
try {
val store = myStoreProfile.value
if (store == null) {
_errorMessage.postValue("Data toko tidak tersedia")
Log.e(TAG, "Store data is null")
return@launch
}
val response = repository.updateStoreApproval(
storeName = storeName,
description = description,
storeType = storeType,
latitude = latitude,
longitude = longitude,
storeProvince = storeProvince,
storeCity = storeCity,
storeSubdistrict = storeSubdistrict,
storeVillage = storeVillage,
storeStreet = storeStreet,
storePostalCode = storePostalCode,
storeAddressDetail = storeAddressDetail,
userPhone = userPhone,
paymentsToUpdate = paymentsToUpdate,
paymentIdToDelete = paymentIdToDelete,
storeCourier = storeCourier,
storeImage = storeImage,
ktpImage = ktpImage,
npwpDocument = npwpDocument,
nibDocument = nibDocument
)
if (response != null) {
if (response.isSuccessful) {
_updateStoreProfileResult.postValue(response.body())
Log.d(TAG, "Update successful: ${response.body()}")
} else {
_errorMessage.postValue("Gagal memperbarui profil")
Log.e(TAG, "Update failed: ${response.errorBody()?.string()}")
}
} else {
_errorMessage.postValue("Terjadi kesalahan jaringan atau server")
Log.e(TAG, "Repository returned null response")
}
} catch (e: Exception) {
_errorMessage.postValue(e.message ?: "Unexpected error")
Log.e(TAG, "Exception updating store profile", e)
}
}
}
} }

View File

@ -371,6 +371,7 @@
android:background="@drawable/bg_text_field" android:background="@drawable/bg_text_field"
android:hint="Isi stok produk di sini" android:hint="Isi stok produk di sini"
android:padding="8dp" android:padding="8dp"
android:inputType="number"
style="@style/body_small" /> style="@style/body_small" />
</LinearLayout> </LinearLayout>
@ -413,6 +414,7 @@
android:hint="Isi minimum pemesanan produk di sini" android:hint="Isi minimum pemesanan produk di sini"
android:padding="8dp" android:padding="8dp"
style="@style/body_small" style="@style/body_small"
android:inputType="number"
android:layout_marginTop="10dp" /> android:layout_marginTop="10dp" />
</LinearLayout> </LinearLayout>
@ -711,6 +713,7 @@
android:background="@drawable/bg_text_field" android:background="@drawable/bg_text_field"
android:hint="Isi minimum produk untuk mendapatkan harga grosir di sini" android:hint="Isi minimum produk untuk mendapatkan harga grosir di sini"
android:padding="8dp" android:padding="8dp"
android:inputType="number"
style="@style/body_small" /> style="@style/body_small" />
</LinearLayout> </LinearLayout>

View File

@ -543,49 +543,6 @@
</LinearLayout> </LinearLayout>
<!-- Bank -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_marginBottom="24dp"
android:visibility="gone">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="10. Bank"
style="@style/body_medium"
android:layout_marginEnd="4dp"/>
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="*"
style="@style/body_medium"
android:textColor="@color/red_required"
android:layout_gravity="end"/>
</LinearLayout>
<EditText
android:id="@+id/et_bank_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/bg_text_field"
android:hint="Isi nama bank untuk toko Anda di sini"
android:padding="8dp"
style="@style/body_small"
android:layout_marginTop="10dp"/>
</LinearLayout>
<!-- Nama Bank--> <!-- Nama Bank-->
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
@ -654,6 +611,48 @@
</LinearLayout> </LinearLayout>
<!-- Nama Pemilik Rekening -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_marginBottom="24dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="12. Nama Pemilik Rekening"
style="@style/body_medium"
android:layout_marginEnd="4dp"/>
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="*"
style="@style/body_medium"
android:textColor="@color/red_required"
android:layout_gravity="end"/>
</LinearLayout>
<EditText
android:id="@+id/et_account_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/bg_text_field"
android:hint="Isi nama pemilik rekening"
android:padding="8dp"
style="@style/body_small"
android:layout_marginTop="10dp"/>
</LinearLayout>
<!-- Nomor Rekening --> <!-- Nomor Rekening -->
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
@ -697,48 +696,6 @@
</LinearLayout> </LinearLayout>
<!-- Nama Pemilik Rekening -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_marginBottom="24dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="12. Nama Pemilik Rekening"
style="@style/body_medium"
android:layout_marginEnd="4dp"/>
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="*"
style="@style/body_medium"
android:textColor="@color/red_required"
android:layout_gravity="end"/>
</LinearLayout>
<EditText
android:id="@+id/et_account_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/bg_text_field"
android:hint="Isi nama pemilik rekening"
android:padding="8dp"
style="@style/body_small"
android:layout_marginTop="10dp"/>
</LinearLayout>
<!-- Kurir --> <!-- Kurir -->
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
@ -915,73 +872,6 @@
</LinearLayout> </LinearLayout>
<!-- NIB -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_marginBottom="24dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="16. Dokumen NIB"
style="@style/body_medium"
android:layout_marginEnd="4dp"/>
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="*"
style="@style/body_medium"
android:textColor="@color/red_required"
android:layout_gravity="end"/>
</LinearLayout>
<FrameLayout
android:id="@+id/container_nib"
android:layout_width="match_parent"
android:layout_height="100dp"
android:layout_marginTop="8dp"
android:background="@android:drawable/editbox_background">
<ImageView
android:id="@+id/img_nib"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="centerInside"
android:visibility="gone" />
<LinearLayout
android:id="@+id/layout_upload_nib"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical">
<ImageView
android:layout_width="32dp"
android:layout_height="32dp"
android:src="@android:drawable/ic_menu_upload" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="Unggah dokumen Anda di sini"
android:textColor="#777777" />
</LinearLayout>
</FrameLayout>
</LinearLayout>
<!-- NPWP --> <!-- NPWP -->
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
@ -1049,6 +939,73 @@
</LinearLayout> </LinearLayout>
<!-- NIB -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_marginBottom="24dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="16. Dokumen NIB"
style="@style/body_medium"
android:layout_marginEnd="4dp"/>
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="*"
style="@style/body_medium"
android:textColor="@color/red_required"
android:layout_gravity="end"/>
</LinearLayout>
<FrameLayout
android:id="@+id/container_nib"
android:layout_width="match_parent"
android:layout_height="100dp"
android:layout_marginTop="8dp"
android:background="@android:drawable/editbox_background">
<ImageView
android:id="@+id/img_nib"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="centerInside"
android:visibility="gone" />
<LinearLayout
android:id="@+id/layout_upload_nib"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical">
<ImageView
android:layout_width="32dp"
android:layout_height="32dp"
android:src="@android:drawable/ic_menu_upload" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="Unggah dokumen Anda di sini"
android:textColor="#777777" />
</LinearLayout>
</FrameLayout>
</LinearLayout>
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"