product gaselsai

This commit is contained in:
Gracia
2025-04-15 02:09:46 +07:00
parent 4389cecc24
commit 5a8ae5e12b
10 changed files with 124 additions and 108 deletions

View File

@ -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",

View File

@ -6,6 +6,8 @@
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<application
android:allowBackup="true"

View File

@ -29,7 +29,7 @@ data class Product(
val categoryId: Int? = null,
@field:SerializedName("price")
val price: String? = null,
val price: Int? = null,
@field:SerializedName("name")
val name: String? = null,

View File

@ -1,6 +1,5 @@
package com.alya.ecommerce_serang.data.api.response
package com.alya.ecommerce_serang.data.api.response.product
import com.alya.ecommerce_serang.data.api.response.product.Product
import com.google.gson.annotations.SerializedName
data class CreateProductResponse(

View File

@ -7,6 +7,7 @@ import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import java.util.concurrent.TimeUnit
class ApiConfig {
companion object {

View File

@ -9,6 +9,7 @@ import com.alya.ecommerce_serang.data.api.dto.OrderRequestBuy
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.UpdateCart
import com.alya.ecommerce_serang.data.api.response.product.CreateProductResponse
import com.alya.ecommerce_serang.data.api.response.ViewStoreProductsResponse
import okhttp3.MultipartBody
import okhttp3.RequestBody
@ -34,9 +35,9 @@ import com.alya.ecommerce_serang.data.api.response.profile.ProfileResponse
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.Header
import retrofit2.http.HeaderMap
import retrofit2.http.Multipart
import retrofit2.http.POST
import retrofit2.http.Part
@ -84,6 +85,24 @@ interface ApiService {
@Path("id") storeId: Int
): Response<DetailStoreProductResponse>
@POST("order")
suspend fun postOrder(
@Body request: OrderRequest
): Response<CreateOrderResponse>
@POST("order")
suspend fun postOrderBuyNow(
@Body request: OrderRequestBuy
): Response<CreateOrderResponse>
@GET("profile/address")
suspend fun getAddress(
): Response<AddressResponse>
@POST("profile/addaddress")
suspend fun createAddress(
@Body createAddressRequest: CreateAddressRequest
): Response<CreateAddressResponse>
@GET("mystore")
suspend fun getStore (): Response<StoreResponse>
@ -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<Unit>
): Response<CreateProductResponse>
@GET("cart_item")
suspend fun getCart (): Response<ListCartResponse>

View File

@ -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<Unit> = 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<CreateProductResponse> {
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"
}

View File

@ -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() }

View File

@ -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<Result<CreateProductResponse>>()
val productCreationResult: LiveData<Result<CreateProductResponse>> get() = _productCreationResult
private val _productDetail = MutableLiveData<Product?>()
val productDetail: LiveData<Product?> 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<Result<Unit>> = 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

View File

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