mirror of
https://github.com/shaulascr/ecommerce_serang.git
synced 2025-08-10 09:22:21 +00:00
update registerstore worked
This commit is contained in:
@ -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()
|
||||
|
@ -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))
|
||||
|
@ -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
|
||||
|
@ -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 ?: "",
|
||||
|
191
app/src/main/java/com/alya/ecommerce_serang/utils/ImageUtils.kt
Normal file
191
app/src/main/java/com/alya/ecommerce_serang/utils/ImageUtils.kt
Normal 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
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user