Compare commits

20 Commits

Author SHA1 Message Date
3d8e82e3b5 Merge branch 'screen-features' 2025-08-12 16:32:53 +07:00
d4eacf6c7c fix detail order, kecamata, bank name, checkbox tnc 2025-08-12 16:28:18 +07:00
7351f9c5b7 Merge remote-tracking branch 'origin/master' 2025-08-12 15:02:06 +07:00
9fefe1d818 update apps name 2025-08-12 15:01:50 +07:00
0a8dac4d23 update kecamatan dan bank name 2025-08-12 15:00:37 +07:00
77ea5ed90a top-up 2025-08-12 13:51:19 +07:00
c303b419ed Merge pull request #36
gracia
2025-08-12 02:24:22 +07:00
bad456f842 store status, product fixed, file compression 2025-08-12 02:23:39 +07:00
ed60528049 add account name 2025-08-12 00:44:35 +07:00
a9c2f9c103 fix validation file upliad 2025-08-11 22:47:00 +07:00
d08f5465d1 Merge pull request #35 from shaulascr/master
update
2025-08-11 20:40:16 +07:00
eab3884fd6 Merge remote-tracking branch 'origin/master' 2025-08-11 20:32:11 +07:00
930688f50d update postalcode 2025-08-11 20:31:45 +07:00
b70c671710 Aplikasi BisaUMKM fix 2025-08-07 10:32:58 +07:00
3ac0461c7c Delete unduh/bisaUMKM.txt 2025-08-07 01:46:47 +07:00
61e8e1fe3c BisaUMKM server 2025-08-07 01:44:51 +07:00
39aa079003 BisaUMKM local 2025-08-07 01:41:43 +07:00
577fb27495 Create bisaUMKM.txt 2025-08-07 01:40:03 +07:00
8b76077a77 Merge remote-tracking branch 'origin/master' 2025-08-07 01:16:45 +07:00
6194dca259 update count product, chat, address 2025-08-07 01:11:55 +07:00
50 changed files with 1419 additions and 458 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("com.google.firebase:firebase-analytics")
implementation("com.google.firebase:firebase-messaging-ktx")
}

View File

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

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

@ -90,7 +90,7 @@ data class OrdersItem(
val orderId: Int,
@field:SerializedName("city_id")
val cityId: Int,
val cityId: String,
var displayStatus: String? = null
)

View File

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

View File

