Compare commits

11 Commits

35 changed files with 677 additions and 329 deletions

13
.idea/deviceManager.xml generated Normal file
View File

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DeviceTable">
<option name="columnSorters">
<list>
<ColumnSorterState>
<option name="column" value="Name" />
<option name="order" value="ASCENDING" />
</ColumnSorterState>
</list>
</option>
</component>
</project>

View File

@ -124,7 +124,4 @@ dependencies {
implementation(platform("com.google.firebase:firebase-bom:33.13.0")) implementation(platform("com.google.firebase:firebase-bom:33.13.0"))
implementation("com.google.firebase:firebase-analytics") implementation("com.google.firebase:firebase-analytics")
implementation("com.google.firebase:firebase-messaging-ktx") implementation("com.google.firebase:firebase-messaging-ktx")
} }

View File

@ -29,6 +29,9 @@
android:theme="@style/Theme.Ecommerce_serang" android:theme="@style/Theme.Ecommerce_serang"
android:usesCleartextTraffic="true" android:usesCleartextTraffic="true"
tools:targetApi="31"> tools:targetApi="31">
<activity
android:name=".ui.profile.mystore.StoreSuspendedActivity"
android:exported="false" />
<activity <activity
android:name=".ui.profile.mystore.StoreOnReviewActivity" android:name=".ui.profile.mystore.StoreOnReviewActivity"
android:exported="false" /> android:exported="false" />
@ -82,12 +85,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"
android:exported="false" android:exported="false"

View File

@ -3,11 +3,14 @@ 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

@ -13,6 +13,9 @@ data class AddressResponse(
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
@ -249,7 +250,7 @@ class RegisterStep3Fragment : Fragment() {
binding.autoCompleteDesa.setOnItemClickListener{ _, _, position, _ -> binding.autoCompleteDesa.setOnItemClickListener{ _, _, position, _ ->
val villageId = villagesAdapter.getVillageId(position) val villageId = villagesAdapter.getVillageId(position)
val postalCode = villagesAdapter.getPostalCode(position) // val postalCode = villagesAdapter.getPostalCode(position)
Log.d(TAG, "Village selected at position $position, ID: $villageId") Log.d(TAG, "Village selected at position $position, ID: $villageId")
villageId?.let { id -> villageId?.let { id ->
@ -257,11 +258,11 @@ class RegisterStep3Fragment : Fragment() {
registerViewModel.selectedVillages = id registerViewModel.selectedVillages = id
} }
postalCode?.let { postCode -> // postalCode?.let { postCode ->
registerViewModel.selectedPostalCode = postCode // registerViewModel.selectedPostalCode = postCode
} // }
binding.etKodePos.setText(registerViewModel.selectedPostalCode ?: "") // binding.etKodePos.setText(registerViewModel.selectedPostalCode ?: "")
} }
} }
@ -370,11 +371,14 @@ class RegisterStep3Fragment : Fragment() {
val street = binding.etDetailAlamat.text.toString().trim() val street = binding.etDetailAlamat.text.toString().trim()
val recipient = binding.etNamaPenerima.text.toString().trim() val recipient = binding.etNamaPenerima.text.toString().trim()
val phone = binding.etNomorHp.text.toString().trim() val phone = binding.etNomorHp.text.toString().trim()
val postalCode = binding.etKodePos.text.toString().trim()
val provinceId = registerViewModel.selectedProvinceId?.toInt() ?: 0 val provinceId = registerViewModel.selectedProvinceId?.toInt() ?: 0
val cityId = registerViewModel.selectedCityId.toString() val cityId = registerViewModel.selectedCityId.toString()
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")
@ -383,21 +387,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,7 +405,8 @@ class ChatActivity : 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,6 +435,7 @@ class ChatActivity : AppCompatActivity() {
is String -> { is String -> {
if (img.startsWith("/")) BASE_URL + img.substring(1) else img if (img.startsWith("/")) BASE_URL + img.substring(1) else img
} }
else -> R.drawable.placeholder_image else -> R.drawable.placeholder_image
} }
@ -466,7 +468,8 @@ class ChatActivity : AppCompatActivity() {
Toast.makeText(this@ChatActivity, error, Toast.LENGTH_SHORT).show() Toast.makeText(this@ChatActivity, error, Toast.LENGTH_SHORT).show()
viewModel.clearError() viewModel.clearError()
} }
}) }
}
} }
private fun updateInputHint(state: ChatUiState) { private fun updateInputHint(state: ChatUiState) {

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") Log.d(TAG, "Joining socket room: $roomId")
socketService.joinRoom() socketService.joinRoom(roomId)
}
} }
fun sendTypingStatus(isTyping: Boolean) { fun sendTypingStatus(isTyping: Boolean) {
@ -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(),
@ -1103,3 +1262,7 @@ data class ChatUiState(
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()
Log.d(TAG, "Received new message: $messageJson")
val chatLine = Gson().fromJson(messageJson, ChatLine::class.java) val chatLine = Gson().fromJson(messageJson, ChatLine::class.java)
_newMessages.value = chatLine Log.d(TAG, "Successfully parsed ChatLine: ${chatLine.message}")
_newMessagesLiveData.postValue(chatLine) 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) { } catch (e: Exception) {
Log.e(TAG, "Error parsing new message 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_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)
}
} }
} }
// 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

