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 c1cfce9..4fb8fce 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -6,6 +6,8 @@ + + + @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/StoreProductDetailActivity.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/profile/mystore/product/StoreProductDetailActivity.kt index af53295..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 @@ -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,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) { - 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/layout/activity_store_product_detail.xml b/app/src/main/res/layout/activity_store_product_detail.xml index 760dba0..4a1a522 100644 --- a/app/src/main/res/layout/activity_store_product_detail.xml +++ b/app/src/main/res/layout/activity_store_product_detail.xml @@ -69,15 +69,61 @@ - - + + + + + + + + + + + + + + + + + + + + @@ -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"/> + android:gravity="center" + android:layout_marginTop="10dp"> - + android:gravity="center" + android:layout_marginTop="10dp"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +