update count product, chat, address

This commit is contained in:
shaulascr
2025-08-07 01:11:55 +07:00
parent f43d160fe1
commit 6194dca259
23 changed files with 502 additions and 299 deletions

View File

@ -82,11 +82,11 @@
<!-- android:name="androidx.startup.InitializationProvider" --> <!-- android:name="androidx.startup.InitializationProvider" -->
<!-- android:authorities="${applicationId}.androidx-startup" --> <!-- android:authorities="${applicationId}.androidx-startup" -->
<!-- tools:node="remove" /> --> <!-- tools:node="remove" /> -->
<service <!-- <service-->
android:name=".ui.notif.SimpleWebSocketService" <!-- android:name=".ui.notif.SimpleWebSocketService"-->
android:enabled="true" <!-- android:enabled="true"-->
android:exported="false" <!-- android:exported="false"-->
android:foregroundServiceType="dataSync" /> <!-- android:foregroundServiceType="dataSync" />-->
<activity <activity
android:name=".ui.profile.mystore.chat.ChatStoreActivity" android:name=".ui.profile.mystore.chat.ChatStoreActivity"

View File

@ -2,12 +2,15 @@ package com.alya.ecommerce_serang.data.api.dto
import com.google.gson.annotations.SerializedName import com.google.gson.annotations.SerializedName
data class CreateAddressRequest ( data class CreateAddressRequest(
@SerializedName("userId")
val userId: Int,
@SerializedName("latitude") @SerializedName("latitude")
val lat: Double? = null, val lat: Double,
@SerializedName("longitude") @SerializedName("longitude")
val long: Double? = null, val long: Double,
@SerializedName("street") @SerializedName("street")
val street: String, val street: String,
@ -20,22 +23,22 @@ data class CreateAddressRequest (
@SerializedName("province_id") @SerializedName("province_id")
val provId: Int, val provId: Int,
@SerializedName("postal_code") @SerializedName("postal_code")
val postCode: String, val postCode: String,
@SerializedName("detail") @SerializedName("village_id")
val detailAddress: String? = null, val idVillage: String?, // nullable for now
@SerializedName("user_id") @SerializedName("detail")
val userId: Int, val detailAddress: String,
@SerializedName("is_store_location")
val isStoreLocation: Boolean,
@SerializedName("recipient") @SerializedName("recipient")
val recipient: String, val recipient: String,
@SerializedName("phone") @SerializedName("phone")
val phone: String, val phone: String
@SerializedName("is_store_location")
val isStoreLocation: Boolean
) )

View File

@ -98,5 +98,5 @@ data class Store(
val storeDescription: String, val storeDescription: String,
@field:SerializedName("city_id") @field:SerializedName("city_id")
val cityId: Int val cityId: String
) )

View File

@ -4,15 +4,18 @@ import com.google.gson.annotations.SerializedName
data class AddressResponse( data class AddressResponse(
@field:SerializedName("addresses") @field:SerializedName("addresses")
val addresses: List<AddressesItem>, val addresses: List<AddressesItem>,
@field:SerializedName("message") @field:SerializedName("message")
val message: String val message: String
) )
data class AddressesItem( data class AddressesItem(
@field:SerializedName("village_id")
val villageId: String,
@field:SerializedName("is_store_location") @field:SerializedName("is_store_location")
val isStoreLocation: Boolean, val isStoreLocation: Boolean,
@ -23,7 +26,7 @@ data class AddressesItem(
val userId: Int, val userId: Int,
@field:SerializedName("province_id") @field:SerializedName("province_id")
val provinceId: Int, val provinceId: String,
@field:SerializedName("phone") @field:SerializedName("phone")
val phone: String, val phone: String,
@ -50,5 +53,5 @@ data class AddressesItem(
val longitude: String, val longitude: String,
@field:SerializedName("city_id") @field:SerializedName("city_id")
val cityId: Int val cityId: String
) )

View File

@ -1,6 +1,7 @@
package com.alya.ecommerce_serang.data.repository package com.alya.ecommerce_serang.data.repository
import android.util.Log import android.util.Log
import com.alya.ecommerce_serang.data.api.dto.ProductsItem
import com.alya.ecommerce_serang.data.api.dto.Store import com.alya.ecommerce_serang.data.api.dto.Store
import com.alya.ecommerce_serang.data.api.response.auth.ListStoreTypeResponse import com.alya.ecommerce_serang.data.api.response.auth.ListStoreTypeResponse
import com.alya.ecommerce_serang.data.api.response.customer.product.StoreResponse import com.alya.ecommerce_serang.data.api.response.customer.product.StoreResponse
@ -123,6 +124,20 @@ class MyStoreRepository(private val apiService: ApiService) {
} }
} }
suspend fun fetchMyStoreProducts(): List<ProductsItem> {
return try {
val response = apiService.getStoreProduct()
if (response.isSuccessful) {
response.body()?.products?.filterNotNull() ?: emptyList()
} else {
throw Exception("Failed to fetch store products: ${response.message()}")
}
} catch (e: Exception) {
Log.e("ProductRepository", "Error fetching store products", e)
throw e
}
}
// private fun fetchBalance() { // private fun fetchBalance() {
// showLoading(true) // showLoading(true)
// lifecycleScope.launch { // lifecycleScope.launch {

View File