@ -119,7 +119,7 @@ data class Orders(
val orderId: Int,
@field:SerializedName("city_id")
val cityId: Int
val cityId: String
)
data class OrderListItemsItem(

View File

@ -114,7 +114,7 @@ data class Store(
val storeDescription: String,
@field:SerializedName("city_id")
val cityId: Int
val cityId: String
)
data class ShippingItem(

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

@ -35,7 +35,7 @@ data class Store(
val detail: String,
@SerializedName("is_store_location") val isStoreLocation: Boolean,
@SerializedName("user_id") val userId: Int,
@SerializedName("city_id") val cityId: Int,
@SerializedName("city_id") val cityId: String,
@SerializedName("province_id") val provinceId: Int,
val phone: String?,
val recipient: String?,

View File

@ -132,5 +132,5 @@ data class Orders(
val username: String? = null,
@field:SerializedName("city_id")
val cityId: Int? = null
val cityId: String? = null
)

View File

@ -129,7 +129,7 @@ data class OrdersItem(
val status: String? = null,
@field:SerializedName("city_id")
val cityId: Int? = null,
val cityId: String? = null,
var displayStatus: String? = null
)

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

@ -278,6 +278,11 @@ class UserRepository(private val apiService: ApiService) {
val requestFile = compressedFile.asRequestBody(mimeType.toMediaTypeOrNull())
Log.d(TAG, "$formName compressed size: ${compressedFile.length() / 1024} KB")
val compressedSizeMB = compressedFile.length().toDouble() / (1024 * 1024)
if (compressedSizeMB > 1) {
throw IllegalArgumentException("$formName lebih dari 1 MB setelah kompresi")
}
MultipartBody.Part.createFormData(formName, compressedFile.name, requestFile)
} else {
throw IllegalArgumentException("$formName harus berupa file gambar (JPEG, JPG, atau PNG)")

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
@ -249,7 +250,7 @@ class RegisterStep3Fragment : Fragment() {
binding.autoCompleteDesa.setOnItemClickListener{ _, _, 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")
villageId?.let { id ->
@ -257,11 +258,11 @@ class RegisterStep3Fragment : Fragment() {
registerViewModel.selectedVillages = id
}
postalCode?.let { postCode ->
registerViewModel.selectedPostalCode = postCode
}
// postalCode?.let { 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 recipient = binding.etNamaPenerima.text.toString().trim()
val phone = binding.etNomorHp.text.toString().trim()
val postalCode = binding.etKodePos.text.toString().trim()
val provinceId = registerViewModel.selectedProvinceId?.toInt() ?: 0
val cityId = registerViewModel.selectedCityId.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 - Recipient: $recipient, Phone: $phone")
@ -383,21 +387,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

@ -3,6 +3,7 @@ package com.alya.ecommerce_serang.ui.order.address
import android.content.Context
import android.util.Log
import android.widget.ArrayAdapter
import android.widget.Spinner
import com.alya.ecommerce_serang.data.api.response.customer.order.CitiesItem
import com.alya.ecommerce_serang.data.api.response.customer.order.ProvincesItem
import com.alya.ecommerce_serang.data.api.response.customer.order.SubdistrictsItem
@ -14,85 +15,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>()
@ -176,4 +98,75 @@ class VillagesAdapter(
fun getPostalCode(position: Int): String?{
return villages.getOrNull(position)?.postalCode
}
}
class BankAdapter(
context: Context,
resource: Int = android.R.layout.simple_dropdown_item_1line
) : ArrayAdapter<String>(context, resource, ArrayList()) {
data class BankItem(
val bankName: String,
val bankCode: String? = null,
val description: String? = null
)
private val banks = ArrayList<BankItem>()
init {
loadHardcodedData()
}
private fun loadHardcodedData() {
val defaultBanks = listOf(
BankItem("Bank Mandiri", "008", "PT Bank Mandiri (Persero) Tbk"),
BankItem("Bank BRI", "002", "PT Bank Rakyat Indonesia (Persero) Tbk"),
BankItem("Bank BCA", "014", "PT Bank Central Asia Tbk"),
BankItem("Bank BNI", "009", "PT Bank Negara Indonesia (Persero) Tbk"),
BankItem("Bank BTN", "200", "PT Bank Tabungan Negara (Persero) Tbk"),
BankItem("Bank CIMB Niaga", "022", "PT Bank CIMB Niaga Tbk"),
BankItem("Bank Danamon", "011", "PT Bank Danamon Indonesia Tbk"),
BankItem("Bank Permata", "013", "PT Bank Permata Tbk"),
BankItem("Bank OCBC NISP", "028", "PT Bank OCBC NISP Tbk"),
BankItem("Bank Maybank", "016", "PT Bank Maybank Indonesia Tbk"),
BankItem("Bank Panin", "019", "PT Bank Panin Dubai Syariah Tbk"),
BankItem("Bank UOB", "023", "PT Bank UOB Indonesia"),
BankItem("Bank Mega", "426", "PT Bank Mega Tbk"),
BankItem("Bank Bukopin", "441", "PT Bank Bukopin Tbk"),
BankItem("Bank BJB", "110", "PT Bank Pembangunan Daerah Jawa Barat dan Banten Tbk")
)
updateData(defaultBanks)
}
fun updateData(newBanks: List<BankItem>) {
banks.clear()
banks.addAll(newBanks)
clear()
addAll(banks.map { it.bankName })
notifyDataSetChanged()
}
fun getBankName(position: Int): String? {
return banks.getOrNull(position)?.bankName
}
fun getBankItem(position: Int): BankItem? {
return banks.getOrNull(position)
}
fun getBankCode(position: Int): String? {
return banks.getOrNull(position)?.bankCode
}
fun findPositionByName(bankName: String): Int {
return banks.indexOfFirst { it.bankName == bankName }
}
fun setDefaultSelection(spinner: Spinner, defaultBankName: String) {
val position = findPositionByName(defaultBankName)
if (position >= 0) {
spinner.setSelection(position)
}
}
}

View File

@ -32,6 +32,7 @@ import com.google.android.material.button.MaterialButton
import com.google.android.material.textfield.TextInputLayout
import com.google.gson.Gson
import java.io.File
import java.text.NumberFormat
import java.text.SimpleDateFormat
import java.util.Calendar
import java.util.Locale
@ -88,7 +89,8 @@ class OrderHistoryAdapter(
tvStoreName.text = storeName
// Set total amount
tvTotalAmount.text = order.totalAmount
tvTotalAmount.text = formatCurrency(order.totalAmount.toDouble())
// Set item count
val itemCount = order.orderItems.size
@ -599,6 +601,11 @@ class OrderHistoryAdapter(
}
}
private fun formatCurrency(amount: Double): String {
val formatter = NumberFormat.getCurrencyInstance(Locale("in", "ID"))
return formatter.format(amount).replace(",00", "")
}
}
companion object {

View File

@ -10,6 +10,8 @@ import com.alya.ecommerce_serang.BuildConfig.BASE_URL
import com.alya.ecommerce_serang.R
import com.alya.ecommerce_serang.data.api.response.customer.order.OrderItemsItem
import com.bumptech.glide.Glide
import java.text.NumberFormat
import java.util.Locale
class OrderProductAdapter : RecyclerView.Adapter<OrderProductAdapter.ProductViewHolder>() {
@ -46,7 +48,7 @@ class OrderProductAdapter : RecyclerView.Adapter<OrderProductAdapter.ProductView
tvQuantity.text = "${product.quantity} buah"
// Set price with currency format
tvProductPrice.text = formatCurrency(product.price)
tvProductPrice.text = formatCurrency(product.price.toDouble())
val fullImageUrl = when (val img = product.productImage) {
is String -> {
@ -65,10 +67,9 @@ class OrderProductAdapter : RecyclerView.Adapter<OrderProductAdapter.ProductView
private fun formatCurrency(amount: Int): String {
// In a real app, you would use NumberFormat for proper currency formatting
// For simplicity, just return a basic formatted string
return "Rp${amount}"
private fun formatCurrency(amount: Double): String {
val formatter = NumberFormat.getCurrencyInstance(Locale("in", "ID"))
return formatter.format(amount).replace(",00", "")
}
}
}

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.profile.mystore.MyStoreActivity
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.SessionManager
import com.alya.ecommerce_serang.utils.viewmodel.MyStoreViewModel
@ -73,13 +74,11 @@ class ProfileFragment : Fragment() {
observeUserProfile()
observeStoreStatus()
viewModel.loadUserProfile()
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{
// if (hasStore == true) startActivity(Intent(requireContext(), MyStoreActivity::class.java))
// else startActivity(Intent(requireContext(), RegisterStoreActivity::class.java))
@ -88,8 +87,11 @@ class ProfileFragment : Fragment() {
myStoreViewModel.myStoreProfile.observe(viewLifecycleOwner) { store ->
store?.let {
when (store.storeStatus) {
"process" -> startActivity(Intent(requireContext(), StoreOnReviewActivity::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 {
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){
val fullImageUrl = when (val img = user.image) {
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.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

@ -1,6 +1,5 @@
package com.alya.ecommerce_serang.ui.profile.mystore
import android.Manifest
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
@ -20,7 +19,6 @@ import android.widget.Toast
import androidx.activity.enableEdgeToEdge
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.core.graphics.drawable.toDrawable
import androidx.core.view.ViewCompat
@ -32,8 +30,10 @@ import com.alya.ecommerce_serang.data.api.retrofit.ApiConfig
import com.alya.ecommerce_serang.data.repository.Result
import com.alya.ecommerce_serang.data.repository.UserRepository
import com.alya.ecommerce_serang.databinding.ActivityRegisterStoreBinding
import com.alya.ecommerce_serang.ui.order.address.BankAdapter
import com.alya.ecommerce_serang.ui.order.address.CityAdapter
import com.alya.ecommerce_serang.ui.order.address.ProvinceAdapter
import com.alya.ecommerce_serang.ui.order.address.SubdsitrictAdapter
import com.alya.ecommerce_serang.utils.BaseViewModelFactory
import com.alya.ecommerce_serang.utils.SessionManager
import com.alya.ecommerce_serang.utils.viewmodel.RegisterStoreViewModel
@ -45,6 +45,9 @@ class RegisterStoreActivity : AppCompatActivity() {
private lateinit var provinceAdapter: ProvinceAdapter
private lateinit var cityAdapter: CityAdapter
private lateinit var subdistrictAdapter: SubdsitrictAdapter
private lateinit var bankAdapter: BankAdapter
// Request codes for file picking
private val PICK_STORE_IMAGE_REQUEST = 1001
private val PICK_KTP_REQUEST = 1002
@ -88,6 +91,8 @@ class RegisterStoreActivity : AppCompatActivity() {
provinceAdapter = ProvinceAdapter(this)
cityAdapter = CityAdapter(this)
subdistrictAdapter = SubdsitrictAdapter(this)
bankAdapter = BankAdapter(this)
Log.d(TAG, "onCreate: Adapters initialized")
setupDataBinding()
@ -101,8 +106,12 @@ class RegisterStoreActivity : AppCompatActivity() {
setupObservers()
Log.d(TAG, "onCreate: Observers setup completed")
setupMap()
Log.d(TAG, "onCreate: Map setup completed")
viewModel.latitude.value = "-6.2088"
viewModel.longitude.value = "106.8456"
Log.d(TAG, "Location permission granted, setting default location")
// Toast.makeText(this, "Lokasi dipilih", Toast.LENGTH_SHORT).show()
Log.d(TAG, "Default location set - Lat: ${viewModel.latitude.value}, Long: ${viewModel.longitude.value}")
// Toast.makeText(this, "Lokasi dipilih", Toast.LENGTH_SHORT).show()
setupDocumentUploads()
Log.d(TAG, "onCreate: Document uploads setup completed")
@ -161,16 +170,17 @@ class RegisterStoreActivity : AppCompatActivity() {
viewModel.ktpUri != null &&
viewModel.nibUri != null &&
viewModel.npwpUri != null &&
viewModel.selectedCouriers.isNotEmpty()
binding.btnRegister.isEnabled = isFormValid
viewModel.selectedCouriers.isNotEmpty() &&
!viewModel.accountName.value.isNullOrBlank()
binding.btnRegister.isEnabled = true
if (isFormValid) {
binding.btnRegister.setBackgroundResource(R.drawable.bg_button_active)
binding.btnRegister.setTextColor(ContextCompat.getColor(this, R.color.white))
} else {
binding.btnRegister.setBackgroundResource(R.drawable.bg_button_disabled)
binding.btnRegister.setTextColor(ContextCompat.getColor(this, R.color.black_300))
}
}
@ -225,6 +235,28 @@ class RegisterStoreActivity : AppCompatActivity() {
}
}
viewModel.subdistrictState.observe(this) { state ->
when (state) {
is Result.Loading -> {
Log.d(TAG, "setupobservers: Loading Subdistrict...")
binding.subdistrictProgressBar.visibility = View.VISIBLE
binding.spinnerSubdistrict.isEnabled = false
}
is Result.Success -> {
Log.d(TAG, "setupobservers: Subdistrict loaded successfullti: ${state.data.size} subdistrict")
binding.subdistrictProgressBar.visibility = View.GONE
binding.spinnerSubdistrict.isEnabled = true
subdistrictAdapter.updateData(state.data)
}
is Result.Error -> {
Log.e(TAG, "setupObservers: Error loading subdistrict: ${state.exception.message}")
binding.subdistrictProgressBar.visibility = View.GONE
binding.spinnerCity.isEnabled = true
}
}
}
// Observe registration state
viewModel.registerState.observe(this) { result ->
when (result) {
@ -395,6 +427,11 @@ class RegisterStoreActivity : AppCompatActivity() {
if (cityId != null) {
Log.d(TAG, "Setting city ID: $cityId")
viewModel.cityId.value = cityId
Log.d(TAG, "Fetching subdistrict for city ID: $cityId")
viewModel.getSubdistrict(cityId)
subdistrictAdapter.clear()
binding.spinnerSubdistrict.setSelection(0)
viewModel.selectedCityId = cityId
} else {
Log.e(TAG, "Invalid city ID for position: $position")
@ -406,6 +443,61 @@ class RegisterStoreActivity : AppCompatActivity() {
}
}
//Setup Subdistrict spinner
binding.spinnerSubdistrict.adapter = subdistrictAdapter
binding.spinnerSubdistrict.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
Log.d(TAG, "Subdistrict selected at position: $position")
val subdistrictId = subdistrictAdapter.getSubdistrictId(position)
if (subdistrictId != null) {
Log.d(TAG, "Setting subdistrict ID: $subdistrictId")
viewModel.subdistrict.value = subdistrictId
viewModel.selectedSubdistrict = subdistrictId
} else {
Log.e(TAG, "Invalid subdistrict ID for position: $position")
}
}
override fun onNothingSelected(parent: AdapterView<*>?) {
Log.d(TAG, "No city selected")
}
}
binding.spinnerBankName.adapter = bankAdapter
binding.spinnerBankName.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(
parent: AdapterView<*>?,
view: View?,
position: Int,
id: Long
) {
Log.d(TAG, "Bank selected at position: $position")
val bankName = bankAdapter.getBankName(position)
if (bankName != null) {
Log.d(TAG, "Setting bank name: $bankName")
viewModel.bankName.value = bankName
viewModel.selectedBankName = bankName
// Optional: Log the selected bank details
val selectedBank = bankAdapter.getBankItem(position)
selectedBank?.let {
Log.d(TAG, "Selected bank: ${it.bankName} (Code: ${it.bankCode})")
}
// Hide progress bar if it was showing
binding.storeTypeProgressBar.visibility = View.GONE
} else {
Log.e(TAG, "Invalid bank name for position: $position")
}
}
override fun onNothingSelected(parent: AdapterView<*>?) {
Log.d(TAG, "No bank selected")
viewModel.selectedBankName = null
}
}
// Add initial hints to the spinners
if (provinceAdapter.isEmpty) {
Log.d(TAG, "Adding default province hint")
@ -417,6 +509,16 @@ class RegisterStoreActivity : AppCompatActivity() {
cityAdapter.add("Pilih Kabupaten/Kota")
}
if (subdistrictAdapter.isEmpty) {
Log.d(TAG, "Adding default kecamatan hint")
subdistrictAdapter.add("Pilih Kecamatan")
}
if (bankAdapter.isEmpty) {
Log.d(TAG, "Adding default bank hint")
bankAdapter.add("Pilih Bank")
}
Log.d(TAG, "setupSpinners: Province and city spinners setup completed")
}
@ -500,44 +602,44 @@ class RegisterStoreActivity : AppCompatActivity() {
validateRequiredFields()
}
private fun setupMap() {
Log.d(TAG, "setupMap: Setting up map container")
// This would typically integrate with Google Maps SDK
// For simplicity, we're just using a placeholder
binding.mapContainer.setOnClickListener {
Log.d(TAG, "Map container clicked, checking location permission")
// Request location permission if not granted
if (ContextCompat.checkSelfPermission(
this,
Manifest.permission.ACCESS_FINE_LOCATION
) != PackageManager.PERMISSION_GRANTED
) {
Log.d(TAG, "Location permission not granted, requesting permission")
ActivityCompat.requestPermissions(
this,
arrayOf(Manifest.permission.ACCESS_FINE_LOCATION),
LOCATION_PERMISSION_REQUEST
)
viewModel.latitude.value = "-6.2088"
viewModel.longitude.value = "106.8456"
Log.d(TAG, "Location permission granted, setting default location")
Toast.makeText(this, "Lokasi dipilih", Toast.LENGTH_SHORT).show()
Log.d(TAG, "Default location set - Lat: ${viewModel.latitude.value}, Long: ${viewModel.longitude.value}")
Toast.makeText(this, "Lokasi dipilih", Toast.LENGTH_SHORT).show()
} else {
Log.d(TAG, "Location permission already granted, setting location")
// Show map selection UI
// This would typically launch Maps UI for location selection
// For now, we'll just set some dummy coordinates
viewModel.latitude.value = "-6.2088"
viewModel.longitude.value = "106.8456"
Log.d(TAG, "Location set - Lat: ${viewModel.latitude.value}, Long: ${viewModel.longitude.value}")
Toast.makeText(this, "Lokasi dipilih", Toast.LENGTH_SHORT).show()
}
}
Log.d(TAG, "setupMap: Map container setup completed")
}
// private fun setupMap() {
// Log.d(TAG, "setupMap: Setting up map container")
// // This would typically integrate with Google Maps SDK
// // For simplicity, we're just using a placeholder
// binding.mapContainer.setOnClickListener {
// Log.d(TAG, "Map container clicked, checking location permission")
// // Request location permission if not granted
// if (ContextCompat.checkSelfPermission(
// this,
// Manifest.permission.ACCESS_FINE_LOCATION
// ) != PackageManager.PERMISSION_GRANTED
// ) {
// Log.d(TAG, "Location permission not granted, requesting permission")
// ActivityCompat.requestPermissions(
// this,
// arrayOf(Manifest.permission.ACCESS_FINE_LOCATION),
// LOCATION_PERMISSION_REQUEST
// )
// viewModel.latitude.value = "-6.2088"
// viewModel.longitude.value = "106.8456"
// Log.d(TAG, "Location permission granted, setting default location")
// Toast.makeText(this, "Lokasi dipilih", Toast.LENGTH_SHORT).show()
// Log.d(TAG, "Default location set - Lat: ${viewModel.latitude.value}, Long: ${viewModel.longitude.value}")
// Toast.makeText(this, "Lokasi dipilih", Toast.LENGTH_SHORT).show()
// } else {
// Log.d(TAG, "Location permission already granted, setting location")
// // Show map selection UI
// // This would typically launch Maps UI for location selection
// // For now, we'll just set some dummy coordinates
// viewModel.latitude.value = "-6.2088"
// viewModel.longitude.value = "106.8456"
// Log.d(TAG, "Location set - Lat: ${viewModel.latitude.value}, Long: ${viewModel.longitude.value}")
// Toast.makeText(this, "Lokasi dipilih", Toast.LENGTH_SHORT).show()
// }
// }
//
// Log.d(TAG, "setupMap: Map container setup completed")
// }
private fun setupDataBinding() {
Log.d(TAG, "setupDataBinding: Setting up two-way data binding for text fields")
@ -617,25 +719,36 @@ class RegisterStoreActivity : AppCompatActivity() {
validateRequiredFields()
}
})
//
// binding.etSubdistrict.addTextChangedListener(object : TextWatcher {
// override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
// override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
// override fun afterTextChanged(s: Editable?) {
// viewModel.subdistrict.value = s.toString()
// Log.d(TAG, "Subdistrict updated: ${s.toString()}")
// validateRequiredFields()
// }
// })
binding.etSubdistrict.addTextChangedListener(object : TextWatcher {
// binding.etBankName.addTextChangedListener(object: TextWatcher {
// override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
// override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
// override fun afterTextChanged(s: Editable?) {
// viewModel.bankName.value = s.toString()
// Log.d(TAG, "Bank name updated: ${s.toString()}")
// validateRequiredFields()
// }
// })
binding.etAccountName.addTextChangedListener(object : TextWatcher {
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
override fun afterTextChanged(s: Editable?) {
viewModel.subdistrict.value = s.toString()
Log.d(TAG, "Subdistrict updated: ${s.toString()}")
viewModel.accountName.value = s.toString()
Log.d(TAG, "Account Name updated: ${s.toString()}")
validateRequiredFields()
}
})
binding.etBankName.addTextChangedListener(object: TextWatcher {
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
override fun afterTextChanged(s: Editable?) {
viewModel.bankName.value = s.toString()
Log.d(TAG, "Bank name updated: ${s.toString()}")
validateRequiredFields()
}
})
Log.d(TAG, "setupDataBinding: Text field data binding setup completed")

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

@ -15,11 +15,15 @@ import android.widget.Spinner
import android.widget.TextView
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import androidx.core.widget.doAfterTextChanged
import androidx.lifecycle.lifecycleScope
import com.alya.ecommerce_serang.R
import com.alya.ecommerce_serang.data.api.response.store.profile.Payment
import com.alya.ecommerce_serang.data.api.retrofit.ApiConfig
import com.alya.ecommerce_serang.utils.ImageUtils.compressImage
import com.alya.ecommerce_serang.utils.SessionManager
import kotlinx.coroutines.launch
import okhttp3.MediaType.Companion.toMediaTypeOrNull
@ -38,6 +42,8 @@ class BalanceTopUpActivity : AppCompatActivity() {
private lateinit var spinnerPaymentMethod: Spinner
private lateinit var edtTransactionDate: EditText
private lateinit var datePickerIcon: ImageView
private lateinit var layoutMBankingInstructions: View
private lateinit var layoutATMInstructions: View
private lateinit var btnSend: Button
private lateinit var sessionManager: SessionManager
@ -52,7 +58,22 @@ class BalanceTopUpActivity : AppCompatActivity() {
val imageUri = result.data?.data
imageUri?.let {
selectedImageUri = it
imgPreview.setImageURI(it)
// Compress the image before displaying it
val compressedFile = compressImage(
context = this,
uri = it,
filename = "topup_img",
maxWidth = 1024,
maxHeight = 1024,
quality = 80
)
// Display the compressed image
selectedImageUri = Uri.fromFile(compressedFile)
imgPreview.setImageURI(Uri.fromFile(compressedFile))
validateForm()
}
}
}
@ -71,6 +92,8 @@ class BalanceTopUpActivity : AppCompatActivity() {
spinnerPaymentMethod = findViewById(R.id.spinner_metode_bayar)
edtTransactionDate = findViewById(R.id.edt_tgl_transaksi)
datePickerIcon = findViewById(R.id.img_date_picker)
layoutMBankingInstructions = findViewById(R.id.layout_mbanking_instructions)
layoutATMInstructions = findViewById(R.id.layout_atm_instructions)
btnSend = findViewById(R.id.btn_send)
// Setup header title
@ -98,10 +121,27 @@ class BalanceTopUpActivity : AppCompatActivity() {
// Fetch payment methods
fetchPaymentMethods()
setupClickListeners("1234567890")
// Setup submit button
btnSend.setOnClickListener {
submitForm()
}
// Validate form when any input changes
edtNominal.doAfterTextChanged { validateForm() }
edtTransactionDate.doAfterTextChanged { validateForm() }
spinnerPaymentMethod.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
selectedPaymentId = paymentMethods[position].id
validateForm()
}
override fun onNothingSelected(parent: AdapterView<*>?) {
selectedPaymentId = -1
validateForm()
}
}
}
private fun openGallery() {
@ -200,6 +240,24 @@ class BalanceTopUpActivity : AppCompatActivity() {
}
}
private fun validateForm() {
val isNominalFilled = edtNominal.text.toString().trim().isNotEmpty()
val isPaymentMethodSelected = selectedPaymentId != -1
val isTransactionDateFilled = edtTransactionDate.text.toString().trim().isNotEmpty()
val isImageSelected = selectedImageUri != null
val valid = isNominalFilled && isPaymentMethodSelected && isTransactionDateFilled && isImageSelected
btnSend.isEnabled = valid
btnSend.setTextColor(
if (valid) ContextCompat.getColor(this, R.color.white)
else ContextCompat.getColor(this, R.color.black_300)
)
btnSend.setBackgroundResource(
if (valid) R.drawable.bg_button_active
else R.drawable.bg_button_disabled
)
}
private fun submitForm() {
// Prevent multiple clicks
if (!btnSend.isEnabled) {
@ -316,7 +374,7 @@ class BalanceTopUpActivity : AppCompatActivity() {
// Show a dialog with the success message
runOnUiThread {
androidx.appcompat.app.AlertDialog.Builder(this@BalanceTopUpActivity)
AlertDialog.Builder(this@BalanceTopUpActivity)
.setTitle("Berhasil")
.setMessage(successMessage)
.setPositiveButton("OK") { dialog, _ ->
@ -350,7 +408,7 @@ class BalanceTopUpActivity : AppCompatActivity() {
// Show a dialog with the error message
runOnUiThread {
androidx.appcompat.app.AlertDialog.Builder(this@BalanceTopUpActivity)
AlertDialog.Builder(this@BalanceTopUpActivity)
.setTitle("Error Response")
.setMessage(errorMessage)
.setPositiveButton("OK") { dialog, _ ->
@ -392,4 +450,46 @@ class BalanceTopUpActivity : AppCompatActivity() {
return tempFile
}
private fun setupClickListeners(bankAccountNumber: String) {
// Instructions clicks
layoutMBankingInstructions.setOnClickListener {
showInstructions("mBanking", bankAccountNumber)
}
layoutATMInstructions.setOnClickListener {
showInstructions("ATM", bankAccountNumber)
}
}
private fun showInstructions(type: String, bankAccountNumber: String) {
// Implementasi tampilkan instruksi
val instructions = when (type) {
"mBanking" -> listOf(
"1. Login ke aplikasi mobile banking",
"2. Pilih menu Transfer",
"3. Pilih menu Antar Rekening",
"4. Masukkan nomor rekening tujuan: $bankAccountNumber",
"5. Masukkan nominal saldo yang ingin diisi",
"6. Konfirmasi dan selesaikan transfer"
)
"ATM" -> listOf(
"1. Masukkan kartu ATM dan PIN",
"2. Pilih menu Transfer",
"3. Pilih menu Antar Rekening",
"4. Masukkan kode bank dan nomor rekening tujuan: $bankAccountNumber",
"5. Masukkan nominal saldo yang ingin diisi",
"6. Konfirmasi dan selesaikan transfer"
)
else -> emptyList()
}
// Tampilkan instruksi dalam dialog
val dialog = AlertDialog.Builder(this)
.setTitle("Petunjuk Transfer $type")
.setItems(instructions.toTypedArray(), null)
.setPositiveButton("Tutup", null)
.create()
dialog.show()
}
}

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

@ -8,13 +8,46 @@ import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.MultipartBody
import okhttp3.RequestBody.Companion.asRequestBody
import java.io.File
import java.io.FileInputStream
import java.io.FileOutputStream
import java.util.zip.GZIPOutputStream
object 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? {
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
*/

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

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

View File

@ -11,6 +11,8 @@ import com.alya.ecommerce_serang.data.api.response.auth.RegisterStoreResponse
import com.alya.ecommerce_serang.data.api.response.auth.StoreTypesItem
import com.alya.ecommerce_serang.data.api.response.customer.order.CitiesItem
import com.alya.ecommerce_serang.data.api.response.customer.order.ProvincesItem
import com.alya.ecommerce_serang.data.api.response.customer.order.SubdistrictsItem
import com.alya.ecommerce_serang.data.api.response.customer.order.VillagesItem
import com.alya.ecommerce_serang.data.repository.Result
import com.alya.ecommerce_serang.data.repository.UserRepository
import com.alya.ecommerce_serang.utils.ImageUtils
@ -41,8 +43,17 @@ class RegisterStoreViewModel(
private val _citiesState = MutableLiveData<Result<List<CitiesItem>>>()
val citiesState: LiveData<Result<List<CitiesItem>>> = _citiesState
private val _subdistrictState = MutableLiveData<Result<List<SubdistrictsItem>>>()
val subdistrictState: LiveData<Result<List<SubdistrictsItem>>> = _subdistrictState
private val _villagesState = MutableLiveData<Result<List<VillagesItem>>>()
val villagesState: LiveData<Result<List<VillagesItem>>> = _villagesState
var selectedProvinceId: Int? = null
var selectedCityId: String? = null
var selectedSubdistrict: String? = null
var selectedVillages: String? = null
var selectedBankName: String? = null
// Form fields
val storeName = MutableLiveData<String>()
@ -72,47 +83,89 @@ class RegisterStoreViewModel(
val selectedCouriers = mutableListOf<String>()
fun registerStore(context: Context) {
Log.d(TAG, "Starting registerStore()")
val allowedFileTypes = Regex("^(jpeg|jpg|png|pdf)$", RegexOption.IGNORE_CASE)
// Check each file if present
fun logFileInfo(label: String, uri: Uri?) {
if (uri == null) {
Log.d(TAG, "$label URI: null")
return
}
Log.d(TAG, "$label URI: $uri")
try {
val fileSizeBytes = context.contentResolver.openFileDescriptor(uri, "r")?.use {
it.statSize
} ?: -1
Log.d(TAG, "$label original size: ${fileSizeBytes / 1024} KB")
} catch (e: Exception) {
Log.e(TAG, "Error getting size for $label", e)
}
}
// Log all file info before validation
logFileInfo("Store Image", storeImageUri)
logFileInfo("KTP", ktpUri)
logFileInfo("NPWP", npwpUri)
logFileInfo("NIB", nibUri)
logFileInfo("Persetujuan", persetujuanUri)
logFileInfo("QRIS", qrisUri)
// Check file types
if (storeImageUri != null && !ImageUtils.isAllowedFileType(context, storeImageUri, allowedFileTypes)) {
_errorMessage.value = "Foto toko harus berupa file JPEG, JPG, atau PNG"
Log.e(TAG, _errorMessage.value ?: "Invalid file type for store image")
_registerState.value = Result.Error(Exception(_errorMessage.value ?: "Invalid file type"))
return
}
if (ktpUri != null && !ImageUtils.isAllowedFileType(context, ktpUri, allowedFileTypes)) {
_errorMessage.value = "KTP harus berupa file JPEG, JPG, atau PNG"
Log.e(TAG, _errorMessage.value ?: "Invalid file type for KTP")
_registerState.value = Result.Error(Exception(_errorMessage.value ?: "Invalid file type"))
return
}
if (npwpUri != null && !ImageUtils.isAllowedFileType(context, npwpUri, allowedFileTypes)) {
_errorMessage.value = "NPWP harus berupa file JPEG, JPG, PNG, atau PDF"
Log.e(TAG, _errorMessage.value ?: "Invalid file type for NPWP")
_registerState.value = Result.Error(Exception(_errorMessage.value ?: "Invalid file type"))
return
}
if (nibUri != null && !ImageUtils.isAllowedFileType(context, nibUri, allowedFileTypes)) {
_errorMessage.value = "NIB harus berupa file JPEG, JPG, PNG, atau PDF"
Log.e(TAG, _errorMessage.value ?: "Invalid file type for NIB")
_registerState.value = Result.Error(Exception(_errorMessage.value ?: "Invalid file type"))
return
}
if (persetujuanUri != null && !ImageUtils.isAllowedFileType(context, persetujuanUri, allowedFileTypes)) {
_errorMessage.value = "Persetujuan harus berupa file JPEG, JPG, PNG, atau PDF"
Log.e(TAG, _errorMessage.value ?: "Invalid file type for Persetujuan")
_registerState.value = Result.Error(Exception(_errorMessage.value ?: "Invalid file type"))
return
}
if (qrisUri != null && !ImageUtils.isAllowedFileType(context, qrisUri, allowedFileTypes)) {
_errorMessage.value = "QRIS harus berupa file JPEG, JPG, PNG, atau PDF"
Log.e(TAG, _errorMessage.value ?: "Invalid file type for QRIS")
_registerState.value = Result.Error(Exception(_errorMessage.value ?: "Invalid file type"))
return
}
Log.d(TAG, "File type checks passed. Starting repository.registerStoreUser() call.")
viewModelScope.launch {
try {
_registerState.value = Result.Loading
Log.d(TAG, "Register store request payload: " +
"storeName=${storeName.value}, storeTypeId=${storeTypeId.value}, " +
"lat=${latitude.value}, long=${longitude.value}, " +
"street=${street.value}, subdistrict=${subdistrict.value}, " +
"cityId=${cityId.value}, provinceId=${provinceId.value}, postalCode=${postalCode.value}, " +
"bankName=${bankName.value}, bankNum=${bankNumber.value}, accountName=${accountName.value}, " +
"selectedCouriers=$selectedCouriers")
val result = repository.registerStoreUser(
context = context,
@ -139,25 +192,15 @@ class RegisterStoreViewModel(
accountName = accountName.value ?: ""
)
Log.d(TAG, "Repository returned result: $result")
_registerState.value = result
} catch (e: Exception) {
Log.e(TAG, "Exception during registerStore", e)
_registerState.value = Result.Error(e)
}
}
}
// // Helper function to convert Uri to File
// private fun getFileFromUri(context: Context, uri: Uri): File {
// val inputStream = context.contentResolver.openInputStream(uri)
// val tempFile = File(context.cacheDir, "temp_file_${System.currentTimeMillis()}")
// inputStream?.use { input ->
// tempFile.outputStream().use { output ->
// input.copyTo(output)
// }
// }
// return tempFile
// }
fun validateForm(): Boolean {
// Implement form validation logic
return !(storeName.value.isNullOrEmpty() ||
@ -174,8 +217,6 @@ class RegisterStoreViewModel(
nibUri == null)
}
// Function to fetch store types
fun fetchStoreTypes() {
_isLoadingType.value = true
@ -235,6 +276,52 @@ class RegisterStoreViewModel(
}
}
fun getSubdistrict(cityId: String) {
_subdistrictState.value = Result.Loading
viewModelScope.launch {
try {
selectedSubdistrict = cityId
val result = repository.getListSubdistrict(cityId)
result?.let {
_subdistrictState.postValue(Result.Success(it.subdistricts))
Log.d(TAG, "Cities loaded for province $cityId: ${it.subdistricts.size}")
} ?: run {
_subdistrictState.postValue(Result.Error(Exception("Failed to load cities")))
Log.e(TAG, "City result was null for province $cityId")
}
} catch (e: Exception) {
_subdistrictState.postValue(Result.Error(Exception(e.message ?: "Error loading cities")))
Log.e(TAG, "Error fetching cities for province $cityId", e)
}
}
}
fun getVillages(subdistrictId: String) {
_villagesState.value = Result.Loading
viewModelScope.launch {
try {
selectedVillages = subdistrictId
val result = repository.getListVillages(subdistrictId)
result?.let {
_villagesState.postValue(Result.Success(it.villages))
Log.d(TAG, "Cities loaded for province $subdistrictId: ${it.villages.size}")
} ?: run {
_villagesState.postValue(Result.Error(Exception("Failed to load cities")))
Log.e(TAG, "City result was null for province $subdistrictId")
}
} catch (e: Exception) {
_villagesState.postValue(Result.Error(Exception(e.message ?: "Error loading cities")))
Log.e(TAG, "Error fetching cities for province $subdistrictId", e)
}
}
}
fun isBankSelected(): Boolean {
return !selectedBankName.isNullOrEmpty()
}
companion object {
private const val TAG = "RegisterStoreUserViewModel"
}

View File

@ -26,14 +26,14 @@
android:paddingHorizontal="@dimen/horizontal_safe_area"
android:layout_marginTop="19dp">
<!-- Foto Produk -->
<!-- Bukti Bayar -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_marginBottom="24dp">
<!-- Label Foto Produk -->
<!-- Label Bukti Bayar -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
@ -42,7 +42,7 @@
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Foto Produk"
android:text="Bukti Pembayaran"
style="@style/body_medium"
android:layout_marginEnd="4dp"/>
@ -275,12 +275,73 @@
</LinearLayout>
<!-- Petunjuk -->
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginTop="16dp"
android:background="#E0E0E0" />
<LinearLayout
android:id="@+id/layout_mbanking_instructions"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center"
android:padding="8dp">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Petunjuk Transfer mBanking"
style="@style/body_medium" />
<ImageView
android:layout_width="16dp"
android:layout_height="16dp"
android:src="@drawable/ic_arrow_right" />
</LinearLayout>
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="#E0E0E0" />
<LinearLayout
android:id="@+id/layout_atm_instructions"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center"
android:padding="8dp">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Petunjuk Transfer ATM"
style="@style/body_medium" />
<ImageView
android:layout_width="16dp"
android:layout_height="16dp"
android:src="@drawable/ic_arrow_right" />
</LinearLayout>
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="#E0E0E0" />
</LinearLayout>
<Button
android:id="@+id/btn_send"
android:text="Kirim"
style="@style/button.large.disabled.long"
android:enabled="false"
android:layout_marginBottom="16dp"/>
</LinearLayout>

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

@ -347,41 +347,57 @@
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_marginBottom="24dp">
android:orientation="horizontal">
<LinearLayout
android:layout_width="match_parent"
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal">
android:text="6. Kecamatan"
style="@style/body_medium"
android:layout_marginEnd="4dp"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="6. Kecamatan"
style="@style/body_medium"
android:layout_marginEnd="4dp"/>
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="*"
style="@style/body_medium"
android:textColor="@color/red_required"
android:layout_gravity="end"/>
</LinearLayout>
<EditText
android:id="@+id/et_subdistrict"
android:layout_width="match_parent"
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:background="@drawable/bg_text_field"
android:hint="Isi kecamatan tempat toko Anda di sini"
android:layout_weight="1"
android:text="*"
style="@style/body_medium"
android:textColor="@color/red_required"
android:layout_gravity="end"/>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:background="@drawable/bg_text_field"
android:gravity="center_vertical"
android:layout_marginTop="10dp">
<Spinner
android:id="@+id/spinner_subdistrict"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:padding="8dp"
style="@style/body_small"
android:layout_marginTop="10dp"/>
android:background="@null"/>
<ProgressBar
android:id="@+id/subdistrict_progress_bar"
android:layout_width="24dp"
android:layout_height="24dp"
android:padding="4dp"
android:visibility="gone"/>
<!-- Chevron Down Icon -->
<ImageView
android:layout_width="16dp"
android:layout_height="16dp"
android:src="@drawable/ic_down"
android:layout_marginEnd="8dp"
android:contentDescription="Chevron Down" />
</LinearLayout>
@ -503,7 +519,8 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_marginBottom="24dp">
android:layout_marginBottom="24dp"
android:visibility="gone">
<LinearLayout
android:layout_width="match_parent"
@ -540,6 +557,74 @@
</LinearLayout>
<!-- Nama Bank-->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_marginBottom="24dp">
<!-- Label Jenis UMKM -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="10. Bank"
style="@style/body_medium"
android:layout_marginEnd="4dp"/>
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="*"
style="@style/body_medium"
android:textColor="@color/red_required"
android:layout_gravity="end"/>
</LinearLayout>
<!-- Spinner Dropdown dengan Chevron -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:background="@drawable/bg_text_field"
android:gravity="center_vertical"
android:layout_marginTop="10dp">
<Spinner
android:id="@+id/spinner_bank_name"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:padding="8dp"
style="@style/body_small"
android:background="@null"/>
<ProgressBar
android:id="@+id/bank_name_progress_bar"
android:layout_width="24dp"
android:layout_height="24dp"
android:padding="4dp"
android:visibility="gone"/>
<!-- Chevron Down Icon -->
<ImageView
android:layout_width="16dp"
android:layout_height="16dp"
android:src="@drawable/ic_down"
android:layout_marginEnd="8dp"
android:contentDescription="Chevron Down" />
</LinearLayout>
</LinearLayout>
<!-- Nomor Rekening -->
<LinearLayout
android:layout_width="match_parent"
@ -583,6 +668,48 @@
</LinearLayout>
<!-- Nama Pemilik Rekening -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_marginBottom="24dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="12. Nama Pemilik Rekening"
style="@style/body_medium"
android:layout_marginEnd="4dp"/>
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="*"
style="@style/body_medium"
android:textColor="@color/red_required"
android:layout_gravity="end"/>
</LinearLayout>
<EditText
android:id="@+id/et_account_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/bg_text_field"
android:hint="Isi nama pemilik rekening"
android:padding="8dp"
style="@style/body_small"
android:layout_marginTop="10dp"/>
</LinearLayout>
<!-- Kurir -->
<LinearLayout
android:layout_width="match_parent"
@ -598,7 +725,7 @@
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="12. Pilih Kurir Pengiriman"
android:text="13. Pilih Kurir Pengiriman"
style="@style/body_medium"
android:layout_marginEnd="4dp"/>
@ -651,7 +778,7 @@
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="13. Foto Profil Toko"
android:text="14. Foto Profil Toko"
style="@style/body_medium"
android:layout_marginEnd="4dp"/>
@ -707,7 +834,7 @@
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="14. Dokumen KTP"
android:text="15. Dokumen KTP"
style="@style/body_medium"
android:layout_marginEnd="4dp"/>
@ -774,7 +901,7 @@
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="15. Dokumen NIB"
android:text="16. Dokumen NIB"
style="@style/body_medium"
android:layout_marginEnd="4dp"/>
@ -841,7 +968,7 @@
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="16. Dokumen NPWP"
android:text="17. Dokumen NPWP"
style="@style/body_medium"
android:layout_marginEnd="4dp"/>
@ -896,7 +1023,8 @@
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
android:orientation="horizontal"
android:visibility="gone">
<TextView
android:layout_width="wrap_content"
@ -916,12 +1044,26 @@
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<CheckBox
android:id="@+id/checkbox_approve"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Saya telah membaca dan menyetujui Syarat dan Ketentuan aplikasi" />
</LinearLayout>
<FrameLayout
android:id="@+id/map_container"
android:layout_width="match_parent"
android:layout_height="200dp"
android:layout_marginTop="8dp"
android:background="@android:color/darker_gray">
android:background="@android:color/darker_gray"
android:visibility="gone">
<ImageView
android:layout_width="match_parent"
@ -933,7 +1075,7 @@
<Button
android:id="@+id/btn_register"
style="@style/button.large.disabled.long"
android:enabled="false"
android:enabled="true"
android:layout_marginTop="24dp"
android:layout_marginBottom="16dp"
android:text="Daftar" />

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: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>

View File

@ -1,5 +1,5 @@
<resources>
<string name="app_name">Bisa UMKM</string>
<string name="app_name">PasarKlik</string>
<!--Placeholder-->
@ -146,5 +146,30 @@
<string name="typing">User is typing...</string>
<string name="buka_toko_desc">Mohon untuk melengkapi formulir pendaftaran ini agar dapat mengakses fitur penjual pada aplikasi.</string>
<!-- List Bank -->
<string-array name="bank_name_array">
<item>Allo Bank Indonesia</item>
<item>Bank Digital BCA</item>
<item>Bank Central Asia (BCA)</item>
<item>Bank BCA Syariah</item>
<item>Bank Jago</item>
<item>Bank Mandiri </item>
<item>Bank Kb Bukopi</item>
<item>Bank Jabar Banten Syariah</item>
<item> Bank Mandiri Taspen</item>
<item>Bank Maybank Indonesia </item>
<item>Bank Negara Indonesia (BNI)</item>
<item>Bank Permata</item>
<item>Bank Rakyat Indonesia (BRI)</item>
<item>Bank Saqu Indonesia</item>
<item>Bank Seabank Indonesia</item>
<item>Bank Sinarmas </item>
<item>Bank Syariah Indonesia (BSI)</item>
<item>Bank Tabungan Negara (BTN)</item>
<item>Bank UOB Indonesia</item>
<item>BPD Jawa Barat dan Banten</item>
<item>Super Bank Indonesia</item>
</string-array>
</resources>

View File

@ -1,5 +1,5 @@
[versions]
agp = "8.9.2"
agp = "8.12.0"
glide = "4.16.0"
gson = "2.11.0"
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
distributionBase=GRADLE_USER_HOME
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
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.