From 4e82a36cad2ab6620cc1a53d9f79794a6f58eeee Mon Sep 17 00:00:00 2001 From: Gracia Date: Sat, 12 Apr 2025 04:40:19 +0700 Subject: [PATCH 1/5] product --- .../data/api/retrofit/ApiService.kt | 29 +- .../data/repository/ProductRepository.kt | 73 ++++-- .../product/StoreProductDetailActivity.kt | 247 +++++++++++------- .../utils/viewmodel/ProductViewModel.kt | 14 +- app/src/main/res/drawable/bg_upload.xml | 10 + app/src/main/res/drawable/ic_close.png | Bin 0 -> 521 bytes .../layout/activity_store_product_detail.xml | 245 ++++++++++++++++- 7 files changed, 471 insertions(+), 147 deletions(-) create mode 100644 app/src/main/res/drawable/bg_upload.xml create mode 100644 app/src/main/res/drawable/ic_close.png 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 8e22988..dab4d34 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 @@ -14,13 +14,17 @@ import com.alya.ecommerce_serang.data.api.response.RegisterResponse import com.alya.ecommerce_serang.data.api.response.ReviewProductResponse import com.alya.ecommerce_serang.data.api.response.StoreResponse import com.alya.ecommerce_serang.data.api.response.ViewStoreProductsResponse +import okhttp3.MultipartBody +import okhttp3.RequestBody import retrofit2.Call import retrofit2.Response import retrofit2.http.Body import retrofit2.http.Field import retrofit2.http.FormUrlEncoded import retrofit2.http.GET +import retrofit2.http.Multipart import retrofit2.http.POST +import retrofit2.http.Part import retrofit2.http.Path interface ApiService { @@ -74,19 +78,22 @@ interface ApiService { @GET("category") fun getCategories(): Call + @Multipart @POST("store/createproduct") - @FormUrlEncoded suspend fun addProduct( - @Field("name") name: String, - @Field("description") description: String, - @Field("price") price: Int, - @Field("stock") stock: Int, - @Field("min_order") minOrder: Int, - @Field("weight") weight: Int, - @Field("is_pre_order") isPreOrder: Boolean, - @Field("duration") duration: Int, - @Field("category_id") categoryId: Int, - @Field("is_active") isActive: String + @Part("name") name: RequestBody, + @Part("description") description: RequestBody, + @Part("price") price: RequestBody, + @Part("stock") stock: RequestBody, + @Part("min_order") minOrder: RequestBody, + @Part("weight") weight: RequestBody, + @Part("is_pre_order") isPreOrder: RequestBody, + @Part("duration") duration: RequestBody, + @Part("category_id") categoryId: RequestBody, + @Part("is_active") isActive: RequestBody, + @Part image: MultipartBody.Part?, + @Part sppirt: MultipartBody.Part?, + @Part halal: MultipartBody.Part? ): Response } \ No newline at end of file diff --git a/app/src/main/java/com/alya/ecommerce_serang/data/repository/ProductRepository.kt b/app/src/main/java/com/alya/ecommerce_serang/data/repository/ProductRepository.kt index c218ef2..5b6ca66 100644 --- a/app/src/main/java/com/alya/ecommerce_serang/data/repository/ProductRepository.kt +++ b/app/src/main/java/com/alya/ecommerce_serang/data/repository/ProductRepository.kt @@ -8,6 +8,10 @@ import com.alya.ecommerce_serang.data.api.response.ReviewsItem import com.alya.ecommerce_serang.data.api.retrofit.ApiService import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext +import okhttp3.MediaType.Companion.toMediaTypeOrNull +import okhttp3.MultipartBody +import okhttp3.RequestBody +import java.io.File class ProductRepository(private val apiService: ApiService) { suspend fun getAllProducts(): Result> = @@ -81,12 +85,16 @@ class ProductRepository(private val apiService: ApiService) { } suspend fun fetchMyStoreProducts(): List { - val response = apiService.getStoreProduct() - if (response.isSuccessful) { - val responseBody = response.body() - return responseBody?.products?.filterNotNull() ?: emptyList() - } else { - throw Exception("Failed to fetch store products: ${response.message()}") + return try { + val response = apiService.getStoreProduct() + if (response.isSuccessful) { + response.body()?.products?.filterNotNull() ?: emptyList() + } else { + throw Exception("Failed to fetch store products: ${response.message()}") + } + } catch (e: Exception) { + Log.e("ProductRepository", "Error fetching store products", e) + throw e } } @@ -100,33 +108,50 @@ class ProductRepository(private val apiService: ApiService) { isPreOrder: Boolean, duration: Int, categoryId: Int, - isActive: Boolean + isActive: Boolean, + image: File?, + sppirt: File?, + halal: File? ): Result = withContext(Dispatchers.IO) { try { - val status = if (isActive) "active" else "inactive" + val namePart = RequestBody.create("text/plain".toMediaTypeOrNull(), name) + val descriptionPart = RequestBody.create("text/plain".toMediaTypeOrNull(), description) + val pricePart = RequestBody.create("text/plain".toMediaTypeOrNull(), price.toString()) + val stockPart = RequestBody.create("text/plain".toMediaTypeOrNull(), stock.toString()) + val minOrderPart = RequestBody.create("text/plain".toMediaTypeOrNull(), minOrder.toString()) + val weightPart = RequestBody.create("text/plain".toMediaTypeOrNull(), weight.toString()) + val isPreOrderPart = RequestBody.create("text/plain".toMediaTypeOrNull(), isPreOrder.toString()) + val durationPart = RequestBody.create("text/plain".toMediaTypeOrNull(), duration.toString()) + val categoryIdPart = RequestBody.create("text/plain".toMediaTypeOrNull(), categoryId.toString()) + val isActivePart = RequestBody.create("text/plain".toMediaTypeOrNull(), if (isActive) "1" else "0") + + val imagePart = image?.let { + val req = RequestBody.create("image/*".toMediaTypeOrNull(), it) + MultipartBody.Part.createFormData("image", it.name, req) + } + + val sppirtPart = sppirt?.let { + val req = RequestBody.create("application/pdf".toMediaTypeOrNull(), it) + MultipartBody.Part.createFormData("sppirt", it.name, req) + } + + val halalPart = halal?.let { + val req = RequestBody.create("application/pdf".toMediaTypeOrNull(), it) + MultipartBody.Part.createFormData("halal", it.name, req) + } + val response = apiService.addProduct( - name = name, - description = description, - price = price, - stock = stock, - minOrder = minOrder, - weight = weight, - isPreOrder = isPreOrder, - duration = duration, - categoryId = categoryId, - isActive = status + namePart, descriptionPart, pricePart, stockPart, minOrderPart, + weightPart, isPreOrderPart, durationPart, categoryIdPart, isActivePart, + imagePart, sppirtPart, halalPart ) - if (response.isSuccessful) { - Result.Success(Unit) - } else { - Result.Error(Exception("Failed to add product. Code: ${response.code()}")) - } + if (response.isSuccessful) Result.Success(Unit) + else Result.Error(Exception("Failed: ${response.code()}")) } catch (e: Exception) { Result.Error(e) } } - } // suspend fun fetchStoreDetail(storeId: Int): Store? { diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/profile/mystore/product/StoreProductDetailActivity.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/profile/mystore/product/StoreProductDetailActivity.kt index af53295..bccb5e3 100644 --- a/app/src/main/java/com/alya/ecommerce_serang/ui/profile/mystore/product/StoreProductDetailActivity.kt +++ b/app/src/main/java/com/alya/ecommerce_serang/ui/profile/mystore/product/StoreProductDetailActivity.kt @@ -1,11 +1,16 @@ package com.alya.ecommerce_serang.ui.profile.mystore.product +import android.app.Activity +import android.content.Context +import android.content.Intent +import android.net.Uri import android.os.Bundle import android.text.Editable import android.text.TextWatcher import android.view.View import android.widget.ArrayAdapter import android.widget.Toast +import androidx.activity.result.contract.ActivityResultContracts import com.alya.ecommerce_serang.R import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity @@ -18,6 +23,11 @@ import com.alya.ecommerce_serang.databinding.ActivityStoreProductDetailBinding import com.alya.ecommerce_serang.utils.viewmodel.ProductViewModel import com.alya.ecommerce_serang.utils.BaseViewModelFactory import com.alya.ecommerce_serang.utils.SessionManager +import okhttp3.MediaType +import okhttp3.MediaType.Companion.toMediaTypeOrNull +import okhttp3.MultipartBody +import okhttp3.RequestBody +import java.io.File import kotlin.getValue class StoreProductDetailActivity : AppCompatActivity() { @@ -25,6 +35,9 @@ class StoreProductDetailActivity : AppCompatActivity() { private lateinit var binding: ActivityStoreProductDetailBinding private lateinit var sessionManager: SessionManager private lateinit var categoryList: List + private var imageUri: Uri? = null + private var sppirtUri: Uri? = null + private var halalUri: Uri? = null private val viewModel: ProductViewModel by viewModels { BaseViewModelFactory { @@ -35,14 +48,50 @@ class StoreProductDetailActivity : AppCompatActivity() { } } + private val imagePickerLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> + if (result.resultCode == Activity.RESULT_OK) { + imageUri = result.data?.data + imageUri?.let { + binding.ivPreviewFoto.setImageURI(it) + binding.switcherFotoProduk.showNext() + } + validateForm() + } + } + + private val sppirtLauncher = registerForActivityResult(ActivityResultContracts.GetContent()) { uri -> + if (uri != null && isValidFile(uri)) { + sppirtUri = uri + binding.tvSppirtName.text = File(uri.path ?: "").name + binding.switcherSppirt.showNext() + } + } + + private val halalLauncher = registerForActivityResult(ActivityResultContracts.GetContent()) { uri -> + if (uri != null && isValidFile(uri)) { + halalUri = uri + binding.tvHalalName.text = File(uri.path ?: "").name + binding.switcherHalal.showNext() + } + } + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = ActivityStoreProductDetailBinding.inflate(layoutInflater) setContentView(binding.root) setupHeader() - observeCategories() + + // Fetch categories viewModel.loadCategories() + viewModel.categoryList.observe(this) { result -> + if (result is Result.Success) { + categoryList = result.data + val adapter = ArrayAdapter(this, android.R.layout.simple_spinner_item, categoryList.map { it.name }) + adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item) + binding.spinnerKategoriProduk.adapter = adapter + } + } // Setup Pre-Order visibility binding.switchIsPreOrder.setOnCheckedChangeListener { _, isChecked -> @@ -50,71 +99,123 @@ class StoreProductDetailActivity : AppCompatActivity() { validateForm() } - setupFormValidation() + binding.tvTambahFoto.setOnClickListener { + val intent = Intent(Intent.ACTION_PICK).apply { type = "image/*" } + imagePickerLauncher.launch(intent) + } + + binding.layoutUploadFoto.setOnClickListener { + val intent = Intent(Intent.ACTION_PICK).apply { type = "image/*" } + imagePickerLauncher.launch(intent) + } + + binding.layoutUploadSppirt.setOnClickListener { sppirtLauncher.launch("*/*") } + binding.btnRemoveSppirt.setOnClickListener { + sppirtUri = null + binding.switcherSppirt.showPrevious() + } + + binding.layoutUploadHalal.setOnClickListener { halalLauncher.launch("*/*") } + binding.btnRemoveHalal.setOnClickListener { + halalUri = null + binding.switcherHalal.showPrevious() + } + + binding.btnRemoveFoto.setOnClickListener { + imageUri = null + binding.switcherFotoProduk.showPrevious() + validateForm() + } + + val watcher = 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?) { validateForm() } + } + + listOf( + binding.edtNamaProduk, + binding.edtDeskripsiProduk, + binding.edtHargaProduk, + binding.edtStokProduk, + binding.edtMinOrder, + binding.edtBeratProduk, + binding.edtDurasi + ).forEach { it.addTextChangedListener(watcher) } + validateForm() binding.btnSaveProduct.setOnClickListener { - if (binding.btnSaveProduct.isEnabled) addProduct() - } - } - - private fun setupHeader() { - binding.header.headerTitle.text = "Tambah Produk" - - binding.header.headerLeftIcon.setOnClickListener { - onBackPressedDispatcher.onBackPressed() - } - } - - private fun observeCategories() { - viewModel.categoryList.observe(this) { result -> - when (result) { - is Result.Loading -> { - // Optionally show loading spinner - } - is Result.Success -> { - categoryList = result.data - setupCategorySpinner(categoryList) - } - is Result.Error -> { - Toast.makeText( - this, - "Failed to load categories: ${result.exception.message}", - Toast.LENGTH_SHORT - ).show() - } + if (!binding.btnSaveProduct.isEnabled) { + focusFirstInvalidField() + return@setOnClickListener } + submitProduct() } } - private fun setupCategorySpinner(categories: List) { - val categoryNames = categories.map { it.name } - val adapter = ArrayAdapter(this, android.R.layout.simple_spinner_item, categoryNames) - adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item) - - binding.spinnerKategoriProduk.adapter = adapter + private fun isValidFile(uri: Uri): Boolean { + val mimeType = contentResolver.getType(uri) ?: return false + return listOf("application/pdf", "image/jpeg", "image/png", "image/jpg").contains(mimeType) } - private fun addProduct() { + private fun validateForm() { + val valid = binding.edtNamaProduk.text.isNotBlank() && + binding.edtDeskripsiProduk.text.isNotBlank() && + binding.edtHargaProduk.text.isNotBlank() && + binding.edtStokProduk.text.isNotBlank() && + binding.edtMinOrder.text.isNotBlank() && + binding.edtBeratProduk.text.isNotBlank() && + (!binding.switchIsPreOrder.isChecked || binding.edtDurasi.text.isNotBlank()) && + imageUri != null + + binding.btnSaveProduct.isEnabled = valid + binding.btnSaveProduct.setTextColor( + if (valid) ContextCompat.getColor(this, R.color.white) else ContextCompat.getColor(this, R.color.black_300) + ) + binding.btnSaveProduct.setBackgroundResource( + if (valid) R.drawable.bg_button_active else R.drawable.bg_button_disabled + ) + } + + private fun focusFirstInvalidField() { + when { + binding.edtNamaProduk.text.isBlank() -> binding.edtNamaProduk.requestFocus() + binding.edtDeskripsiProduk.text.isBlank() -> binding.edtDeskripsiProduk.requestFocus() + binding.edtHargaProduk.text.isBlank() -> binding.edtHargaProduk.requestFocus() + binding.edtStokProduk.text.isBlank() -> binding.edtStokProduk.requestFocus() + binding.edtMinOrder.text.isBlank() -> binding.edtMinOrder.requestFocus() + binding.edtBeratProduk.text.isBlank() -> binding.edtBeratProduk.requestFocus() + binding.switchIsPreOrder.isChecked && binding.edtDurasi.text.isBlank() -> binding.edtDurasi.requestFocus() + imageUri == null -> Toast.makeText(this, "Silakan unggah foto produk", Toast.LENGTH_SHORT).show() + } + } + + private fun uriToFile(uri: Uri, context: Context): File { + val inputStream = context.contentResolver.openInputStream(uri) + val file = File.createTempFile("upload_", ".tmp", context.cacheDir) + inputStream?.use { input -> file.outputStream().use { input.copyTo(it) } } + return file + } + + private fun submitProduct() { val name = binding.edtNamaProduk.text.toString() val description = binding.edtDeskripsiProduk.text.toString() - val price = binding.edtHargaProduk.text.toString().toIntOrNull() ?: 0 - val stock = binding.edtStokProduk.text.toString().toIntOrNull() ?: 0 - val minOrder = binding.edtMinOrder.text.toString().toIntOrNull() ?: 1 - val weight = binding.edtBeratProduk.text.toString().toIntOrNull() ?: 0 + val price = binding.edtHargaProduk.text.toString().toInt() + val stock = binding.edtStokProduk.text.toString().toInt() + val minOrder = binding.edtMinOrder.text.toString().toInt() + val weight = binding.edtBeratProduk.text.toString().toInt() val isPreOrder = binding.switchIsPreOrder.isChecked - val duration = binding.edtDurasi.text.toString().toIntOrNull() ?: 0 + val duration = if (isPreOrder) binding.edtDurasi.text.toString().toInt() else 0 val isActive = binding.switchIsActive.isChecked - val categoryPosition = binding.spinnerKategoriProduk.selectedItemPosition - val categoryId = categoryList.getOrNull(categoryPosition)?.id ?: 0 + val categoryId = categoryList.getOrNull(binding.spinnerKategoriProduk.selectedItemPosition)?.id ?: 0 - if (isPreOrder && duration == 0) { - Toast.makeText(this, "Durasi wajib diisi jika pre-order diaktifkan.", Toast.LENGTH_SHORT).show() - return - } + val imageFile = imageUri?.let { uriToFile(it, this) } + val sppirtFile = sppirtUri?.let { uriToFile(it, this) } + val halalFile = halalUri?.let { uriToFile(it, this) } viewModel.addProduct( - name, description, price, stock, minOrder, weight, isPreOrder, duration, categoryId, isActive + name, description, price, stock, minOrder, weight, isPreOrder, duration, categoryId, isActive, imageFile, sppirtFile, halalFile ).observe(this) { result -> when (result) { is Result.Loading -> binding.btnSaveProduct.isEnabled = false @@ -123,53 +224,15 @@ class StoreProductDetailActivity : AppCompatActivity() { finish() } is Result.Error -> { - binding.btnSaveProduct.isEnabled = true Toast.makeText(this, "Gagal: ${result.exception.message}", Toast.LENGTH_SHORT).show() + binding.btnSaveProduct.isEnabled = true } } } } - private fun setupFormValidation() { - val watcher = 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?) { - validateForm() - } - } - - // Watch all fields - binding.edtNamaProduk.addTextChangedListener(watcher) - binding.edtDeskripsiProduk.addTextChangedListener(watcher) - binding.edtHargaProduk.addTextChangedListener(watcher) - binding.edtStokProduk.addTextChangedListener(watcher) - binding.edtMinOrder.addTextChangedListener(watcher) - binding.edtBeratProduk.addTextChangedListener(watcher) - binding.edtDurasi.addTextChangedListener(watcher) - } - - private fun validateForm() { - val isNameValid = binding.edtNamaProduk.text.toString().isNotBlank() - val isDescriptionValid = binding.edtDeskripsiProduk.text.toString().isNotBlank() - val isPriceValid = binding.edtHargaProduk.text.toString().isNotBlank() - val isStockValid = binding.edtStokProduk.text.toString().isNotBlank() - val isMinOrderValid = binding.edtMinOrder.text.toString().isNotBlank() - val isWeightValid = binding.edtBeratProduk.text.toString().isNotBlank() - val isPreOrderChecked = binding.switchIsPreOrder.isChecked - val isDurationValid = !isPreOrderChecked || binding.edtDurasi.text.toString().isNotBlank() - - val isFormValid = isNameValid && isDescriptionValid && isPriceValid && - isStockValid && isMinOrderValid && isWeightValid && isDurationValid - - if (isFormValid) { - binding.btnSaveProduct.isEnabled = true - binding.btnSaveProduct.setBackgroundResource(R.drawable.bg_button_active) - binding.btnSaveProduct.setTextColor(ContextCompat.getColor(this, R.color.white)) - } else { - binding.btnSaveProduct.isEnabled = false - binding.btnSaveProduct.setBackgroundResource(R.drawable.bg_button_disabled) - binding.btnSaveProduct.setTextColor(ContextCompat.getColor(this, R.color.black_300)) - } + private fun setupHeader() { + binding.header.headerTitle.text = "Tambah Produk" + binding.header.headerLeftIcon.setOnClickListener { onBackPressedDispatcher.onBackPressed() } } } \ No newline at end of file diff --git a/app/src/main/java/com/alya/ecommerce_serang/utils/viewmodel/ProductViewModel.kt b/app/src/main/java/com/alya/ecommerce_serang/utils/viewmodel/ProductViewModel.kt index 5f8e94f..17af70f 100644 --- a/app/src/main/java/com/alya/ecommerce_serang/utils/viewmodel/ProductViewModel.kt +++ b/app/src/main/java/com/alya/ecommerce_serang/utils/viewmodel/ProductViewModel.kt @@ -13,6 +13,7 @@ import com.alya.ecommerce_serang.data.api.response.ReviewsItem import com.alya.ecommerce_serang.data.repository.ProductRepository import com.alya.ecommerce_serang.data.repository.Result import kotlinx.coroutines.launch +import java.io.File class ProductViewModel(private val repository: ProductRepository) : ViewModel() { @@ -74,17 +75,15 @@ class ProductViewModel(private val repository: ProductRepository) : ViewModel() isPreOrder: Boolean, duration: Int, categoryId: Int, - isActive: Boolean + isActive: Boolean, + image: File?, + sppirt: File?, + halal: File? ): LiveData> = liveData { emit(Result.Loading) - val result = repository.addProduct( - name, description, price, stock, minOrder, weight, isPreOrder, duration, categoryId, isActive - ) - emit(result) + emit(repository.addProduct(name, description, price, stock, minOrder, weight, isPreOrder, duration, categoryId, isActive, image, sppirt, halal)) } - - // Optional: for store detail if you need it later // fun loadStoreDetail(storeId: Int) { // viewModelScope.launch { @@ -92,4 +91,5 @@ class ProductViewModel(private val repository: ProductRepository) : ViewModel() // _storeDetail.value = storeResult // } // } + } \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_upload.xml b/app/src/main/res/drawable/bg_upload.xml new file mode 100644 index 0000000..093fc1d --- /dev/null +++ b/app/src/main/res/drawable/bg_upload.xml @@ -0,0 +1,10 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_close.png b/app/src/main/res/drawable/ic_close.png new file mode 100644 index 0000000000000000000000000000000000000000..72e947c11f560b30b246e87114cb3c66cd0b44e6 GIT binary patch literal 521 zcmV+k0`~ohP)C*DBUIwM|$n#T+dEd=tCKa0-1Z_^Hx@EszFlw4`(eRy?RhAgL%@b3j zBK)qZo=ewuME?Pl1S&iu5@13lkl`7z0OKox4$pWAFj9#a;TdlM##JI#__iwoU;-8< z;BXghnxg%T6rOnE-C<*s{AW2q(V}Z3zRlSy@`@~pZ79?*a9<%Iv zVH#hy2SA0{U?vWShwAO6pjhOqTeFL93j)K>6Wlf_ilQiraz^|CP@Pb${MxIt00000 LNkvXXu0mjf6V%v9 literal 0 HcmV?d00001 diff --git a/app/src/main/res/layout/activity_store_product_detail.xml b/app/src/main/res/layout/activity_store_product_detail.xml index 760dba0..4a1a522 100644 --- a/app/src/main/res/layout/activity_store_product_detail.xml +++ b/app/src/main/res/layout/activity_store_product_detail.xml @@ -69,15 +69,61 @@ - - + + + + + + + + + + + + + + + + + + + + @@ -245,7 +291,7 @@ android:layout_height="wrap_content" android:text="Harga Produk" style="@style/body_medium" - android:layout_marginRight="4dp"/> + android:layout_marginEnd="4dp"/> + android:gravity="center" + android:layout_marginTop="10dp"> - + android:gravity="center" + android:layout_marginTop="10dp"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Date: Sat, 12 Apr 2025 17:04:44 +0700 Subject: [PATCH 2/5] product --- .idea/AndroidProjectSystem.xml | 6 ++++++ app/build.gradle.kts | 4 ++-- 2 files changed, 8 insertions(+), 2 deletions(-) create mode 100644 .idea/AndroidProjectSystem.xml diff --git a/.idea/AndroidProjectSystem.xml b/.idea/AndroidProjectSystem.xml new file mode 100644 index 0000000..4a53bee --- /dev/null +++ b/.idea/AndroidProjectSystem.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 07c27e4..b33adf8 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -25,7 +25,7 @@ android { buildTypes { release { - buildConfigField("String", "BASE_URL", "\"http://192.168.100.156:3000/\"") + buildConfigField("String", "BASE_URL", "\"http://192.168.20.238:3000/\"") isMinifyEnabled = false proguardFiles( getDefaultProguardFile("proguard-android-optimize.txt"), @@ -33,7 +33,7 @@ android { ) } debug { - buildConfigField("String", "BASE_URL", "\"http://192.168.100.156:3000/\"") + buildConfigField("String", "BASE_URL", "\"http://192.168.20.238:3000/\"") } } compileOptions { From 5a8ae5e12bd007ab0a5e50521f59cf81ada4e698 Mon Sep 17 00:00:00 2001 From: Gracia Date: Tue, 15 Apr 2025 02:09:46 +0700 Subject: [PATCH 3/5] product gaselsai --- app/build.gradle.kts | 6 +- app/src/main/AndroidManifest.xml | 2 + .../ecommerce_serang/data/api/dto/Product.kt | 2 +- .../{ => product}/CreateProductResponse.kt | 3 +- .../data/api/retrofit/ApiConfig.kt | 1 + .../data/api/retrofit/ApiService.kt | 27 ++++- .../data/repository/ProductRepository.kt | 64 +++++------- .../product/StoreProductDetailActivity.kt | 99 +++++++++---------- .../utils/viewmodel/ProductViewModel.kt | 26 +++-- gradle/libs.versions.toml | 2 + 10 files changed, 124 insertions(+), 108 deletions(-) rename app/src/main/java/com/alya/ecommerce_serang/data/api/response/{ => product}/CreateProductResponse.kt (80%) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 468bd22..d01aae7 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -38,14 +38,14 @@ android { buildTypes { release { - buildConfigField("String", - "BASE_URL", - "\"${localProperties["BASE_URL"] ?: "http://default-url.com/"}\"") isMinifyEnabled = false proguardFiles( getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro" ) + buildConfigField("String", + "BASE_URL", + "\"${localProperties["BASE_URL"] ?: "http://default-url.com/"}\"") } debug { buildConfigField("String", diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index c1cfce9..4fb8fce 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -6,6 +6,8 @@ + + + @POST("order") + suspend fun postOrder( + @Body request: OrderRequest + ): Response + + @POST("order") + suspend fun postOrderBuyNow( + @Body request: OrderRequestBuy + ): Response + + @GET("profile/address") + suspend fun getAddress( + ): Response + + @POST("profile/addaddress") + suspend fun createAddress( + @Body createAddressRequest: CreateAddressRequest + ): Response @GET("mystore") suspend fun getStore (): Response @@ -106,11 +125,11 @@ interface ApiService { @Part("is_pre_order") isPreOrder: RequestBody, @Part("duration") duration: RequestBody, @Part("category_id") categoryId: RequestBody, - @Part("is_active") isActive: RequestBody, + @Part("status") status: RequestBody, @Part image: MultipartBody.Part?, @Part sppirt: MultipartBody.Part?, @Part halal: MultipartBody.Part? - ): Response + ): Response @GET("cart_item") suspend fun getCart (): Response diff --git a/app/src/main/java/com/alya/ecommerce_serang/data/repository/ProductRepository.kt b/app/src/main/java/com/alya/ecommerce_serang/data/repository/ProductRepository.kt index 30d9d3f..1a8781c 100644 --- a/app/src/main/java/com/alya/ecommerce_serang/data/repository/ProductRepository.kt +++ b/app/src/main/java/com/alya/ecommerce_serang/data/repository/ProductRepository.kt @@ -4,11 +4,13 @@ import android.util.Log import com.alya.ecommerce_serang.data.api.dto.CartItem import com.alya.ecommerce_serang.data.api.dto.CategoryItem import com.alya.ecommerce_serang.data.api.dto.ProductsItem +import com.alya.ecommerce_serang.data.api.response.product.CreateProductResponse import com.alya.ecommerce_serang.data.api.response.cart.AddCartResponse import com.alya.ecommerce_serang.data.api.response.product.ProductResponse import com.alya.ecommerce_serang.data.api.response.product.ReviewsItem import com.alya.ecommerce_serang.data.api.response.product.StoreProduct import com.alya.ecommerce_serang.data.api.retrofit.ApiService +import com.alya.ecommerce_serang.utils.SessionManager import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import okhttp3.MediaType.Companion.toMediaTypeOrNull @@ -158,51 +160,39 @@ class ProductRepository(private val apiService: ApiService) { isPreOrder: Boolean, duration: Int, categoryId: Int, - isActive: Boolean, - image: File?, - sppirt: File?, - halal: File? - ): Result = withContext(Dispatchers.IO) { - try { - val namePart = RequestBody.create("text/plain".toMediaTypeOrNull(), name) - val descriptionPart = RequestBody.create("text/plain".toMediaTypeOrNull(), description) - val pricePart = RequestBody.create("text/plain".toMediaTypeOrNull(), price.toString()) - val stockPart = RequestBody.create("text/plain".toMediaTypeOrNull(), stock.toString()) - val minOrderPart = RequestBody.create("text/plain".toMediaTypeOrNull(), minOrder.toString()) - val weightPart = RequestBody.create("text/plain".toMediaTypeOrNull(), weight.toString()) - val isPreOrderPart = RequestBody.create("text/plain".toMediaTypeOrNull(), isPreOrder.toString()) - val durationPart = RequestBody.create("text/plain".toMediaTypeOrNull(), duration.toString()) - val categoryIdPart = RequestBody.create("text/plain".toMediaTypeOrNull(), categoryId.toString()) - val isActivePart = RequestBody.create("text/plain".toMediaTypeOrNull(), if (isActive) "1" else "0") - - val imagePart = image?.let { - val req = RequestBody.create("image/*".toMediaTypeOrNull(), it) - MultipartBody.Part.createFormData("image", it.name, req) - } - - val sppirtPart = sppirt?.let { - val req = RequestBody.create("application/pdf".toMediaTypeOrNull(), it) - MultipartBody.Part.createFormData("sppirt", it.name, req) - } - - val halalPart = halal?.let { - val req = RequestBody.create("application/pdf".toMediaTypeOrNull(), it) - MultipartBody.Part.createFormData("halal", it.name, req) - } - + status: String, + imagePart: MultipartBody.Part?, + sppirtPart: MultipartBody.Part?, + halalPart: MultipartBody.Part? + ): Result { + return try { val response = apiService.addProduct( - namePart, descriptionPart, pricePart, stockPart, minOrderPart, - weightPart, isPreOrderPart, durationPart, categoryIdPart, isActivePart, - imagePart, sppirtPart, halalPart + name = RequestBody.create("text/plain".toMediaTypeOrNull(), name), + description = RequestBody.create("text/plain".toMediaTypeOrNull(), description), + price = RequestBody.create("text/plain".toMediaTypeOrNull(), price.toString()), + stock = RequestBody.create("text/plain".toMediaTypeOrNull(), stock.toString()), + minOrder = RequestBody.create("text/plain".toMediaTypeOrNull(), minOrder.toString()), + weight = RequestBody.create("text/plain".toMediaTypeOrNull(), weight.toString()), + isPreOrder = RequestBody.create("text/plain".toMediaTypeOrNull(), isPreOrder.toString()), + duration = RequestBody.create("text/plain".toMediaTypeOrNull(), duration.toString()), + categoryId = RequestBody.create("text/plain".toMediaTypeOrNull(), categoryId.toString()), + status = RequestBody.create("text/plain".toMediaTypeOrNull(), status), + image = imagePart, + sppirt = sppirtPart, + halal = halalPart ) - if (response.isSuccessful) Result.Success(Unit) - else Result.Error(Exception("Failed: ${response.code()}")) + if (response.isSuccessful) { + Result.Success(response.body()!!) + } else { + Result.Error(Exception("Failed to create product: ${response.code()}")) + } } catch (e: Exception) { Result.Error(e) } } + companion object { private const val TAG = "ProductRepository" } diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/profile/mystore/product/StoreProductDetailActivity.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/profile/mystore/product/StoreProductDetailActivity.kt index bccb5e3..cb7a24e 100644 --- a/app/src/main/java/com/alya/ecommerce_serang/ui/profile/mystore/product/StoreProductDetailActivity.kt +++ b/app/src/main/java/com/alya/ecommerce_serang/ui/profile/mystore/product/StoreProductDetailActivity.kt @@ -5,8 +5,7 @@ import android.content.Context import android.content.Intent import android.net.Uri import android.os.Bundle -import android.text.Editable -import android.text.TextWatcher +import android.util.Log import android.view.View import android.widget.ArrayAdapter import android.widget.Toast @@ -23,11 +22,11 @@ import com.alya.ecommerce_serang.databinding.ActivityStoreProductDetailBinding import com.alya.ecommerce_serang.utils.viewmodel.ProductViewModel import com.alya.ecommerce_serang.utils.BaseViewModelFactory import com.alya.ecommerce_serang.utils.SessionManager -import okhttp3.MediaType import okhttp3.MediaType.Companion.toMediaTypeOrNull import okhttp3.MultipartBody import okhttp3.RequestBody import java.io.File +import java.io.FileOutputStream import kotlin.getValue class StoreProductDetailActivity : AppCompatActivity() { @@ -62,7 +61,7 @@ class StoreProductDetailActivity : AppCompatActivity() { private val sppirtLauncher = registerForActivityResult(ActivityResultContracts.GetContent()) { uri -> if (uri != null && isValidFile(uri)) { sppirtUri = uri - binding.tvSppirtName.text = File(uri.path ?: "").name + binding.tvSppirtName.text = getFileName(uri) binding.switcherSppirt.showNext() } } @@ -70,7 +69,7 @@ class StoreProductDetailActivity : AppCompatActivity() { private val halalLauncher = registerForActivityResult(ActivityResultContracts.GetContent()) { uri -> if (uri != null && isValidFile(uri)) { halalUri = uri - binding.tvHalalName.text = File(uri.path ?: "").name + binding.tvHalalName.text = getFileName(uri) binding.switcherHalal.showNext() } } @@ -109,6 +108,12 @@ class StoreProductDetailActivity : AppCompatActivity() { imagePickerLauncher.launch(intent) } + binding.btnRemoveFoto.setOnClickListener { + imageUri = null + binding.switcherFotoProduk.showPrevious() + validateForm() + } + binding.layoutUploadSppirt.setOnClickListener { sppirtLauncher.launch("*/*") } binding.btnRemoveSppirt.setOnClickListener { sppirtUri = null @@ -121,33 +126,10 @@ class StoreProductDetailActivity : AppCompatActivity() { binding.switcherHalal.showPrevious() } - binding.btnRemoveFoto.setOnClickListener { - imageUri = null - binding.switcherFotoProduk.showPrevious() - validateForm() - } - - val watcher = 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?) { validateForm() } - } - - listOf( - binding.edtNamaProduk, - binding.edtDeskripsiProduk, - binding.edtHargaProduk, - binding.edtStokProduk, - binding.edtMinOrder, - binding.edtBeratProduk, - binding.edtDurasi - ).forEach { it.addTextChangedListener(watcher) } - validateForm() binding.btnSaveProduct.setOnClickListener { if (!binding.btnSaveProduct.isEnabled) { - focusFirstInvalidField() return@setOnClickListener } submitProduct() @@ -159,6 +141,10 @@ class StoreProductDetailActivity : AppCompatActivity() { return listOf("application/pdf", "image/jpeg", "image/png", "image/jpg").contains(mimeType) } + private fun getFileName(uri: Uri): String { + return uri.lastPathSegment?.split("/")?.last() ?: "unknown_file" + } + private fun validateForm() { val valid = binding.edtNamaProduk.text.isNotBlank() && binding.edtDeskripsiProduk.text.isNotBlank() && @@ -178,23 +164,15 @@ class StoreProductDetailActivity : AppCompatActivity() { ) } - private fun focusFirstInvalidField() { - when { - binding.edtNamaProduk.text.isBlank() -> binding.edtNamaProduk.requestFocus() - binding.edtDeskripsiProduk.text.isBlank() -> binding.edtDeskripsiProduk.requestFocus() - binding.edtHargaProduk.text.isBlank() -> binding.edtHargaProduk.requestFocus() - binding.edtStokProduk.text.isBlank() -> binding.edtStokProduk.requestFocus() - binding.edtMinOrder.text.isBlank() -> binding.edtMinOrder.requestFocus() - binding.edtBeratProduk.text.isBlank() -> binding.edtBeratProduk.requestFocus() - binding.switchIsPreOrder.isChecked && binding.edtDurasi.text.isBlank() -> binding.edtDurasi.requestFocus() - imageUri == null -> Toast.makeText(this, "Silakan unggah foto produk", Toast.LENGTH_SHORT).show() - } - } + private fun uriToNamedFile(uri: Uri, context: Context, prefix: String): File { + val extension = context.contentResolver.getType(uri)?.substringAfter("/") ?: "jpg" + val filename = "$prefix-${System.currentTimeMillis()}.$extension" + val file = File(context.cacheDir, filename) + + context.contentResolver.openInputStream(uri)?.use { input -> + FileOutputStream(file).use { output -> input.copyTo(output) } + } - private fun uriToFile(uri: Uri, context: Context): File { - val inputStream = context.contentResolver.openInputStream(uri) - val file = File.createTempFile("upload_", ".tmp", context.cacheDir) - inputStream?.use { input -> file.outputStream().use { input.copyTo(it) } } return file } @@ -207,30 +185,47 @@ class StoreProductDetailActivity : AppCompatActivity() { val weight = binding.edtBeratProduk.text.toString().toInt() val isPreOrder = binding.switchIsPreOrder.isChecked val duration = if (isPreOrder) binding.edtDurasi.text.toString().toInt() else 0 - val isActive = binding.switchIsActive.isChecked + val status = if (binding.switchIsActive.isChecked) "active" else "inactive" val categoryId = categoryList.getOrNull(binding.spinnerKategoriProduk.selectedItemPosition)?.id ?: 0 - val imageFile = imageUri?.let { uriToFile(it, this) } - val sppirtFile = sppirtUri?.let { uriToFile(it, this) } - val halalFile = halalUri?.let { uriToFile(it, this) } + val imageFile = imageUri?.let { File(it.path) } + val sppirtFile = sppirtUri?.let { uriToNamedFile(it, this, "sppirt") } + val halalFile = halalUri?.let { uriToNamedFile(it, this, "halal") } + + Log.d("File URI", "SPPIRT URI: ${sppirtUri.toString()}") + Log.d("File URI", "Halal URI: ${halalUri.toString()}") + + val imagePart = imageFile?.let { createPartFromFile("image", it) } + val sppirtPart = sppirtFile?.let { createPartFromFile("sppirt", it) } + val halalPart = halalFile?.let { createPartFromFile("halal", it) } viewModel.addProduct( - name, description, price, stock, minOrder, weight, isPreOrder, duration, categoryId, isActive, imageFile, sppirtFile, halalFile - ).observe(this) { result -> + name, description, price, stock, minOrder, weight, isPreOrder, duration, categoryId, status, imagePart, sppirtPart, halalPart + ) + + viewModel.productCreationResult.observe(this) { result -> when (result) { is Result.Loading -> binding.btnSaveProduct.isEnabled = false is Result.Success -> { - Toast.makeText(this, "Produk berhasil ditambahkan!", Toast.LENGTH_SHORT).show() + val product = result.data.product + Toast.makeText(this, "Product Created: ${product?.productName}", Toast.LENGTH_SHORT).show() finish() } is Result.Error -> { - Toast.makeText(this, "Gagal: ${result.exception.message}", Toast.LENGTH_SHORT).show() + Log.e("ProductDetailActivity", "Error: ${result.exception.message}") binding.btnSaveProduct.isEnabled = true } } } } + fun createPartFromFile(field: String, file: File?): MultipartBody.Part? { + return file?.let { + val requestBody = RequestBody.create("application/octet-stream".toMediaTypeOrNull(), it) + MultipartBody.Part.createFormData(field, it.name, requestBody) + } + } + private fun setupHeader() { binding.header.headerTitle.text = "Tambah Produk" binding.header.headerLeftIcon.setOnClickListener { onBackPressedDispatcher.onBackPressed() } diff --git a/app/src/main/java/com/alya/ecommerce_serang/utils/viewmodel/ProductViewModel.kt b/app/src/main/java/com/alya/ecommerce_serang/utils/viewmodel/ProductViewModel.kt index 3a85335..f8f6d9c 100644 --- a/app/src/main/java/com/alya/ecommerce_serang/utils/viewmodel/ProductViewModel.kt +++ b/app/src/main/java/com/alya/ecommerce_serang/utils/viewmodel/ProductViewModel.kt @@ -3,20 +3,23 @@ package com.alya.ecommerce_serang.utils.viewmodel import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel -import androidx.lifecycle.liveData import androidx.lifecycle.viewModelScope import com.alya.ecommerce_serang.data.api.dto.CategoryItem import com.alya.ecommerce_serang.data.api.dto.ProductsItem +import com.alya.ecommerce_serang.data.api.response.product.CreateProductResponse import com.alya.ecommerce_serang.data.api.response.product.Product import com.alya.ecommerce_serang.data.api.response.product.ReviewsItem import com.alya.ecommerce_serang.data.api.response.product.StoreProduct import com.alya.ecommerce_serang.data.repository.ProductRepository import com.alya.ecommerce_serang.data.repository.Result import kotlinx.coroutines.launch -import java.io.File +import okhttp3.MultipartBody class ProductViewModel(private val repository: ProductRepository) : ViewModel() { + private val _productCreationResult = MutableLiveData>() + val productCreationResult: LiveData> get() = _productCreationResult + private val _productDetail = MutableLiveData() val productDetail: LiveData get() = _productDetail @@ -75,13 +78,18 @@ class ProductViewModel(private val repository: ProductRepository) : ViewModel() isPreOrder: Boolean, duration: Int, categoryId: Int, - isActive: Boolean, - image: File?, - sppirt: File?, - halal: File? - ): LiveData> = liveData { - emit(Result.Loading) - emit(repository.addProduct(name, description, price, stock, minOrder, weight, isPreOrder, duration, categoryId, isActive, image, sppirt, halal)) + status: String, + imagePart: MultipartBody.Part?, + sppirtPart: MultipartBody.Part?, + halalPart: MultipartBody.Part? + ) { + _productCreationResult.value = Result.Loading + viewModelScope.launch { + val result = repository.addProduct( + name, description, price, stock, minOrder, weight, isPreOrder, duration, categoryId, status, imagePart, sppirtPart, halalPart + ) + _productCreationResult.value = result + } } // Optional: for store detail if you need it later diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 9c73f59..e967142 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -20,10 +20,12 @@ lifecycleViewmodelKtx = "2.8.7" fragmentKtx = "1.5.6" navigationFragmentKtx = "2.8.5" navigationUiKtx = "2.8.5" +recyclerview = "1.4.0" [libraries] androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } androidx-hilt-lifecycle-viewmodel = { module = "androidx.hilt:hilt-lifecycle-viewmodel", version.ref = "hiltLifecycleViewmodel" } +androidx-recyclerview = { module = "androidx.recyclerview:recyclerview", version.ref = "recyclerview" } hilt-android = { module = "com.google.dagger:hilt-android", version.ref = "hiltAndroid" } junit = { group = "junit", name = "junit", version.ref = "junit" } androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" } From 98b9cd64b76c9e90ed1b907a108f72a7b3e7cbd3 Mon Sep 17 00:00:00 2001 From: Gracia Date: Fri, 18 Apr 2025 02:35:27 +0700 Subject: [PATCH 4/5] ui update sells --- app/src/main/res/drawable/ic_person.png | Bin 0 -> 1112 bytes .../main/res/drawable/shape_sells_title.xml | 8 + app/src/main/res/layout/activity_my_store.xml | 2 +- app/src/main/res/layout/item_sells_order.xml | 292 ++++++++++++++++++ .../main/res/layout/item_store_product.xml | 2 +- 5 files changed, 302 insertions(+), 2 deletions(-) create mode 100644 app/src/main/res/drawable/ic_person.png create mode 100644 app/src/main/res/drawable/shape_sells_title.xml create mode 100644 app/src/main/res/layout/item_sells_order.xml diff --git a/app/src/main/res/drawable/ic_person.png b/app/src/main/res/drawable/ic_person.png new file mode 100644 index 0000000000000000000000000000000000000000..ab1e6ef5286b8edc04f1a78ac9f07597e5c7e3c8 GIT binary patch literal 1112 zcmV-e1gHCnP)@~0drDELIAGL9O(c600d`2O+f$vv5yP!i}3`36Bth*oj~V?5N3Ra zU{wl+unY7OEAy*Lp!Ofz&vf_n^Z=1aBoc{4B9Xg^U;`LqngcYl6xeaF1rHTl81;F) zK+V)Dz)N$wpFsBbwKeCGo`vhaF-INLL@fds^FDKM=a_H!G`V2#kRFjk8Fc}SIi;J+ zSAK0)x)HA6%Ta`*J`_gWlO~i<6F|iJWS51{ zx{g2|w#^Iie}b+14(QH#RA$jQgc52YaAMy5L_h$6J1}#yJoCjqNBn&Dj1Pt3J&kEn z#RHDbyIll~XGpZo4O5&yM}gY2XQX+FbO#Ek2#~-9Ue*<_&5^-EhG+a&%sw;Og#t=B zK(gs0bnPD~fp>Fy%D0&(0{=k)wInzVUHd0)A+-rW2};K>X9$(i4xu7|ID`t99lCe6 z;MQ^dfI!J?ohubWRnZYZM#0L>Vf%d1y^Q~l!Q+yuRD+szfG>eVvjL~N;nV3k-B)`K z4H_@|fyd=6nC?M=EhxdH_bFV|2#cNV)+=-Rh1FmG>RR6y7%!8XbDq_P+IuQOFYs%D z7FNP_+UDp0N~q2PT+=`1M~>ymeYijoRRMgQ`$Z3c$_b3&@omx+QEije))M1F$1u*H zVDa*}@cF}(Dw|iQ)}G+i9B*5eiI6}1KPF;0hhKb?ob|(n$e^={=4j-uQgaB5TU&l@ zpZQNgauouwx&O3Cifs(@IzvEB0B`IFja^^la3=8a?yJJGy*)2LpJnk63)`D)n&ePm ztpfPPj+MrqIB8+!0%#i7w@$jL3@+9w#>xVWN7`-gCxS;@MpSO9^mn-n3GrexZ}$c94%SI(Tbd_E3?^0?_zf8vnV%E$s0L zdwiBjEy;N=%i>XC%=7&EMUzGd|6J3Ko2m(ur+a`3-bE1}Q-ljtpppRBc6+j$uJd&~ zJHtI1(W9X~tbj2ecAUFxIlqDtQ3c_rDQv$*YFN@T{%^X0oR6yGSRqXRCQZX#orq zpF{d}_V?`5cKi4Y8{p*R?I9$Y=^=7J?w8$BF5M4c`5H_{n=;-ba@h$T9|e1nVM+_Y za&}X5{an@&ay4#@+)h^R3NtY}{KMRN4~PQbjuVq$*$Y#y!AB6a9h!BLV$ee*5{X12 ekw_$RBYyzUV0a_EHGne!0000 + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_my_store.xml b/app/src/main/res/layout/activity_my_store.xml index 1f6501a..8342ce0 100644 --- a/app/src/main/res/layout/activity_my_store.xml +++ b/app/src/main/res/layout/activity_my_store.xml @@ -83,7 +83,7 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +