From e71a39747cba41ccc349a1eb1a998424ad027970 Mon Sep 17 00:00:00 2001 From: shaulascr Date: Thu, 15 May 2025 03:41:38 +0700 Subject: [PATCH 1/5] add registerstoreuser --- app/src/main/AndroidManifest.xml | 8 +- .../api/response/auth/HasStoreResponse.kt | 9 + .../response/auth/ListStoreTypeResponse.kt | 21 + .../response/auth/RegisterStoreResponse.kt | 57 ++ .../data/api/retrofit/ApiService.kt | 42 +- .../data/repository/UserRepository.kt | 201 ++++++ .../ui/auth/RegisterStoreActivity.kt | 601 ++++++++++++++++++ .../ui/auth/RegisterStoreViewModel.kt | 202 ++++++ .../ui/profile/ProfileFragment.kt | 16 +- .../utils/viewmodel/ProfileViewModel.kt | 26 + .../res/layout/activity_register_store.xml | 578 +++++++++++++++++ 11 files changed, 1751 insertions(+), 10 deletions(-) create mode 100644 app/src/main/java/com/alya/ecommerce_serang/data/api/response/auth/HasStoreResponse.kt create mode 100644 app/src/main/java/com/alya/ecommerce_serang/data/api/response/auth/ListStoreTypeResponse.kt create mode 100644 app/src/main/java/com/alya/ecommerce_serang/data/api/response/auth/RegisterStoreResponse.kt create mode 100644 app/src/main/java/com/alya/ecommerce_serang/ui/auth/RegisterStoreActivity.kt create mode 100644 app/src/main/java/com/alya/ecommerce_serang/ui/auth/RegisterStoreViewModel.kt create mode 100644 app/src/main/res/layout/activity_register_store.xml diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 6a794d7..5ce0012 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -29,6 +29,9 @@ android:theme="@style/Theme.Ecommerce_serang" android:usesCleartextTraffic="true" tools:targetApi="31"> + @@ -64,7 +67,7 @@ + android:exported="false" /> @@ -157,6 +160,7 @@ + @@ -168,6 +172,4 @@ android:value="fcm_default_channel" /> - - \ No newline at end of file diff --git a/app/src/main/java/com/alya/ecommerce_serang/data/api/response/auth/HasStoreResponse.kt b/app/src/main/java/com/alya/ecommerce_serang/data/api/response/auth/HasStoreResponse.kt new file mode 100644 index 0000000..cd0a881 --- /dev/null +++ b/app/src/main/java/com/alya/ecommerce_serang/data/api/response/auth/HasStoreResponse.kt @@ -0,0 +1,9 @@ +package com.alya.ecommerce_serang.data.api.response.auth + +import com.google.gson.annotations.SerializedName + +data class HasStoreResponse( + + @field:SerializedName("hasStore") + val hasStore: Boolean +) diff --git a/app/src/main/java/com/alya/ecommerce_serang/data/api/response/auth/ListStoreTypeResponse.kt b/app/src/main/java/com/alya/ecommerce_serang/data/api/response/auth/ListStoreTypeResponse.kt new file mode 100644 index 0000000..c30af1f --- /dev/null +++ b/app/src/main/java/com/alya/ecommerce_serang/data/api/response/auth/ListStoreTypeResponse.kt @@ -0,0 +1,21 @@ +package com.alya.ecommerce_serang.data.api.response.auth + +import com.google.gson.annotations.SerializedName + +data class ListStoreTypeResponse( + + @field:SerializedName("storeTypes") + val storeTypes: List, + + @field:SerializedName("message") + val message: String +) + +data class StoreTypesItem( + + @field:SerializedName("name") + val name: String, + + @field:SerializedName("id") + val id: Int +) diff --git a/app/src/main/java/com/alya/ecommerce_serang/data/api/response/auth/RegisterStoreResponse.kt b/app/src/main/java/com/alya/ecommerce_serang/data/api/response/auth/RegisterStoreResponse.kt new file mode 100644 index 0000000..973e594 --- /dev/null +++ b/app/src/main/java/com/alya/ecommerce_serang/data/api/response/auth/RegisterStoreResponse.kt @@ -0,0 +1,57 @@ +package com.alya.ecommerce_serang.data.api.response.auth + +import com.google.gson.annotations.SerializedName + +data class RegisterStoreResponse( + + @field:SerializedName("store") + val store: Store, + + @field:SerializedName("message") + val message: String +) + +data class Store( + + @field:SerializedName("image") + val image: String, + + @field:SerializedName("ktp") + val ktp: String, + + @field:SerializedName("nib") + val nib: String, + + @field:SerializedName("npwp") + val npwp: String, + + @field:SerializedName("address_id") + val addressId: Int, + + @field:SerializedName("description") + val description: String, + + @field:SerializedName("store_type_id") + val storeTypeId: Int, + + @field:SerializedName("is_on_leave") + val isOnLeave: Boolean, + + @field:SerializedName("balance") + val balance: String, + + @field:SerializedName("user_id") + val userId: Int, + + @field:SerializedName("name") + val name: String, + + @field:SerializedName("persetujuan") + val persetujuan: String, + + @field:SerializedName("id") + val id: Int, + + @field:SerializedName("status") + val status: String +) diff --git a/app/src/main/java/com/alya/ecommerce_serang/data/api/retrofit/ApiService.kt b/app/src/main/java/com/alya/ecommerce_serang/data/api/retrofit/ApiService.kt index 05322c5..3c67690 100644 --- a/app/src/main/java/com/alya/ecommerce_serang/data/api/retrofit/ApiService.kt +++ b/app/src/main/java/com/alya/ecommerce_serang/data/api/retrofit/ApiService.kt @@ -21,9 +21,12 @@ import com.alya.ecommerce_serang.data.api.dto.StoreAddressResponse import com.alya.ecommerce_serang.data.api.dto.UpdateCart import com.alya.ecommerce_serang.data.api.dto.UpdateChatRequest import com.alya.ecommerce_serang.data.api.response.auth.CheckStoreResponse +import com.alya.ecommerce_serang.data.api.response.auth.HasStoreResponse +import com.alya.ecommerce_serang.data.api.response.auth.ListStoreTypeResponse import com.alya.ecommerce_serang.data.api.response.auth.LoginResponse import com.alya.ecommerce_serang.data.api.response.auth.OtpResponse import com.alya.ecommerce_serang.data.api.response.auth.RegisterResponse +import com.alya.ecommerce_serang.data.api.response.auth.RegisterStoreResponse import com.alya.ecommerce_serang.data.api.response.chat.ChatHistoryResponse import com.alya.ecommerce_serang.data.api.response.chat.ChatListResponse import com.alya.ecommerce_serang.data.api.response.chat.SendChatResponse @@ -73,6 +76,7 @@ import retrofit2.http.Multipart import retrofit2.http.POST import retrofit2.http.PUT import retrofit2.http.Part +import retrofit2.http.PartMap import retrofit2.http.Path import retrofit2.http.Query @@ -85,17 +89,41 @@ interface ApiService { @GET("checkstore") suspend fun checkStore (): Response -// @Multipart -// @POST("registerstore") -// suspend fun registerStore( -// -// ): Response<> + @Multipart + @POST("registerstore") + suspend fun registerStore( + @Part("description") description: RequestBody, + @Part("store_type_id") storeTypeId: RequestBody, + @Part("latitude") latitude: RequestBody, + @Part("longitude") longitude: RequestBody, + @Part("street") street: RequestBody, + @Part("subdistrict") subdistrict: RequestBody, + @Part("city_id") cityId: RequestBody, + @Part("province_id") provinceId: RequestBody, + @Part("postal_code") postalCode: RequestBody, + @Part("detail") detail: RequestBody, + @Part("bank_name") bankName: RequestBody, + @Part("bank_num") bankNum: RequestBody, + @Part("store_name") storeName: RequestBody, + @Part storeimg: MultipartBody.Part?, + @Part ktp: MultipartBody.Part?, + @Part npwp: MultipartBody.Part?, + @Part nib: MultipartBody.Part?, + @Part persetujuan: MultipartBody.Part?, + @PartMap couriers: Map, + @Part qris: MultipartBody.Part?, + @Part("account_name") accountName: RequestBody, + ): Response @POST("otp") suspend fun getOTP( @Body otpRequest: OtpRequest ):OtpResponse + @GET("checkstore") + suspend fun checkStoreUser( + ): HasStoreResponse + @POST("login") suspend fun login( @Body loginRequest: LoginRequest @@ -105,6 +133,10 @@ interface ApiService { suspend fun allCategory( ): Response + @GET("storetype") + suspend fun listTypeStore( + ): Response + @GET("product") suspend fun getAllProduct(): Response diff --git a/app/src/main/java/com/alya/ecommerce_serang/data/repository/UserRepository.kt b/app/src/main/java/com/alya/ecommerce_serang/data/repository/UserRepository.kt index ff202ee..8f16c34 100644 --- a/app/src/main/java/com/alya/ecommerce_serang/data/repository/UserRepository.kt +++ b/app/src/main/java/com/alya/ecommerce_serang/data/repository/UserRepository.kt @@ -7,13 +7,22 @@ import com.alya.ecommerce_serang.data.api.dto.LoginRequest import com.alya.ecommerce_serang.data.api.dto.OtpRequest import com.alya.ecommerce_serang.data.api.dto.RegisterRequest import com.alya.ecommerce_serang.data.api.dto.UserProfile +import com.alya.ecommerce_serang.data.api.response.auth.HasStoreResponse +import com.alya.ecommerce_serang.data.api.response.auth.ListStoreTypeResponse import com.alya.ecommerce_serang.data.api.response.auth.LoginResponse import com.alya.ecommerce_serang.data.api.response.auth.OtpResponse +import com.alya.ecommerce_serang.data.api.response.auth.RegisterStoreResponse +import com.alya.ecommerce_serang.data.api.response.customer.order.ListCityResponse +import com.alya.ecommerce_serang.data.api.response.customer.order.ListProvinceResponse 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 okhttp3.MediaType.Companion.toMediaTypeOrNull +import okhttp3.MultipartBody +import okhttp3.RequestBody +import okhttp3.RequestBody.Companion.asRequestBody import okhttp3.RequestBody.Companion.toRequestBody +import java.io.File class UserRepository(private val apiService: ApiService) { //post data without message/response @@ -21,6 +30,31 @@ class UserRepository(private val apiService: ApiService) { return apiService.getOTP(OtpRequest(email)) } + suspend fun listStoreType(): Result{ + return try{ + val response = apiService.listTypeStore() + if (response.isSuccessful) { + response.body()?.let { + Result.Success(it) + } ?: Result.Error(Exception("No store type")) + } else { + throw Exception("No response ${response.errorBody()?.string()}") + } + } catch (e:Exception){ + Result.Error(e) + } + } + + suspend fun getListProvinces(): ListProvinceResponse? { + val response = apiService.getListProv() + return if (response.isSuccessful) response.body() else null + } + + suspend fun getListCities(provId : Int): ListCityResponse? { + val response = apiService.getCityProvId(provId) + return if (response.isSuccessful) response.body() else null + } + suspend fun registerUser(request: RegisterRequest): String { val response = apiService.register(request) // API call @@ -32,6 +66,169 @@ class UserRepository(private val apiService: ApiService) { } } + suspend fun registerStoreUser( + context: Context, + description: String, + storeTypeId: Int, + latitude: String, + longitude: String, + street: String, + subdistrict: String, + cityId: Int, + provinceId: Int, + postalCode: Int, + detail: String?, + bankName: String, + bankNum: Int, + storeName: String, + storeImg: Uri?, + ktp: Uri?, + npwp: Uri?, + nib: Uri?, + persetujuan: Uri?, + couriers: List, + qris: Uri?, + accountName: String + ): Result { + return try { + val descriptionPart = description.toRequestBody("text/plain".toMediaTypeOrNull()) + val storeTypeIdPart = storeTypeId.toString().toRequestBody("text/plain".toMediaTypeOrNull()) + val latitudePart = latitude.toRequestBody("text/plain".toMediaTypeOrNull()) + val longitudePart = longitude.toRequestBody("text/plain".toMediaTypeOrNull()) + val streetPart = street.toRequestBody("text/plain".toMediaTypeOrNull()) + val subdistrictPart = subdistrict.toRequestBody("text/plain".toMediaTypeOrNull()) + 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 bankNamePart = bankName.toRequestBody("text/plain".toMediaTypeOrNull()) + val bankNumPart = bankNum.toString().toRequestBody("text/plain".toMediaTypeOrNull()) + val storeNamePart = storeName.toRequestBody("text/plain".toMediaTypeOrNull()) + val accountNamePart = accountName.toRequestBody("text/plain".toMediaTypeOrNull()) + + + // Create a Map for courier values + val courierMap = HashMap() + couriers.forEach { courier -> + courierMap["couriers[]"] = 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 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 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 mimeType = context.contentResolver.getType(it) ?: "application/octet-stream" + val requestFile = file.asRequestBody(mimeType.toMediaTypeOrNull()) + MultipartBody.Part.createFormData("npwp", file.name, requestFile) + } + + 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 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 mimeType = context.contentResolver.getType(it) ?: "application/octet-stream" + val requestFile = file.asRequestBody(mimeType.toMediaTypeOrNull()) + MultipartBody.Part.createFormData("persetujuan", file.name, requestFile) + } + + 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 mimeType = context.contentResolver.getType(it) ?: "application/octet-stream" + val requestFile = file.asRequestBody(mimeType.toMediaTypeOrNull()) + MultipartBody.Part.createFormData("qris", file.name, requestFile) + } + + // Make the API call + val response = apiService.registerStore( + descriptionPart, + storeTypeIdPart, + latitudePart, + longitudePart, + streetPart, + subdistrictPart, + cityIdPart, + provinceIdPart, + postalCodePart, + detailPart ?: "".toRequestBody("text/plain".toMediaTypeOrNull()), + bankNamePart, + bankNumPart, + storeNamePart, + storeImgPart, + ktpPart, + npwpPart, + nibPart, + persetujuanPart, + courierMap, + qrisPart, + accountNamePart + ) + + // Check if response is successful + if (response.isSuccessful) { + Result.Success(response.body() ?: throw Exception("Response body is null")) + } else { + Result.Error(Exception("Registration failed with code: ${response.code()}")) + } + + } catch (e: Exception) { + Result.Error(e) + } + } + suspend fun login(email: String, password: String): Result { return try { val response = apiService.login(LoginRequest(email, password)) @@ -130,6 +327,10 @@ class UserRepository(private val apiService: ApiService) { Result.Error(e) } } + + suspend fun checkStore(): HasStoreResponse{ + return apiService.checkStoreUser() + } companion object{ private const val TAG = "UserRepository" diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/auth/RegisterStoreActivity.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/auth/RegisterStoreActivity.kt new file mode 100644 index 0000000..4cdfb77 --- /dev/null +++ b/app/src/main/java/com/alya/ecommerce_serang/ui/auth/RegisterStoreActivity.kt @@ -0,0 +1,601 @@ +package com.alya.ecommerce_serang.ui.auth + +import android.Manifest +import android.app.Activity +import android.content.Intent +import android.content.pm.PackageManager +import android.net.Uri +import android.os.Bundle +import android.provider.MediaStore +import android.text.Editable +import android.text.TextWatcher +import android.util.Log +import android.view.View +import android.view.ViewGroup +import android.widget.AdapterView +import android.widget.ArrayAdapter +import android.widget.ImageView +import android.widget.LinearLayout +import android.widget.TextView +import android.widget.Toast +import androidx.activity.enableEdgeToEdge +import androidx.activity.viewModels +import androidx.appcompat.app.AppCompatActivity +import androidx.core.app.ActivityCompat +import androidx.core.content.ContextCompat +import androidx.core.view.ViewCompat +import androidx.core.view.WindowCompat +import androidx.core.view.WindowInsetsCompat +import com.alya.ecommerce_serang.data.api.response.auth.StoreTypesItem +import com.alya.ecommerce_serang.data.api.retrofit.ApiConfig +import com.alya.ecommerce_serang.data.repository.Result +import com.alya.ecommerce_serang.data.repository.UserRepository +import com.alya.ecommerce_serang.databinding.ActivityRegisterStoreBinding +import com.alya.ecommerce_serang.ui.order.address.CityAdapter +import com.alya.ecommerce_serang.ui.order.address.ProvinceAdapter +import com.alya.ecommerce_serang.utils.BaseViewModelFactory +import com.alya.ecommerce_serang.utils.SessionManager + +class RegisterStoreActivity : AppCompatActivity() { + + private lateinit var binding: ActivityRegisterStoreBinding + private lateinit var sessionManager: SessionManager + + private lateinit var provinceAdapter: ProvinceAdapter + private lateinit var cityAdapter: CityAdapter + // Request codes for file picking + private val PICK_STORE_IMAGE_REQUEST = 1001 + private val PICK_KTP_REQUEST = 1002 + private val PICK_NPWP_REQUEST = 1003 + private val PICK_NIB_REQUEST = 1004 + private val PICK_PERSETUJUAN_REQUEST = 1005 + private val PICK_QRIS_REQUEST = 1006 + + // Location request code + private val LOCATION_PERMISSION_REQUEST = 2001 + + private val viewModel: RegisterStoreViewModel by viewModels { + BaseViewModelFactory { + val apiService = ApiConfig.getApiService(sessionManager) + val orderRepository = UserRepository(apiService) + RegisterStoreViewModel(orderRepository) + } + } + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + binding = ActivityRegisterStoreBinding.inflate(layoutInflater) + setContentView(binding.root) + + sessionManager = SessionManager(this) + + WindowCompat.setDecorFitsSystemWindows(window, false) + + enableEdgeToEdge() + + // Apply insets to your root layout + ViewCompat.setOnApplyWindowInsetsListener(binding.root) { view, windowInsets -> + val systemBars = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()) + view.setPadding( + systemBars.left, + systemBars.top, + systemBars.right, + systemBars.bottom + ) + windowInsets + } + + provinceAdapter = ProvinceAdapter(this) + cityAdapter = CityAdapter(this) + + setupDataBinding() + setupSpinners() // Location spinners + + // Setup observers + setupStoreTypesObserver() // Store type observer + setupObservers() + + setupMap() + setupDocumentUploads() + setupCourierSelection() + + viewModel.fetchStoreTypes() + viewModel.getProvinces() + + + // Setup register button + binding.btnRegister.setOnClickListener { + if (viewModel.validateForm()) { + viewModel.registerStore(this) + } else { + Toast.makeText(this, "Harap lengkapi semua field yang wajib diisi", Toast.LENGTH_SHORT).show() + } + } + } + + private fun setupObservers() { + // Observe province state + viewModel.provincesState.observe(this) { state -> + when (state) { + is Result.Loading -> { + Log.d(TAG, "Loading provinces...") + binding.provinceProgressBar?.visibility = View.VISIBLE + binding.spinnerProvince.isEnabled = false + } + is Result.Success -> { + Log.d(TAG, "Provinces loaded: ${state.data.size}") + binding.provinceProgressBar?.visibility = View.GONE + binding.spinnerProvince.isEnabled = true + + // Update adapter with data + provinceAdapter.updateData(state.data) + } + is Result.Error -> { +// Log.e(TAG, "Error loading provinces: ${state.}") + binding.provinceProgressBar?.visibility = View.GONE + binding.spinnerProvince.isEnabled = true + +// Toast.makeText(this, "Gagal memuat provinsi: ${state.message}", Toast.LENGTH_SHORT).show() + } + } + } + + // Observe city state + viewModel.citiesState.observe(this) { state -> + when (state) { + is Result.Loading -> { + Log.d(TAG, "Loading cities...") + binding.cityProgressBar?.visibility = View.VISIBLE + binding.spinnerCity.isEnabled = false + } + is Result.Success -> { + Log.d(TAG, "Cities loaded: ${state.data.size}") + binding.cityProgressBar?.visibility = View.GONE + binding.spinnerCity.isEnabled = true + + // Update adapter with data + cityAdapter.updateData(state.data) + } + is Result.Error -> { +// Log.e(TAG, "Error loading cities: ${state.message}") + binding.cityProgressBar?.visibility = View.GONE + binding.spinnerCity.isEnabled = true + +// Toast.makeText(this, "Gagal memuat kota: ${state.message}", Toast.LENGTH_SHORT).show() + } + } + } + + // Observe registration state + viewModel.registerState.observe(this) { result -> + when (result) { + is Result.Loading -> { + showLoading(true) + } + is Result.Success -> { + showLoading(false) + Toast.makeText(this, "Toko berhasil didaftarkan", Toast.LENGTH_SHORT).show() + finish() // Return to previous screen + } + is Result.Error -> { + showLoading(false) + Toast.makeText(this, "Gagal mendaftarkan toko: ${result.exception.message}", Toast.LENGTH_SHORT).show() + } + } + } + } + + private fun setupStoreTypesObserver() { + // Observe loading state + viewModel.isLoadingType.observe(this) { isLoading -> + if (isLoading) { + // Show loading indicator for store types spinner + binding.spinnerStoreType.isEnabled = false + binding.storeTypeProgressBar?.visibility = View.VISIBLE + } else { + binding.spinnerStoreType.isEnabled = true + binding.storeTypeProgressBar?.visibility = View.GONE + } + } + + // Observe error messages + viewModel.errorMessage.observe(this) { errorMsg -> + if (errorMsg.isNotEmpty()) { + 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}") + 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 }) { + storeTypes + } else { + val defaultItem = StoreTypesItem(name = "Pilih Jenis UMKM", id = 0) + listOf(defaultItem) + storeTypes + } + + // Setup spinner with API data + setupStoreTypeSpinner(displayList) + } + } + } + + private fun setupStoreTypeSpinner(storeTypes: List) { + Log.d(TAG, "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( + this, + android.R.layout.simple_spinner_item, + storeTypes + ) { + override fun getView(position: Int, convertView: View?, parent: ViewGroup): View { + val view = super.getView(position, convertView, parent) + (view as TextView).text = getItem(position)?.name ?: "" + return view + } + + override fun getDropDownView(position: Int, convertView: View?, parent: ViewGroup): View { + val view = super.getDropDownView(position, convertView, parent) + (view as TextView).text = getItem(position)?.name ?: "" + return view + } + + // Override toString to ensure proper display + override fun getItem(position: Int): StoreTypesItem? { + return super.getItem(position) + } + } + + adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item) + + // Set adapter to spinner + binding.spinnerStoreType.adapter = adapter + + // 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}") + } + } + + override fun onNothingSelected(parent: AdapterView<*>?) { + Log.d(TAG, "No store type selected") + } + } + + // Hide progress bar after setup + binding.storeTypeProgressBar?.visibility = View.GONE + } + + private fun setupSpinners() { + // 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 + viewModel.getCities(provinceId) + + // Reset city selection when province changes + cityAdapter.clear() + binding.spinnerCity.setSelection(0) + } else { + Log.e(TAG, "Invalid province ID for position: $position") + } + } + + override fun onNothingSelected(parent: AdapterView<*>?) { + // Do nothing + } + } + + // 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 + viewModel.selectedCityId = cityId + } else { + Log.e(TAG, "Invalid city ID for position: $position") + } + } + + override fun onNothingSelected(parent: AdapterView<*>?) { + // Do nothing + } + } + + // Add initial hints to the spinners + if (provinceAdapter.isEmpty) { + provinceAdapter.add("Pilih Provinsi") + } + + if (cityAdapter.isEmpty) { + cityAdapter.add("Pilih Kabupaten/Kota") + } + } + +// 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() { + // Store Image + binding.containerStoreImg.setOnClickListener { + pickImage(PICK_STORE_IMAGE_REQUEST) + } + + // KTP + binding.containerKtp.setOnClickListener { + pickImage(PICK_KTP_REQUEST) + } + + // NIB + binding.containerNib.setOnClickListener { + pickDocument(PICK_NIB_REQUEST) + } + + // NPWP + binding.containerNpwp?.setOnClickListener { + pickImage(PICK_NPWP_REQUEST) + } + + // SPPIRT + binding.containerSppirt.setOnClickListener { + pickDocument(PICK_PERSETUJUAN_REQUEST) + } + + // Halal + binding.containerHalal.setOnClickListener { + pickDocument(PICK_QRIS_REQUEST) + } + } + + private fun pickImage(requestCode: Int) { + val intent = Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI) + startActivityForResult(intent, requestCode) + } + + private fun pickDocument(requestCode: Int) { + val intent = Intent(Intent.ACTION_OPEN_DOCUMENT) + intent.addCategory(Intent.CATEGORY_OPENABLE) + intent.type = "*/*" + val mimeTypes = arrayOf("application/pdf", "image/jpeg", "image/png") + intent.putExtra(Intent.EXTRA_MIME_TYPES, mimeTypes) + startActivityForResult(intent, requestCode) + } + + private fun setupCourierSelection() { + binding.checkboxJne.setOnCheckedChangeListener { _, isChecked -> + handleCourierSelection("jne", isChecked) + } + + binding.checkboxJnt.setOnCheckedChangeListener { _, isChecked -> + handleCourierSelection("jnt", isChecked) + } + + binding.checkboxPos.setOnCheckedChangeListener { _, isChecked -> + handleCourierSelection("pos", isChecked) + } + } + + private fun handleCourierSelection(courier: String, isSelected: Boolean) { + if (isSelected) { + if (!viewModel.selectedCouriers.contains(courier)) { + viewModel.selectedCouriers.add(courier) + } + } else { + viewModel.selectedCouriers.remove(courier) + } + } + + private fun setupMap() { + // This would typically integrate with Google Maps SDK + // For simplicity, we're just using a placeholder + binding.mapContainer.setOnClickListener { + // Request location permission if not granted + if (ContextCompat.checkSelfPermission( + this, + Manifest.permission.ACCESS_FINE_LOCATION + ) != PackageManager.PERMISSION_GRANTED + ) { + ActivityCompat.requestPermissions( + this, + arrayOf(Manifest.permission.ACCESS_FINE_LOCATION), + LOCATION_PERMISSION_REQUEST + + ) + viewModel.latitude.value = "-6.2088" + viewModel.longitude.value = "106.8456" + Toast.makeText(this, "Lokasi dipilih", Toast.LENGTH_SHORT).show() + } else { + // 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" + Toast.makeText(this, "Lokasi dipilih", Toast.LENGTH_SHORT).show() + } + } + } + + private fun setupDataBinding() { + // 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() + } + }) + + binding.etStoreDescription.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.storeDescription.value = s.toString() + } + }) + + binding.etStreet.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.street.value = s.toString() + } + }) + + binding.etPostalCode.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?) { + try { + viewModel.postalCode.value = s.toString().toInt() + } catch (e: NumberFormatException) { + // Handle invalid input + //show toast + } + } + }) + + binding.etAddressDetail.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.addressDetail.value = s.toString() + } + }) + + binding.etBankNumber.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.bankNumber.value = s.toString().toInt() + } + }) + + binding.etSubdistrict.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.subdistrict.value = s.toString() + } + }) + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + super.onActivityResult(requestCode, resultCode, data) + if (resultCode == Activity.RESULT_OK && data != null) { + val uri = data.data + when (requestCode) { + PICK_STORE_IMAGE_REQUEST -> { + viewModel.storeImageUri = uri + updateImagePreview(uri, binding.imgStore, binding.layoutUploadStoreImg) + } + PICK_KTP_REQUEST -> { + viewModel.ktpUri = uri + updateImagePreview(uri, binding.imgKtp, binding.layoutUploadKtp) + } + PICK_NPWP_REQUEST -> { + viewModel.npwpUri = uri + updateDocumentPreview(binding.layoutUploadNpwp) + } + PICK_NIB_REQUEST -> { + viewModel.nibUri = uri + updateDocumentPreview(binding.layoutUploadNib) + } + PICK_PERSETUJUAN_REQUEST -> { + viewModel.persetujuanUri = uri + updateDocumentPreview(binding.layoutUploadSppirt) + } + PICK_QRIS_REQUEST -> { + viewModel.qrisUri = uri + updateDocumentPreview(binding.layoutUploadHalal) + } + } + } + } + + private fun updateImagePreview(uri: Uri?, imageView: ImageView, uploadLayout: LinearLayout) { + uri?.let { + imageView.setImageURI(it) + imageView.visibility = View.VISIBLE + uploadLayout.visibility = View.GONE + } + } + + private fun updateDocumentPreview(uploadLayout: LinearLayout) { + // For documents, we just show a success indicator + val checkIcon = ImageView(this) + checkIcon.setImageResource(android.R.drawable.ic_menu_gallery) + val successText = TextView(this) + successText.text = "Dokumen berhasil diunggah" + + uploadLayout.removeAllViews() + uploadLayout.addView(checkIcon) + uploadLayout.addView(successText) + } + + //later implement get location form gps + override fun onRequestPermissionsResult( + requestCode: Int, + permissions: Array, + grantResults: IntArray + ) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults) + if (requestCode == LOCATION_PERMISSION_REQUEST) { + if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) { + // Permission granted, proceed with location selection + viewModel.latitude.value = "-6.2088" + viewModel.longitude.value = "106.8456" + Toast.makeText(this, "Lokasi dipilih", Toast.LENGTH_SHORT).show() + } else { + viewModel.latitude.value = "-6.2088" + viewModel.longitude.value = "106.8456" + } + } + } + + private fun showLoading(isLoading: Boolean) { + if (isLoading) { + // Show loading indicator + binding.btnRegister.isEnabled = false + binding.btnRegister.text = "Mendaftar..." + } else { + // Hide loading indicator + binding.btnRegister.isEnabled = true + binding.btnRegister.text = "Daftar" + } + } + + companion object { + private const val TAG = "RegisterStoreActivity" + } +} \ No newline at end of file diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/auth/RegisterStoreViewModel.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/auth/RegisterStoreViewModel.kt new file mode 100644 index 0000000..41bfbf1 --- /dev/null +++ b/app/src/main/java/com/alya/ecommerce_serang/ui/auth/RegisterStoreViewModel.kt @@ -0,0 +1,202 @@ +package com.alya.ecommerce_serang.ui.auth + +import android.content.Context +import android.net.Uri +import android.util.Log +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.alya.ecommerce_serang.data.api.response.auth.RegisterStoreResponse +import com.alya.ecommerce_serang.data.api.response.auth.StoreTypesItem +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 kotlinx.coroutines.launch + +class RegisterStoreViewModel( + private val repository: UserRepository +) : ViewModel() { + + // LiveData for UI state + private val _registerState = MutableLiveData>() + val registerState: LiveData> = _registerState + + private val _storeTypes = MutableLiveData>() + val storeTypes: LiveData> = _storeTypes + + // LiveData for error messages + private val _errorMessage = MutableLiveData() + val errorMessage: LiveData = _errorMessage + + // LiveData for loading state + private val _isLoadingType = MutableLiveData() + val isLoadingType: LiveData = _isLoadingType + + private val _provincesState = MutableLiveData>>() + val provincesState: LiveData>> = _provincesState + + private val _citiesState = MutableLiveData>>() + val citiesState: LiveData>> = _citiesState + + var selectedProvinceId: Int? = null + var selectedCityId: Int? = null + + // Form fields + val storeName = MutableLiveData() + val storeDescription = MutableLiveData() + val storeTypeId = MutableLiveData() + val latitude = MutableLiveData() + val longitude = MutableLiveData() + val street = MutableLiveData() + val subdistrict = MutableLiveData() + val cityId = MutableLiveData() + val provinceId = MutableLiveData() + val postalCode = MutableLiveData() + val addressDetail = MutableLiveData() + val bankName = MutableLiveData() + val bankNumber = MutableLiveData() + val accountName = MutableLiveData() + + // Files + var storeImageUri: Uri? = null + var ktpUri: Uri? = null + var npwpUri: Uri? = null + var nibUri: Uri? = null + var persetujuanUri: Uri? = null + var qrisUri: Uri? = null + + // Selected couriers + val selectedCouriers = mutableListOf() + + fun registerStore(context: Context) { + viewModelScope.launch { + try { + _registerState.value = Result.Loading + + val result = repository.registerStoreUser( + context = context, + description = storeDescription.value ?: "", + storeTypeId = storeTypeId.value ?: 0, + latitude = latitude.value ?: "", + longitude = longitude.value ?: "", + street = street.value ?: "", + subdistrict = subdistrict.value ?: "", + cityId = cityId.value ?: 0, + provinceId = provinceId.value ?: 0, + postalCode = postalCode.value ?: 0, + detail = addressDetail.value, + bankName = bankName.value ?: "", + bankNum = bankNumber.value ?: 0, + storeName = storeName.value ?: "", + storeImg = storeImageUri, + ktp = ktpUri, + npwp = npwpUri, + nib = nibUri, + persetujuan = persetujuanUri, + couriers = selectedCouriers, + qris = qrisUri, + accountName = accountName.value ?: "" + ) + + _registerState.value = result + } catch (e: Exception) { + _registerState.value = com.alya.ecommerce_serang.data.repository.Result.Error(e) + } + } + } + +// // Helper function to convert Uri to File +// private fun getFileFromUri(context: Context, uri: Uri): File { +// val inputStream = context.contentResolver.openInputStream(uri) +// val tempFile = File(context.cacheDir, "temp_file_${System.currentTimeMillis()}") +// inputStream?.use { input -> +// tempFile.outputStream().use { output -> +// input.copyTo(output) +// } +// } +// return tempFile +// } + + fun validateForm(): Boolean { + // Implement form validation logic + return !(storeName.value.isNullOrEmpty() || + storeTypeId.value == null || + street.value.isNullOrEmpty() || + subdistrict.value.isNullOrEmpty() || + cityId.value == null || + provinceId.value == null || + postalCode.value == null || + bankName.value.isNullOrEmpty() || + bankNumber.value == null || + selectedCouriers.isEmpty() || + ktpUri == null || + nibUri == null) + } + + + + // Function to fetch store types + fun fetchStoreTypes() { + _isLoadingType.value = true + viewModelScope.launch { + when (val result = repository.listStoreType()) { + is Result.Success -> { + _storeTypes.value = result.data.storeTypes + _isLoadingType.value = false + } + is Result.Error -> { + _errorMessage.value = result.exception.message ?: "Unknown error occurred" + _isLoadingType.value = false + } + is Result.Loading -> { + _isLoadingType.value = true + } + } + } + } + + fun getProvinces() { + _provincesState.value = Result.Loading + viewModelScope.launch { + try { + val result = repository.getListProvinces() + if (result?.provinces != null) { + _provincesState.postValue(Result.Success(result.provinces)) + Log.d(TAG, "Provinces loaded: ${result.provinces.size}") + } else { + _provincesState.postValue(Result.Error(Exception("Failed to load provinces"))) + Log.e(TAG, "Province result was null or empty") + } + } catch (e: Exception) { + _provincesState.postValue(Result.Error(Exception(e.message ?: "Error loading provinces"))) + Log.e(TAG, "Error fetching provinces", e) + } + } + } + + fun getCities(provinceId: Int){ + _citiesState.value = Result.Loading + viewModelScope.launch { + try { + selectedProvinceId = provinceId + val result = repository.getListCities(provinceId) + result?.let { + _citiesState.postValue(Result.Success(it.cities)) + Log.d(TAG, "Cities loaded for province $provinceId: ${it.cities.size}") + } ?: run { + _citiesState.postValue(Result.Error(Exception("Failed to load cities"))) + Log.e(TAG, "City result was null for province $provinceId") + } + } catch (e: Exception) { + _citiesState.postValue(Result.Error(Exception(e.message ?: "Error loading cities"))) + Log.e(TAG, "Error fetching cities for province $provinceId", e) + } + } + } + + companion object { + private const val TAG = "RegisterStoreUserViewModel" + } +} \ No newline at end of file diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/profile/ProfileFragment.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/profile/ProfileFragment.kt index d1921f9..959175f 100644 --- a/app/src/main/java/com/alya/ecommerce_serang/ui/profile/ProfileFragment.kt +++ b/app/src/main/java/com/alya/ecommerce_serang/ui/profile/ProfileFragment.kt @@ -15,6 +15,7 @@ import com.alya.ecommerce_serang.data.api.dto.UserProfile import com.alya.ecommerce_serang.data.api.retrofit.ApiConfig import com.alya.ecommerce_serang.data.repository.UserRepository import com.alya.ecommerce_serang.databinding.FragmentProfileBinding +import com.alya.ecommerce_serang.ui.auth.RegisterStoreActivity import com.alya.ecommerce_serang.ui.order.history.HistoryActivity import com.alya.ecommerce_serang.ui.profile.mystore.MyStoreActivity import com.alya.ecommerce_serang.utils.BaseViewModelFactory @@ -53,10 +54,21 @@ class ProfileFragment : Fragment() { super.onViewCreated(view, savedInstanceState) observeUserProfile() viewModel.loadUserProfile() + viewModel.checkStoreUser() binding.cardBukaToko.setOnClickListener{ - val intentBuka = Intent(requireContext(), MyStoreActivity::class.java) - startActivity(intentBuka) + val hasStore = viewModel.checkStore.value +// val hasStore = false + + Log.d("Profile Fragment", "Check store $hasStore") + + if (hasStore == true){ + val intentBuka = Intent(requireContext(), MyStoreActivity::class.java) + startActivity(intentBuka) + } else { + val intentBuka = Intent(requireContext(), RegisterStoreActivity::class.java) + startActivity(intentBuka) + } } binding.btnDetailProfile.setOnClickListener{ diff --git a/app/src/main/java/com/alya/ecommerce_serang/utils/viewmodel/ProfileViewModel.kt b/app/src/main/java/com/alya/ecommerce_serang/utils/viewmodel/ProfileViewModel.kt index 3a08b14..21347a2 100644 --- a/app/src/main/java/com/alya/ecommerce_serang/utils/viewmodel/ProfileViewModel.kt +++ b/app/src/main/java/com/alya/ecommerce_serang/utils/viewmodel/ProfileViewModel.kt @@ -8,6 +8,7 @@ import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.alya.ecommerce_serang.data.api.dto.UserProfile +import com.alya.ecommerce_serang.data.api.response.auth.HasStoreResponse import com.alya.ecommerce_serang.data.api.response.customer.profile.EditProfileResponse import com.alya.ecommerce_serang.data.repository.Result import com.alya.ecommerce_serang.data.repository.UserRepository @@ -23,6 +24,9 @@ class ProfileViewModel(private val userRepository: UserRepository) : ViewModel() private val _editProfileResult = MutableLiveData>() val editProfileResult: LiveData> = _editProfileResult + private val _checkStore = MutableLiveData() + val checkStore: LiveData = _checkStore + fun loadUserProfile(){ viewModelScope.launch { when (val result = userRepository.fetchUserProfile()){ @@ -33,6 +37,28 @@ class ProfileViewModel(private val userRepository: UserRepository) : ViewModel() } } + fun checkStoreUser(){ + viewModelScope.launch { + try { + // Call the repository function to request OTP + val response: HasStoreResponse = userRepository.checkStore() + + // Log and store success message + Log.d("RegisterViewModel", "OTP Response: ${response.hasStore}") + _checkStore.value = response.hasStore // Store the message for UI feedback + + } catch (exception: Exception) { + // Handle any errors and update state + _checkStore.value = false + + // Log the error for debugging + Log.e("RegisterViewModel", "Error:", exception) + } + } + } + + + fun editProfileDirect( context: Context, username: String, diff --git a/app/src/main/res/layout/activity_register_store.xml b/app/src/main/res/layout/activity_register_store.xml new file mode 100644 index 0000000..f84430b --- /dev/null +++ b/app/src/main/res/layout/activity_register_store.xml @@ -0,0 +1,578 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +