update add review, fix cart, fix order

This commit is contained in:
shaulascr
2025-05-13 11:07:30 +07:00
parent dfdb00cf98
commit edddecaaf0
15 changed files with 687 additions and 84 deletions

View File

@ -29,6 +29,9 @@
android:theme="@style/Theme.Ecommerce_serang"
android:usesCleartextTraffic="true"
tools:targetApi="31">
<activity
android:name=".ui.order.review.CreateReviewActivity"
android:exported="false" />
<activity
android:name=".ui.cart.MainActivity"
android:exported="false" />

View File

@ -10,5 +10,13 @@ data class ReviewProductItem (
val rating : Int,
@SerializedName("review_text")
val reviewTxt : String
val reviewTxt : String = ""
)
data class ReviewUIItem(
val orderItemId: Int,
val productName: String,
val productImage: String,
var rating: Int = 5, // Default rating is 5 stars
var reviewText: String = "" // Empty by default, to be filled by user
)

View File

@ -4,6 +4,12 @@ import com.google.gson.annotations.SerializedName
data class DetailStoreProductResponse(
@field:SerializedName("shipping")
val shipping: List<ShippingItemDetail>,
@field:SerializedName("payment")
val payment: List<PaymentItemDetail>,
@field:SerializedName("store")
val store: StoreProduct,
@ -11,28 +17,11 @@ data class DetailStoreProductResponse(
val message: String
)
data class PaymentInfoItem(
val id: Int = 1,
@field:SerializedName("qris_image")
val qrisImage: String? = null,
@field:SerializedName("bank_num")
val bankNum: String,
@field:SerializedName("name")
val name: String
)
data class StoreProduct(
@field:SerializedName("store_id")
val storeId: Int,
@field:SerializedName("shipping_service")
val shippingService: List<ShippingServiceItem>,
@field:SerializedName("store_rating")
val storeRating: String,
@ -45,21 +34,36 @@ data class StoreProduct(
@field:SerializedName("store_type")
val storeType: String,
@field:SerializedName("payment_info")
val paymentInfo: List<PaymentInfoItem>,
@field:SerializedName("store_location")
val storeLocation: String,
@field:SerializedName("store_image")
val storeImage: String,
val storeImage: String? = null,
@field:SerializedName("status")
val status: String
)
data class ShippingServiceItem(
data class ShippingItemDetail(
@field:SerializedName("courier")
val courier: String
)
data class PaymentItemDetail(
@field:SerializedName("qris_image")
val qrisImage: String,
@field:SerializedName("bank_num")
val bankNum: String,
@field:SerializedName("account_name")
val accountName: Any,
@field:SerializedName("bank_name")
val bankName: String,
@field:SerializedName("id")
val id: Int
)

View File

@ -8,15 +8,18 @@ import com.alya.ecommerce_serang.data.api.dto.CreateAddressRequest
import com.alya.ecommerce_serang.data.api.dto.OrderRequest
import com.alya.ecommerce_serang.data.api.dto.OrderRequestBuy
import com.alya.ecommerce_serang.data.api.dto.OrdersItem
import com.alya.ecommerce_serang.data.api.dto.ReviewProductItem
import com.alya.ecommerce_serang.data.api.dto.UpdateCart
import com.alya.ecommerce_serang.data.api.dto.UserProfile
import com.alya.ecommerce_serang.data.api.response.customer.cart.DataItemCart
import com.alya.ecommerce_serang.data.api.response.customer.order.CourierCostResponse
import com.alya.ecommerce_serang.data.api.response.customer.order.CreateOrderResponse
import com.alya.ecommerce_serang.data.api.response.customer.order.CreateReviewResponse
import com.alya.ecommerce_serang.data.api.response.customer.order.ListCityResponse
import com.alya.ecommerce_serang.data.api.response.customer.order.ListProvinceResponse
import com.alya.ecommerce_serang.data.api.response.customer.order.OrderDetailResponse
import com.alya.ecommerce_serang.data.api.response.customer.order.OrderListResponse
import com.alya.ecommerce_serang.data.api.response.customer.product.PaymentItemDetail
import com.alya.ecommerce_serang.data.api.response.customer.product.ProductResponse
import com.alya.ecommerce_serang.data.api.response.customer.product.StoreProduct
import com.alya.ecommerce_serang.data.api.response.customer.product.StoreResponse
@ -179,8 +182,6 @@ class OrderRepository(private val apiService: ApiService) {
}
}
suspend fun updateCart(updateCart: UpdateCart): Result<String> {
return try {
val response = apiService.updateCart(updateCart)
@ -236,6 +237,27 @@ class OrderRepository(private val apiService: ApiService) {
}
}
suspend fun fetchPaymentStore(storeId: Int): Result<List<PaymentItemDetail?>> {
return try {
val response = apiService.getDetailStore(storeId)
if (response.isSuccessful) {
val store = response.body()?.payment
if (store != null) {
Result.Success(store)
} else {
Result.Error(Exception("Payment details not found"))
}
} else {
val errorMsg = response.errorBody()?.string() ?: "Unknown error"
Log.e("Order Repository", "Error fetching store: $errorMsg")
Result.Error(Exception(errorMsg))
}
} catch (e: Exception) {
Log.e("Order Repository", "Exception fetching payment details", e)
Result.Error(e)
}
}
suspend fun addAddress(request: CreateAddressRequest): Result<CreateAddressResponse> {
return try {
Log.d("OrderRepository", "Adding address: $request")
@ -475,4 +497,27 @@ class OrderRepository(private val apiService: ApiService) {
}
}.flowOn(Dispatchers.IO)
suspend fun createReviewProduct(review: ReviewProductItem): Result<CreateReviewResponse>{
return try{
Log.d("Order Repository", "Sending review item product: $review")
val response = apiService.createReview(review)
if (response.isSuccessful){
response.body()?.let { reviewProductResponse ->
Log.d("Order Repository", " Successful create review. Review item rating: ${reviewProductResponse.rating}, orderItemId: ${reviewProductResponse.orderItemId}")
Result.Success(reviewProductResponse)
} ?: run {
Result.Error(Exception("Failed to create review"))
}
} else {
val errorMsg = response.errorBody()?.string() ?: "Unknown Error"
Log.e("Order Repository", "Error create review. Code ${response.code()}, Error: $errorMsg")
Result.Error(Exception(errorMsg))
}
} catch (e:Exception) {
Result.Error(e)
}
}
}

View File

@ -15,7 +15,7 @@ import androidx.recyclerview.widget.RecyclerView
import com.alya.ecommerce_serang.data.api.dto.CheckoutData
import com.alya.ecommerce_serang.data.api.dto.OrderRequest
import com.alya.ecommerce_serang.data.api.dto.OrderRequestBuy
import com.alya.ecommerce_serang.data.api.response.customer.product.PaymentInfoItem
import com.alya.ecommerce_serang.data.api.response.customer.product.PaymentItemDetail
import com.alya.ecommerce_serang.data.api.retrofit.ApiConfig
import com.alya.ecommerce_serang.data.repository.OrderRepository
import com.alya.ecommerce_serang.databinding.ActivityCheckoutBinding
@ -47,7 +47,6 @@ class CheckoutActivity : AppCompatActivity() {
sessionManager = SessionManager(this)
// Setup UI components
setupToolbar()
setupObservers()
@ -100,9 +99,7 @@ class CheckoutActivity : AppCompatActivity() {
updateOrderSummary()
if (data != null) {
viewModel.getPaymentMethods { paymentMethods ->
Log.d("CheckoutActivity", "Loaded ${paymentMethods.size} payment methods")
}
viewModel.getPaymentMethods()
}
}
@ -122,7 +119,7 @@ class CheckoutActivity : AppCompatActivity() {
viewModel.selectedPayment.observe(this) { selectedPayment ->
if (selectedPayment != null) {
Log.d("CheckoutActivity", "Observer notified of selected payment: ${selectedPayment.name}")
Log.d("CheckoutActivity", "Observer notified of selected payment: ${selectedPayment.bankName}")
// Update the adapter ONLY if it exists
paymentAdapter?.let { adapter ->
@ -157,7 +154,7 @@ class CheckoutActivity : AppCompatActivity() {
}
}
private fun setupPaymentMethodsRecyclerView(paymentMethods: List<PaymentInfoItem>) {
private fun setupPaymentMethodsRecyclerView(paymentMethods: List<PaymentItemDetail>) {
if (paymentMethods.isEmpty()) {
Log.e("CheckoutActivity", "Payment methods list is empty")
Toast.makeText(this, "No payment methods available", Toast.LENGTH_SHORT).show()
@ -169,7 +166,7 @@ class CheckoutActivity : AppCompatActivity() {
if (paymentAdapter == null) {
paymentAdapter = PaymentMethodAdapter(paymentMethods) { payment ->
Log.d("CheckoutActivity", "Payment selected in adapter: ${payment.name}")
Log.d("CheckoutActivity", "Payment selected in adapter: ${payment.bankName}")
// Set this payment as selected in the ViewModel
viewModel.setPaymentMethod(payment.id)
@ -182,7 +179,7 @@ class CheckoutActivity : AppCompatActivity() {
}
}
private fun updatePaymentMethodsAdapter(paymentMethods: List<PaymentInfoItem>, selectedId: Int?) {
private fun updatePaymentMethodsAdapter(paymentMethods: List<PaymentItemDetail>, selectedId: Int?) {
Log.d("CheckoutActivity", "Updating payment adapter with ${paymentMethods.size} methods")
// Simple test adapter
@ -198,7 +195,7 @@ class CheckoutActivity : AppCompatActivity() {
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
val payment = paymentMethods[position]
(holder.itemView as TextView).text = "Payment: ${payment.name}"
(holder.itemView as TextView).text = "Payment: ${payment.bankName}"
}
}

View File

@ -10,7 +10,7 @@ import com.alya.ecommerce_serang.data.api.dto.OrderRequest
import com.alya.ecommerce_serang.data.api.dto.OrderRequestBuy
import com.alya.ecommerce_serang.data.api.response.customer.cart.CartItemsItem
import com.alya.ecommerce_serang.data.api.response.customer.cart.DataItemCart
import com.alya.ecommerce_serang.data.api.response.customer.product.PaymentInfoItem
import com.alya.ecommerce_serang.data.api.response.customer.product.PaymentItemDetail
import com.alya.ecommerce_serang.data.api.response.customer.profile.AddressesItem
import com.alya.ecommerce_serang.data.repository.OrderRepository
import com.alya.ecommerce_serang.data.repository.Result
@ -24,12 +24,12 @@ class CheckoutViewModel(private val repository: OrderRepository) : ViewModel() {
private val _addressDetails = MutableLiveData<AddressesItem?>()
val addressDetails: LiveData<AddressesItem?> = _addressDetails
private val _availablePaymentMethods = MutableLiveData<List<PaymentInfoItem>>()
val availablePaymentMethods: LiveData<List<PaymentInfoItem>> = _availablePaymentMethods
private val _availablePaymentMethods = MutableLiveData<List<PaymentItemDetail>>()
val availablePaymentMethods: LiveData<List<PaymentItemDetail>> = _availablePaymentMethods
// Selected payment method
private val _selectedPayment = MutableLiveData<PaymentInfoItem?>()
val selectedPayment: LiveData<PaymentInfoItem?> = _selectedPayment
private val _selectedPayment = MutableLiveData<PaymentItemDetail?>()
val selectedPayment: LiveData<PaymentItemDetail?> = _selectedPayment
private val _isLoading = MutableLiveData<Boolean>()
val isLoading: LiveData<Boolean> = _isLoading
@ -156,48 +156,45 @@ class CheckoutViewModel(private val repository: OrderRepository) : ViewModel() {
}
}
fun getPaymentMethods(callback: (List<PaymentInfoItem>) -> Unit) {
fun getPaymentMethods() {
viewModelScope.launch {
try {
val storeId = _checkoutData.value?.sellerId ?: return@launch
Log.d(TAG, "Attempting to fetch payment methods for storeId: $storeId")
if (storeId == null || storeId <= 0) {
Log.e(TAG, "Invalid storeId: $storeId - cannot fetch payment methods")
val storeId = _checkoutData.value?.sellerId ?: run {
Log.e(TAG, "StoreId is null - cannot fetch payment methods")
_availablePaymentMethods.value = emptyList()
return@launch
}
// Use fetchStoreDetail instead of getStore
val storeResult = repository.fetchStoreDetail(storeId)
Log.d(TAG, "Attempting to fetch payment methods for storeId: $storeId")
if (storeResult is Result.Success && storeResult.data != null) {
// For now, we'll use hardcoded payment ID (1) for all payment methods
// This will be updated once the backend provides proper IDs
val paymentMethodsList = storeResult.data.paymentInfo.map { paymentInfo ->
PaymentInfoItem(
id = paymentInfo.id ?: 1,
name = paymentInfo.name,
bankNum = paymentInfo.bankNum,
qrisImage = paymentInfo.qrisImage
)
}
Log.d(TAG, "Fetched ${paymentMethodsList.size} payment methods")
// Only update if we don't already have payment methods
if (_availablePaymentMethods.value.isNullOrEmpty()) {
_availablePaymentMethods.value = paymentMethodsList
}
callback(paymentMethodsList)
} else {
if (storeId <= 0) {
Log.e(TAG, "Invalid storeId: $storeId - cannot fetch payment methods")
_availablePaymentMethods.value = emptyList()
callback(emptyList())
return@launch
}
val result = repository.fetchPaymentStore(storeId)
when (result) {
is Result.Success -> {
val paymentMethods = result.data?.filterNotNull() ?: emptyList()
Log.d(TAG, "Fetched ${paymentMethods.size} payment methods")
// Update payment methods
_availablePaymentMethods.value = paymentMethods
}
is Result.Error -> {
Log.e(TAG, "Error fetching payment methods: ${result.exception.message}")
_availablePaymentMethods.value = emptyList()
}
is Result.Loading -> {
null
}
}
} catch (e: Exception) {
Log.e(TAG, "Error fetching payment methods", e)
Log.e(TAG, "Exception in getPaymentMethods", e)
_availablePaymentMethods.value = emptyList()
callback(emptyList())
}
}
}
@ -211,7 +208,7 @@ class CheckoutViewModel(private val repository: OrderRepository) : ViewModel() {
if (paymentMethods.isNullOrEmpty()) {
// If no payment methods available, try to fetch them
getPaymentMethods { /* do nothing here */ }
getPaymentMethods()
return@launch
}
@ -224,7 +221,7 @@ class CheckoutViewModel(private val repository: OrderRepository) : ViewModel() {
// Set the selected payment - IMPORTANT: do this first
_selectedPayment.value = selectedPayment
Log.d(TAG, "Payment selected: ID=${selectedPayment.id}, Name=${selectedPayment.name}")
Log.d(TAG, "Payment selected: ID=${selectedPayment.id}, Name=${selectedPayment.bankName}")
// Update the order request with the payment method ID
val currentData = _checkoutData.value ?: return@launch

View File

@ -6,14 +6,14 @@ import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.alya.ecommerce_serang.R
import com.alya.ecommerce_serang.data.api.response.customer.product.PaymentInfoItem
import com.alya.ecommerce_serang.data.api.response.customer.product.PaymentItemDetail
import com.alya.ecommerce_serang.databinding.ItemPaymentMethodBinding
import com.bumptech.glide.Glide
import com.bumptech.glide.request.RequestOptions
class PaymentMethodAdapter(
private val paymentMethods: List<PaymentInfoItem>,
private val onPaymentSelected: (PaymentInfoItem) -> Unit
private val paymentMethods: List<PaymentItemDetail>,
private val onPaymentSelected: (PaymentItemDetail) -> Unit
) : RecyclerView.Adapter<PaymentMethodAdapter.PaymentMethodViewHolder>() {
// Track the selected payment by ID
@ -38,13 +38,13 @@ class PaymentMethodAdapter(
with(holder.binding) {
// Set payment method name
tvPaymentMethodName.text = payment.name
tvPaymentMethodName.text = payment.bankName
// Set radio button state based on selected payment ID
rbPaymentMethod.isChecked = payment.id == selectedPaymentId
// Debug log
Log.d("PaymentAdapter", "Binding item ${payment.name}, checked=${rbPaymentMethod.isChecked}")
Log.d("PaymentAdapter", "Binding item ${payment.bankName}, checked=${rbPaymentMethod.isChecked}")
// Load payment icon if available
if (!payment.qrisImage.isNullOrEmpty()) {
@ -84,7 +84,7 @@ class PaymentMethodAdapter(
// Call the callback ONLY ONCE
onPaymentSelected(payment)
Log.d("PaymentAdapter", "Payment selected: ${payment.name}")
Log.d("PaymentAdapter", "Payment selected: ${payment.bankName}")
}
}
@ -118,7 +118,7 @@ class PaymentMethodAdapter(
}
// Set selected payment object
fun setSelectedPayment(payment: PaymentInfoItem) {
fun setSelectedPayment(payment: PaymentItemDetail) {
setSelectedPaymentId(payment.id)
}
}

View File

@ -7,6 +7,8 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.alya.ecommerce_serang.data.api.dto.CompletedOrderRequest
import com.alya.ecommerce_serang.data.api.dto.OrdersItem
import com.alya.ecommerce_serang.data.api.response.customer.order.OrderListItemsItem
import com.alya.ecommerce_serang.data.api.response.customer.order.Orders
import com.alya.ecommerce_serang.data.api.response.order.CompletedOrderResponse
import com.alya.ecommerce_serang.data.repository.OrderRepository
import com.alya.ecommerce_serang.data.repository.Result
@ -26,6 +28,13 @@ class HistoryViewModel(private val repository: OrderRepository) : ViewModel() {
private val _orderCompletionStatus = MutableLiveData<Result<CompletedOrderResponse>>()
val orderCompletionStatus: LiveData<Result<CompletedOrderResponse>> = _orderCompletionStatus
private val _orderDetails = MutableLiveData<Orders>()
val orderDetails: LiveData<Orders> get() = _orderDetails
// LiveData untuk OrderItems
private val _orderItems = MutableLiveData<List<OrderListItemsItem>>()
val orderItems: LiveData<List<OrderListItemsItem>> get() = _orderItems
private val _isLoading = MutableLiveData<Boolean>()
val isLoading: LiveData<Boolean> = _isLoading
@ -35,6 +44,9 @@ class HistoryViewModel(private val repository: OrderRepository) : ViewModel() {
private val _isSuccess = MutableLiveData<Boolean>()
val isSuccess: LiveData<Boolean> = _isSuccess
private val _error = MutableLiveData<String>()
val error: LiveData<String> get() = _error
fun getOrderList(status: String) {
_orders.value = ViewState.Loading
viewModelScope.launch {
@ -99,4 +111,24 @@ class HistoryViewModel(private val repository: OrderRepository) : ViewModel() {
}
}
}
fun getOrderDetails(orderId: Int) {
_isLoading.value = true
viewModelScope.launch {
try {
val response = repository.getOrderDetails(orderId)
if (response != null) {
_orderDetails.value = response.orders
_orderItems.value = response.orders.orderItems
} else {
_error.value = "Gagal memuat detail pesanan"
}
} catch (e: Exception) {
_error.value = "Terjadi kesalahan: ${e.message}"
Log.e(TAG, "Error fetching order details", e)
} finally {
_isLoading.value = false
}
}
}
}

View File

@ -22,9 +22,13 @@ import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.alya.ecommerce_serang.R
import com.alya.ecommerce_serang.data.api.dto.OrdersItem
import com.alya.ecommerce_serang.data.api.dto.ReviewUIItem
import com.alya.ecommerce_serang.ui.order.detail.PaymentActivity
import com.alya.ecommerce_serang.ui.order.review.CreateReviewActivity
import com.alya.ecommerce_serang.ui.product.ReviewProductActivity
import com.google.android.material.button.MaterialButton
import com.google.android.material.textfield.TextInputLayout
import com.google.gson.Gson
import java.io.File
import java.text.SimpleDateFormat
import java.util.Calendar
@ -236,7 +240,7 @@ class OrderHistoryAdapter(
}
deadlineDate.apply {
visibility = View.VISIBLE
text = formatShipmentDate(order.updatedAt, order.etd.toInt())
text = formatShipmentDate(order.updatedAt, order.etd ?: "0")
}
}
"delivered" -> {
@ -262,6 +266,7 @@ class OrderHistoryAdapter(
visibility = View.VISIBLE
text = itemView.context.getString(R.string.add_review)
setOnClickListener {
addReviewProduct(order)
// Handle click event
}
}
@ -322,9 +327,11 @@ class OrderHistoryAdapter(
}
}
private fun formatShipmentDate(dateString: String, estimate: Int): String {
private fun formatShipmentDate(dateString: String, estimate: String): String {
return try {
// Parse the input date
val estimateTD = if (estimate.isNullOrEmpty()) 0 else estimate.toInt()
val inputFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.getDefault())
inputFormat.timeZone = TimeZone.getTimeZone("UTC")
@ -339,7 +346,7 @@ class OrderHistoryAdapter(
calendar.time = it
// Add estimated days
calendar.add(Calendar.DAY_OF_MONTH, estimate)
calendar.add(Calendar.DAY_OF_MONTH, estimateTD)
outputFormat.format(calendar.time)
} ?: dateString
} catch (e: Exception) {
@ -485,10 +492,70 @@ class OrderHistoryAdapter(
}
dialog.show()
}
private fun addReviewProduct(order: OrdersItem) {
// Use ViewModel to fetch order details
viewModel.getOrderDetails(order.orderId)
// Create loading dialog
// val loadingDialog = Dialog(itemView.context).apply {
// requestWindowFeature(Window.FEATURE_NO_TITLE)
// setContentView(R.layout.dialog_loading)
// window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
// setCancelable(false)
// }
// loadingDialog.show()
viewModel.error.observe(itemView.findViewTreeLifecycleOwner()!!) { errorMsg ->
if (!errorMsg.isNullOrEmpty()) {
Toast.makeText(itemView.context, errorMsg, Toast.LENGTH_SHORT).show()
}
}
// Observe the order details result
viewModel.orderItems.observe(itemView.findViewTreeLifecycleOwner()!!) { orderItems ->
if (orderItems != null && orderItems.isNotEmpty()) {
// For single item review
if (orderItems.size == 1) {
val item = orderItems[0]
val intent = Intent(itemView.context, CreateReviewActivity::class.java).apply {
putExtra("order_item_id", item.orderItemId)
putExtra("product_name", item.productName)
putExtra("product_image", item.productImage)
}
(itemView.context as Activity).startActivityForResult(intent, REQUEST_CODE_REVIEW)
}
// For multiple items
else {
val reviewItems = orderItems.map { item ->
ReviewUIItem(
orderItemId = item.orderItemId,
productName = item.productName,
productImage = item.productImage
)
}
val itemsJson = Gson().toJson(reviewItems)
val intent = Intent(itemView.context, ReviewProductActivity::class.java).apply {
putExtra("order_items", itemsJson)
}
(itemView.context as Activity).startActivityForResult(intent, REQUEST_CODE_REVIEW)
}
} else {
Toast.makeText(
itemView.context,
"No items to review",
Toast.LENGTH_SHORT
).show()
}
}
}
}
companion object {
private const val REQUEST_IMAGE_PICK = 100
const val REQUEST_CODE_REVIEW = 101
private var imagePickCallback: ((Uri) -> Unit)? = null
// This method should be called from the activity's onActivityResult

View File

@ -0,0 +1,64 @@
package com.alya.ecommerce_serang.ui.order.review
import android.text.Editable
import android.text.TextWatcher
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.alya.ecommerce_serang.R
import com.alya.ecommerce_serang.data.api.dto.ReviewUIItem
import com.alya.ecommerce_serang.databinding.ItemReviewProductBinding
import com.bumptech.glide.Glide
class AddReviewAdapter(
private val items: List<ReviewUIItem>,
private val onRatingChanged: (position: Int, rating: Int) -> Unit,
private val onReviewTextChanged: (position: Int, text: String) -> Unit
) : RecyclerView.Adapter<AddReviewAdapter.AddReviewViewHolder>() {
inner class AddReviewViewHolder(private val binding: ItemReviewProductBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(item: ReviewUIItem) {
binding.apply {
tvProductName.text = item.productName
Glide.with(itemView.context)
.load(item.productImage)
.placeholder(R.drawable.placeholder_image)
.error(R.drawable.placeholder_image)
.into(ivProduct)
ratingBar.rating = item.rating.toFloat()
etReviewText.setText(item.reviewText)
ratingBar.setOnRatingBarChangeListener { _, rating, _ ->
onRatingChanged(adapterPosition, rating.toInt())
}
etReviewText.addTextChangedListener(object : TextWatcher {
override fun beforeTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) {}
override fun onTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) {}
override fun afterTextChanged(editable: Editable?) {
onReviewTextChanged(adapterPosition, editable.toString())
}
})
}
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AddReviewViewHolder {
val binding = ItemReviewProductBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
return AddReviewViewHolder(binding)
}
override fun onBindViewHolder(holder: AddReviewViewHolder, position: Int) {
holder.bind(items[position])
}
override fun getItemCount(): Int = items.size
}

View File

@ -0,0 +1,188 @@
package com.alya.ecommerce_serang.ui.order.review
import android.os.Bundle
import android.util.Log
import android.widget.Toast
import androidx.activity.enableEdgeToEdge
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.ViewCompat
import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat
import androidx.recyclerview.widget.LinearLayoutManager
import com.alya.ecommerce_serang.data.api.dto.ReviewProductItem
import com.alya.ecommerce_serang.data.api.dto.ReviewUIItem
import com.alya.ecommerce_serang.data.api.retrofit.ApiConfig
import com.alya.ecommerce_serang.data.repository.OrderRepository
import com.alya.ecommerce_serang.data.repository.Result
import com.alya.ecommerce_serang.databinding.ActivityCreateReviewBinding
import com.alya.ecommerce_serang.utils.BaseViewModelFactory
import com.alya.ecommerce_serang.utils.SessionManager
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
class CreateReviewActivity : AppCompatActivity() {
private lateinit var binding: ActivityCreateReviewBinding
private lateinit var sessionManager: SessionManager
private val reviewItems = mutableListOf<ReviewUIItem>()
private var addReviewAdapter: AddReviewAdapter? = null
private val viewModel : CreateReviewViewModel by viewModels {
BaseViewModelFactory {
val apiService = ApiConfig.getApiService(sessionManager)
val orderRepository = OrderRepository(apiService)
CreateReviewViewModel(orderRepository)
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityCreateReviewBinding.inflate(layoutInflater)
setContentView(binding.root)
sessionManager = SessionManager(this)
WindowCompat.setDecorFitsSystemWindows(window, false)
enableEdgeToEdge()
ViewCompat.setOnApplyWindowInsetsListener(binding.root) { view, windowInsets ->
val systemBars = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
view.setPadding(
systemBars.left,
systemBars.top,
systemBars.right,
systemBars.bottom
)
windowInsets
}
setupToolbar()
getIntentData()
setupRecyclerView()
observeViewModel()
setupSubmitButton()
}
private fun setupToolbar() {
setSupportActionBar(binding.toolbar)
supportActionBar?.setDisplayShowTitleEnabled(false)
binding.btnBack.setOnClickListener { onBackPressed() }
}
private fun getIntentData() {
// First check if multiple items were passed
val orderItemsJson = intent.getStringExtra("order_items")
if (orderItemsJson != null) {
try {
val type = object : TypeToken<List<ReviewUIItem>>() {}.type
val items: List<ReviewUIItem> = Gson().fromJson(orderItemsJson, type)
// Make sure we explicitly set rating and reviewText
reviewItems.addAll(items.map { item ->
ReviewUIItem(
orderItemId = item.orderItemId,
productName = item.productName,
productImage = item.productImage,
rating = 5, // Default to 5 stars
reviewText = "" // Empty by default
)
})
} catch (e: Exception) {
Toast.makeText(this, "Error loading review items", Toast.LENGTH_SHORT).show()
finish()
}
} else {
// Check if a single item was passed
val orderItemId = intent.getIntExtra("order_item_id", -1)
val productName = intent.getStringExtra("product_name") ?: ""
val productImage = intent.getStringExtra("product_image") ?: ""
if (orderItemId != -1) {
reviewItems.add(
ReviewUIItem(
orderItemId = orderItemId,
productName = productName,
productImage = productImage,
rating = 5, // Default to 5 stars
reviewText = "" // Empty by default
)
)
} else {
Toast.makeText(this, "No items to review", Toast.LENGTH_SHORT).show()
finish()
}
}
}
private fun setupRecyclerView() {
addReviewAdapter = AddReviewAdapter(
reviewItems,
onRatingChanged = { position, rating ->
reviewItems[position].rating = rating
},
onReviewTextChanged = { position, text ->
reviewItems[position].reviewText = text
}
)
binding.rvReviewItems.apply {
layoutManager = LinearLayoutManager(this@CreateReviewActivity)
adapter = addReviewAdapter
}
}
private fun observeViewModel() {
viewModel.reviewSubmitStatus.observe(this) { result ->
when (result) {
is Result.Loading -> {
// Show loading indicator
// You can add a ProgressBar in your layout and show/hide it here
}
is Result.Success -> {
// All reviews submitted successfully
Toast.makeText(this, "Ulasan berhasil dikirim", Toast.LENGTH_SHORT).show()
setResult(RESULT_OK)
finish()
}
is Result.Error -> {
// Show error message
Log.e("CreateReviewActivity", "Error: ${result.exception}")
// Toast.makeText(this, result.message, Toast.LENGTH_SHORT).show()
}
}
}
}
private fun setupSubmitButton() {
binding.btnSubmitReview.setOnClickListener {
// Validate all reviews
var isValid = true
for (item in reviewItems) {
if (item.reviewText.isBlank()) {
isValid = false
Toast.makeText(this, "Mohon isi semua ulasan", Toast.LENGTH_SHORT).show()
break
}
}
// In setupSubmitButton() method
if (isValid) {
viewModel.setTotalReviewsToSubmit(reviewItems.size)
// Submit all reviews
for (item in reviewItems) {
Log.d("ReviewActivity", "Submitting review for item ${item.orderItemId}: rating=${item.rating}, text=${item.reviewText}")
val reviewProductItem = ReviewProductItem(
orderItemId = item.orderItemId,
rating = item.rating,
reviewTxt = item.reviewText
)
viewModel.submitReview(reviewProductItem)
}
}
}
}
}

View File

@ -0,0 +1,41 @@
package com.alya.ecommerce_serang.ui.order.review
import android.util.Log
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.alya.ecommerce_serang.data.api.dto.ReviewProductItem
import com.alya.ecommerce_serang.data.api.response.customer.order.CreateReviewResponse
import com.alya.ecommerce_serang.data.repository.OrderRepository
import com.alya.ecommerce_serang.data.repository.Result
import kotlinx.coroutines.launch
class CreateReviewViewModel(private val repository: OrderRepository): ViewModel() {
private val _reviewSubmitStatus = MutableLiveData<Result<CreateReviewResponse>>()
val reviewSubmitStatus: LiveData<Result<CreateReviewResponse>> = _reviewSubmitStatus
private val _reviewsSubmitted = MutableLiveData(0)
private var totalReviewsToSubmit = 0
private var anyFailures = false
fun submitReview(reviewItem: ReviewProductItem) {
viewModelScope.launch {
try {
_reviewSubmitStatus.value = Result.Loading
val result = repository.createReviewProduct(reviewItem)
_reviewSubmitStatus.value = result
} catch (e: Exception) {
anyFailures = true
Log.e("CreateReviewViewModel", "Error create review: ${e.message}")
_reviewSubmitStatus.value = Result.Error(e)
}
}
}
fun setTotalReviewsToSubmit(count: Int) {
totalReviewsToSubmit = count
_reviewsSubmitted.value = 0
anyFailures = false
}
}

View File

@ -0,0 +1,72 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ui.order.review.CreateReviewActivity">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/appBarLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="@color/white">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageButton
android:id="@+id/btnBack"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackgroundBorderless"
android:src="@drawable/ic_back_24"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Tambah Ulasan"
android:textColor="@color/black"
android:textSize="18sp"
android:layout_marginHorizontal="8dp"
android:fontFamily="@font/dmsans_medium"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@+id/btnBack"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/btnSubmitReview"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:text="Kirim Ulasan"
android:fontFamily="@font/dmsans_semibold"
android:textColor="@color/blue_500"
android:textSize="14sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.appcompat.widget.Toolbar>
</com.google.android.material.appbar.AppBarLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rvReviewItems"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toBottomOf="@+id/appBarLayout" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,84 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp"
app:cardCornerRadius="8dp"
app:cardElevation="2dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<ImageView
android:id="@+id/ivProduct"
android:layout_width="48dp"
android:layout_height="48dp"
android:scaleType="centerCrop"
android:src="@drawable/placeholder_image" />
<TextView
android:id="@+id/tvProductName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginStart="12dp"
android:text="Keripik Balado"
android:textColor="@color/black"
android:fontFamily="@font/dmsans_regular"
android:textSize="14sp"
android:textStyle="bold" />
</LinearLayout>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="Rating"
android:fontFamily="@font/dmsans_medium"
android:textColor="@color/black"
android:textSize="14sp" />
<RatingBar
android:id="@+id/ratingBar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:isIndicator="false"
android:numStars="5"
android:progressTint="@color/yellow"
android:stepSize="1.0" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="Ulasan"
android:fontFamily="@font/dmsans_regular"
android:textColor="@color/black"
android:textSize="14sp" />
<EditText
android:id="@+id/etReviewText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:background="@drawable/bg_edit_text_background"
android:gravity="top"
android:hint="Isi jawaban Anda di sini"
android:inputType="textMultiLine"
android:lines="4"
android:fontFamily="@font/dmsans_regular"
android:padding="12dp"
android:textSize="14sp" />
</LinearLayout>
</androidx.cardview.widget.CardView>

View File

@ -41,6 +41,7 @@
<color name="gray_1">#E8ECF2</color>
<color name="soft_gray">#7D8FAB</color>
<color name="blue1">#489EC6</color>
<color name="yellow">#faf069</color>
<color name="bottom_navigation_icon_color_active">#489EC6</color>
<color name="bottom_navigation_icon_color_inactive">#8E8E8E</color>