diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 50af9ab..6693f7e 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -16,7 +16,10 @@ android:supportsRtl="true" android:theme="@style/Theme.Ecommerce_serang" android:usesCleartextTraffic="true" - tools:targetApi="31" > + tools:targetApi="31"> + @@ -58,8 +61,7 @@ android:exported="false" /> - + android:exported="false"> \ No newline at end of file diff --git a/app/src/main/java/com/alya/ecommerce_serang/data/api/response/ReviewProductResponse.kt b/app/src/main/java/com/alya/ecommerce_serang/data/api/response/ReviewProductResponse.kt index d7313b5..f9a927c 100644 --- a/app/src/main/java/com/alya/ecommerce_serang/data/api/response/ReviewProductResponse.kt +++ b/app/src/main/java/com/alya/ecommerce_serang/data/api/response/ReviewProductResponse.kt @@ -20,7 +20,7 @@ data class ReviewsItem( val reviewDate: String, @field:SerializedName("user_image") - val userImage: Any, + val userImage: String? = null, @field:SerializedName("product_id") val productId: Int, 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 c9b951a..5cf478a 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 @@ -5,6 +5,7 @@ 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.response.AllProductResponse import com.alya.ecommerce_serang.data.api.response.CategoryResponse +import com.alya.ecommerce_serang.data.api.response.DetailStoreProductResponse import com.alya.ecommerce_serang.data.api.response.LoginResponse import com.alya.ecommerce_serang.data.api.response.OtpResponse import com.alya.ecommerce_serang.data.api.response.ProductResponse @@ -55,6 +56,11 @@ interface ApiService { @GET("profile") suspend fun getUserProfile(): Response + @GET("store/detail/{id}") + suspend fun getDetailStore ( + @Path("id") storeId: Int + ): Response + @GET("mystore") fun getStore (): Call 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 2c82c17..519b5d4 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 @@ -23,6 +23,7 @@ class ProductRepository(private val apiService: ApiService) { } else { // Return a Result.Error with a custom Exception + Log.e("ProductRepository", "Error: ${response.errorBody()?.string()}") Result.Error(Exception("Failed to fetch products. Code: ${response.code()}")) } } catch (e: Exception) { @@ -78,5 +79,19 @@ class ProductRepository(private val apiService: ApiService) { null } } +} -} \ No newline at end of file +// suspend fun fetchStoreDetail(storeId: Int): Store? { +// return try { +// val response = apiService.getStore(storeId) +// if (response.isSucessful) { +// response.body()?.store +// } else { +// Log.e("ProductRepository", "Error: ${response.errorBody()?.string()}") +// +// null +// } +// } catch (e: Exception) { +// null +// } +// } \ No newline at end of file diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/product/DetailProductActivity.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/product/DetailProductActivity.kt index b55ed61..e8de51a 100644 --- a/app/src/main/java/com/alya/ecommerce_serang/ui/product/DetailProductActivity.kt +++ b/app/src/main/java/com/alya/ecommerce_serang/ui/product/DetailProductActivity.kt @@ -1,16 +1,21 @@ package com.alya.ecommerce_serang.ui.product +import android.content.Intent import android.os.Bundle import android.util.Log import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity +import androidx.recyclerview.widget.LinearLayoutManager import com.alya.ecommerce_serang.BuildConfig.BASE_URL import com.alya.ecommerce_serang.R +import com.alya.ecommerce_serang.data.api.dto.ProductsItem import com.alya.ecommerce_serang.data.api.response.Product +import com.alya.ecommerce_serang.data.api.response.ReviewsItem import com.alya.ecommerce_serang.data.api.retrofit.ApiConfig import com.alya.ecommerce_serang.data.api.retrofit.ApiService import com.alya.ecommerce_serang.data.repository.ProductRepository import com.alya.ecommerce_serang.databinding.ActivityDetailProductBinding +import com.alya.ecommerce_serang.ui.home.HorizontalProductAdapter import com.alya.ecommerce_serang.utils.BaseViewModelFactory import com.alya.ecommerce_serang.utils.SessionManager import com.bumptech.glide.Glide @@ -19,6 +24,8 @@ class DetailProductActivity : AppCompatActivity() { private lateinit var binding: ActivityDetailProductBinding private lateinit var apiService: ApiService private lateinit var sessionManager: SessionManager + private var productAdapter: HorizontalProductAdapter? = null + private var reviewsAdapter: ReviewsAdapter? = null private val viewModel: ProductViewModel by viewModels { BaseViewModelFactory { @@ -36,19 +43,15 @@ class DetailProductActivity : AppCompatActivity() { apiService = ApiConfig.getApiService(sessionManager) val productId = intent.getIntExtra("PRODUCT_ID", -1) + //nanti tambah get store id dari HomeFragment Product.storeId if (productId == -1) { Log.e("DetailProductActivity", "Invalid Product ID") finish() // Close activity if no valid ID return } -// ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets -> -// val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars()) -// v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom) -// insets -// } - viewModel.loadProductDetail(productId) + viewModel.loadReviews(productId) viewModel.productDetail.observe(this) { product -> if (product != null) { @@ -61,6 +64,7 @@ class DetailProductActivity : AppCompatActivity() { } } observeProductDetail() + observeProductReviews() } private fun observeProductDetail() { viewModel.productDetail.observe(this) { product -> @@ -68,6 +72,12 @@ class DetailProductActivity : AppCompatActivity() { } } + private fun observeProductReviews() { + viewModel.reviewProduct.observe(this) { reviews -> + setupRecyclerViewReviewsProduct(reviews) + } + } + private fun updateUI(product: Product){ binding.tvProductName.text = product.productName binding.tvPrice.text = product.price @@ -79,6 +89,10 @@ class DetailProductActivity : AppCompatActivity() { binding.tvDescription.text = product.description binding.tvSellerName.text = product.storeId.toString() + binding.tvViewAllReviews.setOnClickListener{ + handleAllReviewsClick(product.productId) + } + val fullImageUrl = when (val img = product.image) { is String -> { if (img.startsWith("/")) BASE_URL + img.substring(1) else img @@ -91,5 +105,52 @@ class DetailProductActivity : AppCompatActivity() { .load(fullImageUrl) .placeholder(R.drawable.placeholder_image) .into(binding.ivProductImage) + + setupRecyclerViewOtherProducts() + } + + private fun handleAllReviewsClick(productId: Int) { + val intent = Intent(this, ReviewProductActivity::class.java) + intent.putExtra("PRODUCT_ID", productId) // Pass product ID + startActivity(intent) + } + + private fun setupRecyclerViewOtherProducts(){ + productAdapter = HorizontalProductAdapter( + products = emptyList(), + onClick = { productsItem -> handleProductClick(productsItem) } + ) + + binding.recyclerViewOtherProducts.apply { + adapter = productAdapter + layoutManager = LinearLayoutManager( + context, + LinearLayoutManager.HORIZONTAL, + false + ) + } + } + + private fun setupRecyclerViewReviewsProduct(reviewList: List){ + val limitedReviewList = if (reviewList.isNotEmpty()) listOf(reviewList.first()) else emptyList() + + reviewsAdapter = ReviewsAdapter( + reviewList = limitedReviewList + ) + + binding.recyclerViewReviews.apply { + adapter = reviewsAdapter + layoutManager = LinearLayoutManager( + context, + LinearLayoutManager.VERTICAL, + false + ) + } + } + + private fun handleProductClick(product: ProductsItem) { + val intent = Intent(this, DetailProductActivity::class.java) + intent.putExtra("PRODUCT_ID", product.id) // Pass product ID + startActivity(intent) } } \ No newline at end of file diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/product/ProductViewModel.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/product/ProductViewModel.kt index f822239..87bb380 100644 --- a/app/src/main/java/com/alya/ecommerce_serang/ui/product/ProductViewModel.kt +++ b/app/src/main/java/com/alya/ecommerce_serang/ui/product/ProductViewModel.kt @@ -5,6 +5,8 @@ import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.alya.ecommerce_serang.data.api.response.Product +import com.alya.ecommerce_serang.data.api.response.ReviewsItem +import com.alya.ecommerce_serang.data.api.response.Store import com.alya.ecommerce_serang.data.repository.ProductRepository import kotlinx.coroutines.launch @@ -13,10 +15,30 @@ class ProductViewModel(private val repository: ProductRepository) : ViewModel() private val _productDetail = MutableLiveData() val productDetail: LiveData get() = _productDetail + private val _storeDetail = MutableLiveData() + val storeDetail : LiveData get() = _storeDetail + + private val _reviewProduct = MutableLiveData>() + val reviewProduct: LiveData> get() = _reviewProduct + fun loadProductDetail(productId: Int) { viewModelScope.launch { val result = repository.fetchProductDetail(productId) _productDetail.value = result?.product } } -} \ No newline at end of file + + fun loadReviews(productId: Int) { + viewModelScope.launch { + val reviews = repository.fetchProductReview(productId) + _reviewProduct.value = reviews ?: emptyList() + } + } +} + +// fun loadStoreDetail(storeId: Int){ +// viewModelScope.launch { +// val storeResult = repository.fetchStoreDetail(storeId) +// _storeDetail.value = storeResult +// } +// } \ No newline at end of file diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/product/ReviewProductActivity.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/product/ReviewProductActivity.kt new file mode 100644 index 0000000..ebcb6a4 --- /dev/null +++ b/app/src/main/java/com/alya/ecommerce_serang/ui/product/ReviewProductActivity.kt @@ -0,0 +1,79 @@ +package com.alya.ecommerce_serang.ui.product + +import android.os.Bundle +import android.util.Log +import android.view.View +import androidx.activity.enableEdgeToEdge +import androidx.activity.viewModels +import androidx.appcompat.app.AppCompatActivity +import androidx.recyclerview.widget.LinearLayoutManager +import com.alya.ecommerce_serang.data.api.retrofit.ApiConfig +import com.alya.ecommerce_serang.data.api.retrofit.ApiService +import com.alya.ecommerce_serang.data.repository.ProductRepository +import com.alya.ecommerce_serang.databinding.ActivityReviewProductBinding +import com.alya.ecommerce_serang.utils.BaseViewModelFactory +import com.alya.ecommerce_serang.utils.SessionManager + +class ReviewProductActivity : AppCompatActivity() { + private lateinit var binding: ActivityReviewProductBinding + private lateinit var apiService: ApiService + private var reviewsAdapter: ReviewsAdapter? = null + private lateinit var sessionManager: SessionManager + private val viewModel: ProductViewModel by viewModels { + BaseViewModelFactory { + val apiService = ApiConfig.getApiService(sessionManager) + val productRepository = ProductRepository(apiService) + ProductViewModel(productRepository) + } + } + + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + binding = ActivityReviewProductBinding.inflate(layoutInflater) + setContentView(binding.root) + + enableEdgeToEdge() + + sessionManager = SessionManager(this) + apiService = ApiConfig.getApiService(sessionManager) + + val productId = intent.getIntExtra("PRODUCT_ID", -1) // Get the product ID + if (productId == -1) { + Log.e("ReviewProductActivity", "Invalid Product ID") + finish() // Close the activity if the product ID is invalid + return + } + + setupRecyclerView() + viewModel.loadReviews(productId) // Fetch reviews using productId + + observeReviews() // Observe review data + } + + private fun observeReviews() { + viewModel.reviewProduct.observe(this) { reviews -> + if (reviews.isNotEmpty()) { + reviewsAdapter?.setReviews(reviews) + binding.tvNoReviews.visibility = View.GONE + } else { + binding.tvNoReviews.visibility = View.VISIBLE // Show "No Reviews" message + } + } + } + + private fun setupRecyclerView() { + reviewsAdapter = ReviewsAdapter( + reviewList = emptyList() + ) + + binding.rvReviewsProduct.apply { + adapter = reviewsAdapter + layoutManager = LinearLayoutManager( + context, + LinearLayoutManager.VERTICAL, + false + ) + } + } +} diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/product/ReviewsAdapter.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/product/ReviewsAdapter.kt new file mode 100644 index 0000000..6588d28 --- /dev/null +++ b/app/src/main/java/com/alya/ecommerce_serang/ui/product/ReviewsAdapter.kt @@ -0,0 +1,61 @@ +package com.alya.ecommerce_serang.ui.product + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.TextView +import androidx.recyclerview.widget.RecyclerView +import com.alya.ecommerce_serang.R +import com.alya.ecommerce_serang.data.api.response.ReviewsItem +import java.text.SimpleDateFormat +import java.util.Locale +import java.util.TimeZone + +class ReviewsAdapter( + private var reviewList: List +) : RecyclerView.Adapter() { + + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ReviewViewHolder { + val view = LayoutInflater.from(parent.context) + .inflate(R.layout.item_review, parent, false) + return ReviewViewHolder(view) + } + + override fun onBindViewHolder(holder: ReviewViewHolder, position: Int) { + val review = reviewList[position] + + with(holder) { + tvReviewerName.text = review.username + tvReviewRating.text = review.rating.toString() + tvReviewText.text = review.reviewText + tvDateReview.text = formatDate(review.reviewDate) + } + } + + override fun getItemCount(): Int = reviewList.size + + fun setReviews(reviews: List) { + reviewList = reviews + notifyDataSetChanged() + } + + private fun formatDate(dateString: String): String { + return try { + val inputFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.getDefault()) //from json + inputFormat.timeZone = TimeZone.getTimeZone("UTC") //get timezone + val outputFormat = SimpleDateFormat("dd MMMM yyyy", Locale.getDefault()) // new format + val date = inputFormat.parse(dateString) // Parse from json format + outputFormat.format(date!!) // convert to new format + } catch (e: Exception) { + dateString // Return original if error occurs + } + } + + class ReviewViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { + val tvReviewRating: TextView = itemView.findViewById(R.id.tvReviewRating) + val tvReviewerName: TextView = itemView.findViewById(R.id.tvUsername) + val tvReviewText: TextView = itemView.findViewById(R.id.tvReviewText) + val tvDateReview: TextView = itemView.findViewById(R.id.date_review) + } +} \ No newline at end of file diff --git a/app/src/main/res/layout/activity_detail_profile.xml b/app/src/main/res/layout/activity_detail_profile.xml index 389195b..55d422d 100644 --- a/app/src/main/res/layout/activity_detail_profile.xml +++ b/app/src/main/res/layout/activity_detail_profile.xml @@ -15,6 +15,7 @@ android:orientation="horizontal" android:gravity="center_vertical" android:padding="16dp" + android:layout_marginTop="16dp" app:layout_constraintTop_toTopOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent"> diff --git a/app/src/main/res/layout/activity_review_product.xml b/app/src/main/res/layout/activity_review_product.xml new file mode 100644 index 0000000..8203d26 --- /dev/null +++ b/app/src/main/res/layout/activity_review_product.xml @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/item_review.xml b/app/src/main/res/layout/item_review.xml index 0b1ad13..a59ed37 100644 --- a/app/src/main/res/layout/item_review.xml +++ b/app/src/main/res/layout/item_review.xml @@ -50,6 +50,14 @@ android:textColor="@color/black" /> +