Merge pull request #14 from shaulascr/gracia

Gracia
This commit is contained in:
Gracia Hotmauli
2025-04-15 02:11:17 +07:00
committed by GitHub
13 changed files with 507 additions and 183 deletions

View File

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

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,7 +9,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
@ -32,10 +35,12 @@ 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
import retrofit2.http.PUT
import retrofit2.http.Path
@ -108,20 +113,23 @@ interface ApiService {
@GET("category")
fun getCategories(): Call<CategoryResponse>
@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<Unit>
@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<CreateProductResponse>
@GET("cart_item")
suspend fun getCart (): Response<ListCartResponse>

View File

@ -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<List<ProductsItem>> =
@ -131,12 +137,16 @@ class ProductRepository(private val apiService: ApiService) {
}
suspend fun fetchMyStoreProducts(): List<ProductsItem> {
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<Unit> = withContext(Dispatchers.IO) {
try {
val status = if (isActive) "active" else "inactive"
status: String,
imagePart: MultipartBody.Part?,
sppirtPart: MultipartBody.Part?,
halalPart: MultipartBody.Part?
): Result<CreateProductResponse> {
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"
}

View File

@ -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<CategoryItem>
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,136 @@ 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 { 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, 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 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()
}
}
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<CategoryItem>) {
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() }
}
}

View File

@ -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<Result<CreateProductResponse>>()
val productCreationResult: LiveData<Result<CreateProductResponse>> get() = _productCreationResult
private val _productDetail = MutableLiveData<Product?>()
val productDetail: LiveData<Product?> get() = _productDetail
@ -74,17 +78,20 @@ class ProductViewModel(private val repository: ProductRepository) : ViewModel()
isPreOrder: Boolean,
duration: Int,
categoryId: Int,
isActive: Boolean
): LiveData<Result<Unit>> = 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
// }
// }
}

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<stroke
android:width="1dp"
android:color="@color/black_100"
android:dashWidth="5dp"
android:dashGap="5dp" />
<solid android:color="@color/white" />
</shape>

Binary file not shown.

After

Width:  |  Height:  |  Size: 521 B

View File

