From ec0e5801f3e43168ad7f1dd82a83ab1c48b0ca16 Mon Sep 17 00:00:00 2001 From: shaulascr Date: Sun, 20 Apr 2025 22:31:01 +0700 Subject: [PATCH] add complaint (in dialog) --- .../data/api/dto/ComplaintRequest.kt | 16 ++ .../api/response/order/ComplaintResponse.kt | 36 ++++ .../data/api/retrofit/ApiService.kt | 9 + .../data/repository/OrderRepository.kt | 81 ++++++++ .../ui/order/history/HistoryViewModel.kt | 40 ++++ .../ui/order/history/OrderHistoryAdapter.kt | 184 +++++++++++++++++- .../utils/viewmodel/HomeViewModel.kt | 17 -- .../main/res/drawable/bg_dashboard_border.xml | 11 ++ .../main/res/layout/dialog_cancel_order.xml | 103 ++++++++++ app/src/main/res/values/strings.xml | 24 +++ app/src/main/res/values/styles.xml | 18 ++ 11 files changed, 521 insertions(+), 18 deletions(-) create mode 100644 app/src/main/java/com/alya/ecommerce_serang/data/api/dto/ComplaintRequest.kt create mode 100644 app/src/main/java/com/alya/ecommerce_serang/data/api/response/order/ComplaintResponse.kt create mode 100644 app/src/main/res/drawable/bg_dashboard_border.xml create mode 100644 app/src/main/res/layout/dialog_cancel_order.xml diff --git a/app/src/main/java/com/alya/ecommerce_serang/data/api/dto/ComplaintRequest.kt b/app/src/main/java/com/alya/ecommerce_serang/data/api/dto/ComplaintRequest.kt new file mode 100644 index 0000000..3b598c5 --- /dev/null +++ b/app/src/main/java/com/alya/ecommerce_serang/data/api/dto/ComplaintRequest.kt @@ -0,0 +1,16 @@ +package com.alya.ecommerce_serang.data.api.dto + +import com.google.gson.annotations.SerializedName +import okhttp3.MultipartBody + +data class ComplaintRequest ( + @SerializedName("order_id") + val orderId: Int, + + @SerializedName("description") + val description: String, + + @SerializedName("complaintimg") + val complaintImg: MultipartBody.Part + +) \ No newline at end of file diff --git a/app/src/main/java/com/alya/ecommerce_serang/data/api/response/order/ComplaintResponse.kt b/app/src/main/java/com/alya/ecommerce_serang/data/api/response/order/ComplaintResponse.kt new file mode 100644 index 0000000..a7116c3 --- /dev/null +++ b/app/src/main/java/com/alya/ecommerce_serang/data/api/response/order/ComplaintResponse.kt @@ -0,0 +1,36 @@ +package com.alya.ecommerce_serang.data.api.response.order + +import com.google.gson.annotations.SerializedName + +data class ComplaintResponse( + + @field:SerializedName("voucher") + val voucher: Voucher, + + @field:SerializedName("message") + val message: String +) + +data class Voucher( + + @field:SerializedName("solution") + val solution: Any, + + @field:SerializedName("evidence") + val evidence: String, + + @field:SerializedName("description") + val description: String, + + @field:SerializedName("created_at") + val createdAt: String, + + @field:SerializedName("id") + val id: Int, + + @field:SerializedName("order_id") + val orderId: Int, + + @field:SerializedName("status") + val status: String +) 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 5426eb4..ceec7e6 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 @@ -19,6 +19,7 @@ import com.alya.ecommerce_serang.data.api.response.cart.AddCartResponse import com.alya.ecommerce_serang.data.api.response.cart.ListCartResponse import com.alya.ecommerce_serang.data.api.response.cart.UpdateCartResponse import com.alya.ecommerce_serang.data.api.response.order.AddEvidenceResponse +import com.alya.ecommerce_serang.data.api.response.order.ComplaintResponse import com.alya.ecommerce_serang.data.api.response.order.CompletedOrderResponse import com.alya.ecommerce_serang.data.api.response.order.CourierCostResponse import com.alya.ecommerce_serang.data.api.response.order.CreateOrderResponse @@ -187,4 +188,12 @@ interface ApiService { suspend fun confirmOrder( @Body confirmOrder : CompletedOrderRequest ): Response + + @Multipart + @POST("addcomplaint") + suspend fun addComplaint( + @Part("order_id") orderId: RequestBody, + @Part("description") description: RequestBody, + @Part complaintimg: MultipartBody.Part + ): Response } \ No newline at end of file diff --git a/app/src/main/java/com/alya/ecommerce_serang/data/repository/OrderRepository.kt b/app/src/main/java/com/alya/ecommerce_serang/data/repository/OrderRepository.kt index 94cfa06..f9e621a 100644 --- a/app/src/main/java/com/alya/ecommerce_serang/data/repository/OrderRepository.kt +++ b/app/src/main/java/com/alya/ecommerce_serang/data/repository/OrderRepository.kt @@ -10,6 +10,7 @@ import com.alya.ecommerce_serang.data.api.dto.OrderRequestBuy import com.alya.ecommerce_serang.data.api.dto.UserProfile import com.alya.ecommerce_serang.data.api.response.cart.DataItem import com.alya.ecommerce_serang.data.api.response.order.AddEvidenceResponse +import com.alya.ecommerce_serang.data.api.response.order.ComplaintResponse import com.alya.ecommerce_serang.data.api.response.order.CompletedOrderResponse import com.alya.ecommerce_serang.data.api.response.order.CourierCostResponse import com.alya.ecommerce_serang.data.api.response.order.CreateOrderResponse @@ -23,7 +24,16 @@ import com.alya.ecommerce_serang.data.api.response.product.StoreResponse import com.alya.ecommerce_serang.data.api.response.profile.AddressResponse import com.alya.ecommerce_serang.data.api.response.profile.CreateAddressResponse import com.alya.ecommerce_serang.data.api.retrofit.ApiService +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.flowOn +import okhttp3.MediaType.Companion.toMediaTypeOrNull +import okhttp3.MultipartBody +import okhttp3.RequestBody.Companion.asRequestBody +import okhttp3.RequestBody.Companion.toRequestBody import retrofit2.Response +import java.io.File class OrderRepository(private val apiService: ApiService) { @@ -351,4 +361,75 @@ suspend fun uploadPaymentProof(request: AddEvidenceMultipartRequest): Result> = flow { + emit(Result.Loading) + + try { + // Debug logging + Log.d("OrderRepository", "Submitting complaint for order: $orderId") + Log.d("OrderRepository", "Reason: $reason") + Log.d("OrderRepository", "Image file: ${imageFile?.absolutePath ?: "null"}") + + // Create form data for the multipart request + // Explicitly convert orderId to string to ensure correct formatting + val orderIdRequestBody = orderId.toString().toRequestBody("text/plain".toMediaTypeOrNull()) + val reasonRequestBody = reason.toRequestBody("text/plain".toMediaTypeOrNull()) + + // Create the image part for the API + val imagePart = if (imageFile != null && imageFile.exists()) { + // Use the actual image file + // Use asRequestBody() for files which is more efficient + val imageRequestBody = imageFile.asRequestBody("image/*".toMediaTypeOrNull()) + MultipartBody.Part.createFormData( + "complaintimg", + imageFile.name, + imageRequestBody + ) + } else { + // Create a simple empty part if no image + val dummyRequestBody = "".toRequestBody("text/plain".toMediaTypeOrNull()) + MultipartBody.Part.createFormData( + "complaintimg", + "", + dummyRequestBody + ) + } + + // Log request details before making the API call + Log.d("OrderRepository", "Making API call to add complaint") + Log.d("OrderRepository", "orderId: $orderId (as string)") + + val response = apiService.addComplaint( + orderIdRequestBody, + reasonRequestBody, + imagePart + ) + + Log.d("OrderRepository", "Response code: ${response.code()}") + Log.d("OrderRepository", "Response message: ${response.message()}") + + if (response.isSuccessful && response.body() != null) { + val complaintResponse = response.body() as ComplaintResponse + emit(Result.Success(complaintResponse)) + } else { + // Get the error message from the response if possible + val errorBody = response.errorBody()?.string() + val errorMessage = if (!errorBody.isNullOrEmpty()) { + "Server error: $errorBody" + } else { + "Failed to submit complaint: ${response.code()} ${response.message()}" + } + Log.e("OrderRepository", errorMessage) + emit(Result.Error(Exception(errorMessage))) + } + } catch (e: Exception) { + Log.e("OrderRepository", "Error submitting complaint: ${e.message}") + emit(Result.Error(e)) + } + }.flowOn(Dispatchers.IO) + } \ No newline at end of file diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/order/history/HistoryViewModel.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/order/history/HistoryViewModel.kt index 1ac5abd..f294043 100644 --- a/app/src/main/java/com/alya/ecommerce_serang/ui/order/history/HistoryViewModel.kt +++ b/app/src/main/java/com/alya/ecommerce_serang/ui/order/history/HistoryViewModel.kt @@ -12,6 +12,7 @@ import com.alya.ecommerce_serang.data.repository.OrderRepository import com.alya.ecommerce_serang.data.repository.Result import com.alya.ecommerce_serang.ui.order.address.ViewState import kotlinx.coroutines.launch +import java.io.File class HistoryViewModel(private val repository: OrderRepository) : ViewModel() { @@ -25,6 +26,15 @@ class HistoryViewModel(private val repository: OrderRepository) : ViewModel() { private val _orderCompletionStatus = MutableLiveData>() val orderCompletionStatus: LiveData> = _orderCompletionStatus + private val _isLoading = MutableLiveData() + val isLoading: LiveData = _isLoading + + private val _message = MutableLiveData() + val message: LiveData = _message + + private val _isSuccess = MutableLiveData() + val isSuccess: LiveData = _isSuccess + fun getOrderList(status: String) { _orders.value = ViewState.Loading viewModelScope.launch { @@ -51,12 +61,42 @@ class HistoryViewModel(private val repository: OrderRepository) : ViewModel() { } } fun confirmOrderCompleted(orderId: Int, status: String) { + Log.d(TAG, "Confirming order completed: orderId=$orderId, status=$status") viewModelScope.launch { _orderCompletionStatus.value = Result.Loading val request = CompletedOrderRequest(orderId, status) + Log.d(TAG, "Sending order completion request: $request") val result = repository.confirmOrderCompleted(request) + Log.d(TAG, "Order completion result: $result") _orderCompletionStatus.value = result } } + + fun cancelOrderWithImage(orderId: String, reason: String, imageFile: File?) { + Log.d(TAG, "Cancelling order with image: orderId=$orderId, reason=$reason, hasImage=${imageFile != null}") + viewModelScope.launch { + repository.submitComplaint(orderId, reason, imageFile).collect { result -> + when (result) { + is Result.Loading -> { + Log.d(TAG, "Submitting complaint: Loading") + _isLoading.value = true + } + is Result.Success -> { + Log.d(TAG, "Complaint submitted successfully: ${result.data.message}") + _message.value = result.data.message + _isSuccess.value = true + _isLoading.value = false + } + is Result.Error -> { + val errorMessage = result.exception.message ?: "Error submitting complaint" + Log.e(TAG, "Error submitting complaint: $errorMessage", result.exception) + _message.value = errorMessage + _isSuccess.value = false + _isLoading.value = false + } + } + } + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/order/history/OrderHistoryAdapter.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/order/history/OrderHistoryAdapter.kt index 2dc20b7..007c8f7 100644 --- a/app/src/main/java/com/alya/ecommerce_serang/ui/order/history/OrderHistoryAdapter.kt +++ b/app/src/main/java/com/alya/ecommerce_serang/ui/order/history/OrderHistoryAdapter.kt @@ -1,17 +1,31 @@ package com.alya.ecommerce_serang.ui.order.history +import android.app.Activity +import android.app.Dialog import android.content.Intent +import android.graphics.Color +import android.net.Uri +import android.provider.MediaStore import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import android.view.Window +import android.widget.ArrayAdapter +import android.widget.AutoCompleteTextView +import android.widget.ImageView +import android.widget.ProgressBar import android.widget.TextView +import android.widget.Toast +import androidx.lifecycle.findViewTreeLifecycleOwner import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import com.alya.ecommerce_serang.R import com.alya.ecommerce_serang.data.api.response.order.OrdersItem import com.alya.ecommerce_serang.ui.order.detail.PaymentActivity import com.google.android.material.button.MaterialButton +import com.google.android.material.textfield.TextInputLayout +import java.io.File import java.text.SimpleDateFormat import java.util.Calendar import java.util.Locale @@ -128,6 +142,13 @@ class OrderHistoryAdapter( visibility = View.VISIBLE text = itemView.context.getString(R.string.dl_pending) } + btnLeft.apply { + visibility = View.VISIBLE + text = itemView.context.getString(R.string.canceled_order_btn) + setOnClickListener { + showCancelOrderDialog(order.orderId.toString()) + } + } deadlineDate.apply { visibility = View.VISIBLE text = formatDate(order.createdAt) @@ -146,6 +167,7 @@ class OrderHistoryAdapter( visibility = View.VISIBLE text = itemView.context.getString(R.string.canceled_order_btn) setOnClickListener { + showCancelOrderDialog(order.orderId.toString()) } } @@ -177,7 +199,13 @@ class OrderHistoryAdapter( visibility = View.VISIBLE text = itemView.context.getString(R.string.dl_processed) } - + btnLeft.apply { + visibility = View.VISIBLE + text = itemView.context.getString(R.string.canceled_order_btn) + setOnClickListener { + showCancelOrderDialog(order.orderId.toString()) + } + } } "shipped" -> { // Untuk status shipped, tampilkan "Lacak Pengiriman" dan "Terima Barang" @@ -193,6 +221,7 @@ class OrderHistoryAdapter( visibility = View.VISIBLE text = itemView.context.getString(R.string.claim_complaint) setOnClickListener { + showCancelOrderDialog(order.orderId.toString()) // Handle click event } } @@ -318,5 +347,158 @@ class OrderHistoryAdapter( dateString } } + + private fun showCancelOrderDialog(orderId: String) { + val context = itemView.context + val dialog = Dialog(context) + dialog.requestWindowFeature(Window.FEATURE_NO_TITLE) + dialog.setContentView(R.layout.dialog_cancel_order) + dialog.setCancelable(true) + + // Set the dialog width to match parent + val window = dialog.window + window?.setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT) + + // Get references to the views in the dialog + val spinnerCancelReason = dialog.findViewById(R.id.spinnerCancelReason) + val tilCancelReason = dialog.findViewById(R.id.tilCancelReason) + val btnCancelDialog = dialog.findViewById(R.id.btnCancelDialog) + val btnConfirmCancel = dialog.findViewById(R.id.btnConfirmCancel) + val ivComplaintImage = dialog.findViewById(R.id.ivComplaintImage) + val tvSelectImage = dialog.findViewById(R.id.tvSelectImage) + + // Set up the reasons dropdown + val reasons = context.resources.getStringArray(R.array.cancellation_reasons) + val adapter = ArrayAdapter(context, android.R.layout.simple_dropdown_item_1line, reasons) + spinnerCancelReason.setAdapter(adapter) + + // For storing the selected image URI + var selectedImageUri: Uri? = null + + // Set click listener for image selection + ivComplaintImage.setOnClickListener { + // Create an intent to open the image picker + val galleryIntent = Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI) + (context as? Activity)?.startActivityForResult(galleryIntent, REQUEST_IMAGE_PICK) + + // Set up result handler in the activity + val activity = context as? Activity + activity?.let { + // Remove any existing callbacks to avoid memory leaks + if (imagePickCallback != null) { + imagePickCallback = null + } + + // Create a new callback for this specific dialog + imagePickCallback = { uri -> + selectedImageUri = uri + + // Load and display the selected image + ivComplaintImage.setImageURI(uri) + tvSelectImage.visibility = View.GONE + } + } + } + + // Set click listeners for buttons + btnCancelDialog.setOnClickListener { + dialog.dismiss() + } + + btnConfirmCancel.setOnClickListener { + val reason = spinnerCancelReason.text.toString().trim() + + if (reason.isEmpty()) { + tilCancelReason.error = context.getString(R.string.please_select_cancellation_reason) + return@setOnClickListener + } + + // Clear error if any + tilCancelReason.error = null + + // Convert selected image to file if available + val imageFile = selectedImageUri?.let { uri -> + try { + // Get the file path from URI + val filePathColumn = arrayOf(MediaStore.Images.Media.DATA) + val cursor = context.contentResolver.query(uri, filePathColumn, null, null, null) + cursor?.use { + if (it.moveToFirst()) { + val columnIndex = it.getColumnIndex(filePathColumn[0]) + val filePath = it.getString(columnIndex) + return@let File(filePath) + } + } + null + } catch (e: Exception) { + Log.e("OrderHistoryAdapter", "Error getting file from URI: ${e.message}") + null + } + } + + // Show loading indicator + val loadingView = View(context).apply { + layoutParams = ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT + ) + setBackgroundColor(Color.parseColor("#80000000")) + + val progressBar = ProgressBar(context).apply { + layoutParams = ViewGroup.LayoutParams( + ViewGroup.LayoutParams.WRAP_CONTENT, + ViewGroup.LayoutParams.WRAP_CONTENT + ) + } + +// addView(progressBar) +// (progressBar.layoutParams as? ViewGroup.MarginLayoutParams)?.apply { +// gravity = Gravity.CENTER +// } + } + + dialog.addContentView(loadingView, loadingView.layoutParams) + + // Call the ViewModel to cancel the order with image + viewModel.cancelOrderWithImage(orderId, reason, imageFile) + + // Observe for success/failure + viewModel.isSuccess.observe(itemView.findViewTreeLifecycleOwner()!!) { isSuccess -> + // Remove loading indicator + (loadingView.parent as? ViewGroup)?.removeView(loadingView) + + if (isSuccess) { + Toast.makeText(context, context.getString(R.string.order_canceled_successfully), Toast.LENGTH_SHORT).show() + dialog.dismiss() + + // Find the order in the list and remove it or update its status + val position = orders.indexOfFirst { it.orderId.toString() == orderId } + if (position != -1) { + orders.removeAt(position) + notifyItemRemoved(position) + notifyItemRangeChanged(position, orders.size) + } + } else { + Toast.makeText(context, viewModel.message.value ?: context.getString(R.string.failed_to_cancel_order), Toast.LENGTH_SHORT).show() + } + } + } + dialog.show() + } + } + + companion object { + private const val REQUEST_IMAGE_PICK = 100 + private var imagePickCallback: ((Uri) -> Unit)? = null + + // This method should be called from the activity's onActivityResult + fun handleImageResult(requestCode: Int, resultCode: Int, data: Intent?) { + if (requestCode == REQUEST_IMAGE_PICK && resultCode == Activity.RESULT_OK && data != null) { + val selectedImageUri = data.data + selectedImageUri?.let { uri -> + imagePickCallback?.invoke(uri) + } + } + } } } \ No newline at end of file diff --git a/app/src/main/java/com/alya/ecommerce_serang/utils/viewmodel/HomeViewModel.kt b/app/src/main/java/com/alya/ecommerce_serang/utils/viewmodel/HomeViewModel.kt index ce73b87..0e321ba 100644 --- a/app/src/main/java/com/alya/ecommerce_serang/utils/viewmodel/HomeViewModel.kt +++ b/app/src/main/java/com/alya/ecommerce_serang/utils/viewmodel/HomeViewModel.kt @@ -52,23 +52,6 @@ class HomeViewModel ( loadProducts() loadCategories() } - -// private fun fetchUserData() { -// viewModelScope.launch { -// try { -// val response = apiService.getProtectedData() // Example API request -// if (response.isSuccessful) { -// val data = response.body() -// Log.d("HomeFragment", "User Data: $data") -// // Update UI with data -// } else { -// Log.e("HomeFragment", "Error: ${response.message()}") -// } -// } catch (e: Exception) { -// Log.e("HomeFragment", "Exception: ${e.message}") -// } -// } -// } } sealed class HomeUiState { diff --git a/app/src/main/res/drawable/bg_dashboard_border.xml b/app/src/main/res/drawable/bg_dashboard_border.xml new file mode 100644 index 0000000..315ffee --- /dev/null +++ b/app/src/main/res/drawable/bg_dashboard_border.xml @@ -0,0 +1,11 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_cancel_order.xml b/app/src/main/res/layout/dialog_cancel_order.xml new file mode 100644 index 0000000..7b7406d --- /dev/null +++ b/app/src/main/res/layout/dialog_cancel_order.xml @@ -0,0 +1,103 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 33d8c23..36c0bcf 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -93,5 +93,29 @@ Pesanan Diterima Beri Ulasan + Warning Icon + Apakah anda yakin ingin membatalkan pesanan? + Alasan Batalkan Pesanan + Kembali + Batalkan Pesanan + Order canceled successfully + Failed to cancel order + Please select a reason for cancellation + Unggah Bukti Komplain + Complaint evidence image + Tekan untuk unggah foto + Please select an image as evidence + Image is too large. Please select a smaller image. + + + + Found a better price elsewhere + Changed my mind about the product + Ordered the wrong item + Shipping time is too long + Financial reasons + Other reason + + \ No newline at end of file diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index e482613..06b23b1 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -7,4 +7,22 @@ 12dp + + + + \ No newline at end of file