add complaint (in dialog)

This commit is contained in:
shaulascr
2025-04-20 22:31:01 +07:00
parent 8d13991e83
commit ec0e5801f3
11 changed files with 521 additions and 18 deletions

View File

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

View File

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

View File

@ -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<CompletedOrderResponse>
@Multipart
@POST("addcomplaint")
suspend fun addComplaint(
@Part("order_id") orderId: RequestBody,
@Part("description") description: RequestBody,
@Part complaintimg: MultipartBody.Part
): Response<ComplaintResponse>
}

View File

@ -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<Add
}
}
fun submitComplaint(
orderId: String,
reason: String,
imageFile: File?
): Flow<Result<ComplaintResponse>> = 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)
}

View File

@ -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<Result<CompletedOrderResponse>>()
val orderCompletionStatus: LiveData<Result<CompletedOrderResponse>> = _orderCompletionStatus
private val _isLoading = MutableLiveData<Boolean>()
val isLoading: LiveData<Boolean> = _isLoading
private val _message = MutableLiveData<String>()
val message: LiveData<String> = _message
private val _isSuccess = MutableLiveData<Boolean>()
val isSuccess: LiveData<Boolean> = _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
}
}
}
}
}
}

View File

@ -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<AutoCompleteTextView>(R.id.spinnerCancelReason)
val tilCancelReason = dialog.findViewById<TextInputLayout>(R.id.tilCancelReason)
val btnCancelDialog = dialog.findViewById<MaterialButton>(R.id.btnCancelDialog)
val btnConfirmCancel = dialog.findViewById<MaterialButton>(R.id.btnConfirmCancel)
val ivComplaintImage = dialog.findViewById<ImageView>(R.id.ivComplaintImage)
val tvSelectImage = dialog.findViewById<TextView>(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)
}
}
}
}
}

View File

@ -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 {

View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<stroke
android:width="2dp"
android:color="#BCBCBC"
android:dashWidth="10dp"
android:dashGap="6dp" />
<corners android:radius="8dp" />
<solid android:color="#F5F5F5" />
</shape>

View File

@ -0,0 +1,103 @@
<?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"
app:cardCornerRadius="16dp"
app:cardElevation="8dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="24dp">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:text="@string/cancel_order_confirmation"
android:textAlignment="center"
android:fontFamily="@font/dmsans_semibold"
android:textAppearance="@style/TextAppearance.MaterialComponents.Headline6" />
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/tilCancelReason"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.ExposedDropdownMenu"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:hint="@string/reason_for_cancellation">
<AutoCompleteTextView
android:id="@+id/spinnerCancelReason"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="none"
android:focusable="false" />
</com.google.android.material.textfield.TextInputLayout>
<!-- Image Upload Section -->
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/upload_evidence"
android:layout_marginBottom="8dp"
android:fontFamily="@font/dmsans_medium"
android:textAppearance="@style/TextAppearance.MaterialComponents.Body1" />
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp">
<ImageView
android:id="@+id/ivComplaintImage"
android:layout_width="match_parent"
android:layout_height="150dp"
android:scaleType="centerCrop"
android:background="@drawable/bg_dashboard_border"
android:contentDescription="@string/complaint_image" />
<TextView
android:id="@+id/tvSelectImage"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="@string/tap_to_select_image"
android:drawableTop="@drawable/baseline_upload_file_24"
android:drawablePadding="8dp"
android:textAppearance="@style/TextAppearance.MaterialComponents.Body2" />
</FrameLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<com.google.android.material.button.MaterialButton
android:id="@+id/btnCancelDialog"
style="@style/RoundedBorderStyle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:layout_weight="1"
android:fontFamily="@font/dmsans_semibold"
android:textColor="@color/blue_500"
android:text="@string/cancel" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btnConfirmCancel"
style="@style/RoundedBorderStyleFilled"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_weight="1"
android:fontFamily="@font/dmsans_semibold"
android:text="@string/confirm" />
</LinearLayout>
</LinearLayout>
</androidx.cardview.widget.CardView>

View File

@ -93,5 +93,29 @@
<string name="claim_order">Pesanan Diterima </string>
<string name="add_review">Beri Ulasan </string>
<string name="warning_icon">Warning Icon</string>
<string name="cancel_order_confirmation">Apakah anda yakin ingin membatalkan pesanan?</string>
<string name="reason_for_cancellation">Alasan Batalkan Pesanan</string>
<string name="cancel">Kembali</string>
<string name="confirm">Batalkan Pesanan</string>
<string name="order_canceled_successfully">Order canceled successfully</string>
<string name="failed_to_cancel_order">Failed to cancel order</string>
<string name="please_select_cancellation_reason">Please select a reason for cancellation</string>
<string name="upload_evidence">Unggah Bukti Komplain</string>
<string name="complaint_image">Complaint evidence image</string>
<string name="tap_to_select_image">Tekan untuk unggah foto</string>
<string name="please_select_image">Please select an image as evidence</string>
<string name="image_too_large">Image is too large. Please select a smaller image.</string>
<!-- Cancellation Reasons -->
<string-array name="cancellation_reasons">
<item>Found a better price elsewhere</item>
<item>Changed my mind about the product</item>
<item>Ordered the wrong item</item>
<item>Shipping time is too long</item>
<item>Financial reasons</item>
<item>Other reason</item>
</string-array>
</resources>

View File

@ -7,4 +7,22 @@
<item name="android:padding">12dp</item>
<!-- Add more style attributes as needed -->
</style>
<style name="RoundedBorderStyle">
<!-- This style can be applied to views -->
<!-- <item name="android:background">@drawable/bg_button_outline</item>-->
<item name="strokeColor">@color/blue_500</item>
<item name="strokeWidth">2dp</item>
<item name="cornerRadius">8dp</item>
<item name="backgroundTint">@android:color/transparent</item>
</style>
<style name="RoundedBorderStyleFilled">
<!-- This style can be applied to views -->
<!-- <item name="android:background">@drawable/bg_button_outline</item>-->
<item name="strokeColor">@color/blue_500</item>
<item name="strokeWidth">2dp</item>
<item name="cornerRadius">8dp</item>
<item name="backgroundTint">@color/blue_500</item>
</style>
</resources>