From 5a8ae5e12bd007ab0a5e50521f59cf81ada4e698 Mon Sep 17 00:00:00 2001 From: Gracia Date: Tue, 15 Apr 2025 02:09:46 +0700 Subject: [PATCH] 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" }