@ -30,6 +30,7 @@ import com.alya.ecommerce_serang.utils.BaseViewModelFactory
import com.alya.ecommerce_serang.utils.SessionManager import com.alya.ecommerce_serang.utils.SessionManager
import com.alya.ecommerce_serang.utils.viewmodel.RegisterViewModel import com.alya.ecommerce_serang.utils.viewmodel.RegisterViewModel
import com.google.android.material.progressindicator.LinearProgressIndicator import com.google.android.material.progressindicator.LinearProgressIndicator
import com.google.gson.Gson
class RegisterStep3Fragment : Fragment() { class RegisterStep3Fragment : Fragment() {
private var _binding: FragmentRegisterStep3Binding? = null private var _binding: FragmentRegisterStep3Binding? = null
@ -376,6 +377,8 @@ class RegisterStep3Fragment : Fragment() {
val subDistrict = registerViewModel.selectedSubdistrict.toString() val subDistrict = registerViewModel.selectedSubdistrict.toString()
val postalCode = registerViewModel.selectedPostalCode.toString() val postalCode = registerViewModel.selectedPostalCode.toString()
val villageId = registerViewModel.selectedVillages ?: ""
Log.d(TAG, "Address data - Street: $street, SubDistrict: $subDistrict, PostalCode: $postalCode") Log.d(TAG, "Address data - Street: $street, SubDistrict: $subDistrict, PostalCode: $postalCode")
Log.d(TAG, "Address data - Recipient: $recipient, Phone: $phone") Log.d(TAG, "Address data - Recipient: $recipient, Phone: $phone")
Log.d(TAG, "Address data - ProvinceId: $provinceId, CityId: $cityId") Log.d(TAG, "Address data - ProvinceId: $provinceId, CityId: $cityId")
@ -383,21 +386,25 @@ class RegisterStep3Fragment : Fragment() {
// Create address request // Create address request
val addressRequest = CreateAddressRequest( val addressRequest = CreateAddressRequest(
userId = user.id, // must match the type expected in the DB
lat = defaultLatitude, lat = defaultLatitude,
long = defaultLongitude, long = defaultLongitude,
street = street, street = street,
subDistrict = subDistrict, subDistrict = subDistrict,
cityId = cityId, cityId = cityId, // ⚠️ Make sure this is Int
provId = provinceId, provId = provinceId,
postCode = postalCode, postCode = postalCode,
idVillage = villageId, // Or provide a real ID if needed
detailAddress = street, detailAddress = street,
userId = userId, isStoreLocation = false,
recipient = recipient, recipient = recipient,
phone = phone, phone = phone
isStoreLocation = false
) )
Log.d(TAG, "Address request created: $addressRequest") Log.d(TAG, "Address request created: $addressRequest")
val gson = Gson()
val jsonString = gson.toJson(addressRequest)
Log.d(TAG, "Request JSON: $jsonString")
// Show loading // Show loading
binding.progressBar.visibility = View.VISIBLE binding.progressBar.visibility = View.VISIBLE

View File

@ -146,7 +146,9 @@ class CartActivity : AppCompatActivity() {
private fun observeViewModel() { private fun observeViewModel() {
viewModel.cartItems.observe(this) { cartItems -> viewModel.cartItems.observe(this) { cartItems ->
if (cartItems.isNullOrEmpty()) { if (cartItems.isNullOrEmpty()) {
binding.emptyCart.visibility = View.VISIBLE
showEmptyState(true) showEmptyState(true)
} else { } else {
showEmptyState(false) showEmptyState(false)
storeAdapter.submitList(cartItems) storeAdapter.submitList(cartItems)

View File

@ -27,6 +27,7 @@ import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsAnimationCompat import androidx.core.view.WindowInsetsAnimationCompat
import androidx.core.view.WindowInsetsCompat import androidx.core.view.WindowInsetsCompat
import androidx.lifecycle.Observer import androidx.lifecycle.Observer
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import com.alya.ecommerce_serang.BuildConfig.BASE_URL import com.alya.ecommerce_serang.BuildConfig.BASE_URL
import com.alya.ecommerce_serang.R import com.alya.ecommerce_serang.R
@ -170,8 +171,7 @@ class ChatActivity : AppCompatActivity() {
// If opened from ChatListFragment with a valid chatRoomId // If opened from ChatListFragment with a valid chatRoomId
if (chatRoomId > 0) { if (chatRoomId > 0) {
// Directly set the chatRoomId and load chat history viewModel.setChatRoomId(chatRoomId)
viewModel._chatRoomId.value = chatRoomId
} }
} }
@ -405,68 +405,71 @@ class ChatActivity : AppCompatActivity() {
} }
}) })
viewModel.state.observe(this, Observer { state -> lifecycleScope.launchWhenStarted {
Log.d(TAG, "State updated - Messages: ${state.messages.size}") viewModel.state.collect() { state ->
Log.d(TAG, "State updated - Messages: ${state.messages.size}")
// Update messages // Update messages
val previousCount = chatAdapter.itemCount val previousCount = chatAdapter.itemCount
val displayItems = viewModel.getDisplayItems() val displayItems = viewModel.getDisplayItems()
chatAdapter.submitList(displayItems) { chatAdapter.submitList(displayItems) {
Log.d(TAG, "Messages submitted to adapter") Log.d(TAG, "Messages submitted to adapter")
// Only auto-scroll for new messages or initial load // Only auto-scroll for new messages or initial load
if (previousCount == 0 || state.messages.size > previousCount) { if (previousCount == 0 || state.messages.size > previousCount) {
scrollToBottomInstant() scrollToBottomInstant()
}
}
// layout attach product
if (!state.productName.isNullOrEmpty()) {
binding.tvProductName.text = state.productName
binding.tvProductPrice.text = state.productPrice
binding.ratingBar.rating = state.productRating
binding.tvRating.text = state.productRating.toString()
binding.tvSellerName.text = state.storeName
binding.tvStoreName.text = state.storeName
val fullImageUrl = when (val img = state.productImageUrl) {
is String -> {
if (img.startsWith("/")) BASE_URL + img.substring(1) else img
} }
else -> R.drawable.placeholder_image
} }
if (!state.productImageUrl.isNullOrEmpty()) { // layout attach product
Glide.with(this@ChatActivity) if (!state.productName.isNullOrEmpty()) {
.load(fullImageUrl) binding.tvProductName.text = state.productName
.centerCrop() binding.tvProductPrice.text = state.productPrice
.placeholder(R.drawable.placeholder_image) binding.ratingBar.rating = state.productRating
.error(R.drawable.placeholder_image) binding.tvRating.text = state.productRating.toString()
.into(binding.imgProduct) binding.tvSellerName.text = state.storeName
binding.tvStoreName.text = state.storeName
val fullImageUrl = when (val img = state.productImageUrl) {
is String -> {
if (img.startsWith("/")) BASE_URL + img.substring(1) else img
}
else -> R.drawable.placeholder_image
}
if (!state.productImageUrl.isNullOrEmpty()) {
Glide.with(this@ChatActivity)
.load(fullImageUrl)
.centerCrop()
.placeholder(R.drawable.placeholder_image)
.error(R.drawable.placeholder_image)
.into(binding.imgProduct)
}
updateProductCardUI(state.hasProductAttachment)
binding.productContainer.visibility = View.GONE
} else {
binding.productContainer.visibility = View.GONE
} }
updateProductCardUI(state.hasProductAttachment)
binding.productContainer.visibility = View.GONE updateInputHint(state)
} else {
binding.productContainer.visibility = View.GONE // Update attachment hint
if (state.hasAttachment) {
binding.layoutAttachImage.visibility = View.VISIBLE
} else {
binding.editTextMessage.hint = getString(R.string.write_message)
}
// Show error if any
state.error?.let { error ->
Toast.makeText(this@ChatActivity, error, Toast.LENGTH_SHORT).show()
viewModel.clearError()
}
} }
}
updateInputHint(state)
// Update attachment hint
if (state.hasAttachment) {
binding.layoutAttachImage.visibility = View.VISIBLE
} else {
binding.editTextMessage.hint = getString(R.string.write_message)
}
// Show error if any
state.error?.let { error ->
Toast.makeText(this@ChatActivity, error, Toast.LENGTH_SHORT).show()
viewModel.clearError()
}
})
} }
private fun updateInputHint(state: ChatUiState) { private fun updateInputHint(state: ChatUiState) {
@ -492,7 +495,7 @@ class ChatActivity : AppCompatActivity() {
Toast.makeText(this, "Opening: ${productInfo.productName}", Toast.LENGTH_SHORT).show() Toast.makeText(this, "Opening: ${productInfo.productName}", Toast.LENGTH_SHORT).show()
// You can navigate to product detail here // You can navigate to product detail here
navigateToProductDetail(productInfo.productId) navigateToProductDetail(productInfo.productId)
} }
private fun navigateToProductDetail(productId: Int) { private fun navigateToProductDetail(productId: Int) {

View File

@ -209,7 +209,7 @@ class ChatAdapter(
binding.tvProductPrice.text = product.productPrice binding.tvProductPrice.text = product.productPrice
// Load product image // Load product image
val fullImageUrl = if (product.productImage.startsWith("/")) { val fullImageUrl = if (product.productImage!!.startsWith("/")) {
BASE_URL + product.productImage.substring(1) BASE_URL + product.productImage.substring(1)
} else { } else {
product.productImage product.productImage
@ -246,7 +246,7 @@ class ChatAdapter(
binding.tvProductPrice.text = product.productPrice binding.tvProductPrice.text = product.productPrice
// Load product image // Load product image
val fullImageUrl = if (product.productImage.startsWith("/")) { val fullImageUrl = if (product.productImage!!.startsWith("/")) {
BASE_URL + product.productImage.substring(1) BASE_URL + product.productImage.substring(1)
} else { } else {
product.productImage product.productImage

View File

@ -14,6 +14,9 @@ import com.alya.ecommerce_serang.data.repository.Result
import com.alya.ecommerce_serang.utils.Constants import com.alya.ecommerce_serang.utils.Constants
import com.alya.ecommerce_serang.utils.SessionManager import com.alya.ecommerce_serang.utils.SessionManager
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.io.File import java.io.File
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
@ -56,9 +59,9 @@ class ChatViewModel @Inject constructor(
// Product attachment flag // Product attachment flag
private var shouldAttachProduct = false private var shouldAttachProduct = false
// UI state using LiveData // use state for more seamless responsive
private val _state = MutableLiveData(ChatUiState()) private val _state = MutableStateFlow(ChatUiState())
val state: LiveData<ChatUiState> = _state val state: StateFlow<ChatUiState> = _state
private val _isLoading = MutableLiveData<Boolean>() private val _isLoading = MutableLiveData<Boolean>()
val isLoading: LiveData<Boolean> = _isLoading val isLoading: LiveData<Boolean> = _isLoading
@ -93,6 +96,8 @@ class ChatViewModel @Inject constructor(
init { init {
Log.d(TAG, "ChatViewModel initialized") Log.d(TAG, "ChatViewModel initialized")
socketService.connect() // 🛠 force connection
setupSocketListeners() // 🛠 always listen, even before user data
initializeUser() initializeUser()
} }
@ -113,6 +118,7 @@ class ChatViewModel @Inject constructor(
updateState { it.copy(error = "User authentication error. Please login again.") } updateState { it.copy(error = "User authentication error. Please login again.") }
} else { } else {
Log.d(TAG, "Setting up socket listeners...") Log.d(TAG, "Setting up socket listeners...")
socketService.connect()
setupSocketListeners() setupSocketListeners()
} }
} }
@ -231,26 +237,116 @@ class ChatViewModel @Inject constructor(
if (connectionState is ConnectionState.Connected) { if (connectionState is ConnectionState.Connected) {
Log.d(TAG, "Socket connected, joining room...") Log.d(TAG, "Socket connected, joining room...")
socketService.joinRoom() val roomId = _chatRoomId.value
if (roomId != null && roomId > 0) {
socketService.joinRoom(roomId)
}
} }
} }
} }
// viewModelScope.launch {
// socketService.newMessages.collect { chatLine ->
// chatLine?.let {
// Log.d(TAG, "NEW message received in ViewModel: ${it.message}")
// val updatedMessages = _state.value.messages.toMutableList()
// updatedMessages.add(convertChatLineToUiMessage(it))
// updateState { it.copy(messages = updatedMessages) }
//
// if (it.senderId != currentUserId) {
// updateMessageStatus(it.id, Constants.STATUS_READ)
// }
// }
// }
// }
viewModelScope.launch { viewModelScope.launch {
socketService.newMessages.collect { chatLine -> socketService.newMessages.collect { chatLine ->
chatLine?.let { Log.d("ChatViewModel", "Collected new message from SocketIOService: ${chatLine.message}")
Log.d(TAG, "New message received via socket - ID: ${it.id}, SenderID: ${it.senderId}") chatLine?.let { incomingChatLine ->
val currentMessages = _state.value?.messages ?: listOf() // 1. First update: Add the message to the list (potentially without full product info)
val updatedMessages = currentMessages.toMutableList().apply { _state.update { currentState ->
add(convertChatLineToUiMessage(it)) val existingMessageIndex =
currentState.messages.indexOfFirst { it.id == incomingChatLine.id }
val messagesAfterInitialUpdate = if (existingMessageIndex != -1) {
// If message exists (e.g., status update), just update it
val updatedList = currentState.messages.toMutableList()
updatedList[existingMessageIndex] = mapChatLineToUiMessage(
incomingChatLine,
updatedList[existingMessageIndex].productInfo
) // Preserve existing productInfo if any
updatedList
} else {
// New message, add it
(currentState.messages + mapChatLineToUiMessage(incomingChatLine)).distinctBy { msg -> msg.id }
}
// Sort after any update/addition
currentState.copy(messages = messagesAfterInitialUpdate.sortedBy { msg ->
SimpleDateFormat(
"yyyy-MM-dd HH:mm:ss",
Locale.getDefault()
).parse(msg.createdAt)?.time
})
} }
updateState { it.copy(messages = updatedMessages) }
if (it.senderId != currentUserId) { // 2. If it's a product message and needs details, fetch them
Log.d(TAG, "Marking message as read: ${it.id}") if (incomingChatLine.productId != 0) { // Check if it's a product message
updateMessageStatus(it.id, Constants.STATUS_READ) viewModelScope.launch {
Log.d(
TAG,
"Fetching product detail for ID: ${incomingChatLine.productId}"
)
// Call your repository function directly
val productResponse =
chatRepository.fetchProductDetail(incomingChatLine.productId)
if (productResponse != null && productResponse.product != null) {
val fetchedProduct =
productResponse.product // Access the nested product object
Log.d(
TAG,
"Successfully fetched product: ${fetchedProduct.productName}"
)
// Create a complete ProductInfo object
val fullProductInfo = ProductInfo(
productId = fetchedProduct.productId,
productName = fetchedProduct.productName, // Use productName from fetched data
productPrice = fetchedProduct.price, // Use productPrice from fetched data
productImage = fetchedProduct.image, // Use productImage from fetched data
productRating = fetchedProduct.rating.toFloat(),
storeName = fetchedProduct.productName // Use storeName from fetched data
)
// --- PHASE 3: Second UI update (fill in full product info) ---
_state.update { currentState ->
val updatedMessages = currentState.messages.map { msg ->
if (msg.id == incomingChatLine.id) {
// Found the message, update its productInfo with full details
msg.copy(productInfo = fullProductInfo)
} else {
msg
}
}
currentState.copy(messages = updatedMessages)
}
} else {
Log.e(
TAG,
"Failed to fetch product detail for ID ${incomingChatLine.productId} or product data is null."
)
// Optionally, update message status to indicate error in product loading
}
}
} }
} }
// // Your existing logic for clearing typing status etc.
// if (incomingChatLine.isTyping == false && incomingChatLine.from?.id != sessionManager.getUserId()?.toIntOrNull()) {
// _state.update { it.copy(isOtherUserTyping = false) }
// }
} }
} }
@ -271,10 +367,10 @@ class ChatViewModel @Inject constructor(
if (roomId <= 0) { if (roomId <= 0) {
Log.e(TAG, "Cannot join room: Invalid room ID") Log.e(TAG, "Cannot join room: Invalid room ID")
return return
} else if (roomId > 0){
Log.d(TAG, "Joining socket room: $roomId")
socketService.joinRoom(roomId)
} }
Log.d(TAG, "Joining socket room: $roomId")
socketService.joinRoom()
} }
fun sendTypingStatus(isTyping: Boolean) { fun sendTypingStatus(isTyping: Boolean) {
@ -728,7 +824,7 @@ class ChatViewModel @Inject constructor(
} }
} }
//update message status //update message status
fun updateMessageStatus(messageId: Int, status: String) { fun updateMessageStatus(messageId: Int, status: String) {
Log.d(TAG, "Updating message status - ID: $messageId, Status: $status") Log.d(TAG, "Updating message status - ID: $messageId, Status: $status")
@ -756,7 +852,7 @@ class ChatViewModel @Inject constructor(
} }
} }
//set image attachment //set image attachment
fun setSelectedImageFile(file: File?) { fun setSelectedImageFile(file: File?) {
selectedImageFile = file selectedImageFile = file
updateState { it.copy(hasAttachment = file != null) } updateState { it.copy(hasAttachment = file != null) }
@ -791,7 +887,7 @@ class ChatViewModel @Inject constructor(
) )
} }
// convert chat history item to ui // convert chat history item to ui
private fun convertChatLineToUiMessageHistory(chatItem: ChatItem): ChatUiMessage { private fun convertChatLineToUiMessageHistory(chatItem: ChatItem): ChatUiMessage {
val formattedTime = formatTimestamp(chatItem.createdAt) val formattedTime = formatTimestamp(chatItem.createdAt)
@ -932,7 +1028,7 @@ class ChatViewModel @Inject constructor(
} }
} }
//format price //format price
private fun formatPrice(price: String): String { private fun formatPrice(price: String): String {
return if (price.startsWith("Rp")) price else "Rp$price" return if (price.startsWith("Rp")) price else "Rp$price"
} }
@ -958,9 +1054,7 @@ class ChatViewModel @Inject constructor(
// helper function to update live data // helper function to update live data
private fun updateState(update: (ChatUiState) -> ChatUiState) { private fun updateState(update: (ChatUiState) -> ChatUiState) {
_state.value?.let { _state.value = update(_state.value)
_state.value = update(it)
}
} }
//clear any error messages //clear any error messages
@ -1053,6 +1147,73 @@ class ChatViewModel @Inject constructor(
private fun isThisYear(messageCalendar: Calendar, today: Calendar): Boolean { private fun isThisYear(messageCalendar: Calendar, today: Calendar): Boolean {
return messageCalendar.get(Calendar.YEAR) == today.get(Calendar.YEAR) return messageCalendar.get(Calendar.YEAR) == today.get(Calendar.YEAR)
} }
fun setChatRoomId(roomId: Int) {
_chatRoomId.value = roomId
joinSocketRoom(roomId)
loadChatHistory(roomId)
}
private fun convertToUiMessage(chatLine: ChatLine): ChatUiMessage {
val formattedTime = formatTimestamp(chatLine.createdAt)
return ChatUiMessage(
id = chatLine.id,
message = chatLine.message,
attachment = chatLine.attachment,
status = chatLine.status,
time = formattedTime, // or format from createdAt if needed
isSentByMe = chatLine.senderId == currentUserId,
messageType = MessageType.TEXT, // or detect from chatLine if needed
productInfo = null, // optional, if applicable
createdAt = chatLine.createdAt
)
}
private fun mapChatLineToUiMessage(chatLine: ChatLine, fetchedProductInfo: ProductInfo? = null): ChatUiMessage {
val isSentByMe = chatLine.senderId == sessionManager.getUserId()?.toIntOrNull() // Using senderId now
val formattedTime = try {
val inputFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.getDefault()).apply {
timeZone = TimeZone.getTimeZone("UTC")
}
val outputFormat = SimpleDateFormat("HH:mm", Locale.getDefault())
val date = inputFormat.parse(chatLine.createdAt)
date?.let { outputFormat.format(it) } ?: ""
} catch (e: Exception) {
Log.e("ChatViewModel", "Error parsing date: ${chatLine.createdAt}", e)
""
}
// Determine message type based on what ChatLine provides
val messageType = when {
chatLine.attachment?.isNotEmpty() == true -> MessageType.IMAGE
chatLine.productId != 0 -> MessageType.PRODUCT // If productId is non-zero, it's a product message
else -> MessageType.TEXT
}
// Initialize productInfo: if fetchedProductInfo is provided, use it.
// Otherwise, if ChatLine has a productId, create a ProductInfo with just the ID.
// If no productId, it's null.
val productInfo = fetchedProductInfo ?: if (chatLine.productId != 0) {
// Create a placeholder ProductInfo with just the ID for initial display
// The full details will be fetched later
ProductInfo(productId = chatLine.productId)
} else {
null
}
return ChatUiMessage(
id = chatLine.id,
message = chatLine.message,
attachment = chatLine.attachment,
status = chatLine.status,
time = formattedTime,
isSentByMe = isSentByMe,
messageType = messageType,
productInfo = productInfo, // Use the determined productInfo
createdAt = chatLine.createdAt
)
}
} }
enum class MessageType { enum class MessageType {
@ -1062,12 +1223,12 @@ enum class MessageType {
} }
data class ProductInfo( data class ProductInfo(
val productId: Int, val productId: Int, // Keep productId here
val productName: String, val productName: String? = null, // Make nullable
val productPrice: String, val productPrice: String? = null, // Make nullable
val productImage: String, val productImage: String? = null, // Make nullable
val productRating: Float, val productRating: Float = 0f, // Default value
val storeName: String val storeName: String? = null
) )
// representing chat messages to UI // representing chat messages to UI
@ -1083,8 +1244,6 @@ data class ChatUiMessage(
val createdAt: String val createdAt: String
) )
// representing UI state to screen // representing UI state to screen
data class ChatUiState( data class ChatUiState(
val messages: List<ChatUiMessage> = emptyList(), val messages: List<ChatUiMessage> = emptyList(),
@ -1102,4 +1261,8 @@ data class ChatUiState(
val productImageUrl: String = "", val productImageUrl: String = "",
val productRating: Float = 0f, val productRating: Float = 0f,
val storeName: String = "" val storeName: String = ""
) )
//data class ChatUiState(
// val messages: List<ChatUiMessage> = emptyList()
//)

View File

@ -10,14 +10,24 @@ import com.alya.ecommerce_serang.utils.SessionManager
import com.google.gson.Gson import com.google.gson.Gson
import io.socket.client.IO import io.socket.client.IO
import io.socket.client.Socket import io.socket.client.Socket
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import org.json.JSONObject import org.json.JSONObject
import java.net.URISyntaxException import java.net.URISyntaxException
import javax.inject.Inject
import javax.inject.Singleton
class SocketIOService( @Singleton
class SocketIOService @Inject constructor(
private val sessionManager: SessionManager private val sessionManager: SessionManager
) { ) {
private val serviceScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
private val TAG = "SocketIOService" private val TAG = "SocketIOService"
// Socket.IO client // Socket.IO client
@ -30,8 +40,8 @@ class SocketIOService(
private val _connectionState = MutableStateFlow<ConnectionState>(ConnectionState.Disconnected()) private val _connectionState = MutableStateFlow<ConnectionState>(ConnectionState.Disconnected())
val connectionState: StateFlow<ConnectionState> = _connectionState val connectionState: StateFlow<ConnectionState> = _connectionState
private val _newMessages = MutableStateFlow<ChatLine?>(null) private val _newMessages = MutableSharedFlow<ChatLine>(extraBufferCapacity = 1) // Using extraBufferCapacity for a non-suspending emit
val newMessages: StateFlow<ChatLine?> = _newMessages val newMessages: SharedFlow<ChatLine> = _newMessages
private val _typingStatus = MutableStateFlow<TypingStatus?>(null) private val _typingStatus = MutableStateFlow<TypingStatus?>(null)
val typingStatus: StateFlow<TypingStatus?> = _typingStatus val typingStatus: StateFlow<TypingStatus?> = _typingStatus
@ -85,63 +95,95 @@ class SocketIOService(
* Sets up Socket.IO event listeners * Sets up Socket.IO event listeners
*/ */
private fun setupSocketListeners() { private fun setupSocketListeners() {
socket?.let { socket ->
// Connection events
socket.on(Socket.EVENT_CONNECT) {
Log.d(TAG, "Socket.IO connected")
isConnected = true
_connectionState.value = ConnectionState.Connected
_connectionStateLiveData.postValue(ConnectionState.Connected)
}
socket.on(Socket.EVENT_DISCONNECT) { socket?.on(Constants.EVENT_NEW_MESSAGE) { args -> // Use the event name your server emits
Log.d(TAG, "Socket.IO disconnected") Log.d(TAG, "Raw event received on ${Constants.EVENT_NEW_MESSAGE}: ${args.firstOrNull()}") // Check raw args
isConnected = false
_connectionState.value = ConnectionState.Disconnected("Disconnected from server")
_connectionStateLiveData.postValue(ConnectionState.Disconnected("Disconnected from server"))
}
socket.on(Socket.EVENT_CONNECT_ERROR) { args -> if (args.isNotEmpty()) {
val error = if (args.isNotEmpty() && args[0] != null) args[0].toString() else "Unknown error"
Log.e(TAG, "Socket.IO connection error: $error")
isConnected = false
_connectionState.value = ConnectionState.Error("Connection error: $error")
_connectionStateLiveData.postValue(ConnectionState.Error("Connection error: $error"))
}
// Chat events
socket.on(Constants.EVENT_NEW_MESSAGE) { args ->
try { try {
if (args.isNotEmpty() && args[0] != null) { val messageJson = args[0].toString()
val messageJson = args[0].toString() val chatLine = Gson().fromJson(messageJson, ChatLine::class.java)
Log.d(TAG, "Received new message: $messageJson") Log.d(TAG, "Successfully parsed ChatLine: ${chatLine.message}")
val chatLine = Gson().fromJson(messageJson, ChatLine::class.java) Log.d(TAG, "Emitting new message to _newMessages SharedFlow...") // New log
_newMessages.value = chatLine
_newMessagesLiveData.postValue(chatLine) // Use the serviceScope to launch a coroutine for emit()
serviceScope.launch {
_newMessages.emit(chatLine) // This ensures every message is processed
Log.d(TAG, "New message emitted to SharedFlow.") // New log after emit
} }
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "Error parsing new message event", e) Log.e(TAG, "Error parsing or emitting new message: ${e.message}", e)
}
}
socket.on(Constants.EVENT_TYPING) { args ->
try {
if (args.isNotEmpty() && args[0] != null) {
val typingData = args[0] as JSONObject
val userId = typingData.getInt("userId")
val roomId = typingData.getInt("roomId")
val isTyping = typingData.getBoolean("isTyping")
Log.d(TAG, "Received typing status: User $userId in room $roomId is typing: $isTyping")
val status = TypingStatus(userId, roomId, isTyping)
_typingStatus.value = status
_typingStatusLiveData.postValue(status)
}
} catch (e: Exception) {
Log.e(TAG, "Error parsing typing event", e)
} }
} else {
Log.w(TAG, "Received empty args for ${Constants.EVENT_NEW_MESSAGE}")
} }
} }
// socket?.on(Constants.EVENT_NEW_MESSAGE) { args ->
// if (args.isNotEmpty()) {
// val messageJson = args[0].toString()
// val chatLine = Gson().fromJson(messageJson, ChatLine::class.java)
// Log.d("SocketIOService", "Message received: ${chatLine.message}")
// _newMessages.value = chatLine
// }
// }
// socket?.let { socket ->
// // Connection events
// socket.on(Socket.EVENT_CONNECT) {
// Log.d(TAG, "Socket.IO connected")
// isConnected = true
// _connectionState.value = ConnectionState.Connected
// _connectionStateLiveData.postValue(ConnectionState.Connected)
// }
//
// socket.on(Socket.EVENT_DISCONNECT) {
// Log.d(TAG, "Socket.IO disconnected")
// isConnected = false
// _connectionState.value = ConnectionState.Disconnected("Disconnected from server")
// _connectionStateLiveData.postValue(ConnectionState.Disconnected("Disconnected from server"))
// }
//
// socket.on(Socket.EVENT_CONNECT_ERROR) { args ->
// val error = if (args.isNotEmpty() && args[0] != null) args[0].toString() else "Unknown error"
// Log.e(TAG, "Socket.IO connection error: $error")
// isConnected = false
// _connectionState.value = ConnectionState.Error("Connection error: $error")
// _connectionStateLiveData.postValue(ConnectionState.Error("Connection error: $error"))
// }
//
// // Chat events
// socket.on(Constants.EVENT_NEW_MESSAGE) { args ->
// try {
// if (args.isNotEmpty() && args[0] != null) {
// val messageJson = args[0].toString()
// Log.d(TAG, "Received new message: $messageJson")
// val chatLine = Gson().fromJson(messageJson, ChatLine::class.java)
// _newMessages.value = chatLine
// _newMessagesLiveData.postValue(chatLine)
// }
// } catch (e: Exception) {
// Log.e(TAG, "Error parsing new message event", e)
// }
// }
//
// socket.on(Constants.EVENT_TYPING) { args ->
// try {
// if (args.isNotEmpty() && args[0] != null) {
// val typingData = args[0] as JSONObject
// val userId = typingData.getInt("userId")
// val roomId = typingData.getInt("roomId")
// val isTyping = typingData.getBoolean("isTyping")
//
// Log.d(TAG, "Received typing status: User $userId in room $roomId is typing: $isTyping")
// val status = TypingStatus(userId, roomId, isTyping)
// _typingStatus.value = status
// _typingStatusLiveData.postValue(status)
// }
// } catch (e: Exception) {
// Log.e(TAG, "Error parsing typing event", e)
// }
// }
// }
} }
/** /**
@ -159,22 +201,29 @@ class SocketIOService(
/** /**
* Joins a specific chat room * Joins a specific chat room
*/ */
fun joinRoom() { fun joinRoom(roomId: Int) {
// if (!isConnected) {
// connect()
// return
// }
//
// // Get user ID from SessionManager
// val userId = sessionManager.getUserId()
// if (userId.isNullOrEmpty()) {
// Log.e(TAG, "Cannot join room: User ID is null or empty")
// return
// }
//
// // Join the room using the current user's ID
// socket?.emit("joinRoom", roomId) // ✅
// Log.d(TAG, "Joined room ID: $roomId")
// Log.d(TAG, "Joined room for user: $userId")
if (!isConnected) { if (!isConnected) {
connect() connect()
return
} }
// Get user ID from SessionManager socket?.emit("joinRoom", roomId)
val userId = sessionManager.getUserId() Log.d(TAG, "Joined room ID: $roomId")
if (userId.isNullOrEmpty()) {
Log.e(TAG, "Cannot join room: User ID is null or empty")
return
}
// Join the room using the current user's ID
socket?.emit("joinRoom", userId)
Log.d(TAG, "Joined room for user: $userId")
} }
/** /**

View File

@ -333,18 +333,19 @@ class AddAddressActivity : AppCompatActivity() {
// Create request with all fields // Create request with all fields
val request = CreateAddressRequest( val request = CreateAddressRequest(
userId = userId,
lat = latitude!!, // Safe to use !! as we've checked above lat = latitude!!, // Safe to use !! as we've checked above
long = longitude!!, long = longitude!!,
street = street, street = street,
subDistrict = subDistrict, subDistrict = subDistrict,
cityId = cityId, cityId = cityId, // ⚠️ Make sure this is Int
provId = provinceId, provId = provinceId,
postCode = postalCode, postCode = postalCode,
idVillage = "", // Or provide a real ID if needed
detailAddress = street, detailAddress = street,
userId = userId, isStoreLocation = false,
recipient = recipient, recipient = recipient,
phone = phone, phone = phone
isStoreLocation = isStoreLocation
) )
Log.d(TAG, "Form validation successful, submitting address: $request") Log.d(TAG, "Form validation successful, submitting address: $request")

View File

@ -14,85 +14,6 @@ class ProvinceAdapter(
resource: Int = android.R.layout.simple_dropdown_item_1line resource: Int = android.R.layout.simple_dropdown_item_1line
) : ArrayAdapter<String>(context, resource, ArrayList()) { ) : ArrayAdapter<String>(context, resource, ArrayList()) {
// companion object {
// private const val TAG = "ProvinceAdapter"
// }
//
// // --- Static list of provinces ---------------------------------------------------------------
// private val provinces = listOf(
// ProvincesItem(1, "Aceh"),
// ProvincesItem(2, "Sumatera Utara"),
// ProvincesItem(3, "Sumatera Barat"),
// ProvincesItem(4, "Riau"),
// ProvincesItem(5, "Kepulauan Riau"),
// ProvincesItem(6, "Jambi"),
// ProvincesItem(7, "Sumatera Selatan"),
// ProvincesItem(8, "Kepulauan Bangka Belitung"),
// ProvincesItem(9, "Bengkulu"),
// ProvincesItem(10, "Lampung"),
// ProvincesItem(11, "DKI Jakarta"),
// ProvincesItem(12, "Jawa Barat"),
// ProvincesItem(13, "Banten"),
// ProvincesItem(14, "Jawa Tengah"),
// ProvincesItem(15, "Daerah Istimewa Yogyakarta"),
// ProvincesItem(16, "Jawa Timur"),
// ProvincesItem(17, "Bali"),
// ProvincesItem(18, "Nusa Tenggara Barat"),
// ProvincesItem(19, "Nusa Tenggara Timur"),
// ProvincesItem(20, "Kalimantan Barat"),
// ProvincesItem(21, "Kalimantan Tengah"),
// ProvincesItem(22, "Kalimantan Selatan"),
// ProvincesItem(23, "Kalimantan Timur"),
// ProvincesItem(24, "Kalimantan Utara"),
// ProvincesItem(25, "Sulawesi Utara"),
// ProvincesItem(26, "Gorontalo"),
// ProvincesItem(27, "Sulawesi Tengah"),
// ProvincesItem(28, "Sulawesi Barat"),
// ProvincesItem(29, "Sulawesi Selatan"),
// ProvincesItem(30, "Sulawesi Tenggara"),
// ProvincesItem(31, "Maluku"),
// ProvincesItem(32, "Maluku Utara"),
// ProvincesItem(33, "Papua Barat"),
// ProvincesItem(34, "Papua"),
// ProvincesItem(35, "Papua Tengah"),
// ProvincesItem(36, "Papua Pegunungan"),
// ProvincesItem(37, "Papua Selatan"),
// ProvincesItem(38, "Papua Barat Daya")
// )
//
// // --- Init block -----------------------------------------------------------------------------
// init {
// addAll(getProvinceNames()) // prepopulate adapter
// Log.d(TAG, "Adapter created with ${count} provinces")
// }
//
// // --- Public helper functions ----------------------------------------------------------------
// fun updateData(newProvinces: List<ProvincesItem>) {
// // If you actually want to replace the list, comment this line
// // provinces = newProvinces // (make `provinces` var instead of val)
//
// clear()
// addAll(newProvinces.map { it.province })
// notifyDataSetChanged()
//
// Log.d(TAG, "updateData(): updated with ${newProvinces.size} provinces")
// }
//
// fun getProvinceId(position: Int): Int? {
// val id = provinces.getOrNull(position)?.provinceId
// Log.d(TAG, "getProvinceId(): position=$position, id=$id")
// return id
// }
//
// fun getProvinceItem(position: Int): ProvincesItem? {
// val item = provinces.getOrNull(position)
// Log.d(TAG, "getProvinceItem(): position=$position, item=$item")
// return item
// }
//
// // --- Private helpers ------------------------------------------------------------------------
// private fun getProvinceNames(): List<String> = provinces.map { it.province }
//call from endpoint //call from endpoint
private val provinces = ArrayList<ProvincesItem>() private val provinces = ArrayList<ProvincesItem>()

View File

@ -21,7 +21,6 @@ import com.alya.ecommerce_serang.ui.profile.mystore.chat.ChatListStoreActivity
import com.alya.ecommerce_serang.ui.profile.mystore.product.ProductActivity import com.alya.ecommerce_serang.ui.profile.mystore.product.ProductActivity
import com.alya.ecommerce_serang.ui.profile.mystore.profile.DetailStoreProfileActivity import com.alya.ecommerce_serang.ui.profile.mystore.profile.DetailStoreProfileActivity
import com.alya.ecommerce_serang.ui.profile.mystore.review.ReviewActivity import com.alya.ecommerce_serang.ui.profile.mystore.review.ReviewActivity
import com.alya.ecommerce_serang.ui.profile.mystore.review.ReviewFragment
import com.alya.ecommerce_serang.ui.profile.mystore.sells.SellsActivity import com.alya.ecommerce_serang.ui.profile.mystore.sells.SellsActivity
import com.alya.ecommerce_serang.utils.BaseViewModelFactory import com.alya.ecommerce_serang.utils.BaseViewModelFactory
import com.alya.ecommerce_serang.utils.SessionManager import com.alya.ecommerce_serang.utils.SessionManager
@ -52,14 +51,16 @@ class MyStoreActivity : AppCompatActivity() {
enableEdgeToEdge() enableEdgeToEdge()
binding.header.headerTitle.text = "Toko Saya"
binding.header.headerLeftIcon.setOnClickListener { binding.headerMyStore.headerTitle.text = "Toko Saya"
binding.headerMyStore.headerLeftIcon.setOnClickListener {
onBackPressed() onBackPressed()
finish() finish()
} }
viewModel.loadMyStore() viewModel.loadMyStore()
viewModel.loadMyStoreProducts()
viewModel.myStoreProfile.observe(this){ user -> viewModel.myStoreProfile.observe(this){ user ->
user?.let { myStoreProfileOverview(it) } user?.let { myStoreProfileOverview(it) }
@ -68,9 +69,9 @@ class MyStoreActivity : AppCompatActivity() {
viewModel.errorMessage.observe(this) { error -> viewModel.errorMessage.observe(this) { error ->
Toast.makeText(this, error, Toast.LENGTH_SHORT).show() Toast.makeText(this, error, Toast.LENGTH_SHORT).show()
} }
setUpClickListeners() setUpClickListeners()
getCountOrder() getCountOrder()
observeViewModel()
viewModel.fetchBalance() viewModel.fetchBalance()
fetchBalance() fetchBalance()
} }
@ -170,13 +171,11 @@ class MyStoreActivity : AppCompatActivity() {
when (result) { when (result) {
is com.alya.ecommerce_serang.data.repository.Result.Loading -> is com.alya.ecommerce_serang.data.repository.Result.Loading ->
null null
// binding.progressBar.isVisible = true
is com.alya.ecommerce_serang.data.repository.Result.Success -> is com.alya.ecommerce_serang.data.repository.Result.Success ->
viewModel.formattedBalance.observe(this) { viewModel.formattedBalance.observe(this) {
binding.tvBalance.text = it binding.tvBalance.text = it
} }
is Result.Error -> { is Result.Error -> {
// binding.progressBar.isVisible = false
Log.e( Log.e(
"MyStoreActivity", "MyStoreActivity",
"Gagal memuat saldo: ${result.exception.localizedMessage}" "Gagal memuat saldo: ${result.exception.localizedMessage}"
@ -186,15 +185,29 @@ class MyStoreActivity : AppCompatActivity() {
} }
} }
private fun observeViewModel() {
viewModel.productList.observe(this) { result ->
when (result) {
is Result.Loading -> {
null
}
is Result.Success -> {
val productList = result.data
val count = productList.size
Log.d("MyStoreActivty", "You have $count products")
// Example: update UI
binding.tvNumProduct.text = "$count produk"
}
is Result.Error -> {
Log.e("MyStoreActivity", "Failed load product : ${result.exception.message}" )
}
}
}
}
companion object { companion object {
private const val PROFILE_REQUEST_CODE = 100 private const val PROFILE_REQUEST_CODE = 100
} }
// private fun navigateToSellsFragment(status: String) {
// val sellsFragment = SellsListFragment.newInstance(status)
// supportFragmentManager.beginTransaction()
// .replace(android.R.id.content, sellsFragment)
// .addToBackStack(null)
// .commit()
// }
} }

View File

@ -25,6 +25,7 @@ import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsAnimationCompat import androidx.core.view.WindowInsetsAnimationCompat
import androidx.core.view.WindowInsetsCompat import androidx.core.view.WindowInsetsCompat
import androidx.lifecycle.Observer import androidx.lifecycle.Observer
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import com.alya.ecommerce_serang.BuildConfig.BASE_URL import com.alya.ecommerce_serang.BuildConfig.BASE_URL
import com.alya.ecommerce_serang.R import com.alya.ecommerce_serang.R
@ -373,7 +374,8 @@ class ChatStoreActivity : AppCompatActivity() {
} }
}) })
viewModel.state.observe(this, Observer { state -> lifecycleScope.launchWhenStarted {
viewModel.state.collect { state ->
Log.d(TAG, "State updated - Messages: ${state.messages.size}") Log.d(TAG, "State updated - Messages: ${state.messages.size}")
// Update messages // Update messages
@ -434,7 +436,8 @@ class ChatStoreActivity : AppCompatActivity() {
Toast.makeText(this@ChatStoreActivity, error, Toast.LENGTH_SHORT).show() Toast.makeText(this@ChatStoreActivity, error, Toast.LENGTH_SHORT).show()
viewModel.clearError() viewModel.clearError()
} }
}) }
}
} }
private fun showOptionsMenu() { private fun showOptionsMenu() {

View File

@ -12,30 +12,29 @@ import android.view.View
import android.widget.ArrayAdapter import android.widget.ArrayAdapter
import android.widget.Toast import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import com.alya.ecommerce_serang.R
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.net.toUri
import androidx.core.widget.doAfterTextChanged
import com.alya.ecommerce_serang.BuildConfig.BASE_URL
import com.alya.ecommerce_serang.R
import com.alya.ecommerce_serang.data.api.dto.CategoryItem import com.alya.ecommerce_serang.data.api.dto.CategoryItem
import com.alya.ecommerce_serang.data.api.dto.Preorder import com.alya.ecommerce_serang.data.api.dto.Preorder
import com.alya.ecommerce_serang.data.api.dto.Wholesale
import com.alya.ecommerce_serang.data.api.retrofit.ApiConfig import com.alya.ecommerce_serang.data.api.retrofit.ApiConfig
import com.alya.ecommerce_serang.data.repository.ProductRepository import com.alya.ecommerce_serang.data.repository.ProductRepository
import com.alya.ecommerce_serang.data.repository.Result import com.alya.ecommerce_serang.data.repository.Result
import com.alya.ecommerce_serang.databinding.ActivityDetailStoreProductBinding import com.alya.ecommerce_serang.databinding.ActivityDetailStoreProductBinding
import com.alya.ecommerce_serang.utils.viewmodel.ProductViewModel
import com.alya.ecommerce_serang.utils.BaseViewModelFactory import com.alya.ecommerce_serang.utils.BaseViewModelFactory
import com.alya.ecommerce_serang.utils.SessionManager import com.alya.ecommerce_serang.utils.SessionManager
import com.alya.ecommerce_serang.utils.viewmodel.ProductViewModel
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
import okhttp3.MediaType.Companion.toMediaTypeOrNull import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.MultipartBody import okhttp3.MultipartBody
import okhttp3.RequestBody import okhttp3.RequestBody
import java.io.File import java.io.File
import java.io.FileOutputStream import java.io.FileOutputStream
import kotlin.getValue
import androidx.core.net.toUri
import androidx.core.widget.doAfterTextChanged
import com.alya.ecommerce_serang.BuildConfig.BASE_URL
import com.alya.ecommerce_serang.data.api.dto.Wholesale
class DetailStoreProductActivity : AppCompatActivity() { class DetailStoreProductActivity : AppCompatActivity() {
@ -93,7 +92,7 @@ class DetailStoreProductActivity : AppCompatActivity() {
val isEditing = intent.getBooleanExtra("is_editing", false) val isEditing = intent.getBooleanExtra("is_editing", false)
productId = intent.getIntExtra("product_id", -1) productId = intent.getIntExtra("product_id", -1)
binding.header.headerTitle.text = if (isEditing) "Ubah Produk" else "Tambah Produk" binding.headerStoreProduct.headerTitle.text = if (isEditing) "Ubah Produk" else "Tambah Produk"
if (isEditing && productId != null && productId != -1) { if (isEditing && productId != null && productId != -1) {
viewModel.loadProductDetail(productId!!) viewModel.loadProductDetail(productId!!)
@ -140,7 +139,7 @@ class DetailStoreProductActivity : AppCompatActivity() {
} }
} }
binding.header.headerLeftIcon.setOnClickListener { binding.headerStoreProduct.headerLeftIcon.setOnClickListener {
onBackPressedDispatcher.onBackPressed() onBackPressedDispatcher.onBackPressed()
} }
} }

View File

@ -11,9 +11,9 @@ import com.alya.ecommerce_serang.data.api.retrofit.ApiConfig
import com.alya.ecommerce_serang.data.repository.ProductRepository import com.alya.ecommerce_serang.data.repository.ProductRepository
import com.alya.ecommerce_serang.data.repository.Result import com.alya.ecommerce_serang.data.repository.Result
import com.alya.ecommerce_serang.databinding.ActivityProductBinding import com.alya.ecommerce_serang.databinding.ActivityProductBinding
import com.alya.ecommerce_serang.utils.viewmodel.ProductViewModel
import com.alya.ecommerce_serang.utils.BaseViewModelFactory import com.alya.ecommerce_serang.utils.BaseViewModelFactory
import com.alya.ecommerce_serang.utils.SessionManager import com.alya.ecommerce_serang.utils.SessionManager
import com.alya.ecommerce_serang.utils.viewmodel.ProductViewModel
class ProductActivity : AppCompatActivity() { class ProductActivity : AppCompatActivity() {
@ -94,14 +94,14 @@ class ProductActivity : AppCompatActivity() {
} }
private fun setupHeader() { private fun setupHeader() {
binding.header.headerTitle.text = "Produk Saya" binding.headerListProduct.headerTitle.text = "Produk Saya"
binding.header.headerRightText.visibility = View.VISIBLE binding.headerListProduct.headerRightText.visibility = View.VISIBLE
binding.header.headerLeftIcon.setOnClickListener { binding.headerListProduct.headerLeftIcon.setOnClickListener {
onBackPressedDispatcher.onBackPressed() onBackPressedDispatcher.onBackPressed()
} }
binding.header.headerRightText.setOnClickListener { binding.headerListProduct.headerRightText.setOnClickListener {
val intent = Intent(this, DetailStoreProductActivity::class.java) val intent = Intent(this, DetailStoreProductActivity::class.java)
intent.putExtra("is_editing", false) intent.putExtra("is_editing", false)
startActivity(intent) startActivity(intent)
@ -111,4 +111,6 @@ class ProductActivity : AppCompatActivity() {
private fun setupRecyclerView() { private fun setupRecyclerView() {
binding.rvStoreProduct.layoutManager = LinearLayoutManager(this) binding.rvStoreProduct.layoutManager = LinearLayoutManager(this)
} }
} }

View File

@ -6,6 +6,7 @@ import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.map import androidx.lifecycle.map
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.alya.ecommerce_serang.data.api.dto.ProductsItem
import com.alya.ecommerce_serang.data.api.dto.Store import com.alya.ecommerce_serang.data.api.dto.Store
import com.alya.ecommerce_serang.data.api.response.auth.StoreTypesItem import com.alya.ecommerce_serang.data.api.response.auth.StoreTypesItem
import com.alya.ecommerce_serang.data.api.response.store.StoreResponse import com.alya.ecommerce_serang.data.api.response.store.StoreResponse
@ -38,6 +39,9 @@ class MyStoreViewModel(private val repository: MyStoreRepository): ViewModel() {
private val _balanceResult = MutableLiveData<Result<StoreResponse>>() private val _balanceResult = MutableLiveData<Result<StoreResponse>>()
val balanceResult: LiveData<Result<StoreResponse>> get() = _balanceResult val balanceResult: LiveData<Result<StoreResponse>> get() = _balanceResult
private val _productList = MutableLiveData<Result<List<ProductsItem>>>()
val productList: LiveData<Result<List<ProductsItem>>> get() = _productList
fun loadMyStore(){ fun loadMyStore(){
viewModelScope.launch { viewModelScope.launch {
when (val result = repository.fetchMyStoreProfile()){ when (val result = repository.fetchMyStoreProfile()){
@ -158,6 +162,18 @@ class MyStoreViewModel(private val repository: MyStoreRepository): ViewModel() {
} }
} }
fun loadMyStoreProducts() {
viewModelScope.launch {
_productList.value = Result.Loading
try {
val result = repository.fetchMyStoreProducts()
_productList.value = Result.Success(result)
} catch (e: Exception) {
_productList.value = Result.Error(e)
}
}
}
private fun String.toRequestBody(): RequestBody = private fun String.toRequestBody(): RequestBody =
RequestBody.create("text/plain".toMediaTypeOrNull(), this) RequestBody.create("text/plain".toMediaTypeOrNull(), this)
} }

View File

@ -122,12 +122,14 @@
android:src="@drawable/outline_shopping_cart_24" /> android:src="@drawable/outline_shopping_cart_24" />
<TextView <TextView
android:id="@+id/emptyCart"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="16dp" android:layout_marginTop="16dp"
android:text="Keranjang Anda kosong" android:visibility="gone"
android:text="Keranjang anda kosong"
android:textColor="@android:color/black" android:textColor="@android:color/black"
android:textSize="18sp" /> android:textSize="16sp" />
<TextView <TextView
android:layout_width="wrap_content" android:layout_width="wrap_content"

View File

@ -10,7 +10,7 @@
tools:context=".ui.profile.mystore.product.DetailStoreProductActivity"> tools:context=".ui.profile.mystore.product.DetailStoreProductActivity">
<include <include
android:id="@+id/header" android:id="@+id/headerStoreProduct"
layout="@layout/header" /> layout="@layout/header" />
<ScrollView <ScrollView

View File

@ -10,7 +10,7 @@
tools:context=".ui.profile.mystore.MyStoreActivity"> tools:context=".ui.profile.mystore.MyStoreActivity">
<include <include
android:id="@+id/header" android:id="@+id/headerMyStore"
layout="@layout/header" /> layout="@layout/header" />
<ScrollView <ScrollView
@ -422,6 +422,7 @@
android:background="@color/black_50"/> android:background="@color/black_50"/>
<androidx.constraintlayout.widget.ConstraintLayout <androidx.constraintlayout.widget.ConstraintLayout
android:visibility="gone"
android:id="@+id/layout_help" android:id="@+id/layout_help"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"

View File

@ -10,7 +10,7 @@
android:orientation="vertical"> android:orientation="vertical">
<include <include
android:id="@+id/header" android:id="@+id/headerListProduct"
layout="@layout/header" /> layout="@layout/header" />
<!-- Search Bar --> <!-- Search Bar -->

View File

@ -48,8 +48,8 @@
android:layout_marginTop="16dp" android:layout_marginTop="16dp"
android:gravity="center" android:gravity="center"
android:visibility="gone" android:visibility="gone"
android:text="Keranjang Anda kosong" android:text="Pesan anda kosong"
android:textColor="@android:color/black" android:textColor="@android:color/black"
android:textSize="18sp" /> android:textSize="16sp" />
</LinearLayout> </LinearLayout>