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:authorities="${applicationId}.androidx-startup" -->
<!-- tools:node="remove" /> -->
<service
android:name=".ui.notif.SimpleWebSocketService"
android:enabled="true"
android:exported="false"
android:foregroundServiceType="dataSync" />
<!-- <service-->
<!-- android:name=".ui.notif.SimpleWebSocketService"-->
<!-- android:enabled="true"-->
<!-- android:exported="false"-->
<!-- android:foregroundServiceType="dataSync" />-->
<activity
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
data class CreateAddressRequest (
data class CreateAddressRequest(
@SerializedName("userId")
val userId: Int,
@SerializedName("latitude")
val lat: Double? = null,
val lat: Double,
@SerializedName("longitude")
val long: Double? = null,
val long: Double,
@SerializedName("street")
val street: String,
@ -20,22 +23,22 @@ data class CreateAddressRequest (
@SerializedName("province_id")
val provId: Int,
@SerializedName("postal_code")
val postCode: String,
@SerializedName("detail")
val detailAddress: String? = null,
@SerializedName("village_id")
val idVillage: String?, // nullable for now
@SerializedName("user_id")
val userId: Int,
@SerializedName("detail")
val detailAddress: String,
@SerializedName("is_store_location")
val isStoreLocation: Boolean,
@SerializedName("recipient")
val recipient: String,
@SerializedName("phone")
val phone: String,
@SerializedName("is_store_location")
val isStoreLocation: Boolean
val phone: String
)

View File

@ -98,5 +98,5 @@ data class Store(
val storeDescription: String,
@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(
@field:SerializedName("addresses")
@field:SerializedName("addresses")
val addresses: List<AddressesItem>,
@field:SerializedName("message")
@field:SerializedName("message")
val message: String
)
data class AddressesItem(
@field:SerializedName("village_id")
val villageId: String,
@field:SerializedName("is_store_location")
val isStoreLocation: Boolean,
@ -23,7 +26,7 @@ data class AddressesItem(
val userId: Int,
@field:SerializedName("province_id")
val provinceId: Int,
val provinceId: String,
@field:SerializedName("phone")
val phone: String,
@ -50,5 +53,5 @@ data class AddressesItem(
val longitude: String,
@field:SerializedName("city_id")
val cityId: Int
val cityId: String
)

View File

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

View File

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

View File

@ -27,6 +27,7 @@ import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsAnimationCompat
import androidx.core.view.WindowInsetsCompat
import androidx.lifecycle.Observer
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
import com.alya.ecommerce_serang.BuildConfig.BASE_URL
import com.alya.ecommerce_serang.R
@ -170,8 +171,7 @@ class ChatActivity : AppCompatActivity() {
// If opened from ChatListFragment with a valid chatRoomId
if (chatRoomId > 0) {
// Directly set the chatRoomId and load chat history
viewModel._chatRoomId.value = chatRoomId
viewModel.setChatRoomId(chatRoomId)
}
}
@ -405,68 +405,71 @@ class ChatActivity : AppCompatActivity() {
}
})
viewModel.state.observe(this, Observer { state ->
Log.d(TAG, "State updated - Messages: ${state.messages.size}")
lifecycleScope.launchWhenStarted {
viewModel.state.collect() { state ->
Log.d(TAG, "State updated - Messages: ${state.messages.size}")
// Update messages
val previousCount = chatAdapter.itemCount
// Update messages
val previousCount = chatAdapter.itemCount
val displayItems = viewModel.getDisplayItems()
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) {
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
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) {
scrollToBottomInstant()
}
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)
// 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()) {
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
} else {
binding.productContainer.visibility = View.GONE
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()
}
}
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) {
@ -492,7 +495,7 @@ class ChatActivity : AppCompatActivity() {
Toast.makeText(this, "Opening: ${productInfo.productName}", Toast.LENGTH_SHORT).show()
// You can navigate to product detail here
navigateToProductDetail(productInfo.productId)
navigateToProductDetail(productInfo.productId)
}
private fun navigateToProductDetail(productId: Int) {

View File

@ -209,7 +209,7 @@ class ChatAdapter(
binding.tvProductPrice.text = product.productPrice
// Load product image
val fullImageUrl = if (product.productImage.startsWith("/")) {
val fullImageUrl = if (product.productImage!!.startsWith("/")) {
BASE_URL + product.productImage.substring(1)
} else {
product.productImage
@ -246,7 +246,7 @@ class ChatAdapter(
binding.tvProductPrice.text = product.productPrice
// Load product image
val fullImageUrl = if (product.productImage.startsWith("/")) {
val fullImageUrl = if (product.productImage!!.startsWith("/")) {
BASE_URL + product.productImage.substring(1)
} else {
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.SessionManager
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 java.io.File
import java.text.SimpleDateFormat
@ -56,9 +59,9 @@ class ChatViewModel @Inject constructor(
// Product attachment flag
private var shouldAttachProduct = false
// UI state using LiveData
private val _state = MutableLiveData(ChatUiState())
val state: LiveData<ChatUiState> = _state
// use state for more seamless responsive
private val _state = MutableStateFlow(ChatUiState())
val state: StateFlow<ChatUiState> = _state
private val _isLoading = MutableLiveData<Boolean>()
val isLoading: LiveData<Boolean> = _isLoading
@ -93,6 +96,8 @@ class ChatViewModel @Inject constructor(
init {
Log.d(TAG, "ChatViewModel initialized")
socketService.connect() // 🛠 force connection
setupSocketListeners() // 🛠 always listen, even before user data
initializeUser()
}
@ -113,6 +118,7 @@ class ChatViewModel @Inject constructor(
updateState { it.copy(error = "User authentication error. Please login again.") }
} else {
Log.d(TAG, "Setting up socket listeners...")
socketService.connect()
setupSocketListeners()
}
}
@ -231,26 +237,116 @@ class ChatViewModel @Inject constructor(
if (connectionState is ConnectionState.Connected) {
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 {
socketService.newMessages.collect { chatLine ->
chatLine?.let {
Log.d(TAG, "New message received via socket - ID: ${it.id}, SenderID: ${it.senderId}")
val currentMessages = _state.value?.messages ?: listOf()
val updatedMessages = currentMessages.toMutableList().apply {
add(convertChatLineToUiMessage(it))
Log.d("ChatViewModel", "Collected new message from SocketIOService: ${chatLine.message}")
chatLine?.let { incomingChatLine ->
// 1. First update: Add the message to the list (potentially without full product info)
_state.update { currentState ->
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) {
Log.d(TAG, "Marking message as read: ${it.id}")
updateMessageStatus(it.id, Constants.STATUS_READ)
// 2. If it's a product message and needs details, fetch them
if (incomingChatLine.productId != 0) { // Check if it's a product message
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) {
Log.e(TAG, "Cannot join room: Invalid room ID")
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) {
@ -728,7 +824,7 @@ class ChatViewModel @Inject constructor(
}
}
//update message status
//update message status
fun updateMessageStatus(messageId: Int, status: String) {
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?) {
selectedImageFile = file
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 {
val formattedTime = formatTimestamp(chatItem.createdAt)
@ -932,7 +1028,7 @@ class ChatViewModel @Inject constructor(
}
}
//format price
//format price
private fun formatPrice(price: String): String {
return if (price.startsWith("Rp")) price else "Rp$price"
}
@ -958,9 +1054,7 @@ class ChatViewModel @Inject constructor(
// helper function to update live data
private fun updateState(update: (ChatUiState) -> ChatUiState) {
_state.value?.let {
_state.value = update(it)
}
_state.value = update(_state.value)
}
//clear any error messages
@ -1053,6 +1147,73 @@ class ChatViewModel @Inject constructor(
private fun isThisYear(messageCalendar: Calendar, today: Calendar): Boolean {
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 {
@ -1062,12 +1223,12 @@ enum class MessageType {
}
data class ProductInfo(
val productId: Int,
val productName: String,
val productPrice: String,
val productImage: String,
val productRating: Float,
val storeName: String
val productId: Int, // Keep productId here
val productName: String? = null, // Make nullable
val productPrice: String? = null, // Make nullable
val productImage: String? = null, // Make nullable
val productRating: Float = 0f, // Default value
val storeName: String? = null
)
// representing chat messages to UI
@ -1083,8 +1244,6 @@ data class ChatUiMessage(
val createdAt: String
)
// representing UI state to screen
data class ChatUiState(
val messages: List<ChatUiMessage> = emptyList(),
@ -1102,4 +1261,8 @@ data class ChatUiState(
val productImageUrl: String = "",
val productRating: Float = 0f,
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 io.socket.client.IO
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.SharedFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import org.json.JSONObject
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 serviceScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
private val TAG = "SocketIOService"
// Socket.IO client
@ -30,8 +40,8 @@ class SocketIOService(
private val _connectionState = MutableStateFlow<ConnectionState>(ConnectionState.Disconnected())
val connectionState: StateFlow<ConnectionState> = _connectionState
private val _newMessages = MutableStateFlow<ChatLine?>(null)
val newMessages: StateFlow<ChatLine?> = _newMessages
private val _newMessages = MutableSharedFlow<ChatLine>(extraBufferCapacity = 1) // Using extraBufferCapacity for a non-suspending emit
val newMessages: SharedFlow<ChatLine> = _newMessages
private val _typingStatus = MutableStateFlow<TypingStatus?>(null)
val typingStatus: StateFlow<TypingStatus?> = _typingStatus
@ -85,63 +95,95 @@ class SocketIOService(
* Sets up Socket.IO event listeners
*/
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) {
Log.d(TAG, "Socket.IO disconnected")
isConnected = false
_connectionState.value = ConnectionState.Disconnected("Disconnected from server")
_connectionStateLiveData.postValue(ConnectionState.Disconnected("Disconnected from server"))
}
socket?.on(Constants.EVENT_NEW_MESSAGE) { args -> // Use the event name your server emits
Log.d(TAG, "Raw event received on ${Constants.EVENT_NEW_MESSAGE}: ${args.firstOrNull()}") // Check raw args
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 ->
if (args.isNotEmpty()) {
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)
val messageJson = args[0].toString()
val chatLine = Gson().fromJson(messageJson, ChatLine::class.java)
Log.d(TAG, "Successfully parsed ChatLine: ${chatLine.message}")
Log.d(TAG, "Emitting new message to _newMessages SharedFlow...") // New log
// 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) {
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)
Log.e(TAG, "Error parsing or emitting new message: ${e.message}", 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
*/
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) {
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", userId)
Log.d(TAG, "Joined room for user: $userId")
socket?.emit("joinRoom", roomId)
Log.d(TAG, "Joined room ID: $roomId")
}
/**

View File

@ -333,18 +333,19 @@ class AddAddressActivity : AppCompatActivity() {
// Create request with all fields
val request = CreateAddressRequest(
userId = userId,
lat = latitude!!, // Safe to use !! as we've checked above
long = longitude!!,
street = street,
subDistrict = subDistrict,
cityId = cityId,
cityId = cityId, // ⚠️ Make sure this is Int
provId = provinceId,
postCode = postalCode,
idVillage = "", // Or provide a real ID if needed
detailAddress = street,
userId = userId,
isStoreLocation = false,
recipient = recipient,
phone = phone,
isStoreLocation = isStoreLocation
phone = phone
)
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
) : 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
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.profile.DetailStoreProfileActivity
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.utils.BaseViewModelFactory
import com.alya.ecommerce_serang.utils.SessionManager
@ -52,14 +51,16 @@ class MyStoreActivity : AppCompatActivity() {
enableEdgeToEdge()
binding.header.headerTitle.text = "Toko Saya"
binding.header.headerLeftIcon.setOnClickListener {
binding.headerMyStore.headerTitle.text = "Toko Saya"
binding.headerMyStore.headerLeftIcon.setOnClickListener {
onBackPressed()
finish()
}
viewModel.loadMyStore()
viewModel.loadMyStoreProducts()
viewModel.myStoreProfile.observe(this){ user ->
user?.let { myStoreProfileOverview(it) }
@ -68,9 +69,9 @@ class MyStoreActivity : AppCompatActivity() {
viewModel.errorMessage.observe(this) { error ->
Toast.makeText(this, error, Toast.LENGTH_SHORT).show()
}
setUpClickListeners()
getCountOrder()
observeViewModel()
viewModel.fetchBalance()
fetchBalance()
}
@ -170,13 +171,11 @@ class MyStoreActivity : AppCompatActivity() {
when (result) {
is com.alya.ecommerce_serang.data.repository.Result.Loading ->
null
// binding.progressBar.isVisible = true
is com.alya.ecommerce_serang.data.repository.Result.Success ->
viewModel.formattedBalance.observe(this) {
binding.tvBalance.text = it
}
is Result.Error -> {
// binding.progressBar.isVisible = false
Log.e(
"MyStoreActivity",
"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 {
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.WindowInsetsCompat
import androidx.lifecycle.Observer
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
import com.alya.ecommerce_serang.BuildConfig.BASE_URL
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}")
// Update messages
@ -434,7 +436,8 @@ class ChatStoreActivity : AppCompatActivity() {
Toast.makeText(this@ChatStoreActivity, error, Toast.LENGTH_SHORT).show()
viewModel.clearError()
}
})
}
}
}
private fun showOptionsMenu() {

View File

@ -12,30 +12,29 @@ import android.view.View
import android.widget.ArrayAdapter
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import com.alya.ecommerce_serang.R
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
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.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.repository.ProductRepository
import com.alya.ecommerce_serang.data.repository.Result
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.SessionManager
import com.alya.ecommerce_serang.utils.viewmodel.ProductViewModel
import com.bumptech.glide.Glide
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.MultipartBody
import okhttp3.RequestBody
import java.io.File
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() {
@ -93,7 +92,7 @@ class DetailStoreProductActivity : AppCompatActivity() {
val isEditing = intent.getBooleanExtra("is_editing", false)
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) {
viewModel.loadProductDetail(productId!!)
@ -140,7 +139,7 @@ class DetailStoreProductActivity : AppCompatActivity() {
}
}
binding.header.headerLeftIcon.setOnClickListener {
binding.headerStoreProduct.headerLeftIcon.setOnClickListener {
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.Result
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.SessionManager
import com.alya.ecommerce_serang.utils.viewmodel.ProductViewModel
class ProductActivity : AppCompatActivity() {
@ -94,14 +94,14 @@ class ProductActivity : AppCompatActivity() {
}
private fun setupHeader() {
binding.header.headerTitle.text = "Produk Saya"
binding.header.headerRightText.visibility = View.VISIBLE
binding.headerListProduct.headerTitle.text = "Produk Saya"
binding.headerListProduct.headerRightText.visibility = View.VISIBLE
binding.header.headerLeftIcon.setOnClickListener {
binding.headerListProduct.headerLeftIcon.setOnClickListener {
onBackPressedDispatcher.onBackPressed()
}
binding.header.headerRightText.setOnClickListener {
binding.headerListProduct.headerRightText.setOnClickListener {
val intent = Intent(this, DetailStoreProductActivity::class.java)
intent.putExtra("is_editing", false)
startActivity(intent)
@ -111,4 +111,6 @@ class ProductActivity : AppCompatActivity() {
private fun setupRecyclerView() {
binding.rvStoreProduct.layoutManager = LinearLayoutManager(this)
}
}

View File

@ -6,6 +6,7 @@ import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.map
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.response.auth.StoreTypesItem
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>>()
val balanceResult: LiveData<Result<StoreResponse>> get() = _balanceResult
private val _productList = MutableLiveData<Result<List<ProductsItem>>>()
val productList: LiveData<Result<List<ProductsItem>>> get() = _productList
fun loadMyStore(){
viewModelScope.launch {
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 =
RequestBody.create("text/plain".toMediaTypeOrNull(), this)
}

View File

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

View File

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

View File

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

View File

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

View File

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