mirror of
https://github.com/shaulascr/ecommerce_serang.git
synced 2025-08-14 11:02:21 +00:00
add complaint (in dialog)
This commit is contained in:
@ -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
|
||||
|
||||
)
|
@ -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
|
||||
)
|
@ -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>
|
||||
}
|
@ -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)
|
||||
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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 {
|
||||
|
Reference in New Issue
Block a user