@ -69,15 +69,61 @@
</LinearLayout>
<!-- ImageView untuk preview gambar -->
<ImageView
android:id="@+id/iv_preview_foto"
android:layout_width="72dp"
android:layout_height="72dp"
android:layout_marginTop="8dp"
android:scaleType="centerCrop"
android:contentDescription="Preview Gambar"
android:src="@drawable/ic_upload"/>
<!-- ViewSwitcher untuk Upload Gambar Produk -->
<ViewSwitcher
android:id="@+id/switcher_foto_produk"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:layout_marginBottom="24dp">
<!-- Tampilan saat belum ada gambar di-upload -->
<FrameLayout
android:id="@+id/layout_upload_foto"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/bg_upload"
android:gravity="center"
android:padding="13dp">
<ImageView
android:layout_width="44dp"
android:layout_height="44dp"
android:src="@drawable/ic_upload"
android:contentDescription="Ikon Upload" />
</FrameLayout>
<!-- Tampilan saat gambar sudah di-upload -->
<FrameLayout
android:layout_width="70dp"
android:layout_height="70dp"
android:background="@drawable/bg_upload"
android:layout_gravity="center">
<ImageView
android:id="@+id/iv_preview_foto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="centerCrop"
android:contentDescription="Preview Gambar"
android:src="@drawable/placeholder_image"/>
<ImageButton
android:id="@+id/btn_remove_foto"
android:layout_width="12dp"
android:layout_height="12dp"
android:layout_gravity="top|end"
android:layout_margin="6dp"
android:elevation="8dp"
android:src="@drawable/ic_close"
android:contentDescription="Hapus Gambar"
android:padding="4dp"
app:tint="@color/white" />
</FrameLayout>
</ViewSwitcher>
</LinearLayout>
@ -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"/>
<TextView
android:layout_width="0dp"
@ -409,7 +455,8 @@
android:layout_height="wrap_content"
android:orientation="horizontal"
android:background="@drawable/bg_text_field"
android:gravity="center">
android:gravity="center"
android:layout_marginTop="10dp">
<EditText
android:id="@+id/edt_berat_produk"
@ -549,7 +596,6 @@
style="@style/body_medium"
android:layout_marginEnd="4dp"/>
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
@ -567,7 +613,8 @@
android:layout_height="wrap_content"
android:orientation="horizontal"
android:background="@drawable/bg_text_field"
android:gravity="center">
android:gravity="center"
android:layout_marginTop="10dp">
<EditText
android:id="@+id/edt_durasi"
@ -592,6 +639,178 @@
</LinearLayout>
<!-- SPPIRT -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_marginBottom="24dp">
<!-- Label SPPIRT -->
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="SPPIRT"
style="@style/body_medium"/>
<!-- ViewSwitcher: Switches antara upload box dan uploaded preview SPPIRT -->
<ViewSwitcher
android:id="@+id/switcher_sppirt"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp">
<!-- Upload Prompt Layout -->
<FrameLayout
android:id="@+id/layout_upload_sppirt"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/bg_upload"
android:padding="20dp">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_gravity="center"
android:gravity="center">
<ImageView
android:layout_width="48dp"
android:layout_height="48dp"
android:src="@drawable/ic_upload"
android:contentDescription="Ikon Unggah" />
<TextView
style="@style/body_small"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Unggah dokumen SPPIRT Anda di sini"
android:textColor="@color/black_300"
android:layout_marginTop="8dp"/>
</LinearLayout>
</FrameLayout>
<!-- Uploaded File Preview Layout -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:background="@drawable/bg_text_field"
android:gravity="center"
android:layout_marginTop="10dp"
android:padding="8dp">
<TextView
android:id="@+id/tv_sppirt_name"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="sppirt.pdf"
style="@style/body_small"/>
<ImageButton
android:id="@+id/btn_remove_sppirt"
android:layout_width="12dp"
android:layout_height="12dp"
android:src="@drawable/ic_close"
app:tint="@color/black_300"
android:contentDescription="Hapus unggahan" />
</LinearLayout>
</ViewSwitcher>
</LinearLayout>
<!-- Halal -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_marginBottom="24dp">
<!-- Label Halal -->
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Sertifikasi Halal"
style="@style/body_medium"/>
<!-- ViewSwitcher: Switches antara upload box dan uploaded preview halal -->
<ViewSwitcher
android:id="@+id/switcher_halal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp">
<!-- Upload Prompt Layout -->
<FrameLayout
android:id="@+id/layout_upload_halal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/bg_upload"
android:padding="20dp">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_gravity="center"
android:gravity="center">
<ImageView
android:layout_width="48dp"
android:layout_height="48dp"
android:src="@drawable/ic_upload"
android:contentDescription="Ikon Unggah" />
<TextView
style="@style/body_small"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Unggah dokumen Sertifikasi Halal Anda di sini"
android:textColor="@color/black_300"
android:layout_marginTop="8dp"/>
</LinearLayout>
</FrameLayout>
<!-- Uploaded File Preview Layout -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:background="@drawable/bg_text_field"
android:gravity="center"
android:layout_marginTop="10dp"
android:padding="8dp">
<TextView
android:id="@+id/tv_halal_name"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="sppirt.pdf"
style="@style/body_small"/>
<ImageButton
android:id="@+id/btn_remove_halal"
android:layout_width="12dp"
android:layout_height="12dp"
android:src="@drawable/ic_close"
app:tint="@color/black_300"
android:contentDescription="Hapus unggahan" />
</LinearLayout>
</ViewSwitcher>
</LinearLayout>
<!-- Produk Aktif -->
<LinearLayout
android:layout_width="match_parent"

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