Merge branch 'screen-features'

This commit is contained in:
shaulascr
2025-06-18 16:44:08 +07:00
26 changed files with 443 additions and 84 deletions

View File

@ -80,7 +80,7 @@ data class Orders(
val cancelReason: String? = null,
@field:SerializedName("total_amount")
val totalAmount: String? = null,
val totalAmount: Int? = null,
@field:SerializedName("user_id")
val userId: Int,

View File

@ -394,7 +394,10 @@ class ChatActivity : AppCompatActivity() {
// Update messages
val previousCount = chatAdapter.itemCount
chatAdapter.submitList(state.messages) {
val displayItems = viewModel.getDisplayItems()
chatAdapter.submitList(displayItems) {
Log.d(TAG, "Messages submitted to adapter")
// Only auto-scroll for new messages or initial load
if (previousCount == 0 || state.messages.size > previousCount) {
@ -426,17 +429,15 @@ class ChatActivity : AppCompatActivity() {
.error(R.drawable.placeholder_image)
.into(binding.imgProduct)
}
updateProductCardUI(state.hasProductAttachment)
binding.productContainer.visibility = View.VISIBLE
binding.productContainer.visibility = View.GONE
} else {
binding.productContainer.visibility = View.GONE
}
updateInputHint(state)
// Update product card visual feedback
updateProductCardUI(state.hasProductAttachment)
// Update attachment hint
if (state.hasAttachment) {
binding.editTextMessage.hint = getString(R.string.image_attached)

View File

@ -8,6 +8,7 @@ import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import com.alya.ecommerce_serang.BuildConfig.BASE_URL
import com.alya.ecommerce_serang.R
import com.alya.ecommerce_serang.databinding.ItemDateHeaderBinding
import com.alya.ecommerce_serang.databinding.ItemMessageProductReceivedBinding
import com.alya.ecommerce_serang.databinding.ItemMessageProductSentBinding
import com.alya.ecommerce_serang.databinding.ItemMessageReceivedBinding
@ -17,22 +18,29 @@ import com.bumptech.glide.Glide
class ChatAdapter(
private val onProductClick: ((ProductInfo) -> Unit)? = null
) : ListAdapter<ChatUiMessage, RecyclerView.ViewHolder>(ChatMessageDiffCallback()) {
) : ListAdapter<ChatDisplayItem, RecyclerView.ViewHolder>(ChatMessageDiffCallback()) {
companion object {
private const val VIEW_TYPE_MESSAGE_SENT = 1
private const val VIEW_TYPE_MESSAGE_RECEIVED = 2
private const val VIEW_TYPE_PRODUCT_SENT = 3
private const val VIEW_TYPE_PRODUCT_RECEIVED = 4
private const val VIEW_TYPE_DATE_HEADER = 5
}
override fun getItemViewType(position: Int): Int {
val message = getItem(position)
return when {
message.messageType == MessageType.PRODUCT && message.isSentByMe -> VIEW_TYPE_PRODUCT_SENT
message.messageType == MessageType.PRODUCT && !message.isSentByMe -> VIEW_TYPE_PRODUCT_RECEIVED
message.isSentByMe -> VIEW_TYPE_MESSAGE_SENT
else -> VIEW_TYPE_MESSAGE_RECEIVED
val item = getItem(position)
return when (item) {
is ChatDisplayItem.DateHeaderItem -> VIEW_TYPE_DATE_HEADER
is ChatDisplayItem.MessageItem -> {
val message = item.chatUiMessage
when {
message.messageType == MessageType.PRODUCT && message.isSentByMe -> VIEW_TYPE_PRODUCT_SENT
message.messageType == MessageType.PRODUCT && !message.isSentByMe -> VIEW_TYPE_PRODUCT_RECEIVED
message.isSentByMe -> VIEW_TYPE_MESSAGE_SENT
else -> VIEW_TYPE_MESSAGE_RECEIVED
}
}
}
}
@ -40,6 +48,10 @@ class ChatAdapter(
val inflater = LayoutInflater.from(parent.context)
return when (viewType) {
VIEW_TYPE_DATE_HEADER -> {
val binding = ItemDateHeaderBinding.inflate(inflater, parent, false)
DateHeaderViewHolder(binding)
}
VIEW_TYPE_MESSAGE_SENT -> {
val binding = ItemMessageSentBinding.inflate(inflater, parent, false)
SentMessageViewHolder(binding)
@ -61,13 +73,34 @@ class ChatAdapter(
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
val message = getItem(position)
val item = getItem(position)
when (holder) {
is SentMessageViewHolder -> holder.bind(message)
is ReceivedMessageViewHolder -> holder.bind(message)
is SentProductViewHolder -> holder.bind(message)
is ReceivedProductViewHolder -> holder.bind(message)
is DateHeaderViewHolder -> {
if (item is ChatDisplayItem.DateHeaderItem) {
holder.bind(item)
}
}
is SentMessageViewHolder -> {
if (item is ChatDisplayItem.MessageItem) {
holder.bind(item.chatUiMessage)
}
}
is ReceivedMessageViewHolder -> {
if (item is ChatDisplayItem.MessageItem) {
holder.bind(item.chatUiMessage)
}
}
is SentProductViewHolder -> {
if (item is ChatDisplayItem.MessageItem) {
holder.bind(item.chatUiMessage)
}
}
is ReceivedProductViewHolder -> {
if (item is ChatDisplayItem.MessageItem) {
holder.bind(item.chatUiMessage)
}
}
}
}
@ -233,17 +266,36 @@ class ChatAdapter(
}
}
}
inner class DateHeaderViewHolder(private val binding: ItemDateHeaderBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(item: ChatDisplayItem.DateHeaderItem) {
binding.tvDate.text = item.formattedDate
}
}
}
/**
* DiffUtil callback for optimizing RecyclerView updates
*/
class ChatMessageDiffCallback : DiffUtil.ItemCallback<ChatUiMessage>() {
override fun areItemsTheSame(oldItem: ChatUiMessage, newItem: ChatUiMessage): Boolean {
return oldItem.id == newItem.id
class ChatMessageDiffCallback : DiffUtil.ItemCallback<ChatDisplayItem>() {
override fun areItemsTheSame(oldItem: ChatDisplayItem, newItem: ChatDisplayItem): Boolean {
return when {
oldItem is ChatDisplayItem.MessageItem && newItem is ChatDisplayItem.MessageItem ->
oldItem.chatUiMessage.id == newItem.chatUiMessage.id
oldItem is ChatDisplayItem.DateHeaderItem && newItem is ChatDisplayItem.DateHeaderItem ->
oldItem.date == newItem.date
else -> false
}
}
override fun areContentsTheSame(oldItem: ChatUiMessage, newItem: ChatUiMessage): Boolean {
override fun areContentsTheSame(oldItem: ChatDisplayItem, newItem: ChatDisplayItem): Boolean {
return oldItem == newItem
}
}
sealed class ChatDisplayItem {
data class MessageItem(val chatUiMessage: ChatUiMessage) : ChatDisplayItem()
data class DateHeaderItem(val date: String, val formattedDate: String) : ChatDisplayItem()
}

View File

@ -16,6 +16,9 @@ import com.alya.ecommerce_serang.utils.SessionManager
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.launch
import java.io.File
import java.text.SimpleDateFormat
import java.util.Calendar
import java.util.Date
import java.util.Locale
import java.util.TimeZone
import javax.inject.Inject
@ -737,7 +740,8 @@ class ChatViewModel @Inject constructor(
attachment = chatLine.attachment ?: "",
status = chatLine.status,
time = formattedTime,
isSentByMe = chatLine.senderId == currentUserId
isSentByMe = chatLine.senderId == currentUserId,
createdAt = chatLine.createdAt
)
}
@ -781,7 +785,8 @@ class ChatViewModel @Inject constructor(
time = formattedTime,
isSentByMe = chatItem.senderId == currentUserId,
messageType = messageType,
productInfo = productInfo
productInfo = productInfo,
createdAt = chatItem.createdAt
)
// Fetch product info for non-current-user products
@ -923,6 +928,85 @@ class ChatViewModel @Inject constructor(
Log.d(TAG, "ChatViewModel cleared - Disconnecting socket")
socketService.disconnect()
}
fun getDisplayItems(): List<ChatDisplayItem> {
return transformMessagesToDisplayItems(state.value?.messages ?: emptyList())
}
private fun transformMessagesToDisplayItems(messages: List<ChatUiMessage>): List<ChatDisplayItem> {
if (messages.isEmpty()) return emptyList()
val displayItems = mutableListOf<ChatDisplayItem>()
var lastDate: String? = null
for (message in messages) {
// Extract date from message timestamp
val messageDate = extractDateFromTimestamp(message.createdAt) // You need to implement this
// Add date header if this is a new day
if (messageDate != lastDate) {
val formattedDate = formatDateHeader(messageDate) // You need to implement this
displayItems.add(ChatDisplayItem.DateHeaderItem(messageDate, formattedDate))
lastDate = messageDate
}
// Add the message
displayItems.add(ChatDisplayItem.MessageItem(message))
}
return displayItems
}
private fun extractDateFromTimestamp(timestamp: String): String {
return try {
// Parse ISO 8601 format: "2025-05-27T08:36:53.946Z"
val inputFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.getDefault())
inputFormat.timeZone = TimeZone.getTimeZone("UTC")
val date = inputFormat.parse(timestamp)
val outputFormat = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault())
outputFormat.format(date ?: Date())
} catch (e: Exception) {
Log.e(TAG, "Error parsing timestamp: $timestamp", e)
return timestamp.take(10)
}
}
private fun formatDateHeader(dateString: String): String {
return try {
val dateFormat = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault())
val messageDate = dateFormat.parse(dateString) ?: return dateString
val today = Calendar.getInstance()
val yesterday = Calendar.getInstance().apply { add(Calendar.DAY_OF_YEAR, -1) }
val messageCalendar = Calendar.getInstance().apply { time = messageDate }
when {
isSameDay(messageCalendar, today) -> "Today"
isSameDay(messageCalendar, yesterday) -> "Yesterday"
isThisYear(messageCalendar, today) -> {
// Show "Mon, Dec 15" format for this year
SimpleDateFormat("EEE, MMM dd", Locale.getDefault()).format(messageDate)
}
else -> {
// Show "Dec 15, 2024" format for other years
SimpleDateFormat("MMM dd, yyyy", Locale.getDefault()).format(messageDate)
}
}
} catch (e: Exception) {
Log.e(TAG, "Error formatting date: $dateString", e)
dateString // Fallback to raw date
}
}
private fun isSameDay(cal1: Calendar, cal2: Calendar): Boolean {
return cal1.get(Calendar.YEAR) == cal2.get(Calendar.YEAR) &&
cal1.get(Calendar.DAY_OF_YEAR) == cal2.get(Calendar.DAY_OF_YEAR)
}
private fun isThisYear(messageCalendar: Calendar, today: Calendar): Boolean {
return messageCalendar.get(Calendar.YEAR) == today.get(Calendar.YEAR)
}
}
enum class MessageType {
@ -949,9 +1033,12 @@ data class ChatUiMessage(
val time: String,
val isSentByMe: Boolean,
val messageType: MessageType = MessageType.TEXT,
val productInfo: ProductInfo? = null
val productInfo: ProductInfo? = null,
val createdAt: String
)
// representing UI state to screen
data class ChatUiState(
val messages: List<ChatUiMessage> = emptyList(),

View File

@ -6,7 +6,6 @@ import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.inputmethod.EditorInfo
import androidx.core.view.isVisible
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
@ -110,26 +109,16 @@ class HomeFragment : Fragment() {
private fun setupSearchView() {
binding.searchContainer.search.apply {
// When user clicks the search box, navigate to search fragment
// Make it non-editable so it acts like a button
isFocusable = false
isFocusableInTouchMode = false
isClickable = true
setOnClickListener {
findNavController().navigate(
HomeFragmentDirections.actionHomeFragmentToSearchHomeFragment(null)
)
}
// Handle search action if user presses search on keyboard
setOnEditorActionListener { _, actionId, _ ->
if (actionId == EditorInfo.IME_ACTION_SEARCH) {
val query = text.toString().trim()
if (query.isNotEmpty()) {
findNavController().navigate(
HomeFragmentDirections.actionHomeFragmentToSearchHomeFragment(query)
)
}
return@setOnEditorActionListener true
}
false
}
}
// Setup cart and notification buttons

View File

@ -2,6 +2,7 @@ package com.alya.ecommerce_serang.ui.home
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
@ -11,6 +12,8 @@ import com.alya.ecommerce_serang.data.api.dto.ProductsItem
import com.alya.ecommerce_serang.data.api.response.customer.product.StoreItem
import com.alya.ecommerce_serang.databinding.ItemProductGridBinding
import com.bumptech.glide.Glide
import java.text.NumberFormat
import java.util.Locale
class HorizontalProductAdapter(
private var products: List<ProductsItem>,
@ -32,8 +35,17 @@ class HorizontalProductAdapter(
Log.d("ProductAdapter", "Loading image: $fullImageUrl")
tvProductName.text = product.name
tvProductPrice.text = product.price
rating.text = product.rating
tvProductPrice.text = formatCurrency(product.price.toDouble())
val ratingStr = product.rating
val ratingValue = ratingStr?.toFloatOrNull()
if (ratingValue != null && ratingValue > 0f) {
binding.rating.text = String.format("%.1f", ratingValue)
binding.rating.visibility = View.VISIBLE
} else {
binding.rating.text = "Belum ada rating"
binding.rating.setCompoundDrawablesWithIntrinsicBounds(null, null, null, null)
}
// Load image using Glide
Glide.with(itemView)
@ -77,6 +89,11 @@ class HorizontalProductAdapter(
diffResult.dispatchUpdatesTo(this)
}
private fun formatCurrency(amount: Double): String {
val formatter = NumberFormat.getCurrencyInstance(Locale("in", "ID"))
return formatter.format(amount).replace(",00", "")
}
class ProductDiffCallback(
private val oldList: List<ProductsItem>,
private val newList: List<ProductsItem>

View File

@ -76,6 +76,7 @@ class SearchHomeFragment : Fragment() {
// Setup search view
binding.searchView.apply {
clearFocus()
setOnQueryTextListener(object : androidx.appcompat.widget.SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(query: String?): Boolean {
query?.let {
@ -105,13 +106,12 @@ class SearchHomeFragment : Fragment() {
}
})
// Request focus and show keyboard
if (args.query.isNullOrEmpty()) {
requestFocus()
postDelayed({
post {
val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
imm.showSoftInput(findFocus(), InputMethodManager.SHOW_IMPLICIT)
}, 200)
imm.showSoftInput(this, InputMethodManager.SHOW_IMPLICIT)
}
}
}
}

View File

@ -12,6 +12,8 @@ import com.alya.ecommerce_serang.data.api.dto.ProductsItem
import com.alya.ecommerce_serang.data.api.response.customer.product.StoreItem
import com.alya.ecommerce_serang.databinding.ItemProductGridBinding
import com.bumptech.glide.Glide
import java.text.NumberFormat
import java.util.Locale
class SearchResultsAdapter(
private val onItemClick: (ProductsItem) -> Unit,
@ -46,7 +48,7 @@ class SearchResultsAdapter(
fun bind(product: ProductsItem) {
binding.tvProductName.text = product.name
binding.tvProductPrice.text = product.price
binding.tvProductPrice.text = formatCurrency(product.price.toDouble())
val fullImageUrl = if (product.image.startsWith("/")) {
BASE_URL + product.image.removePrefix("/") // Append base URL if the path starts with "/"
@ -71,6 +73,11 @@ class SearchResultsAdapter(
super.submitList(list)
}
private fun formatCurrency(amount: Double): String {
val formatter = NumberFormat.getCurrencyInstance(Locale("in", "ID"))
return formatter.format(amount).replace(",00", "")
}
companion object {
private val DIFF_CALLBACK = object : DiffUtil.ItemCallback<ProductsItem>() {
override fun areItemsTheSame(oldItem: ProductsItem, newItem: ProductsItem): Boolean {

View File

@ -48,6 +48,12 @@ class PaymentMethodAdapter(
// Load payment icon if available
if (!payment.qrisImage.isNullOrEmpty()) {
// val fullImageUrl = if (payment.qrisImage.startsWith("/")) {
// BASE_URL + payment.qrisImage.removePrefix("/") // Append base URL if the path starts with "/"
// } else {
// payment.qrisImage// Use as is if it's already a full URL
// }
Glide.with(ivPaymentMethod.context)
.load(payment.qrisImage)
.apply(

View File

@ -6,6 +6,8 @@ import androidx.recyclerview.widget.RecyclerView
import com.alya.ecommerce_serang.data.api.response.customer.order.CourierCostsItem
import com.alya.ecommerce_serang.data.api.response.customer.order.ServicesItem
import com.alya.ecommerce_serang.databinding.ItemShippingOrderBinding
import java.text.NumberFormat
import java.util.Locale
class ShippingAdapter(
private val onItemSelected: (CourierCostsItem, ServicesItem) -> Unit
@ -30,7 +32,7 @@ class ShippingAdapter(
// Combine courier name and service
courierNameCost.text = "${courierCostsItem.courier} - ${service.service}"
estDate.text = "Estimasi ${service.etd} hari"
costPrice.text = "Rp${service.cost}"
costPrice.text = formatCurrency(service.cost.toDouble())
// Single click handler for both item and radio button
val onClickAction = {
@ -90,6 +92,11 @@ class ShippingAdapter(
}
}
private fun formatCurrency(amount: Double): String {
val formatter = NumberFormat.getCurrencyInstance(Locale("in", "ID"))
return formatter.format(amount).replace(",00", "")
}
override fun getItemCount(): Int {
return courierCostsList.sumOf { it.services.size }
}

View File

@ -1,8 +1,8 @@
package com.alya.ecommerce_serang.ui.order.detail
import android.Manifest
import android.R
import android.app.Activity
import android.app.AlertDialog
import android.app.DatePickerDialog
import android.content.Intent
import android.content.pm.PackageManager
@ -12,6 +12,7 @@ import android.os.Bundle
import android.provider.MediaStore
import android.util.Log
import android.view.View
import android.view.ViewGroup
import android.webkit.MimeTypeMap
import android.widget.AdapterView
import android.widget.ArrayAdapter
@ -25,6 +26,7 @@ import androidx.core.content.ContextCompat
import androidx.core.view.ViewCompat
import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat
import com.alya.ecommerce_serang.R
import com.alya.ecommerce_serang.data.api.dto.AddEvidenceMultipartRequest
import com.alya.ecommerce_serang.data.api.retrofit.ApiConfig
import com.alya.ecommerce_serang.data.repository.OrderRepository
@ -37,6 +39,7 @@ import okhttp3.MultipartBody
import okhttp3.RequestBody.Companion.asRequestBody
import okhttp3.RequestBody.Companion.toRequestBody
import java.io.File
import java.text.NumberFormat
import java.text.SimpleDateFormat
import java.util.Calendar
import java.util.Locale
@ -59,11 +62,9 @@ class AddEvidencePaymentActivity : AppCompatActivity() {
}
private val paymentMethods = arrayOf(
"Pilih metode pembayaran",
"Transfer Bank",
"E-Wallet",
"Virtual Account",
"Cash on Delivery"
"QRIS",
)
// private val getContent = registerForActivityResult(ActivityResultContracts.GetContent()) { uri: Uri? ->
@ -128,11 +129,8 @@ class AddEvidencePaymentActivity : AppCompatActivity() {
}
private fun setupUI() {
// Set product details\
// Setup payment methods spinner
val adapter = ArrayAdapter(this, R.layout.simple_spinner_item, paymentMethods)
adapter.setDropDownViewResource(R.layout.simple_spinner_dropdown_item)
val paymentMethods = listOf("Transfer Bank", "COD", "QRIS")
val adapter = SpinnerCardAdapter(this, paymentMethods)
binding.spinnerPaymentMethod.adapter = adapter
}
@ -219,15 +217,23 @@ class AddEvidencePaymentActivity : AppCompatActivity() {
private fun showImagePickerOptions() {
val options = arrayOf(
"Pilih dari Galeri",
"Batal"
"Kembali"
)
androidx.appcompat.app.AlertDialog.Builder(this)
.setTitle("Pilih Bukti Pembayaran")
.setItems(options) { dialog, which ->
val adapter = object : ArrayAdapter<String>(this, R.layout.item_dialog_add_evidence, R.id.tvOption, options) {
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
val view = super.getView(position, convertView, parent)
val divider = view.findViewById<View>(R.id.divider)
divider.visibility = if (position == count - 1) View.GONE else View.VISIBLE
return view
}
}
AlertDialog.Builder(this)
.setAdapter(adapter) { dialog, which ->
when (which) {
0 -> openGallery() // Gallery
1 -> dialog.dismiss() // Cancel
0 -> openGallery()
1 -> dialog.dismiss()
}
}
.show()
@ -440,6 +446,8 @@ class AddEvidencePaymentActivity : AppCompatActivity() {
}
companion object {
private const val PERMISSION_REQUEST_CODE = 100
private const val TAG = "AddEvidenceActivity"

View File

@ -17,6 +17,7 @@ import com.alya.ecommerce_serang.data.repository.OrderRepository
import com.alya.ecommerce_serang.databinding.ActivityPaymentBinding
import com.alya.ecommerce_serang.utils.BaseViewModelFactory
import com.alya.ecommerce_serang.utils.SessionManager
import java.text.NumberFormat
import java.text.SimpleDateFormat
import java.util.Calendar
import java.util.Locale
@ -100,6 +101,7 @@ class PaymentActivity : AppCompatActivity() {
// Log.d(TAG, "Button clicked - showing toast")
// Toast.makeText(this@PaymentActivity, "Button works! OrderID: $orderId", Toast.LENGTH_LONG).show()
// }
binding.btnUploadPaymentProof.apply {
isEnabled = true
isClickable = true
@ -134,7 +136,7 @@ class PaymentActivity : AppCompatActivity() {
Log.d(TAG, "Order details received: $order")
// Set total amount
binding.tvTotalAmount.text = order.totalAmount ?: "Rp0"
binding.tvTotalAmount.text = formatCurrency(order.totalAmount?.toDouble() ?: 0.00)
Log.d(TAG, "Total Amount: ${order.totalAmount}")
@ -202,6 +204,11 @@ class PaymentActivity : AppCompatActivity() {
}
}
private fun formatCurrency(amount: Double): String {
val formatter = NumberFormat.getCurrencyInstance(Locale("in", "ID"))
return formatter.format(amount).replace(",00", "")
}
private fun showInstructions(type: String) {
// Implementasi tampilkan instruksi
val instructions = when (type) {

View File

@ -0,0 +1,33 @@
package com.alya.ecommerce_serang.ui.order.detail
import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ArrayAdapter
import android.widget.TextView
import com.alya.ecommerce_serang.R
class SpinnerCardAdapter(
context: Context,
private val items: List<String>
) : ArrayAdapter<String>(context, 0, items) {
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
// View shown when Spinner is collapsed
return createCardView(position, convertView, parent)
}
override fun getDropDownView(position: Int, convertView: View?, parent: ViewGroup): View {
// View shown for dropdown items
return createCardView(position, convertView, parent)
}
private fun createCardView(position: Int, convertView: View?, parent: ViewGroup): View {
val inflater = LayoutInflater.from(context)
val view = convertView ?: inflater.inflate(R.layout.item_dialog_spinner_card, parent, false)
val textView = view.findViewById<TextView>(R.id.tvOption)
textView.text = items[position]
return view
}
}

View File

@ -6,6 +6,7 @@ import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import com.alya.ecommerce_serang.BuildConfig.BASE_URL
import com.alya.ecommerce_serang.R
import com.alya.ecommerce_serang.data.api.response.customer.order.OrderListItemsItem
import com.bumptech.glide.Glide
@ -39,11 +40,18 @@ class DetailOrderItemsAdapter : RecyclerView.Adapter<DetailOrderItemsAdapter.Det
private val tvProductName: TextView = itemView.findViewById(R.id.tvProductName)
private val tvQuantity: TextView = itemView.findViewById(R.id.tvQuantity)
private val tvPrice: TextView = itemView.findViewById(R.id.tvPrice)
private val tvStoreName: TextView = itemView.findViewById(R.id.tvStoreName)
fun bind(item: OrderListItemsItem) {
val fullImageUrl = if (item.productImage.startsWith("/")) {
BASE_URL + item.productImage.removePrefix("/") // Append base URL if the path starts with "/"
} else {
item.productImage // Use as is if it's already a full URL
}
// Load product image
Glide.with(itemView.context)
.load(item.productImage)
.load(fullImageUrl)
.placeholder(R.drawable.placeholder_image)
.error(R.drawable.placeholder_image)
.into(ivProduct)
@ -52,7 +60,8 @@ class DetailOrderItemsAdapter : RecyclerView.Adapter<DetailOrderItemsAdapter.Det
tvProductName.text = item.productName
tvQuantity.text = "${item.quantity} buah"
tvPrice.text = "Rp${newPrice}"
tvPrice.text = newPrice
tvStoreName.text = item.storeName
}
}

View File

@ -42,6 +42,7 @@ 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.NumberFormat
import java.text.SimpleDateFormat
import java.util.Calendar
import java.util.Locale
@ -172,7 +173,6 @@ class DetailOrderStatusActivity : AppCompatActivity() {
try {
// Set order date and payment deadline
binding.tvOrderDate.text = formatDate(orders.createdAt)
binding.tvPaymentDeadline.text = formatDatePay(orders.updatedAt)
Log.d(TAG, "populateOrderDetails: Order created at ${orders.createdAt}, formatted as ${binding.tvOrderDate.text}")
@ -197,10 +197,10 @@ class DetailOrderStatusActivity : AppCompatActivity() {
Log.d(TAG, "populateOrderDetails: Payment method=${orders.payInfoName ?: "Tidak tersedia"}")
// Set subtotal, shipping cost, and total
val subtotal = orders.totalAmount?.toIntOrNull()?.minus(orders.shipmentPrice.toIntOrNull() ?: 0) ?: 0
binding.tvSubtotal.text = "Rp$subtotal"
binding.tvShippingCost.text = "Rp${orders.shipmentPrice}"
binding.tvTotal.text = "Rp${orders.totalAmount}"
val subtotal = orders.totalAmount?.minus(orders.shipmentPrice.toIntOrNull() ?: 0) ?: 0
binding.tvSubtotal.text = formatCurrency(subtotal.toDouble())
binding.tvShippingCost.text = formatCurrency(orders.shipmentPrice.toDouble())
binding.tvTotal.text = formatCurrency(orders.totalAmount?.toDouble() ?: 0.00)
Log.d(TAG, "populateOrderDetails: Subtotal=$subtotal, Shipping=${orders.shipmentPrice}, Total=${orders.totalAmount}")
@ -255,6 +255,8 @@ class DetailOrderStatusActivity : AppCompatActivity() {
binding.tvStatusHeader.text = "Belum Dibayar"
binding.tvStatusNote.visibility = View.VISIBLE
binding.tvStatusNote.text = "Pesanan ini harus dibayar sebelum ${formatDatePay(orders.updatedAt)}"
binding.tvPaymentDeadlineLabel.text = "Batas Bayar:"
binding.tvPaymentDeadline.text = formatDatePay(orders.updatedAt)
// Set buttons
binding.btnSecondary.apply {
@ -286,6 +288,8 @@ class DetailOrderStatusActivity : AppCompatActivity() {
binding.tvStatusHeader.text = "Sudah Dibayar"
binding.tvStatusNote.visibility = View.VISIBLE
binding.tvStatusNote.text = "Menunggu pesanan dikonfirmasi penjual ${formatDatePay(orders.updatedAt)}"
binding.tvPaymentDeadlineLabel.text = "Batas konfirmasi penjual:"
binding.tvPaymentDeadline.text = formatDatePay(orders.updatedAt)
// Set buttons
binding.btnSecondary.apply {
@ -304,6 +308,8 @@ class DetailOrderStatusActivity : AppCompatActivity() {
binding.tvStatusHeader.text = "Sedang Diproses"
binding.tvStatusNote.visibility = View.VISIBLE
binding.tvStatusNote.text = "Penjual sedang memproses pesanan Anda"
binding.tvPaymentDeadlineLabel.text = "Batas diproses penjual:"
binding.tvPaymentDeadline.text = formatDatePay(orders.updatedAt)
binding.btnSecondary.apply {
visibility = View.VISIBLE
@ -326,6 +332,8 @@ class DetailOrderStatusActivity : AppCompatActivity() {
binding.tvStatusHeader.text = "Sudah Dikirim"
binding.tvStatusNote.visibility = View.VISIBLE
binding.tvStatusNote.text = "Pesanan Anda sedang dalam perjalanan. Akan sampai sekitar ${formatShipmentDate(orders.updatedAt, orders.etd ?: "0")}"
binding.tvPaymentDeadlineLabel.text = "Estimasi pesanan sampai:"
binding.tvPaymentDeadline.text = formatShipmentDate(orders.updatedAt, orders.etd ?: "0")
binding.btnSecondary.apply {
visibility = View.VISIBLE
@ -358,6 +366,8 @@ class DetailOrderStatusActivity : AppCompatActivity() {
binding.tvStatusHeader.text = "Pesanan Selesai"
binding.tvStatusNote.visibility = View.GONE
binding.tvPaymentDeadlineLabel.text = "Pesanan selesai:"
binding.tvPaymentDeadline.text = formatDate(orders.autoCompletedAt.toString())
binding.btnPrimary.apply {
visibility = View.VISIBLE
@ -379,6 +389,8 @@ class DetailOrderStatusActivity : AppCompatActivity() {
binding.tvStatusHeader.text = "Pesanan Selesai"
binding.tvStatusNote.visibility = View.VISIBLE
binding.tvStatusNote.text = "Pesanan dibatalkan: ${orders.cancelReason ?: "Alasan tidak diberikan"}"
binding.tvPaymentDeadlineLabel.text = "Tanggal dibatalkan: "
binding.tvPaymentDeadline.text = formatDate(orders.cancelDate.toString())
binding.btnSecondary.apply {
visibility = View.GONE
@ -604,7 +616,8 @@ class DetailOrderStatusActivity : AppCompatActivity() {
val inputFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.getDefault())
inputFormat.timeZone = TimeZone.getTimeZone("UTC")
val outputFormat = SimpleDateFormat("HH:mm dd MMMM yyyy", Locale("id", "ID"))
val timeFormat = SimpleDateFormat("HH:mm", Locale("id", "ID"))
val dateFormat = SimpleDateFormat("dd MMMM yyyy", Locale("id", "ID"))
val date = inputFormat.parse(dateString)
@ -614,7 +627,10 @@ class DetailOrderStatusActivity : AppCompatActivity() {
calendar.set(Calendar.HOUR_OF_DAY, 23)
calendar.set(Calendar.MINUTE, 59)
val formatted = outputFormat.format(calendar.time)
val timePart = timeFormat.format(calendar.time)
val datePart = dateFormat.format(calendar.time)
val formatted = "$timePart\n$datePart"
Log.d(TAG, "formatDate: Formatted date: $formatted")
formatted
} ?: dateString
@ -640,9 +656,13 @@ class DetailOrderStatusActivity : AppCompatActivity() {
calendar.add(Calendar.HOUR, 24)
val dueDate = calendar.time
// Format due date for display
val dueDateFormat = SimpleDateFormat("dd MMM yyyy", Locale.getDefault())
val formatted = dueDateFormat.format(calendar.time)
val timeFormat = SimpleDateFormat("HH:mm", Locale("id", "ID"))
val dateFormat = SimpleDateFormat("dd MMM yyyy", Locale("id", "ID"))
val timePart = timeFormat.format(dueDate)
val datePart = dateFormat.format(dueDate)
val formatted = "$timePart\n$datePart"
Log.d(TAG, "formatDatePay: Formatted payment date: $formatted")
formatted
@ -729,6 +749,11 @@ class DetailOrderStatusActivity : AppCompatActivity() {
}
}
private fun formatCurrency(amount: Double): String {
val formatter = NumberFormat.getCurrencyInstance(Locale("in", "ID"))
return formatter.format(amount).replace(",00", "")
}
override fun onDestroy() {
Log.d(TAG, "onDestroy: Cleaning up references")
super.onDestroy()

View File

@ -11,6 +11,8 @@ import com.alya.ecommerce_serang.data.api.dto.ProductsItem
import com.alya.ecommerce_serang.data.api.response.customer.product.StoreItem
import com.alya.ecommerce_serang.databinding.ItemProductHorizontalBinding
import com.bumptech.glide.Glide
import java.text.NumberFormat
import java.util.Locale
class OtherProductAdapter (
private var products: List<ProductsItem>,
@ -32,7 +34,7 @@ class OtherProductAdapter (
Log.d("ProductAdapter", "Loading image: $fullImageUrl")
tvProductName.text = product.name
tvProductPrice.text = product.price
tvProductPrice.text = formatCurrency(product.price.toDouble())
rating.text = product.rating
// Load image using Glide
@ -77,6 +79,11 @@ class OtherProductAdapter (
diffResult.dispatchUpdatesTo(this)
}
private fun formatCurrency(amount: Double): String {
val formatter = NumberFormat.getCurrencyInstance(Locale("in", "ID"))
return formatter.format(amount).replace(",00", "")
}
class ProductDiffCallback(
private val oldList: List<ProductsItem>,
private val newList: List<ProductsItem>

View File

@ -18,6 +18,7 @@ import com.alya.ecommerce_serang.data.api.retrofit.ApiConfig
import com.alya.ecommerce_serang.data.repository.ProductRepository
import com.alya.ecommerce_serang.data.repository.Result
import com.alya.ecommerce_serang.databinding.ActivityListProductBinding
import com.alya.ecommerce_serang.ui.cart.CartActivity
import com.alya.ecommerce_serang.ui.product.DetailProductActivity
import com.alya.ecommerce_serang.ui.product.ProductUserViewModel
import com.alya.ecommerce_serang.utils.BaseViewModelFactory
@ -60,13 +61,23 @@ class ListProductActivity : AppCompatActivity() {
windowInsets
}
setupToolbar()
setupObserver()
setupRecyclerView()
viewModel.loadProductsList()
}
private fun setupToolbar(){
binding.searchContainerList.btnBack.setOnClickListener{
finish()
}
binding.searchContainerList.btnCart.setOnClickListener{
val intent = Intent(this, CartActivity::class.java)
startActivity(intent)
}
}
private fun setupRecyclerView() {
binding.rvProductsList.apply {

View File

@ -11,6 +11,8 @@ import com.alya.ecommerce_serang.data.api.dto.ProductsItem
import com.alya.ecommerce_serang.data.api.response.customer.product.StoreItem
import com.alya.ecommerce_serang.databinding.ItemProductGridBinding
import com.bumptech.glide.Glide
import java.text.NumberFormat
import java.util.Locale
class ListProductAdapter(
private var products: List<ProductsItem>,
@ -24,7 +26,7 @@ class ListProductAdapter(
fun bind(product: ProductsItem) = with(binding) {
tvProductName.text = product.name
tvProductPrice.text = product.price
tvProductPrice.text = formatCurrency(product.price.toDouble())
rating.text = product.rating
val fullImageUrl = if (product.image.startsWith("/")) {
@ -68,6 +70,11 @@ class ListProductAdapter(
notifyDataSetChanged()
}
private fun formatCurrency(amount: Double): String {
val formatter = NumberFormat.getCurrencyInstance(Locale("in", "ID"))
return formatter.format(amount).replace(",00", "")
}
class ProductDiffCallback(
private val oldList: List<ProductsItem>,
private val newList: List<ProductsItem>

View File

@ -378,7 +378,9 @@ class ChatStoreActivity : AppCompatActivity() {
// Update messages
val previousCount = chatAdapter.itemCount
chatAdapter.submitList(state.messages) {
val displayItems = viewModel.getDisplayItems()
chatAdapter.submitList(displayItems) {
Log.d(TAG, "Messages submitted to adapter")
// Only auto-scroll for new messages or initial load
if (previousCount == 0 || state.messages.size > previousCount) {

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="@color/black_200" />
<corners android:radius="12dp" />
</shape>

View File

@ -4,7 +4,6 @@
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="@style/Theme.Ecommerce_serang"
tools:context=".ui.product.listproduct.ListProductActivity">
<include

View File

@ -30,7 +30,9 @@
android:layout_weight="1"
android:background="@drawable/search_background"
android:iconifiedByDefault="false"
android:queryHint="Search products..." />
android:queryHint="Search products..."
app:iconifiedByDefault="false"
app:queryHint="Search products..." />
</LinearLayout>

View File

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center"
android:paddingVertical="8dp">
<TextView
android:id="@+id/tvDate"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/bg_date_header"
android:paddingHorizontal="12dp"
android:paddingVertical="4dp"
android:text="Today"
android:textColor="@color/white"
android:textSize="12sp"
android:textStyle="bold" />
</LinearLayout>

View File

@ -0,0 +1,34 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:card_view="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp"
card_view:cardCornerRadius="12dp"
card_view:cardElevation="6dp"
android:foreground="?android:attr/selectableItemBackground">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="8dp">
<TextView
android:id="@+id/tvOption"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fontFamily="@font/dmsans_semibold"
android:text="Item 1"
android:textColor="@color/black_500"
android:textSize="16sp" />
<View
android:id="@+id/divider"
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="@android:color/darker_gray"
android:layout_marginTop="4dp" />
</LinearLayout>
</androidx.cardview.widget.CardView>

View File

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:card_view="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp"
card_view:cardElevation="4dp"
card_view:cardCornerRadius="8dp"
android:foreground="?android:attr/selectableItemBackground">
<TextView
android:id="@+id/tvOption"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="16dp"
android:textColor="@color/black_500"
android:text="Item 1"
android:fontFamily="@font/dmsans_semibold"
android:textSize="16sp"
android:gravity="center_vertical" />
</androidx.cardview.widget.CardView>

View File

@ -4,7 +4,7 @@
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="8dp">
android:paddingHorizontal="8dp">
<TextView
android:id="@+id/tvStoreName"
@ -68,6 +68,7 @@
android:textColor="@color/blue_500"
android:textSize="14sp"
android:textStyle="bold"
android:paddingBottom="4dp"
app:layout_constraintStart_toEndOf="@+id/ivProduct"
app:layout_constraintTop_toBottomOf="@+id/tvQuantity" />