update registerstore worked

This commit is contained in:
shaulascr
2025-05-21 17:44:08 +07:00
parent 02b7d7559a
commit 4d195fe483
5 changed files with 540 additions and 109 deletions

View File

@ -22,9 +22,9 @@ class ApiConfig {
val client = OkHttpClient.Builder()
.addInterceptor(loggingInterceptor)
.addInterceptor(authInterceptor)
.connectTimeout(60, TimeUnit.SECONDS) // Increase to 60 seconds
.readTimeout(60, TimeUnit.SECONDS) // Increase to 60 seconds
.writeTimeout(60, TimeUnit.SECONDS)
.connectTimeout(180, TimeUnit.SECONDS) // 3 minutes
.readTimeout(300, TimeUnit.SECONDS) // 5 minutes
.writeTimeout(300, TimeUnit.SECONDS) // 5 minutes
.build()
val retrofit = Retrofit.Builder()

View File

@ -24,6 +24,9 @@ import com.alya.ecommerce_serang.data.api.response.customer.order.ListProvinceRe
import com.alya.ecommerce_serang.data.api.response.customer.profile.EditProfileResponse
import com.alya.ecommerce_serang.data.api.retrofit.ApiService
import com.alya.ecommerce_serang.utils.FileUtils
import com.alya.ecommerce_serang.utils.ImageUtils
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.MultipartBody
import okhttp3.RequestBody
@ -32,6 +35,9 @@ import okhttp3.RequestBody.Companion.toRequestBody
import java.io.File
class UserRepository(private val apiService: ApiService) {
private val ALLOWED_FILE_TYPES = Regex("^(jpeg|jpg|png|pdf)$", RegexOption.IGNORE_CASE)
//post data without message/response
suspend fun requestOtpRep(email: String): OtpResponse {
return apiService.getOTP(OtpRequest(email))
@ -84,7 +90,7 @@ class UserRepository(private val apiService: ApiService) {
cityId: Int,
provinceId: Int,
postalCode: Int,
detail: String?,
detail: String,
bankName: String,
bankNum: Int,
storeName: String,
@ -98,6 +104,17 @@ class UserRepository(private val apiService: ApiService) {
accountName: String
): Result<RegisterStoreResponse> {
return try {
Log.d("RegisterStoreRepo", "Registration params: " +
"storeName=$storeName, " +
"storeTypeId=$storeTypeId, " +
"location=($latitude,$longitude), " +
"address=$street, $subdistrict, cityId=$cityId, provinceId=$provinceId, " +
"postalCode=$postalCode, " +
"bankDetails=$bankName, $bankNum, $accountName, " +
"couriers=${couriers.joinToString()}, " +
"files: storeImg=${storeImg != null}, ktp=${ktp != null}, npwp=${npwp != null}, " +
"nib=${nib != null}, persetujuan=${persetujuan != null}, qris=${qris != null}")
val descriptionPart = description.toRequestBody("text/plain".toMediaTypeOrNull())
val storeTypeIdPart = storeTypeId.toString().toRequestBody("text/plain".toMediaTypeOrNull())
val latitudePart = latitude.toRequestBody("text/plain".toMediaTypeOrNull())
@ -107,7 +124,7 @@ class UserRepository(private val apiService: ApiService) {
val cityIdPart = cityId.toString().toRequestBody("text/plain".toMediaTypeOrNull())
val provinceIdPart = provinceId.toString().toRequestBody("text/plain".toMediaTypeOrNull())
val postalCodePart = postalCode.toString().toRequestBody("text/plain".toMediaTypeOrNull())
val detailPart = detail?.toRequestBody("text/plain".toMediaTypeOrNull())
val detailPart = detail.toRequestBody("text/plain".toMediaTypeOrNull())
val bankNamePart = bankName.toRequestBody("text/plain".toMediaTypeOrNull())
val bankNumPart = bankNum.toString().toRequestBody("text/plain".toMediaTypeOrNull())
val storeNamePart = storeName.toRequestBody("text/plain".toMediaTypeOrNull())
@ -116,89 +133,56 @@ class UserRepository(private val apiService: ApiService) {
// Create a Map for courier values
val courierMap = HashMap<String, RequestBody>()
couriers.forEach { courier ->
courierMap["couriers[]"] = courier.toRequestBody("text/plain".toMediaTypeOrNull())
couriers.forEachIndexed { index, courier ->
// Add index to make keys unique
courierMap["couriers[$index]"] = courier.toRequestBody("text/plain".toMediaTypeOrNull())
}
// Convert URIs to MultipartBody.Part
val storeImgPart = storeImg?.let {
val inputStream = context.contentResolver.openInputStream(it)
val file = File(context.cacheDir, "store_img_${System.currentTimeMillis()}")
inputStream?.use { input ->
file.outputStream().use { output ->
input.copyTo(output)
}
}
val mimeType = context.contentResolver.getType(it) ?: "application/octet-stream"
val requestFile = file.asRequestBody(mimeType.toMediaTypeOrNull())
MultipartBody.Part.createFormData("storeimg", file.name, requestFile)
val storeImgPart = try {
processImageFile(context, storeImg, "storeimg", "store_img")
} catch (e: IllegalArgumentException) {
return Result.Error(Exception("Foto toko: ${e.message}"))
}
val ktpPart = ktp?.let {
val inputStream = context.contentResolver.openInputStream(it)
val file = File(context.cacheDir, "ktp_${System.currentTimeMillis()}")
inputStream?.use { input ->
file.outputStream().use { output ->
input.copyTo(output)
}
}
val mimeType = context.contentResolver.getType(it) ?: "application/octet-stream"
val requestFile = file.asRequestBody(mimeType.toMediaTypeOrNull())
MultipartBody.Part.createFormData("ktp", file.name, requestFile)
val ktpPart = try {
processImageFile(context, ktp, "ktp", "ktp")
} catch (e: IllegalArgumentException) {
return Result.Error(Exception("KTP: ${e.message}"))
}
val npwpPart = npwp?.let {
val inputStream = context.contentResolver.openInputStream(it)
val file = File(context.cacheDir, "npwp_${System.currentTimeMillis()}")
inputStream?.use { input ->
file.outputStream().use { output ->
input.copyTo(output)
}
val npwpPart = try {
npwp?.let {
processDocumentFile(context, it, "npwp", "npwp")
}
val mimeType = context.contentResolver.getType(it) ?: "application/octet-stream"
val requestFile = file.asRequestBody(mimeType.toMediaTypeOrNull())
MultipartBody.Part.createFormData("npwp", file.name, requestFile)
} catch (e: IllegalArgumentException) {
return Result.Error(Exception("NPWP: ${e.message}"))
}
val nibPart = nib?.let {
val inputStream = context.contentResolver.openInputStream(it)
val file = File(context.cacheDir, "nib_${System.currentTimeMillis()}")
inputStream?.use { input ->
file.outputStream().use { output ->
input.copyTo(output)
}
}
val mimeType = context.contentResolver.getType(it) ?: "application/octet-stream"
val requestFile = file.asRequestBody(mimeType.toMediaTypeOrNull())
MultipartBody.Part.createFormData("nib", file.name, requestFile)
val nibPart = try {
processDocumentFile(context, nib, "nib", "nib")
} catch (e: IllegalArgumentException) {
return Result.Error(Exception("NIB: ${e.message}"))
}
val persetujuanPart = persetujuan?.let {
val inputStream = context.contentResolver.openInputStream(it)
val file = File(context.cacheDir, "persetujuan_${System.currentTimeMillis()}")
inputStream?.use { input ->
file.outputStream().use { output ->
input.copyTo(output)
}
val persetujuanPart = try {
persetujuan?.let {
processDocumentFile(context, it, "persetujuan", "persetujuan")
}
val mimeType = context.contentResolver.getType(it) ?: "application/octet-stream"
val requestFile = file.asRequestBody(mimeType.toMediaTypeOrNull())
MultipartBody.Part.createFormData("persetujuan", file.name, requestFile)
} catch (e: IllegalArgumentException) {
return Result.Error(Exception("Persetujuan: ${e.message}"))
}
val qrisPart = qris?.let {
val inputStream = context.contentResolver.openInputStream(it)
val file = File(context.cacheDir, "qris_${System.currentTimeMillis()}")
inputStream?.use { input ->
file.outputStream().use { output ->
input.copyTo(output)
}
val qrisPart = try {
qris?.let {
processDocumentFile(context, it, "qris", "qris")
}
val mimeType = context.contentResolver.getType(it) ?: "application/octet-stream"
val requestFile = file.asRequestBody(mimeType.toMediaTypeOrNull())
MultipartBody.Part.createFormData("qris", file.name, requestFile)
} catch (e: IllegalArgumentException) {
return Result.Error(Exception("QRIS: ${e.message}"))
}
Log.d("RegisterStoreRepo", "All parts prepared, making API call")
// Make the API call
val response = apiService.registerStore(
descriptionPart,
@ -210,7 +194,7 @@ class UserRepository(private val apiService: ApiService) {
cityIdPart,
provinceIdPart,
postalCodePart,
detailPart ?: "".toRequestBody("text/plain".toMediaTypeOrNull()),
detailPart,
bankNamePart,
bankNumPart,
storeNamePart,
@ -226,16 +210,128 @@ class UserRepository(private val apiService: ApiService) {
// Check if response is successful
if (response.isSuccessful) {
Log.d("RegisterStoreRepo", "Registration successful")
Result.Success(response.body() ?: throw Exception("Response body is null"))
} else {
Result.Error(Exception("Registration failed with code: ${response.code()}"))
val errorBody = response.errorBody()?.string() ?: "No error details"
Log.e("RegisterStore", "Registration failed: ${response.code()}, Error: $errorBody")
Result.Error(Exception("Registration failed with code: ${response.code()}\nDetails: $errorBody"))
}
} catch (e: Exception) {
Log.e("RegisterStoreRepo", "Registration exception", e)
Result.Error(e)
}
}
private suspend fun processImageFile(
context: Context,
uri: Uri?,
formName: String,
filePrefix: String
): MultipartBody.Part? {
if (uri == null) {
Log.d(TAG, "$formName is null, skipping")
return null
}
return withContext(Dispatchers.IO) {
try {
Log.d(TAG, "Processing $formName image")
// Check file type
val mimeType = context.contentResolver.getType(uri) ?: "application/octet-stream"
Log.d(TAG, "$formName MIME type: $mimeType")
// Validate file type
if (!ImageUtils.isAllowedFileType(context, uri, ALLOWED_FILE_TYPES)) {
Log.e(TAG, "$formName has invalid file type: $mimeType")
throw IllegalArgumentException("$formName hanya menerima file JPEG, JPG, atau PNG")
}
// Only compress image files, not PDFs
if (mimeType.startsWith("image/")) {
Log.d(TAG, "Compressing $formName image")
// Compress image
val compressedFile = ImageUtils.compressImage(
context = context,
uri = uri,
filename = filePrefix,
maxWidth = 1024,
maxHeight = 1024,
quality = 80
)
val requestFile = compressedFile.asRequestBody(mimeType.toMediaTypeOrNull())
Log.d(TAG, "$formName compressed size: ${compressedFile.length() / 1024} KB")
MultipartBody.Part.createFormData(formName, compressedFile.name, requestFile)
} else {
throw IllegalArgumentException("$formName harus berupa file gambar (JPEG, JPG, atau PNG)")
}
} catch (e: Exception) {
Log.e(TAG, "Error processing $formName image", e)
throw e
}
}
}
// Process document files (handle PDFs separately)
private suspend fun processDocumentFile(
context: Context,
uri: Uri?,
formName: String,
filePrefix: String
): MultipartBody.Part? {
if (uri == null) {
Log.d(TAG, "$formName is null, skipping")
return null
}
return withContext(Dispatchers.IO) {
try {
Log.d(TAG, "Processing $formName document")
val mimeType = context.contentResolver.getType(uri) ?: "application/octet-stream"
Log.d(TAG, "$formName MIME type: $mimeType")
// Validate file type
if (!ImageUtils.isAllowedFileType(context, uri, ALLOWED_FILE_TYPES)) {
Log.e(TAG, "$formName has invalid file type: $mimeType")
throw IllegalArgumentException("$formName hanya menerima file JPEG, JPG, PNG, atau PDF")
}
// For image documents, compress them
if (mimeType.startsWith("image/")) {
return@withContext processImageFile(context, uri, formName, filePrefix)
}
// For PDFs, copy as is
if (mimeType.contains("pdf")) {
val inputStream = context.contentResolver.openInputStream(uri)
val file = File(context.cacheDir, "${filePrefix}_${System.currentTimeMillis()}.pdf")
inputStream?.use { input ->
file.outputStream().use { output ->
input.copyTo(output)
}
}
Log.d(TAG, "$formName PDF size: ${file.length() / 1024} KB")
val requestFile = file.asRequestBody(mimeType.toMediaTypeOrNull())
MultipartBody.Part.createFormData(formName, file.name, requestFile)
} else {
throw IllegalArgumentException("$formName harus berupa file PDF atau gambar (JPEG, JPG, PNG)")
}
} catch (e: Exception) {
Log.e(TAG, "Error processing $formName document", e)
throw e
}
}
}
suspend fun login(email: String, password: String): Result<LoginResponse> {
return try {
val response = apiService.login(LoginRequest(email, password))

View File

@ -63,17 +63,22 @@ class RegisterStoreActivity : AppCompatActivity() {
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Log.d(TAG, "onCreate: Starting RegisterStoreActivity")
binding = ActivityRegisterStoreBinding.inflate(layoutInflater)
setContentView(binding.root)
sessionManager = SessionManager(this)
Log.d(TAG, "onCreate: SessionManager initialized")
WindowCompat.setDecorFitsSystemWindows(window, false)
Log.d(TAG, "onCreate: Window decoration set")
enableEdgeToEdge()
Log.d(TAG, "onCreate: Edge-to-edge enabled")
// Apply insets to your root layout
ViewCompat.setOnApplyWindowInsetsListener(binding.root) { view, windowInsets ->
Log.d(TAG, "onCreate: Applying window insets")
val systemBars = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
view.setPadding(
systemBars.left,
@ -86,43 +91,63 @@ class RegisterStoreActivity : AppCompatActivity() {
provinceAdapter = ProvinceAdapter(this)
cityAdapter = CityAdapter(this)
Log.d(TAG, "onCreate: Adapters initialized")
setupDataBinding()
Log.d(TAG, "onCreate: Data binding setup completed")
setupSpinners() // Location spinners
Log.d(TAG, "onCreate: Spinners setup completed")
// Setup observers
setupStoreTypesObserver() // Store type observer
setupObservers()
Log.d(TAG, "onCreate: Observers setup completed")
setupMap()
setupDocumentUploads()
setupCourierSelection()
Log.d(TAG, "onCreate: Map setup completed")
setupDocumentUploads()
Log.d(TAG, "onCreate: Document uploads setup completed")
setupCourierSelection()
Log.d(TAG, "onCreate: Courier selection setup completed")
Log.d(TAG, "onCreate: Fetching store types from API")
viewModel.fetchStoreTypes()
Log.d(TAG, "onCreate: Fetching provinces from API")
viewModel.getProvinces()
// Setup register button
binding.btnRegister.setOnClickListener {
Log.d(TAG, "Register button clicked")
if (viewModel.validateForm()) {
Log.d(TAG, "Form validation successful, proceeding with registration")
viewModel.registerStore(this)
} else {
Log.e(TAG, "Form validation failed")
Toast.makeText(this, "Harap lengkapi semua field yang wajib diisi", Toast.LENGTH_SHORT).show()
}
}
Log.d(TAG, "onCreate: RegisterStoreActivity setup completed")
}
private fun setupObservers() {
Log.d(TAG, "setupObservers: Setting up LiveData observers")
// Observe province state
viewModel.provincesState.observe(this) { state ->
when (state) {
is Result.Loading -> {
Log.d(TAG, "Loading provinces...")
Log.d(TAG, "setupObservers: Loading provinces...")
binding.provinceProgressBar?.visibility = View.VISIBLE
binding.spinnerProvince.isEnabled = false
}
is Result.Success -> {
Log.d(TAG, "Provinces loaded: ${state.data.size}")
Log.d(TAG, "setupObservers: Provinces loaded successfully: ${state.data.size} provinces")
binding.provinceProgressBar?.visibility = View.GONE
binding.spinnerProvince.isEnabled = true
@ -130,11 +155,9 @@ class RegisterStoreActivity : AppCompatActivity() {
provinceAdapter.updateData(state.data)
}
is Result.Error -> {
// Log.e(TAG, "Error loading provinces: ${state.}")
Log.e(TAG, "setupObservers: Error loading provinces: ${state.exception.message}")
binding.provinceProgressBar?.visibility = View.GONE
binding.spinnerProvince.isEnabled = true
// Toast.makeText(this, "Gagal memuat provinsi: ${state.message}", Toast.LENGTH_SHORT).show()
}
}
}
@ -143,12 +166,12 @@ class RegisterStoreActivity : AppCompatActivity() {
viewModel.citiesState.observe(this) { state ->
when (state) {
is Result.Loading -> {
Log.d(TAG, "Loading cities...")
Log.d(TAG, "setupObservers: Loading cities...")
binding.cityProgressBar?.visibility = View.VISIBLE
binding.spinnerCity.isEnabled = false
}
is Result.Success -> {
Log.d(TAG, "Cities loaded: ${state.data.size}")
Log.d(TAG, "setupObservers: Cities loaded successfully: ${state.data.size} cities")
binding.cityProgressBar?.visibility = View.GONE
binding.spinnerCity.isEnabled = true
@ -156,11 +179,9 @@ class RegisterStoreActivity : AppCompatActivity() {
cityAdapter.updateData(state.data)
}
is Result.Error -> {
// Log.e(TAG, "Error loading cities: ${state.message}")
Log.e(TAG, "setupObservers: Error loading cities: ${state.exception.message}")
binding.cityProgressBar?.visibility = View.GONE
binding.spinnerCity.isEnabled = true
// Toast.makeText(this, "Gagal memuat kota: ${state.message}", Toast.LENGTH_SHORT).show()
}
}
}
@ -169,29 +190,38 @@ class RegisterStoreActivity : AppCompatActivity() {
viewModel.registerState.observe(this) { result ->
when (result) {
is Result.Loading -> {
Log.d(TAG, "setupObservers: Store registration in progress...")
showLoading(true)
}
is Result.Success -> {
Log.d(TAG, "setupObservers: Store registration successful")
showLoading(false)
Toast.makeText(this, "Toko berhasil didaftarkan", Toast.LENGTH_SHORT).show()
finish() // Return to previous screen
}
is Result.Error -> {
Log.e(TAG, "setupObservers: Store registration failed: ${result.exception.message}")
showLoading(false)
Toast.makeText(this, "Gagal mendaftarkan toko: ${result.exception.message}", Toast.LENGTH_SHORT).show()
}
}
}
Log.d(TAG, "setupObservers: Observers setup completed")
}
private fun setupStoreTypesObserver() {
Log.d(TAG, "setupStoreTypesObserver: Setting up store types observer")
// Observe loading state
viewModel.isLoadingType.observe(this) { isLoading ->
if (isLoading) {
Log.d(TAG, "setupStoreTypesObserver: Loading store types...")
// Show loading indicator for store types spinner
binding.spinnerStoreType.isEnabled = false
binding.storeTypeProgressBar?.visibility = View.VISIBLE
} else {
Log.d(TAG, "setupStoreTypesObserver: Store types loading completed")
binding.spinnerStoreType.isEnabled = true
binding.storeTypeProgressBar?.visibility = View.GONE
}
@ -200,30 +230,37 @@ class RegisterStoreActivity : AppCompatActivity() {
// Observe error messages
viewModel.errorMessage.observe(this) { errorMsg ->
if (errorMsg.isNotEmpty()) {
Log.e(TAG, "setupStoreTypesObserver: Error loading store types: $errorMsg")
Toast.makeText(this, "Error loading store types: $errorMsg", Toast.LENGTH_SHORT).show()
}
}
// Observe store types data
viewModel.storeTypes.observe(this) { storeTypes ->
Log.d(TAG, "Store types loaded: ${storeTypes.size}")
Log.d(TAG, "setupStoreTypesObserver: Store types loaded: ${storeTypes.size}")
if (storeTypes.isNotEmpty()) {
// Add "Pilih Jenis UMKM" as the first item if it's not already there
val displayList = if (storeTypes.any { it.name == "Pilih Jenis UMKM" || it.id == 0 }) {
Log.d(TAG, "setupStoreTypesObserver: Default item already exists in store types list")
storeTypes
} else {
Log.d(TAG, "setupStoreTypesObserver: Adding default item to store types list")
val defaultItem = StoreTypesItem(name = "Pilih Jenis UMKM", id = 0)
listOf(defaultItem) + storeTypes
}
// Setup spinner with API data
setupStoreTypeSpinner(displayList)
} else {
Log.w(TAG, "setupStoreTypesObserver: Received empty store types list")
}
}
Log.d(TAG, "setupStoreTypesObserver: Store types observer setup completed")
}
private fun setupStoreTypeSpinner(storeTypes: List<StoreTypesItem>) {
Log.d(TAG, "Setting up store type spinner with ${storeTypes.size} items")
Log.d(TAG, "setupStoreTypeSpinner: Setting up store type spinner with ${storeTypes.size} items")
// Create a custom adapter to display just the name but hold the whole object
val adapter = object : ArrayAdapter<StoreTypesItem>(
@ -250,9 +287,11 @@ class RegisterStoreActivity : AppCompatActivity() {
}
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
Log.d(TAG, "setupStoreTypeSpinner: Store type adapter created")
// Set adapter to spinner
binding.spinnerStoreType.adapter = adapter
Log.d(TAG, "setupStoreTypeSpinner: Adapter set to spinner")
// Set item selection listener
binding.spinnerStoreType.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
@ -264,6 +303,8 @@ class RegisterStoreActivity : AppCompatActivity() {
// 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")
}
}
@ -274,9 +315,12 @@ class RegisterStoreActivity : AppCompatActivity() {
// Hide progress bar after setup
binding.storeTypeProgressBar?.visibility = View.GONE
Log.d(TAG, "setupStoreTypeSpinner: Store type spinner setup completed")
}
private fun setupSpinners() {
Log.d(TAG, "setupSpinners: Setting up province and city spinners")
// Setup province spinner
binding.spinnerProvince.adapter = provinceAdapter
binding.spinnerProvince.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
@ -286,9 +330,11 @@ class RegisterStoreActivity : AppCompatActivity() {
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 {
@ -297,7 +343,7 @@ class RegisterStoreActivity : AppCompatActivity() {
}
override fun onNothingSelected(parent: AdapterView<*>?) {
// Do nothing
Log.d(TAG, "No province selected")
}
}
@ -317,73 +363,74 @@ class RegisterStoreActivity : AppCompatActivity() {
}
override fun onNothingSelected(parent: AdapterView<*>?) {
// Do nothing
Log.d(TAG, "No city selected")
}
}
// 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")
}
Log.d(TAG, "setupSpinners: Province and city spinners setup completed")
}
// private fun setupSubdistrictSpinner(cityId: Int) {
// // This would typically be populated from API based on cityId
// val subdistricts = listOf("Pilih Kecamatan", "Kecamatan 1", "Kecamatan 2", "Kecamatan 3")
// val subdistrictAdapter = ArrayAdapter(this, R.layout.simple_spinner_dropdown_item, subdistricts)
// binding.spinnerSubdistrict.adapter = subdistrictAdapter
// binding.spinnerSubdistrict.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
// override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
// if (position > 0) {
// viewModel.subdistrict.value = subdistricts[position]
// }
// }
// override fun onNothingSelected(parent: AdapterView<*>?) {}
// }
// }
private fun setupDocumentUploads() {
Log.d(TAG, "setupDocumentUploads: Setting up document upload buttons")
// Store Image
binding.containerStoreImg.setOnClickListener {
Log.d(TAG, "Store image container clicked, picking image")
pickImage(PICK_STORE_IMAGE_REQUEST)
}
// KTP
binding.containerKtp.setOnClickListener {
Log.d(TAG, "KTP container clicked, picking image")
pickImage(PICK_KTP_REQUEST)
}
// NIB
binding.containerNib.setOnClickListener {
Log.d(TAG, "NIB container clicked, picking document")
pickDocument(PICK_NIB_REQUEST)
}
// NPWP
binding.containerNpwp?.setOnClickListener {
Log.d(TAG, "NPWP container clicked, picking image")
pickImage(PICK_NPWP_REQUEST)
}
// SPPIRT
binding.containerSppirt.setOnClickListener {
Log.d(TAG, "SPPIRT container clicked, picking document")
pickDocument(PICK_PERSETUJUAN_REQUEST)
}
// Halal
binding.containerHalal.setOnClickListener {
Log.d(TAG, "Halal container clicked, picking document")
pickDocument(PICK_QRIS_REQUEST)
}
Log.d(TAG, "setupDocumentUploads: Document upload buttons setup completed")
}
private fun pickImage(requestCode: Int) {
Log.d(TAG, "pickImage: Launching image picker with request code: $requestCode")
val intent = Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI)
startActivityForResult(intent, requestCode)
}
private fun pickDocument(requestCode: Int) {
Log.d(TAG, "pickDocument: Launching document picker with request code: $requestCode")
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT)
intent.addCategory(Intent.CATEGORY_OPENABLE)
intent.type = "*/*"
@ -393,66 +440,87 @@ class RegisterStoreActivity : AppCompatActivity() {
}
private fun setupCourierSelection() {
Log.d(TAG, "setupCourierSelection: Setting up courier checkboxes")
binding.checkboxJne.setOnCheckedChangeListener { _, isChecked ->
Log.d(TAG, "JNE checkbox ${if (isChecked) "checked" else "unchecked"}")
handleCourierSelection("jne", isChecked)
}
binding.checkboxJnt.setOnCheckedChangeListener { _, isChecked ->
Log.d(TAG, "JNT checkbox ${if (isChecked) "checked" else "unchecked"}")
handleCourierSelection("tiki", isChecked)
}
binding.checkboxPos.setOnCheckedChangeListener { _, isChecked ->
Log.d(TAG, "POS checkbox ${if (isChecked) "checked" else "unchecked"}")
handleCourierSelection("pos", isChecked)
}
Log.d(TAG, "setupCourierSelection: Courier checkboxes setup completed")
}
private fun handleCourierSelection(courier: String, isSelected: Boolean) {
if (isSelected) {
if (!viewModel.selectedCouriers.contains(courier)) {
viewModel.selectedCouriers.add(courier)
Log.d(TAG, "handleCourierSelection: Added courier: $courier. Current couriers: ${viewModel.selectedCouriers}")
}
} else {
viewModel.selectedCouriers.remove(courier)
Log.d(TAG, "handleCourierSelection: Removed courier: $courier. Current couriers: ${viewModel.selectedCouriers}")
}
}
private fun setupMap() {
Log.d(TAG, "setupMap: Setting up map container")
// This would typically integrate with Google Maps SDK
// For simplicity, we're just using a placeholder
binding.mapContainer.setOnClickListener {
Log.d(TAG, "Map container clicked, checking location permission")
// Request location permission if not granted
if (ContextCompat.checkSelfPermission(
this,
Manifest.permission.ACCESS_FINE_LOCATION
) != PackageManager.PERMISSION_GRANTED
) {
Log.d(TAG, "Location permission not granted, requesting permission")
ActivityCompat.requestPermissions(
this,
arrayOf(Manifest.permission.ACCESS_FINE_LOCATION),
LOCATION_PERMISSION_REQUEST
)
viewModel.latitude.value = "-6.2088"
viewModel.longitude.value = "106.8456"
Log.d(TAG, "Location permission granted, setting default location")
Toast.makeText(this, "Lokasi dipilih", Toast.LENGTH_SHORT).show()
Log.d(TAG, "Default location set - Lat: ${viewModel.latitude.value}, Long: ${viewModel.longitude.value}")
Toast.makeText(this, "Lokasi dipilih", Toast.LENGTH_SHORT).show()
} else {
Log.d(TAG, "Location permission already granted, setting location")
// Show map selection UI
// This would typically launch Maps UI for location selection
// For now, we'll just set some dummy coordinates
viewModel.latitude.value = "-6.2088"
viewModel.longitude.value = "106.8456"
Log.d(TAG, "Location set - Lat: ${viewModel.latitude.value}, Long: ${viewModel.longitude.value}")
Toast.makeText(this, "Lokasi dipilih", Toast.LENGTH_SHORT).show()
}
}
Log.d(TAG, "setupMap: Map container setup completed")
}
private fun setupDataBinding() {
Log.d(TAG, "setupDataBinding: Setting up two-way data binding for text fields")
// Two-way data binding for text fields
binding.etStoreName.addTextChangedListener(object : TextWatcher {
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?) {
viewModel.storeName.value = s.toString()
Log.d(TAG, "Store name updated: ${s.toString()}")
}
})
@ -461,6 +529,7 @@ class RegisterStoreActivity : AppCompatActivity() {
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
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 ""}")
}
})
@ -469,6 +538,7 @@ class RegisterStoreActivity : AppCompatActivity() {
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
override fun afterTextChanged(s: Editable?) {
viewModel.street.value = s.toString()
Log.d(TAG, "Street address updated: ${s.toString()}")
}
})
@ -478,9 +548,10 @@ class RegisterStoreActivity : AppCompatActivity() {
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
//show toast
Log.e(TAG, "Invalid postal code input: ${s.toString()}, error: $e")
}
}
})
@ -490,6 +561,7 @@ class RegisterStoreActivity : AppCompatActivity() {
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
override fun afterTextChanged(s: Editable?) {
viewModel.addressDetail.value = s.toString()
Log.d(TAG, "Address detail updated: ${s.toString()}")
}
})
@ -497,7 +569,20 @@ 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?) {
viewModel.bankNumber.value = s.toString().toInt()
val input = s.toString()
if (input.isNotEmpty()) {
try {
viewModel.bankNumber.value = input.toInt()
Log.d(TAG, "Bank number updated: $input")
} catch (e: NumberFormatException) {
// Handle invalid input if needed
Log.e(TAG, "Failed to parse bank number. Input: $input, Error: $e")
}
} else {
// Handle empty input - perhaps set to 0 or null depending on your requirements
viewModel.bankNumber.value = 0 // or 0
Log.d(TAG, "Bank number set to default: 0")
}
}
})
@ -506,6 +591,7 @@ class RegisterStoreActivity : AppCompatActivity() {
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
override fun afterTextChanged(s: Editable?) {
viewModel.subdistrict.value = s.toString()
Log.d(TAG, "Subdistrict updated: ${s.toString()}")
}
})
@ -513,46 +599,63 @@ 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?) {
viewModel.subdistrict.value = s.toString()
viewModel.bankName.value = s.toString()
Log.d(TAG, "Bank name updated: ${s.toString()}")
}
})
Log.d(TAG, "setupDataBinding: Text field data binding setup completed")
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
Log.d(TAG, "onActivityResult: Request code: $requestCode, Result code: $resultCode")
if (resultCode == Activity.RESULT_OK && data != null) {
val uri = data.data
Log.d(TAG, "onActivityResult: URI received: $uri")
when (requestCode) {
PICK_STORE_IMAGE_REQUEST -> {
Log.d(TAG, "Store image selected")
viewModel.storeImageUri = uri
updateImagePreview(uri, binding.imgStore, binding.layoutUploadStoreImg)
}
PICK_KTP_REQUEST -> {
Log.d(TAG, "KTP image selected")
viewModel.ktpUri = uri
updateImagePreview(uri, binding.imgKtp, binding.layoutUploadKtp)
}
PICK_NPWP_REQUEST -> {
Log.d(TAG, "NPWP document selected")
viewModel.npwpUri = uri
updateDocumentPreview(binding.layoutUploadNpwp)
}
PICK_NIB_REQUEST -> {
Log.d(TAG, "NIB document selected")
viewModel.nibUri = uri
updateDocumentPreview(binding.layoutUploadNib)
}
PICK_PERSETUJUAN_REQUEST -> {
Log.d(TAG, "SPPIRT document selected")
viewModel.persetujuanUri = uri
updateDocumentPreview(binding.layoutUploadSppirt)
}
PICK_QRIS_REQUEST -> {
Log.d(TAG, "Halal document selected")
viewModel.qrisUri = uri
updateDocumentPreview(binding.layoutUploadHalal)
}
else -> {
Log.w(TAG, "Unknown request code: $requestCode")
}
}
} else {
Log.w(TAG, "File selection canceled or failed")
}
}
private fun updateImagePreview(uri: Uri?, imageView: ImageView, uploadLayout: LinearLayout) {
uri?.let {
Log.d(TAG, "updateImagePreview: Setting image URI: $uri")
imageView.setImageURI(it)
imageView.visibility = View.VISIBLE
uploadLayout.visibility = View.GONE
@ -560,6 +663,7 @@ class RegisterStoreActivity : AppCompatActivity() {
}
private fun updateDocumentPreview(uploadLayout: LinearLayout) {
Log.d(TAG, "updateDocumentPreview: Updating document preview UI")
// For documents, we just show a success indicator
val checkIcon = ImageView(this)
checkIcon.setImageResource(android.R.drawable.ic_menu_gallery)
@ -569,6 +673,7 @@ class RegisterStoreActivity : AppCompatActivity() {
uploadLayout.removeAllViews()
uploadLayout.addView(checkIcon)
uploadLayout.addView(successText)
Log.d(TAG, "updateDocumentPreview: Document preview updated with success indicator")
}
//later implement get location form gps

View File

@ -13,6 +13,7 @@ import com.alya.ecommerce_serang.data.api.response.customer.order.CitiesItem
import com.alya.ecommerce_serang.data.api.response.customer.order.ProvincesItem
import com.alya.ecommerce_serang.data.repository.Result
import com.alya.ecommerce_serang.data.repository.UserRepository
import com.alya.ecommerce_serang.utils.ImageUtils
import kotlinx.coroutines.launch
class RegisterStoreViewModel(
@ -71,6 +72,44 @@ class RegisterStoreViewModel(
val selectedCouriers = mutableListOf<String>()
fun registerStore(context: Context) {
val allowedFileTypes = Regex("^(jpeg|jpg|png|pdf)$", RegexOption.IGNORE_CASE)
// Check each file if present
if (storeImageUri != null && !ImageUtils.isAllowedFileType(context, storeImageUri, allowedFileTypes)) {
_errorMessage.value = "Foto toko harus berupa file JPEG, JPG, atau PNG"
_registerState.value = Result.Error(Exception(_errorMessage.value ?: "Invalid file type"))
return
}
if (ktpUri != null && !ImageUtils.isAllowedFileType(context, ktpUri, allowedFileTypes)) {
_errorMessage.value = "KTP harus berupa file JPEG, JPG, atau PNG"
_registerState.value = Result.Error(Exception(_errorMessage.value ?: "Invalid file type"))
return
}
if (npwpUri != null && !ImageUtils.isAllowedFileType(context, npwpUri, allowedFileTypes)) {
_errorMessage.value = "NPWP harus berupa file JPEG, JPG, PNG, atau PDF"
_registerState.value = Result.Error(Exception(_errorMessage.value ?: "Invalid file type"))
return
}
if (nibUri != null && !ImageUtils.isAllowedFileType(context, nibUri, allowedFileTypes)) {
_errorMessage.value = "NIB harus berupa file JPEG, JPG, PNG, atau PDF"
_registerState.value = Result.Error(Exception(_errorMessage.value ?: "Invalid file type"))
return
}
if (persetujuanUri != null && !ImageUtils.isAllowedFileType(context, persetujuanUri, allowedFileTypes)) {
_errorMessage.value = "Persetujuan harus berupa file JPEG, JPG, PNG, atau PDF"
_registerState.value = Result.Error(Exception(_errorMessage.value ?: "Invalid file type"))
return
}
if (qrisUri != null && !ImageUtils.isAllowedFileType(context, qrisUri, allowedFileTypes)) {
_errorMessage.value = "QRIS harus berupa file JPEG, JPG, PNG, atau PDF"
_registerState.value = Result.Error(Exception(_errorMessage.value ?: "Invalid file type"))
return
}
viewModelScope.launch {
try {
_registerState.value = Result.Loading
@ -86,7 +125,7 @@ class RegisterStoreViewModel(
cityId = cityId.value ?: 0,
provinceId = provinceId.value ?: 0,
postalCode = postalCode.value ?: 0,
detail = addressDetail.value,
detail = addressDetail.value ?: "",
bankName = bankName.value ?: "",
bankNum = bankNumber.value ?: 0,
storeName = storeName.value ?: "",

View File

@ -0,0 +1,191 @@
package com.alya.ecommerce_serang.utils
import android.content.Context
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.net.Uri
import android.util.Log
import java.io.File
import java.io.FileOutputStream
import java.util.Locale
import kotlin.math.min
object ImageUtils {
private const val TAG = "ImageUtils"
private const val MAX_WIDTH = 1024
private const val MAX_HEIGHT = 1024
private const val QUALITY = 80
/**
* Compresses an image from a Uri
*
* @param context The context
* @param uri The URI of the image to compress
* @param maxWidth Maximum width (default 1024px)
* @param maxHeight Maximum height (default 1024px)
* @param quality JPEG quality (0-100, default 80)
* @return A File containing the compressed image
*/
fun compressImage(
context: Context,
uri: Uri,
filename: String,
maxWidth: Int = MAX_WIDTH,
maxHeight: Int = MAX_HEIGHT,
quality: Int = QUALITY
): File {
Log.d(TAG, "Starting image compression for $filename")
// Create input stream and decode the original bitmap
val inputStream = context.contentResolver.openInputStream(uri)
val options = BitmapFactory.Options().apply {
inJustDecodeBounds = true
}
// First decode with inJustDecodeBounds=true to check dimensions
BitmapFactory.decodeStream(inputStream, null, options)
inputStream?.close()
val originalWidth = options.outWidth
val originalHeight = options.outHeight
val mimeType = options.outMimeType ?: "image/jpeg"
Log.d(TAG, "Original size: ${originalWidth}x${originalHeight}, mime: $mimeType")
// Calculate inSampleSize based on required dimensions
val inSampleSize = calculateInSampleSize(options, maxWidth, maxHeight)
// Open a new input stream since we closed the previous one
val newInputStream = context.contentResolver.openInputStream(uri)
// Decode with actual sampling
val decodingOptions = BitmapFactory.Options().apply {
this.inSampleSize = inSampleSize
this.inPreferredConfig = Bitmap.Config.ARGB_8888
}
val sampledBitmap = BitmapFactory.decodeStream(newInputStream, null, decodingOptions)
?: throw IllegalArgumentException("Failed to decode bitmap from URI")
newInputStream?.close()
Log.d(TAG, "Decoded size: ${sampledBitmap.width}x${sampledBitmap.height}, sample size: $inSampleSize")
// Create output file
val extension = when {
mimeType.contains("png") -> ".png"
mimeType.contains("webp") -> ".webp"
else -> ".jpg"
}
val outputFile = File(context.cacheDir, "${filename}_${System.currentTimeMillis()}$extension")
// Scale if still needed (in case inSampleSize couldn't get exact dimensions)
val scaledBitmap = if (sampledBitmap.width > maxWidth || sampledBitmap.height > maxHeight) {
val widthRatio = maxWidth.toFloat() / sampledBitmap.width
val heightRatio = maxHeight.toFloat() / sampledBitmap.height
val scaleFactor = min(widthRatio, heightRatio)
val scaledWidth = (sampledBitmap.width * scaleFactor).toInt()
val scaledHeight = (sampledBitmap.height * scaleFactor).toInt()
Log.d(TAG, "Scaling to: ${scaledWidth}x${scaledHeight}")
Bitmap.createScaledBitmap(sampledBitmap, scaledWidth, scaledHeight, true)
} else {
sampledBitmap
}
// Save to file with compression
val format = when {
mimeType.contains("png") -> Bitmap.CompressFormat.PNG
mimeType.contains("webp") && android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.R ->
Bitmap.CompressFormat.WEBP_LOSSY
mimeType.contains("webp") -> Bitmap.CompressFormat.WEBP
else -> Bitmap.CompressFormat.JPEG
}
FileOutputStream(outputFile).use { out ->
scaledBitmap.compress(format, quality, out)
out.flush()
}
// Clean up
if (scaledBitmap != sampledBitmap) {
scaledBitmap.recycle()
}
sampledBitmap.recycle()
val originalSize = getUriFileSize(context, uri)
val compressedSize = outputFile.length()
Log.d(TAG, "Compression complete. Original size: ${originalSize/1024}KB, " +
"Compressed size: ${compressedSize/1024}KB, " +
"Reduction: ${(100 - (compressedSize * 100 / originalSize))}%")
return outputFile
}
/**
* Calculate the optimal inSampleSize value for bitmap downsampling
*/
private fun calculateInSampleSize(options: BitmapFactory.Options, maxWidth: Int, maxHeight: Int): Int {
val height = options.outHeight
val width = options.outWidth
var inSampleSize = 1
if (height > maxHeight || width > maxWidth) {
val heightRatio = Math.round(height.toFloat() / maxHeight.toFloat())
val widthRatio = Math.round(width.toFloat() / maxWidth.toFloat())
inSampleSize = if (heightRatio < widthRatio) heightRatio else widthRatio
}
// Ensure power of 2 for better performance
var powerOf2 = 1
while (powerOf2 * 2 <= inSampleSize) {
powerOf2 *= 2
}
return powerOf2
}
/**
* Get the file size from a Uri
*/
private fun getUriFileSize(context: Context, uri: Uri): Long {
val cursor = context.contentResolver.query(uri, null, null, null, null)
val sizeIndex = cursor?.getColumnIndex(android.provider.OpenableColumns.SIZE)
cursor?.moveToFirst()
val size = if (sizeIndex != null && sizeIndex >= 0) {
cursor.getLong(sizeIndex)
} else {
// If size can't be determined from cursor, read stream length
context.contentResolver.openInputStream(uri)?.use { it.available().toLong() } ?: 0L
}
cursor?.close()
return size
}
fun isAllowedFileType(context: Context, uri: Uri?, allowedTypes: Regex): Boolean {
if (uri == null) return false
val mimeType = context.contentResolver.getType(uri) ?: ""
Log.d(TAG, "Checking file type: $mimeType")
// Get file extension from mime type
val extension = when {
mimeType.contains("jpeg") || mimeType.contains("jpg") -> "jpg"
mimeType.contains("png") -> "png"
mimeType.contains("pdf") -> "pdf"
else -> {
// If mime type is not helpful, try to get extension from URI
val fileName = uri.path?.substringAfterLast('/') ?: ""
fileName.substringAfterLast('.', "").lowercase(Locale.ROOT)
}
}
val isAllowed = allowedTypes.matches(extension)
Log.d(TAG, "File extension: $extension, Allowed: $isAllowed")
return isAllowed
}
}