diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 329ea8a..d01aae7 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -18,7 +18,7 @@ val localProperties = Properties().apply { android { namespace = "com.alya.ecommerce_serang" - compileSdk = 34 + compileSdk = 35 defaultConfig { applicationId = "com.alya.ecommerce_serang" @@ -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", @@ -79,6 +79,7 @@ dependencies { implementation(libs.androidx.fragment.ktx) implementation(libs.androidx.navigation.fragment.ktx) implementation(libs.androidx.navigation.ui.ktx) + implementation(libs.androidx.recyclerview) testImplementation(libs.junit) androidTestImplementation(libs.androidx.junit) androidTestImplementation(libs.androidx.espresso.core) @@ -94,7 +95,6 @@ dependencies { implementation("de.hdodenhof:circleimageview:3.1.0") - // implementation(libs.hilt.android) // kapt("com.google.dagger:hilt-compiler:2.48") // @@ -103,3 +103,4 @@ dependencies { // kapt("androidx.hilt:hilt-compiler:1.0.0") } + diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 92511bf..42b49fb 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -6,6 +6,8 @@ + + diff --git a/app/src/main/java/com/alya/ecommerce_serang/data/api/dto/Product.kt b/app/src/main/java/com/alya/ecommerce_serang/data/api/dto/Product.kt index 41a98dc..a8d36e5 100644 --- a/app/src/main/java/com/alya/ecommerce_serang/data/api/dto/Product.kt +++ b/app/src/main/java/com/alya/ecommerce_serang/data/api/dto/Product.kt @@ -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, diff --git a/app/src/main/java/com/alya/ecommerce_serang/data/api/response/CreateProductResponse.kt b/app/src/main/java/com/alya/ecommerce_serang/data/api/response/product/CreateProductResponse.kt similarity index 80% rename from app/src/main/java/com/alya/ecommerce_serang/data/api/response/CreateProductResponse.kt rename to app/src/main/java/com/alya/ecommerce_serang/data/api/response/product/CreateProductResponse.kt index f8fd202..fcd9aec 100644 --- a/app/src/main/java/com/alya/ecommerce_serang/data/api/response/CreateProductResponse.kt +++ b/app/src/main/java/com/alya/ecommerce_serang/data/api/response/product/CreateProductResponse.kt @@ -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( diff --git a/app/src/main/java/com/alya/ecommerce_serang/data/api/retrofit/ApiConfig.kt b/app/src/main/java/com/alya/ecommerce_serang/data/api/retrofit/ApiConfig.kt index e8ddcb0..026c995 100644 --- a/app/src/main/java/com/alya/ecommerce_serang/data/api/retrofit/ApiConfig.kt +++ b/app/src/main/java/com/alya/ecommerce_serang/data/api/retrofit/ApiConfig.kt @@ -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 { 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 ceec7e6..1077a9c 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 @@ -11,7 +11,10 @@ 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 import com.alya.ecommerce_serang.data.api.response.auth.LoginResponse import com.alya.ecommerce_serang.data.api.response.auth.OtpResponse import com.alya.ecommerce_serang.data.api.response.auth.RegisterResponse @@ -36,14 +39,14 @@ import com.alya.ecommerce_serang.data.api.response.product.StoreResponse import com.alya.ecommerce_serang.data.api.response.profile.AddressResponse import com.alya.ecommerce_serang.data.api.response.profile.CreateAddressResponse import com.alya.ecommerce_serang.data.api.response.profile.ProfileResponse -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.Header +import retrofit2.http.HeaderMap import retrofit2.http.Multipart import retrofit2.http.POST import retrofit2.http.PUT @@ -142,20 +145,23 @@ 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 - ): Response + @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("status") status: RequestBody, + @Part image: MultipartBody.Part?, + @Part sppirt: MultipartBody.Part?, + @Part halal: MultipartBody.Part? + ): 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 dad6bef..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,13 +4,19 @@ 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 +import okhttp3.MultipartBody +import okhttp3.RequestBody +import java.io.File class ProductRepository(private val apiService: ApiService) { suspend fun getAllProducts(): Result> = @@ -131,12 +137,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 } } @@ -150,33 +160,39 @@ class ProductRepository(private val apiService: ApiService) { isPreOrder: Boolean, duration: Int, categoryId: Int, - isActive: Boolean - ): Result = withContext(Dispatchers.IO) { - try { - val status = if (isActive) "active" else "inactive" + status: String, + imagePart: MultipartBody.Part?, + sppirtPart: MultipartBody.Part?, + halalPart: MultipartBody.Part? + ): Result { + return try { val response = apiService.addProduct( - name = name, - description = description, - price = price, - stock = stock, - minOrder = minOrder, - weight = weight, - isPreOrder = isPreOrder, - duration = duration, - categoryId = categoryId, - isActive = status + 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) + Result.Success(response.body()!!) } else { - Result.Error(Exception("Failed to add product. Code: ${response.code()}")) + 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/ProductAdapter.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/profile/mystore/product/ProductAdapter.kt index 09ff20c..ec831be 100644 --- a/app/src/main/java/com/alya/ecommerce_serang/ui/profile/mystore/product/ProductAdapter.kt +++ b/app/src/main/java/com/alya/ecommerce_serang/ui/profile/mystore/product/ProductAdapter.kt @@ -4,10 +4,14 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.ImageView +import android.widget.PopupMenu import android.widget.TextView +import android.widget.Toast import androidx.core.content.ContextCompat +import androidx.fragment.app.FragmentActivity import androidx.recyclerview.widget.RecyclerView import com.alya.ecommerce_serang.R +import com.alya.ecommerce_serang.data.api.dto.Product import com.alya.ecommerce_serang.data.api.dto.ProductsItem import com.bumptech.glide.Glide @@ -22,27 +26,37 @@ class ProductAdapter( private val tvProductPrice: TextView = itemView.findViewById(R.id.tv_product_price) private val tvProductStock: TextView = itemView.findViewById(R.id.tv_product_stock) private val tvProductStatus: TextView = itemView.findViewById(R.id.tv_product_status) + private val ivMenu: ImageView = itemView.findViewById(R.id.iv_menu) fun bind(product: ProductsItem) { tvProductName.text = product.name tvProductPrice.text = "Rp${product.price}" tvProductStock.text = "Stok: ${product.stock}" - tvProductStatus.text = product.status - // Change color depending on status - tvProductStatus.setTextColor( - ContextCompat.getColor( - itemView.context, - if (product.status.equals("active", true)) - R.color.darkblue_500 else R.color.black_500 - ) - ) + if (product.status.equals("active",true)) { + tvProductStatus.text = "Aktif" + tvProductStatus.setTextColor(ContextCompat.getColor(itemView.context, R.color.darkblue_500)) + tvProductStatus.background = ContextCompat.getDrawable(itemView.context, R.drawable.bg_product_active) + } else { + tvProductStatus.text = "Nonaktif" + tvProductStatus.setTextColor(ContextCompat.getColor(itemView.context, R.color.black_500)) + tvProductStatus.background = ContextCompat.getDrawable(itemView.context, R.drawable.bg_product_inactive) + } Glide.with(itemView.context) .load(product.image) .placeholder(R.drawable.placeholder_image) .into(ivProduct) + ivMenu.setOnClickListener { + // Show Bottom Sheet when menu is clicked + val bottomSheetFragment = ProductOptionsBottomSheetFragment(product) + bottomSheetFragment.show( + (itemView.context as FragmentActivity).supportFragmentManager, + bottomSheetFragment.tag + ) + } + itemView.setOnClickListener { onItemClick(product) } diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/profile/mystore/product/ProductOptionsBottomSheetFragment.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/profile/mystore/product/ProductOptionsBottomSheetFragment.kt new file mode 100644 index 0000000..f2d348a --- /dev/null +++ b/app/src/main/java/com/alya/ecommerce_serang/ui/profile/mystore/product/ProductOptionsBottomSheetFragment.kt @@ -0,0 +1,46 @@ +package com.alya.ecommerce_serang.ui.profile.mystore.product + +import android.os.Bundle +import androidx.fragment.app.Fragment +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import com.alya.ecommerce_serang.R +import com.alya.ecommerce_serang.data.api.dto.ProductsItem +import com.alya.ecommerce_serang.databinding.FragmentProductOptionsBottomSheetBinding +import com.google.android.material.bottomsheet.BottomSheetDialogFragment + +class ProductOptionsBottomSheetFragment(private val product: ProductsItem) : BottomSheetDialogFragment() { + + private var _binding: FragmentProductOptionsBottomSheetBinding? = null + private val binding get() = _binding!! + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + _binding = FragmentProductOptionsBottomSheetBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + binding.btnEditProduct.setOnClickListener { + // Handle editing product + // Example: Open the edit activity or fragment + dismiss() + } + + binding.btnDeleteProduct.setOnClickListener { + // Handle deleting product + // Example: Show confirmation dialog + dismiss() + } + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } +} \ No newline at end of file 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..a22d850 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,15 @@ 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.util.Log 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 +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.Companion.toMediaTypeOrNull +import okhttp3.MultipartBody +import okhttp3.RequestBody +import java.io.File +import java.io.FileOutputStream import kotlin.getValue class StoreProductDetailActivity : AppCompatActivity() { @@ -25,6 +34,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 +47,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 = getFileName(uri) + binding.switcherSppirt.showNext() + } + } + + private val halalLauncher = registerForActivityResult(ActivityResultContracts.GetContent()) { uri -> + if (uri != null && isValidFile(uri)) { + halalUri = uri + binding.tvHalalName.text = getFileName(uri) + 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,126 +98,147 @@ 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.btnRemoveFoto.setOnClickListener { + imageUri = null + binding.switcherFotoProduk.showPrevious() + validateForm() + } + + 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() + } + validateForm() binding.btnSaveProduct.setOnClickListener { - if (binding.btnSaveProduct.isEnabled) addProduct() + if (!binding.btnSaveProduct.isEnabled) { + return@setOnClickListener + } + submitProduct() + } + } + + 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 getFileName(uri: Uri): String { + return uri.lastPathSegment?.split("/")?.last() ?: "unknown_file" + } + + 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 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) } + } + + return file + } + + private fun submitProduct() { + val name = binding.edtNamaProduk.text.toString() + val description = binding.edtDeskripsiProduk.text.toString() + 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 = if (isPreOrder) binding.edtDurasi.text.toString().toInt() else 0 + val status = if (binding.switchIsActive.isChecked) "active" else "inactive" + val categoryId = categoryList.getOrNull(binding.spinnerKategoriProduk.selectedItemPosition)?.id ?: 0 + + val imageFile = imageUri?.let { uriToNamedFile(it, this, "productimg") } + 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("productimg", 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, status, imagePart, sppirtPart, halalPart + ) + + viewModel.productCreationResult.observe(this) { result -> + when (result) { + is Result.Loading -> binding.btnSaveProduct.isEnabled = false + is Result.Success -> { + val product = result.data.product + Toast.makeText(this, "Product Created: ${product?.productName}", Toast.LENGTH_SHORT).show() + finish() + } + is Result.Error -> { + Log.e("ProductDetailActivity", "Error: ${result.exception.message}") + binding.btnSaveProduct.isEnabled = true + } + } + } + } + + fun getMimeType(file: File): String { + val extension = file.extension + return when (extension.lowercase()) { + "jpg", "jpeg" -> "image/jpeg" + "png" -> "image/png" + "pdf" -> "application/pdf" + else -> "application/octet-stream" + } + } + + fun createPartFromFile(field: String, file: File?): MultipartBody.Part? { + return file?.let { + val mimeType = getMimeType(it).toMediaTypeOrNull() + val requestBody = RequestBody.create(mimeType, it) + MultipartBody.Part.createFormData(field, it.name, requestBody) } } 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() - } - } - } - } - - 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 addProduct() { - 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 isPreOrder = binding.switchIsPreOrder.isChecked - val duration = binding.edtDurasi.text.toString().toIntOrNull() ?: 0 - val isActive = binding.switchIsActive.isChecked - val categoryPosition = binding.spinnerKategoriProduk.selectedItemPosition - val categoryId = categoryList.getOrNull(categoryPosition)?.id ?: 0 - - if (isPreOrder && duration == 0) { - Toast.makeText(this, "Durasi wajib diisi jika pre-order diaktifkan.", Toast.LENGTH_SHORT).show() - return - } - - viewModel.addProduct( - name, description, price, stock, minOrder, weight, isPreOrder, duration, categoryId, isActive - ).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() - finish() - } - is Result.Error -> { - binding.btnSaveProduct.isEnabled = true - Toast.makeText(this, "Gagal: ${result.exception.message}", Toast.LENGTH_SHORT).show() - } - } - } - } - - 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)) - } + 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 d573936..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,19 +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 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 @@ -74,17 +78,20 @@ class ProductViewModel(private val repository: ProductRepository) : ViewModel() isPreOrder: Boolean, duration: Int, categoryId: Int, - isActive: Boolean - ): LiveData> = liveData { - emit(Result.Loading) - val result = repository.addProduct( - name, description, price, stock, minOrder, weight, isPreOrder, duration, categoryId, isActive - ) - emit(result) + 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 // fun loadStoreDetail(storeId: Int) { // viewModelScope.launch { @@ -92,4 +99,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 0000000..72e947c Binary files /dev/null and b/app/src/main/res/drawable/ic_close.png differ diff --git a/app/src/main/res/drawable/ic_delete.png b/app/src/main/res/drawable/ic_delete.png new file mode 100644 index 0000000..3b50ef4 Binary files /dev/null and b/app/src/main/res/drawable/ic_delete.png differ diff --git a/app/src/main/res/drawable/ic_location.png b/app/src/main/res/drawable/ic_location.png new file mode 100644 index 0000000..4e59b4b Binary files /dev/null and b/app/src/main/res/drawable/ic_location.png differ 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 0000000..ab1e6ef Binary files /dev/null and b/app/src/main/res/drawable/ic_person.png differ diff --git a/app/src/main/res/drawable/shape_sells_title.xml b/app/src/main/res/drawable/shape_sells_title.xml new file mode 100644 index 0000000..939ef1f --- /dev/null +++ b/app/src/main/res/drawable/shape_sells_title.xml @@ -0,0 +1,8 @@ + + + + + \ 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..5cf0ceb 100644 --- a/app/src/main/res/layout/activity_my_store.xml +++ b/app/src/main/res/layout/activity_my_store.xml @@ -75,7 +75,6 @@