@ -25,6 +25,7 @@ import com.alya.ecommerce_serang.ui.order.address.AddressActivity
import com.alya.ecommerce_serang.ui.order.history.HistoryActivity import com.alya.ecommerce_serang.ui.order.history.HistoryActivity
import com.alya.ecommerce_serang.ui.profile.mystore.MyStoreActivity import com.alya.ecommerce_serang.ui.profile.mystore.MyStoreActivity
import com.alya.ecommerce_serang.ui.profile.mystore.StoreOnReviewActivity import com.alya.ecommerce_serang.ui.profile.mystore.StoreOnReviewActivity
import com.alya.ecommerce_serang.ui.profile.mystore.StoreSuspendedActivity
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.MyStoreViewModel import com.alya.ecommerce_serang.utils.viewmodel.MyStoreViewModel
@ -73,13 +74,11 @@ class ProfileFragment : Fragment() {
observeUserProfile() observeUserProfile()
observeStoreStatus()
viewModel.loadUserProfile() viewModel.loadUserProfile()
viewModel.checkStoreUser() viewModel.checkStoreUser()
val hasStore = viewModel.checkStore.value
Log.d("Profile Fragment", "Check store $hasStore")
binding.tvBukaToko.text = if (hasStore == true) "Toko Saya" else "Buka Toko"
binding.cardBukaToko.setOnClickListener{ binding.cardBukaToko.setOnClickListener{
// if (hasStore == true) startActivity(Intent(requireContext(), MyStoreActivity::class.java)) // if (hasStore == true) startActivity(Intent(requireContext(), MyStoreActivity::class.java))
// else startActivity(Intent(requireContext(), RegisterStoreActivity::class.java)) // else startActivity(Intent(requireContext(), RegisterStoreActivity::class.java))
@ -88,8 +87,11 @@ class ProfileFragment : Fragment() {
myStoreViewModel.myStoreProfile.observe(viewLifecycleOwner) { store -> myStoreViewModel.myStoreProfile.observe(viewLifecycleOwner) { store ->
store?.let { store?.let {
when (store.storeStatus) { when (store.storeStatus) {
"process" -> startActivity(Intent(requireContext(), StoreOnReviewActivity::class.java))
"active" -> startActivity(Intent(requireContext(), MyStoreActivity::class.java)) "active" -> startActivity(Intent(requireContext(), MyStoreActivity::class.java))
else -> startActivity(Intent(requireContext(), StoreOnReviewActivity::class.java)) "inactive" -> startActivity(Intent(requireContext(), MyStoreActivity::class.java))
"suspended" -> startActivity(Intent(requireContext(), StoreSuspendedActivity::class.java))
else -> startActivity(Intent(requireContext(), RegisterStoreActivity::class.java))
} }
} ?: run { } ?: run {
Toast.makeText(requireContext(), "Gagal memuat data toko", Toast.LENGTH_SHORT).show() Toast.makeText(requireContext(), "Gagal memuat data toko", Toast.LENGTH_SHORT).show()
@ -132,6 +134,12 @@ class ProfileFragment : Fragment() {
} }
} }
private fun observeStoreStatus() {
viewModel.checkStore.observe(viewLifecycleOwner) { hasStore ->
binding.tvBukaToko.text = if (hasStore) "Toko Saya" else "Buka Toko"
}
}
private fun updateUI(user: UserProfile) = with(binding){ private fun updateUI(user: UserProfile) = with(binding){
val fullImageUrl = when (val img = user.image) { val fullImageUrl = when (val img = user.image) {
is String -> { is String -> {

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

@ -0,0 +1,26 @@
package com.alya.ecommerce_serang.ui.profile.mystore
import android.os.Bundle
import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import com.alya.ecommerce_serang.R
import com.alya.ecommerce_serang.databinding.ActivityStoreSuspendedBinding
class StoreSuspendedActivity : AppCompatActivity() {
private lateinit var binding: ActivityStoreSuspendedBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityStoreSuspendedBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.header.headerTitle.text = "Toko Dinonaktifkan"
binding.header.headerLeftIcon.setOnClickListener {
onBackPressedDispatcher.onBackPressed()
finish()
}
}
}

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,36 +12,37 @@ 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.FileUtils.compressFile
import com.alya.ecommerce_serang.utils.ImageUtils.compressImage
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() {
private lateinit var binding: ActivityDetailStoreProductBinding private lateinit var binding: ActivityDetailStoreProductBinding
private lateinit var sessionManager: SessionManager private lateinit var sessionManager: SessionManager
private lateinit var categoryList: List<CategoryItem> private var categoryList: List<CategoryItem> = emptyList()
private var imageUri: Uri? = null private var imageUri: Uri? = null
private var sppirtUri: Uri? = null private var sppirtUri: Uri? = null
private var halalUri: Uri? = null private var halalUri: Uri? = null
@ -61,7 +62,10 @@ class DetailStoreProductActivity : AppCompatActivity() {
if (result.resultCode == Activity.RESULT_OK) { if (result.resultCode == Activity.RESULT_OK) {
imageUri = result.data?.data imageUri = result.data?.data
imageUri?.let { imageUri?.let {
binding.ivPreviewFoto.setImageURI(it) compressImage(this, it, "productimg").let { compressedImageFile ->
binding.ivPreviewFoto.setImageURI(Uri.fromFile(compressedImageFile))
imageUri = Uri.fromFile(compressedImageFile)
}
binding.switcherFotoProduk.showNext() binding.switcherFotoProduk.showNext()
hasImage = true hasImage = true
} }
@ -71,19 +75,23 @@ class DetailStoreProductActivity : AppCompatActivity() {
private val sppirtLauncher = registerForActivityResult(ActivityResultContracts.GetContent()) { uri -> private val sppirtLauncher = registerForActivityResult(ActivityResultContracts.GetContent()) { uri ->
if (uri != null && isValidFile(uri)) { if (uri != null && isValidFile(uri)) {
sppirtUri = uri compressFile(this, uri).let { compressedFile ->
binding.tvSppirtName.text = getFileName(uri) sppirtUri = compressedFile?.toUri()
binding.tvSppirtName.text = getFileName(sppirtUri!!)
binding.switcherSppirt.showNext() binding.switcherSppirt.showNext()
} }
} }
}
private val halalLauncher = registerForActivityResult(ActivityResultContracts.GetContent()) { uri -> private val halalLauncher = registerForActivityResult(ActivityResultContracts.GetContent()) { uri ->
if (uri != null && isValidFile(uri)) { if (uri != null && isValidFile(uri)) {
halalUri = uri compressFile(this, uri).let { compressedFile ->
binding.tvHalalName.text = getFileName(uri) halalUri = compressedFile?.toUri()
binding.tvHalalName.text = getFileName(halalUri!!)
binding.switcherHalal.showNext() binding.switcherHalal.showNext()
} }
} }
}
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@ -93,7 +101,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 +148,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

@ -8,13 +8,46 @@ import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.MultipartBody import okhttp3.MultipartBody
import okhttp3.RequestBody.Companion.asRequestBody import okhttp3.RequestBody.Companion.asRequestBody
import java.io.File import java.io.File
import java.io.FileInputStream
import java.io.FileOutputStream import java.io.FileOutputStream
import java.util.zip.GZIPOutputStream
object FileUtils { object FileUtils {
private const val TAG = "FileUtils" private const val TAG = "FileUtils"
/** /**
* Creates a temporary file from a URI in the app's cache directory * Compress a file to GZIP format to reduce its size to below 1MB
* @param context The context
* @param uri The URI of the file to compress
* @param maxSize The target size limit in bytes (1MB = 1048576 bytes)
* @return The compressed file, or null if compression failed
*/
fun compressFile(context: Context, uri: Uri, maxSize: Long = 1048576L): File? {
try {
// Create a temporary file for compressed content
val originalFile = createTempFileFromUri(context, uri, "compressed")
val compressedFile = File(context.cacheDir, "compressed_${System.currentTimeMillis()}.gz")
// Compress the original file into the GZIP file
compressToGZIP(originalFile, compressedFile)
// Check if the compressed file is larger than the allowed size
if (compressedFile.length() <= maxSize) {
Log.d(TAG, "Compression successful. Compressed file size: ${compressedFile.length()} bytes.")
return compressedFile
} else {
// If the file is still too large, you can handle it by reducing quality or adjusting compression logic
Log.e(TAG, "Compressed file exceeds the size limit. Size: ${compressedFile.length()} bytes.")
return null
}
} catch (e: Exception) {
Log.e(TAG, "Error during file compression: ${e.message}", e)
return null
}
}
/**
* Creates a temporary file from the URI in the app's cache directory.
*/ */
fun createTempFileFromUri(context: Context, uri: Uri, prefix: String = "temp"): File? { fun createTempFileFromUri(context: Context, uri: Uri, prefix: String = "temp"): File? {
try { try {
@ -41,6 +74,23 @@ object FileUtils {
} }
} }
/**
* Compress the input file into a GZIP file.
*/
private fun compressToGZIP(inputFile: File?, outputFile: File) {
FileInputStream(inputFile).use { inputStream ->
FileOutputStream(outputFile).use { fileOutputStream ->
GZIPOutputStream(fileOutputStream).use { gzipOutputStream ->
val buffer = ByteArray(1024)
var bytesRead: Int
while (inputStream.read(buffer).also { bytesRead = it } != -1) {
gzipOutputStream.write(buffer, 0, bytesRead)
}
}
}
}
}
/** /**
* Gets the file extension from a URI using ContentResolver * Gets the file extension from a URI using ContentResolver
*/ */

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

@ -47,15 +47,15 @@ class ProfileViewModel(private val userRepository: UserRepository) : ViewModel()
val response: HasStoreResponse = userRepository.checkStore() val response: HasStoreResponse = userRepository.checkStore()
// Log and store success message // Log and store success message
Log.d("RegisterViewModel", "OTP Response: ${response.hasStore}") Log.d("ProfileViewModel", "Has store: ${response.hasStore}")
_checkStore.value = response.hasStore // Store the message for UI feedback _checkStore.postValue(response.hasStore) // Store the message for UI feedback
} catch (exception: Exception) { } catch (exception: Exception) {
// Handle any errors and update state // Handle any errors and update state
_checkStore.value = false _checkStore.postValue(false)
// Log the error for debugging // Log the error for debugging
Log.e("RegisterViewModel", "Error:", exception) Log.e(":ProfileViewModel", "Error:", exception)
} }
} }
} }

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

@ -0,0 +1,39 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ui.profile.mystore.StoreSuspendedActivity"
android:orientation="vertical"
android:fitsSystemWindows="true">
<include
android:id="@+id/header"
layout="@layout/header" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:gravity="center"
android:padding="16dp">
<ImageView
android:layout_width="200dp"
android:layout_height="200dp"
android:src="@drawable/ic_settings"
app:tint="@color/blue_500"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Toko Anda telah dinonaktifkan oleh Admin. Harap hubungi Admin untuk melakukan reaktivasi."
style="@style/body_large"
android:fontFamily="@font/dmsans_extrabold"
android:textAlignment="center" />
</LinearLayout>
</LinearLayout>

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>

View File

@ -1,5 +1,5 @@
[versions] [versions]
agp = "8.9.2" agp = "8.12.0"
glide = "4.16.0" glide = "4.16.0"
gson = "2.11.0" gson = "2.11.0"
hiltAndroid = "2.56.2" # Updated from 2.44 for better compatibility hiltAndroid = "2.56.2" # Updated from 2.44 for better compatibility

View File

@ -1,6 +1,6 @@
#Wed Oct 16 14:37:43 ICT 2024 #Wed Oct 16 14:37:43 ICT 2024
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists

BIN
unduh/bisaUMKM.apk Normal file

Binary file not shown.

BIN
unduh/bisaUMKM_v1.apk Normal file

Binary file not shown.

BIN
unduh/bisaUMKM_v2.apk Normal file

Binary file not shown.