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
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.CancelOrderReq
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.OrderRequestBuy
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.RegisterRequest
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.VerifRegisReq
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.HasStoreResponse
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.profile.StoreDataResponse
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.TopUpResponse
import okhttp3.MultipartBody
import okhttp3.RequestBody
import retrofit2.Call
import retrofit2.Response
import retrofit2.http.Body
import retrofit2.http.DELETE
@ -99,7 +94,6 @@ import retrofit2.http.PUT
import retrofit2.http.Part
import retrofit2.http.PartMap
import retrofit2.http.Path
import retrofit2.http.Query
interface ApiService {
@POST("registeruser")
@ -112,9 +106,6 @@ interface ApiService {
@Body verifRegisReq: VerifRegisReq
):VerifRegisterResponse
@GET("checkstore")
suspend fun checkStore (): Response<CheckStoreResponse>
@Multipart
@POST("registerstore")
suspend fun registerStore(
@ -204,11 +195,6 @@ interface ApiService {
@Path("id") orderId: Int
): Response<OrderDetailResponse>
@POST("order/addevidence")
suspend fun addEvidence(
@Body request : AddEvidenceRequest,
): Response<AddEvidenceResponse>
@Multipart
@POST("order/addevidence")
suspend fun addEvidenceMultipart(
@ -256,15 +242,9 @@ interface ApiService {
@GET("mystore")
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
suspend fun getStoreProduct(): Response<ViewStoreProductsResponse>
@GET("category")
fun getCategories(): Call<CategoryResponse>
@Multipart
@POST("store/createproduct")
suspend fun addProduct(
@ -374,9 +354,6 @@ interface ApiService {
@GET("store/topup")
suspend fun getTopUpHistory(): Response<TopUpResponse>
@GET("store/topup")
suspend fun getFilteredTopUpHistory(@Query("date") date: String): Response<TopUpResponse>
@Multipart
@POST("store/createtopup")
suspend fun addBalanceTopUp(
@ -388,11 +365,6 @@ interface ApiService {
@Part("bank_num") bankNum: RequestBody
): Response<BalanceTopUpResponse>
@PUT("store/payment/update")
suspend fun paymentConfirmation(
@Body confirmPaymentReq : PaymentConfirmRequest
): Response<PaymentConfirmationResponse>
@Multipart
@PUT("mystore/edit")
suspend fun updateStoreProfileMultipart(
@ -404,13 +376,26 @@ interface ApiService {
): Response<StoreDataResponse>
@Multipart
@POST("mystore/payment/add")
suspend fun addPaymentInfo(
@Part("bank_name") bankName: RequestBody,
@Part("bank_num") bankNum: RequestBody,
@Part("account_name") accountName: RequestBody,
@Part qris: MultipartBody.Part?
): Response<GenericResponse>
@PUT("mystore/edit")
suspend fun updateStoreApprovalMultipart(
@Part("store_name") storeName: RequestBody,
@Part("store_description") storeDescription: RequestBody,
@Part("store_type_id") storeTypeId: RequestBody,
@Part("latitude") storeLatitude: RequestBody,
@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
@POST("mystore/payment/add")
@ -421,6 +406,16 @@ interface ApiService {
@Part qris: MultipartBody.Part?
): 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}")
suspend fun deletePaymentInfo(
@Path("id") paymentMethodId: Int
@ -464,15 +459,6 @@ interface ApiService {
@GET("search")
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
@POST("store/sendchat")
suspend fun sendChatMessageStore(

View File

@ -1,17 +1,23 @@
package com.alya.ecommerce_serang.data.repository
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.ShippingServiceRequest
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.store.StoreResponse
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.retrofit.ApiService
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.MultipartBody
import okhttp3.RequestBody
import okhttp3.RequestBody.Companion.asRequestBody
import okhttp3.RequestBody.Companion.toRequestBody
import retrofit2.HttpException
import retrofit2.Response
import java.io.File
import java.io.IOException
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 {
private var TAG = "MyStoreRepository"
}

View File

@ -47,6 +47,7 @@ import okhttp3.RequestBody
import okhttp3.RequestBody.Companion.toRequestBody
import java.io.File
import androidx.core.net.toUri
import com.alya.ecommerce_serang.data.api.dto.PaymentUpdate
class RegisterStoreActivity : AppCompatActivity() {
@ -58,6 +59,21 @@ class RegisterStoreActivity : AppCompatActivity() {
private lateinit var subdistrictAdapter: SubdsitrictAdapter
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
private val PICK_STORE_IMAGE_REQUEST = 1001
private val PICK_KTP_REQUEST = 1002
@ -123,6 +139,10 @@ class RegisterStoreActivity : AppCompatActivity() {
setupSpinners() // Location spinners
Log.d(TAG, "onCreate: Spinners setup completed")
binding.checkboxApprove.setOnCheckedChangeListener { _, _ ->
validateRequiredFields()
}
// Setup observers
setupStoreTypesObserver() // Store type observer
setupObservers()
@ -209,12 +229,27 @@ class RegisterStoreActivity : AppCompatActivity() {
// Prefill spinner for store types
preselectStoreType(store.storeTypeId)
// Prefill province, city, and subdistrict
preselectProvinceCitySubdistrict(
provinceId = store.provinceId,
cityId = store.cityId,
subdistrictId = store.subdistrict
)
// Cache what we want to select later (after data arrives)
wantedProvinceId = store.provinceId
wantedCityId = store.cityId
wantedSubdistrictId = 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()
}
@ -229,64 +264,78 @@ class RegisterStoreActivity : AppCompatActivity() {
else Toast.makeText(this, "Harap lengkapi semua field yang wajib diisi", Toast.LENGTH_SHORT).show()
}
}
validateRequiredFields()
}
private fun preselectStoreType(storeTypeId: Int) {
// The adapter is created in setupStoreTypeSpinner(...)
val adapter = binding.spinnerStoreType.adapter
if (adapter != null) {
val count = adapter.count
for (i in 0 until count) {
val item = adapter.getItem(i) as? StoreTypesItem
if (item?.id == storeTypeId) {
binding.spinnerStoreType.setSelection(i, false)
break
}
val adapter = binding.spinnerStoreType.adapter ?: return
for (i in 0 until adapter.count) {
val item = adapter.getItem(i) as? StoreTypesItem
if (item?.id == storeTypeId) {
binding.spinnerStoreType.setSelection(i, false)
viewModel.storeTypeId.value = storeTypeId
validateRequiredFields()
break
}
}
}
private fun preselectProvinceCitySubdistrict(
provinceId: Int,
cityId: String,
subdistrictId: String
) {
// Province first (this will trigger cities fetch)
val provCount = provinceAdapter.count
for (i in 0 until provCount) {
if (provinceAdapter.getProvinceId(i) == provinceId) {
private fun tryApplyProvince() {
if (provinceApplied) return
val target = wantedProvinceId ?: return
val count = provinceAdapter.count
for (i in 0 until count) {
if (provinceAdapter.getProvinceId(i) == target) {
binding.spinnerProvince.setSelection(i, false)
break
provinceApplied = true
maybeFinishRestoring()
return
}
}
}
// When cities arrive, select the city, then load subdistricts
viewModel.citiesState.observe(this) { state ->
if (state is Result.Success) {
val cityCount = cityAdapter.count
for (i in 0 until cityCount) {
if (cityAdapter.getCityId(i) == cityId) {
binding.spinnerCity.setSelection(i, false)
break
}
}
private fun tryApplyCity() {
if (cityApplied) return
val target = wantedCityId ?: return
val count = cityAdapter.count
for (i in 0 until count) {
if (cityAdapter.getCityId(i) == target) {
binding.spinnerCity.setSelection(i, false)
cityApplied = true
maybeFinishRestoring()
return
}
}
}
// When subdistricts arrive, select the subdistrict
viewModel.subdistrictState.observe(this) { state ->
if (state is Result.Success) {
val subCount = subdistrictAdapter.count
for (i in 0 until subCount) {
if (subdistrictAdapter.getSubdistrictId(i) == subdistrictId) {
binding.spinnerSubdistrict.setSelection(i, false)
break
}
}
private fun tryApplySubdistrict() {
if (subdistrictApplied) return
val target = wantedSubdistrictId ?: return
val count = subdistrictAdapter.count
for (i in 0 until count) {
if (subdistrictAdapter.getSubdistrictId(i) == target) {
binding.spinnerSubdistrict.setSelection(i, false)
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() {
binding.header.main.background = ContextCompat.getColor(this, R.color.blue_500).toDrawable()
binding.header.headerTitle.visibility = View.GONE
@ -301,31 +350,42 @@ class RegisterStoreActivity : AppCompatActivity() {
}
private fun validateRequiredFields() {
val isFormValid = !viewModel.storeName.value.isNullOrBlank() &&
!viewModel.street.value.isNullOrBlank() &&
(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()
val bankName = viewModel.bankName.value?.trim().orEmpty()
val bankSelected = bankName.isNotEmpty() && !bankName.equals("Pilih Bank", ignoreCase = true)
binding.btnRegister.isEnabled = true
if (isFormValid) {
binding.btnRegister.setBackgroundResource(R.drawable.bg_button_active)
binding.btnRegister.setTextColor(ContextCompat.getColor(this, R.color.white))
binding.btnRegister.isEnabled = true
} else {
binding.btnRegister.setBackgroundResource(R.drawable.bg_button_disabled)
binding.btnRegister.setTextColor(ContextCompat.getColor(this, R.color.black_300))
binding.btnRegister.isEnabled = false
val provinceSelected = viewModel.provinceId.value != null
val citySelected = !viewModel.cityId.value.isNullOrBlank()
val subdistrictSelected = !viewModel.subdistrict.value.isNullOrBlank()
val currentStoreType = binding.spinnerStoreType.selectedItem as? StoreTypesItem
val storeTypeSelected = when {
currentStoreType != null -> currentStoreType.id != 0 &&
!currentStoreType.name.equals("Pilih Jenis UMKM", true)
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() {
@ -340,12 +400,10 @@ class RegisterStoreActivity : AppCompatActivity() {
binding.spinnerProvince.isEnabled = false
}
is Result.Success -> {
Log.d(TAG, "setupObservers: Provinces loaded successfully: ${state.data.size} provinces")
binding.provinceProgressBar.visibility = View.GONE
binding.spinnerProvince.isEnabled = true
// Update adapter with data
provinceAdapter.updateData(state.data)
tryApplyProvince()
}
is Result.Error -> {
Log.e(TAG, "setupObservers: Error loading provinces: ${state.exception.message}")
@ -364,12 +422,10 @@ class RegisterStoreActivity : AppCompatActivity() {
binding.spinnerCity.isEnabled = false
}
is Result.Success -> {
Log.d(TAG, "setupObservers: Cities loaded successfully: ${state.data.size} cities")
binding.cityProgressBar.visibility = View.GONE
binding.spinnerCity.isEnabled = true
// Update adapter with data
cityAdapter.updateData(state.data)
tryApplyCity()
}
is Result.Error -> {
Log.e(TAG, "setupObservers: Error loading cities: ${state.exception.message}")
@ -387,11 +443,20 @@ class RegisterStoreActivity : AppCompatActivity() {
binding.spinnerSubdistrict.isEnabled = false
}
is Result.Success -> {
Log.d(TAG, "setupobservers: Subdistrict loaded successfullti: ${state.data.size} subdistrict")
binding.subdistrictProgressBar.visibility = View.GONE
binding.spinnerSubdistrict.isEnabled = true
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 -> {
Log.e(TAG, "setupObservers: Error loading subdistrict: ${state.exception.message}")
@ -466,6 +531,7 @@ class RegisterStoreActivity : AppCompatActivity() {
// Setup spinner with API data
setupStoreTypeSpinner(displayList)
tryApplyBank()
} else {
Log.w(TAG, "setupStoreTypesObserver: Received empty store types list")
}
@ -510,17 +576,10 @@ class RegisterStoreActivity : AppCompatActivity() {
// Set item selection listener
binding.spinnerStoreType.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
val selectedItem = adapter.getItem(position)
Log.d(TAG, "Store type selected: position=$position, item=${selectedItem?.name}, id=${selectedItem?.id}")
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 onItemSelected(parent: AdapterView<*>?, view: View?, pos: Int, id: Long) {
val item = (binding.spinnerStoreType.adapter.getItem(pos) as? StoreTypesItem)
if (item != null && item.id > 0) viewModel.storeTypeId.value = item.id
validateRequiredFields()
}
override fun onNothingSelected(parent: AdapterView<*>?) {
@ -539,22 +598,12 @@ class RegisterStoreActivity : AppCompatActivity() {
// Setup province spinner
binding.spinnerProvince.adapter = provinceAdapter
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")
val provinceId = provinceAdapter.getProvinceId(position)
if (provinceId != null) {
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")
override fun onItemSelected(parent: AdapterView<*>?, view: View?, pos: Int, id: Long) {
provinceAdapter.getProvinceId(pos)?.let {
viewModel.provinceId.value = it
viewModel.getCities(it)
}
validateRequiredFields()
}
override fun onNothingSelected(parent: AdapterView<*>?) {
@ -565,21 +614,13 @@ class RegisterStoreActivity : AppCompatActivity() {
// Setup city spinner
binding.spinnerCity.adapter = cityAdapter
binding.spinnerCity.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
Log.d(TAG, "City selected at position: $position")
val cityId = cityAdapter.getCityId(position)
if (cityId != null) {
Log.d(TAG, "Setting city ID: $cityId")
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")
override fun onItemSelected(p: AdapterView<*>?, v: View?, pos: Int, id: Long) {
cityAdapter.getCityId(pos)?.let {
viewModel.cityId.value = it
viewModel.getSubdistrict(it)
viewModel.selectedCityId = it
}
validateRequiredFields()
}
override fun onNothingSelected(parent: AdapterView<*>?) {
@ -590,16 +631,11 @@ class RegisterStoreActivity : AppCompatActivity() {
//Setup Subdistrict spinner
binding.spinnerSubdistrict.adapter = subdistrictAdapter
binding.spinnerSubdistrict.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
Log.d(TAG, "Subdistrict selected at position: $position")
val subdistrictId = subdistrictAdapter.getSubdistrictId(position)
if (subdistrictId != null) {
Log.d(TAG, "Setting subdistrict ID: $subdistrictId")
viewModel.subdistrict.value = subdistrictId
viewModel.selectedSubdistrict = subdistrictId
} else {
Log.e(TAG, "Invalid subdistrict ID for position: $position")
}
override fun onItemSelected(p: AdapterView<*>?, v: View?, pos: Int, id: Long) {
val selectedId = subdistrictAdapter.getSubdistrictId(pos)
viewModel.subdistrict.value = selectedId ?: "" // empty => not selected
viewModel.selectedSubdistrict = selectedId
validateRequiredFields()
}
override fun onNothingSelected(parent: AdapterView<*>?) {
@ -609,63 +645,31 @@ class RegisterStoreActivity : AppCompatActivity() {
binding.spinnerBankName.adapter = bankAdapter
binding.spinnerBankName.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(
parent: AdapterView<*>?,
view: View?,
position: Int,
id: Long
) {
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 onItemSelected(p: AdapterView<*>?, v: View?, pos: Int, id: Long) {
val bankName = bankAdapter.getBankName(pos)
viewModel.bankName.value = bankName
viewModel.selectedBankName = bankName
validateRequiredFields()
}
override fun onNothingSelected(parent: AdapterView<*>?) { /* no-op */ }
}
tryApplyBank()
// Add initial hints to the spinners
if (provinceAdapter.isEmpty) {
Log.d(TAG, "Adding default province hint")
provinceAdapter.add("Pilih Provinsi")
}
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")
}
// // Add initial hints to the spinners
// if (provinceAdapter.isEmpty) provinceAdapter.add("Pilih Provinsi")
// if (cityAdapter.isEmpty) cityAdapter.add("Pilih Kabupaten/Kota")
// if (subdistrictAdapter.isEmpty) subdistrictAdapter.add("Pilih Kecamatan")
// if (bankAdapter.isEmpty) bankAdapter.add("Pilih Bank")
Log.d(TAG, "setupSpinners: Province and city spinners setup completed")
}
private fun maybeFinishRestoring() {
if (provinceApplied && cityApplied && subdistrictApplied) {
isRestoringSelections = false
}
}
private fun setupDocumentUploads() {
Log.d(TAG, "setupDocumentUploads: Setting up document upload buttons")
@ -805,6 +809,7 @@ class RegisterStoreActivity : AppCompatActivity() {
override fun afterTextChanged(s: Editable?) {
viewModel.storeDescription.value = s.toString()
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 onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
override fun afterTextChanged(s: Editable?) {
try {
viewModel.postalCode.value = s.toString().toInt()
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()
}
viewModel.postalCode.value = s.toString().toIntOrNull() ?: 0
validateRequiredFields()
}
})
@ -839,6 +838,7 @@ class RegisterStoreActivity : AppCompatActivity() {
override fun afterTextChanged(s: Editable?) {
viewModel.addressDetail.value = s.toString()
Log.d(TAG, "Address detail updated: ${s.toString()}")
validateRequiredFields()
}
})
@ -993,30 +993,41 @@ class RegisterStoreActivity : AppCompatActivity() {
}
private fun doUpdateStoreProfile() {
val nameBody: RequestBody = (viewModel.storeName.value ?: "")
.toRequestBody("text/plain".toMediaTypeOrNull())
val typeBody: RequestBody = ((viewModel.storeTypeId.value ?: 0).toString())
.toRequestBody("text/plain".toMediaTypeOrNull())
val descBody: RequestBody = (viewModel.storeDescription.value ?: "")
.toRequestBody("text/plain".toMediaTypeOrNull())
val onLeaveBody: RequestBody = "false"
.toRequestBody("text/plain".toMediaTypeOrNull())
// --- Text parts ---
val nameBody: RequestBody = (viewModel.storeName.value ?: "").toRequestBody("text/plain".toMediaTypeOrNull())
val typeBody: RequestBody = ((viewModel.storeTypeId.value ?: 0).toString()).toRequestBody("text/plain".toMediaTypeOrNull())
val descBody: RequestBody = (viewModel.storeDescription.value ?: "").toRequestBody("text/plain".toMediaTypeOrNull())
// NOTE: is_on_leave is NOT part of approval multipart; keep separate if needed
// --- Build Multipart for store image (optional) ---
// Prefer compressing images to keep payload small; fall back to raw copy if needed.
val latBody: RequestBody = (viewModel.latitude.value ?: "").toRequestBody("text/plain".toMediaTypeOrNull())
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 ->
try {
// (A) Optional safety check: only allow jpg/png/webp
val allowed = Regex("^(jpg|jpeg|png|webp)$", RegexOption.IGNORE_CASE)
if (!ImageUtils.isAllowedFileType(this, uri, allowed)) {
Toast.makeText(this, "Format gambar tidak didukung", Toast.LENGTH_SHORT).show()
null
} else {
// (B) Compress for upload (ke cacheDir), then build multipart
val compressed: File = ImageUtils.compressImage(
context = this,
uri = uri,
filename = "storeimg", // prefix
filename = "storeimg",
maxWidth = 1024,
maxHeight = 1024,
quality = 80
@ -1024,18 +1035,76 @@ class RegisterStoreActivity : AppCompatActivity() {
FileUtils.createMultipartFromFile("storeimg", compressed)
}
} catch (e: Exception) {
// If compression fails, try raw copy as fallback
val rawFile = FileUtils.createTempFileFromUri(this, uri)
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,
storeType = typeBody,
description = descBody,
isOnLeave = onLeaveBody,
storeImage = storeImgPart
storeType = typeBody,
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) {
@ -1049,6 +1118,7 @@ class RegisterStoreActivity : AppCompatActivity() {
}
}
companion object {
private const val TAG = "RegisterStoreActivity"
}

View File

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

View File

@ -6,6 +6,7 @@ import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.map
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.Store
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.MultipartBody
import okhttp3.RequestBody
import okhttp3.RequestBody.Companion.toRequestBody
import java.text.NumberFormat
import java.util.Locale
@ -199,6 +201,80 @@ class MyStoreViewModel(private val repository: MyStoreRepository): ViewModel() {
}
}
private fun String.toRequestBody(): RequestBody =
RequestBody.create("text/plain".toMediaTypeOrNull(), this)
private fun String.toPlain(): RequestBody =
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:hint="Isi stok produk di sini"
android:padding="8dp"
android:inputType="number"
style="@style/body_small" />
</LinearLayout>
@ -413,6 +414,7 @@
android:hint="Isi minimum pemesanan produk di sini"
android:padding="8dp"
style="@style/body_small"
android:inputType="number"
android:layout_marginTop="10dp" />
</LinearLayout>
@ -711,6 +713,7 @@
android:background="@drawable/bg_text_field"
android:hint="Isi minimum produk untuk mendapatkan harga grosir di sini"
android:padding="8dp"
android:inputType="number"
style="@style/body_small" />
</LinearLayout>

View File

@ -543,49 +543,6 @@
</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-->
<LinearLayout
android:layout_width="match_parent"
@ -654,6 +611,48 @@
</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 -->
<LinearLayout
android:layout_width="match_parent"
@ -697,48 +696,6 @@
</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 -->
<LinearLayout
android:layout_width="match_parent"
@ -915,73 +872,6 @@
</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 -->
<LinearLayout
android:layout_width="match_parent"
@ -1049,6 +939,73 @@
</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
android:layout_width="match_parent"
android:layout_height="wrap_content"