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" }