add list notif

This commit is contained in:
shaulascr
2025-05-20 14:57:15 +07:00
parent 0c49be5ae6
commit b5e47050b9
11 changed files with 892 additions and 128 deletions

View File

@ -0,0 +1,33 @@
package com.alya.ecommerce_serang.data.api.response.auth
import com.google.gson.annotations.SerializedName
data class ListNotifResponse(
@field:SerializedName("notif")
val notif: List<NotifItem>,
@field:SerializedName("message")
val message: String
)
data class NotifItem(
@field:SerializedName("user_id")
val userId: Int,
@field:SerializedName("created_at")
val createdAt: String,
@field:SerializedName("id")
val id: Int,
@field:SerializedName("title")
val title: String,
@field:SerializedName("message")
val message: String,
@field:SerializedName("type")
val type: String
)

View File

@ -0,0 +1,33 @@
package com.alya.ecommerce_serang.data.api.response.auth
import com.google.gson.annotations.SerializedName
data class ListStoreNotifResponse(
@field:SerializedName("notifstore")
val notifstore: List<NotifstoreItem>,
@field:SerializedName("message")
val message: String
)
data class NotifstoreItem(
@field:SerializedName("user_id")
val userId: Int,
@field:SerializedName("created_at")
val createdAt: String,
@field:SerializedName("id")
val id: Int,
@field:SerializedName("title")
val title: String,
@field:SerializedName("message")
val message: String,
@field:SerializedName("type")
val type: String
)

View File

@ -26,6 +26,8 @@ import com.alya.ecommerce_serang.data.api.dto.VerifRegisReq
import com.alya.ecommerce_serang.data.api.response.auth.CheckStoreResponse import com.alya.ecommerce_serang.data.api.response.auth.CheckStoreResponse
import com.alya.ecommerce_serang.data.api.response.auth.FcmTokenResponse import com.alya.ecommerce_serang.data.api.response.auth.FcmTokenResponse
import com.alya.ecommerce_serang.data.api.response.auth.HasStoreResponse import com.alya.ecommerce_serang.data.api.response.auth.HasStoreResponse
import com.alya.ecommerce_serang.data.api.response.auth.ListNotifResponse
import com.alya.ecommerce_serang.data.api.response.auth.ListStoreNotifResponse
import com.alya.ecommerce_serang.data.api.response.auth.ListStoreTypeResponse import com.alya.ecommerce_serang.data.api.response.auth.ListStoreTypeResponse
import com.alya.ecommerce_serang.data.api.response.auth.LoginResponse import com.alya.ecommerce_serang.data.api.response.auth.LoginResponse
import com.alya.ecommerce_serang.data.api.response.auth.OtpResponse import com.alya.ecommerce_serang.data.api.response.auth.OtpResponse
@ -454,4 +456,12 @@ interface ApiService {
@GET("chat") @GET("chat")
suspend fun getChatList( suspend fun getChatList(
): Response<ChatListResponse> ): Response<ChatListResponse>
@GET("notification")
suspend fun getNotif(
): Response<ListNotifResponse>
@GET("mystore/notification")
suspend fun getNotifStore(
): Response<ListStoreNotifResponse>
} }

View File

@ -13,6 +13,8 @@ import com.alya.ecommerce_serang.data.api.response.auth.FcmTokenResponse
import com.alya.ecommerce_serang.data.api.response.auth.HasStoreResponse import com.alya.ecommerce_serang.data.api.response.auth.HasStoreResponse
import com.alya.ecommerce_serang.data.api.response.auth.ListStoreTypeResponse import com.alya.ecommerce_serang.data.api.response.auth.ListStoreTypeResponse
import com.alya.ecommerce_serang.data.api.response.auth.LoginResponse import com.alya.ecommerce_serang.data.api.response.auth.LoginResponse
import com.alya.ecommerce_serang.data.api.response.auth.NotifItem
import com.alya.ecommerce_serang.data.api.response.auth.NotifstoreItem
import com.alya.ecommerce_serang.data.api.response.auth.OtpResponse import com.alya.ecommerce_serang.data.api.response.auth.OtpResponse
import com.alya.ecommerce_serang.data.api.response.auth.RegisterResponse import com.alya.ecommerce_serang.data.api.response.auth.RegisterResponse
import com.alya.ecommerce_serang.data.api.response.auth.RegisterStoreResponse import com.alya.ecommerce_serang.data.api.response.auth.RegisterStoreResponse
@ -345,6 +347,36 @@ class UserRepository(private val apiService: ApiService) {
return apiService.updateFcm(request) return apiService.updateFcm(request)
} }
suspend fun getListNotif(): Result<List<NotifItem>> {
return try {
val response = apiService.getNotif()
if (response.isSuccessful){
val chat = response.body()?.notif ?: emptyList()
Result.Success(chat)
} else {
Result.Error(Exception("Failed to fetch list notif. Code: ${response.code()}"))
}
} catch (e: Exception){
Result.Error(e)
}
}
suspend fun getListNotifStore(): Result<List<NotifstoreItem>> {
return try {
val response = apiService.getNotifStore()
if (response.isSuccessful){
val chat = response.body()?.notifstore ?: emptyList()
Result.Success(chat)
} else {
Result.Error(Exception("Failed to fetch list notif. Code: ${response.code()}"))
}
} catch (e: Exception){
Result.Error(e)
}
}
companion object{ companion object{
private const val TAG = "UserRepository" private const val TAG = "UserRepository"
} }

View File

@ -1,67 +1,136 @@
package com.alya.ecommerce_serang.ui.notif package com.alya.ecommerce_serang.ui.notif
import android.content.Context import android.content.Context
import androidx.core.app.NotificationCompat import android.util.Log
import androidx.core.app.NotificationManagerCompat import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.alya.ecommerce_serang.data.api.dto.UserProfile import com.alya.ecommerce_serang.data.api.response.auth.HasStoreResponse
import com.alya.ecommerce_serang.data.api.response.auth.NotifItem
import com.alya.ecommerce_serang.data.api.response.auth.NotifstoreItem
import com.alya.ecommerce_serang.data.repository.Result import com.alya.ecommerce_serang.data.repository.Result
import com.alya.ecommerce_serang.data.repository.UserRepository import com.alya.ecommerce_serang.data.repository.UserRepository
import com.alya.ecommerce_serang.utils.SessionManager import com.alya.ecommerce_serang.utils.SessionManager
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import javax.inject.Inject import javax.inject.Inject
@HiltViewModel @HiltViewModel
class NotifViewModel @Inject constructor( class NotifViewModel @Inject constructor(
private val notificationBuilder: NotificationCompat.Builder,
private val notificationManager: NotificationManagerCompat,
@ApplicationContext private val context: Context, @ApplicationContext private val context: Context,
private val userRepository: UserRepository, private val userRepository: UserRepository,
private val webSocketManager: WebSocketManager,
private val sessionManager: SessionManager private val sessionManager: SessionManager
) : ViewModel() { ) : ViewModel() {
private val _userProfile = MutableStateFlow<Result<UserProfile?>>(Result.Loading) private val _notifList = MutableLiveData<Result<List<NotifItem>>>()
val userProfile: StateFlow<Result<UserProfile?>> = _userProfile.asStateFlow() val notifList: LiveData<Result<List<NotifItem>>> = _notifList
init { private val _checkStore = MutableLiveData<Boolean>()
fetchUserProfile() val checkStore: LiveData<Boolean> = _checkStore
}
// Fetch user profile to get necessary data private val _notifStoreList = MutableLiveData<Result<List<NotifstoreItem>>>()
fun fetchUserProfile() { val notifStoreList: LiveData<Result<List<NotifstoreItem>>> = _notifStoreList
fun getNotifList() {
Log.d(TAG, "getNotifList: Fetching personal notifications")
viewModelScope.launch { viewModelScope.launch {
_userProfile.value = Result.Loading try {
val result = userRepository.fetchUserProfile() Log.d(TAG, "getNotifList: Setting state to Loading")
_userProfile.value = result _notifList.value = Result.Loading
// If successful, save the user ID for WebSocket use Log.d(TAG, "getNotifList: Calling repository to get notifications")
if (result is Result.Success && result.data != null) { val result = userRepository.getListNotif()
sessionManager.saveUserId(result.data.userId.toString())
when (result) {
is Result.Success -> {
Log.d(TAG, "getNotifList: Success, received ${result.data?.size ?: 0} notifications")
if (result.data != null && result.data.isNotEmpty()) {
Log.d(TAG, "getNotifList: First notification - id: ${result.data[0].id}, title: ${result.data[0].title}")
if (result.data.size > 1) {
Log.d(TAG, "getNotifList: Last notification - id: ${result.data[result.data.size-1].id}, title: ${result.data[result.data.size-1].title}")
}
}
}
is Result.Error -> {
Log.e(TAG, "getNotifList: Error fetching notifications", result.exception)
}
is Result.Loading -> {
Log.d(TAG, "getNotifList: State is Loading")
}
}
_notifList.value = result
} catch (e: Exception) {
Log.e(TAG, "getNotifList: Unexpected error", e)
_notifList.value = Result.Error(e)
} }
} }
} }
// Start WebSocket connection fun getNotifStoreList() {
fun startWebSocketConnection() { Log.d(TAG, "getNotifStoreList: Fetching store notifications")
webSocketManager.startWebSocketConnection() viewModelScope.launch {
try {
Log.d(TAG, "getNotifStoreList: Setting state to Loading")
_notifStoreList.value = Result.Loading
Log.d(TAG, "getNotifStoreList: Calling repository to get store notifications")
val result = userRepository.getListNotifStore()
when (result) {
is Result.Success -> {
Log.d(TAG, "getNotifStoreList: Success, received ${result.data?.size ?: 0} store notifications")
if (result.data != null && result.data.isNotEmpty()) {
Log.d(TAG, "getNotifStoreList: First store notification - id: ${result.data[0].id}, title: ${result.data[0].title}")
if (result.data.size > 1) {
Log.d(TAG, "getNotifStoreList: Last store notification - id: ${result.data[result.data.size-1].id}, title: ${result.data[result.data.size-1].title}")
}
}
}
is Result.Error -> {
Log.e(TAG, "getNotifStoreList: Error fetching store notifications", result.exception)
}
is Result.Loading -> {
Log.d(TAG, "getNotifStoreList: State is Loading")
}
} }
// Stop WebSocket connection _notifStoreList.value = result
fun stopWebSocketConnection() { } catch (e: Exception) {
webSocketManager.stopWebSocketConnection() Log.e(TAG, "getNotifStoreList: Unexpected error", e)
_notifStoreList.value = Result.Error(e)
}
}
} }
// Call when ViewModel is cleared (e.g., app closing) fun checkStoreUser() {
override fun onCleared() { Log.d(TAG, "checkStoreUser: Checking if user has a store")
super.onCleared() viewModelScope.launch {
// No need to stop here - the service will manage its own lifecycle try {
// Call the repository function to check store
Log.d(TAG, "checkStoreUser: Calling repository to check store")
val response: HasStoreResponse = userRepository.checkStore()
// Log and store success message
Log.d(TAG, "checkStoreUser: Response received, hasStore=${response.hasStore}")
_checkStore.value = response.hasStore // Store the value for UI feedback
Log.d(TAG, "checkStoreUser: Updated _checkStore value to ${response.hasStore}")
} catch (exception: Exception) {
// Handle any errors and update state
Log.e(TAG, "checkStoreUser: Error checking store", exception)
_checkStore.value = false
Log.d(TAG, "checkStoreUser: Set _checkStore to false due to error")
} }
} }
}
companion object {
private const val TAG = "NotifViewModel" // Constant for logging tag
}
}

View File

@ -1,118 +1,324 @@
package com.alya.ecommerce_serang.ui.notif package com.alya.ecommerce_serang.ui.notif
import android.content.pm.PackageManager
import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.widget.Toast import android.util.Log
import android.view.View
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.ActivityCompat import androidx.recyclerview.widget.LinearLayoutManager
import androidx.core.content.ContextCompat
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import com.alya.ecommerce_serang.data.repository.Result import com.alya.ecommerce_serang.data.repository.Result
import com.alya.ecommerce_serang.databinding.ActivityNotificationBinding import com.alya.ecommerce_serang.databinding.ActivityNotificationBinding
import com.google.android.material.tabs.TabLayout
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch
@AndroidEntryPoint // Required for Hilt private const val TAG = "NotificationActivity"
@AndroidEntryPoint
class NotificationActivity : AppCompatActivity() { class NotificationActivity : AppCompatActivity() {
private lateinit var binding: ActivityNotificationBinding private lateinit var binding: ActivityNotificationBinding
private val viewModel: NotifViewModel by viewModels() private val viewModel: NotifViewModel by viewModels()
// Permission request code private lateinit var personalAdapter: PersonalNotificationAdapter
private val NOTIFICATION_PERMISSION_CODE = 100 private lateinit var storeAdapter: StoreNotificationAdapter
private var hasStore = false
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
Log.d(TAG, "onCreate: Starting NotificationActivity")
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.userProfile.collect { result ->
when (result) {
is com.alya.ecommerce_serang.data.repository.Result.Success -> {
// User profile loaded successfully
// Potentially do something with user profile
}
is com.alya.ecommerce_serang.data.repository.Result.Error -> {
// Handle error - show message, etc.
Toast.makeText(this@NotificationActivity,
"Failed to load profile",
Toast.LENGTH_SHORT
).show()
}
Result.Loading -> {
// Show loading indicator if needed
}
}
}
}
}
// Start WebSocket connection
// viewModel.startWebSocketConnection()
binding = ActivityNotificationBinding.inflate(layoutInflater) binding = ActivityNotificationBinding.inflate(layoutInflater)
setContentView(binding.root) setContentView(binding.root)
// Check and request notification permission for Android 13+ setupToolbar()
requestNotificationPermissionIfNeeded() setupAdapters()
setupTabLayout()
// Set up button click listeners setupSwipeRefresh()
// setupButtonListeners() setupObservers()
// Load initial data
Log.d(TAG, "onCreate: Checking if user has a store")
viewModel.checkStoreUser()
Log.d(TAG, "onCreate: Loading personal notifications")
viewModel.getNotifList()
// Show personal notifications by default
binding.tabLayout.selectTab(binding.tabLayout.getTabAt(0))
} }
// private fun setupButtonListeners() { private fun setupToolbar() {
// binding.simpleNotification.setOnClickListener { binding.btnBack.setOnClickListener {
// viewModel.showSimpleNotification() onBackPressed()
// } }
// }
// binding.updateNotification.setOnClickListener {
// viewModel.updateSimpleNotification()
// }
//
// binding.cancelNotification.setOnClickListener {
// viewModel.cancelSimpleNotification()
// }
// }
private fun requestNotificationPermissionIfNeeded() { private fun setupAdapters() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { Log.d(TAG, "setupAdapters: Creating adapters")
if (ContextCompat.checkSelfPermission(
this, // Create LayoutManager explicitly
android.Manifest.permission.POST_NOTIFICATIONS val layoutManager = LinearLayoutManager(this)
) != PackageManager.PERMISSION_GRANTED Log.d(TAG, "setupAdapters: Created LinearLayoutManager")
) {
ActivityCompat.requestPermissions( // Personal notifications adapter
this, personalAdapter = PersonalNotificationAdapter { notifItem ->
arrayOf(android.Manifest.permission.POST_NOTIFICATIONS), // Handle personal notification click
NOTIFICATION_PERMISSION_CODE Log.d(TAG, "Personal notification clicked: id=${notifItem.id}, type=${notifItem.type}")
) }
Log.d(TAG, "setupAdapters: Created personalAdapter")
// Store notifications adapter
storeAdapter = StoreNotificationAdapter { storeNotifItem ->
// Handle store notification click
Log.d(TAG, "Store notification clicked: id=${storeNotifItem.id}, type=${storeNotifItem.type}")
}
Log.d(TAG, "setupAdapters: Created storeAdapter")
// Configure RecyclerView with explicit steps
binding.recyclerViewNotif.setHasFixedSize(true)
binding.recyclerViewNotif.layoutManager = layoutManager
binding.recyclerViewNotif.adapter = personalAdapter
Log.d(TAG, "setupAdapters: RecyclerView configured with personalAdapter")
Log.d(TAG, "setupAdapters: RecyclerView visibility: ${binding.recyclerViewNotif.visibility == View.VISIBLE}")
}
private fun setupTabLayout() {
binding.tabLayout.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener {
override fun onTabSelected(tab: TabLayout.Tab) {
Log.d(TAG, "Tab selected: position ${tab.position}")
when (tab.position) {
0 -> {
Log.d(TAG, "Showing personal notifications tab")
binding.recyclerViewNotif.adapter = personalAdapter
showPersonalNotifications()
}
1 -> {
Log.d(TAG, "Showing store notifications tab, hasStore=$hasStore")
binding.recyclerViewNotif.adapter = storeAdapter
if (hasStore) {
viewModel.getNotifStoreList()
}
showStoreNotifications()
} }
} }
} }
// Handle permission request result override fun onTabUnselected(tab: TabLayout.Tab) {}
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<out String>,
grantResults: IntArray
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
if (requestCode == NOTIFICATION_PERMISSION_CODE) { override fun onTabReselected(tab: TabLayout.Tab) {}
if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) { })
// Permission granted }
Toast.makeText(this, "Notification permission granted", Toast.LENGTH_SHORT).show()
private fun setupSwipeRefresh() {
binding.swipeRefreshLayout.setOnRefreshListener {
Log.d(TAG, "Swipe refresh triggered, current tab: ${binding.tabLayout.selectedTabPosition}")
when (binding.tabLayout.selectedTabPosition) {
0 -> viewModel.getNotifList()
1 -> {
if (hasStore) {
viewModel.getNotifStoreList()
}
}
}
}
}
private fun setupObservers() {
// Observe checkStore to determine if user has a store
viewModel.checkStore.observe(this) { hasStoreValue ->
Log.d(TAG, "checkStore observed: $hasStoreValue")
// Update the local hasStore variable
hasStore = hasStoreValue
// If we're on the store tab, update UI based on hasStore value
if (binding.tabLayout.selectedTabPosition == 1) {
if (hasStore) {
Log.d(TAG, "User has store, loading store notifications")
viewModel.getNotifStoreList()
} else { } else {
// Permission denied Log.d(TAG, "User doesn't have store, showing empty state")
Toast.makeText(this, "Notification permission denied", Toast.LENGTH_SHORT).show() showEmptyState("Anda belum memiliki toko", true)
// You might want to show a dialog explaining why notifications are important }
}
}
// Observe personal notifications
viewModel.notifList.observe(this) { result ->
Log.d(TAG, "notifList observed: ${result.javaClass.simpleName}")
binding.swipeRefreshLayout.isRefreshing = false
if (binding.tabLayout.selectedTabPosition == 0) {
when (result) {
is Result.Success -> {
val notifications = result.data
Log.d(TAG, "Personal notifications received: ${notifications?.size ?: 0}")
if (notifications.isNullOrEmpty()) {
showEmptyState("Belum Ada Notifikasi", false)
} else {
hideEmptyState()
// Ensure adapter is attached
if (binding.recyclerViewNotif.adapter != personalAdapter) {
Log.d(TAG, "Re-attaching personalAdapter to RecyclerView")
binding.recyclerViewNotif.adapter = personalAdapter
}
personalAdapter.submitList(notifications)
// Force a layout pass
binding.recyclerViewNotif.post {
Log.d(TAG, "Forcing layout pass on RecyclerView")
binding.recyclerViewNotif.requestLayout()
}
}
}
is Result.Error -> {
Log.e(TAG, "Error loading personal notifications", result.exception)
showEmptyState("Gagal memuat notifikasi", false)
}
is Result.Loading -> {
Log.d(TAG, "Loading personal notifications")
} }
} }
} }
} }
// Observe store notifications
viewModel.notifStoreList.observe(this) { result ->
Log.d(TAG, "notifStoreList observed: ${result.javaClass.simpleName}")
binding.swipeRefreshLayout.isRefreshing = false
if (binding.tabLayout.selectedTabPosition == 1) {
when (result) {
is Result.Success -> {
val notifications = result.data
Log.d(TAG, "Store notifications received: ${notifications?.size ?: 0}, hasStore=$hasStore")
if (!hasStore) {
showEmptyState("Anda belum memiliki toko", true)
} else if (notifications.isNullOrEmpty()) {
showEmptyState("Belum Ada Notifikasi Toko", false)
} else {
hideEmptyState()
// Ensure adapter is attached
if (binding.recyclerViewNotif.adapter != storeAdapter) {
Log.d(TAG, "Re-attaching storeAdapter to RecyclerView")
binding.recyclerViewNotif.adapter = storeAdapter
}
storeAdapter.submitList(notifications)
// Force a layout pass
binding.recyclerViewNotif.post {
Log.d(TAG, "Forcing layout pass on RecyclerView")
binding.recyclerViewNotif.requestLayout()
}
}
}
is Result.Error -> {
Log.e(TAG, "Error loading store notifications", result.exception)
showEmptyState("Gagal memuat notifikasi toko", false)
}
is Result.Loading -> {
Log.d(TAG, "Loading store notifications")
}
}
}
}
}
private fun showPersonalNotifications() {
Log.d(TAG, "showPersonalNotifications called")
val result = viewModel.notifList.value
if (result is Result.Success) {
val notifications = result.data
Log.d(TAG, "showPersonalNotifications: Success with ${notifications?.size ?: 0} notifications")
if (notifications.isNullOrEmpty()) {
showEmptyState("Belum Ada Notifikasi", false)
} else {
hideEmptyState()
if (binding.recyclerViewNotif.adapter != personalAdapter) {
Log.d(TAG, "Re-attaching personalAdapter to RecyclerView")
binding.recyclerViewNotif.adapter = personalAdapter
}
personalAdapter.submitList(notifications)
// DEBUG: Debug the RecyclerView state
Log.d(TAG, "RecyclerView visibility: ${binding.recyclerViewNotif.visibility == View.VISIBLE}")
Log.d(TAG, "RecyclerView adapter item count: ${personalAdapter.itemCount}")
}
} else if (result is Result.Error) {
Log.e(TAG, "showPersonalNotifications: Error", result.exception)
showEmptyState("Gagal memuat notifikasi", false)
} else {
Log.d(TAG, "showPersonalNotifications: No data yet, triggering fetch")
// If we don't have data yet, trigger a fetch
viewModel.getNotifList()
}
}
private fun showStoreNotifications() {
Log.d(TAG, "showStoreNotifications called, hasStore=$hasStore")
if (!hasStore) {
showEmptyState("Anda belum memiliki toko", true)
return
}
val result = viewModel.notifStoreList.value
if (result is Result.Success) {
val notifications = result.data
Log.d(TAG, "showStoreNotifications: Success with ${notifications?.size ?: 0} notifications")
if (notifications.isNullOrEmpty()) {
showEmptyState("Belum Ada Notifikasi Toko", false)
} else {
hideEmptyState()
// Ensure adapter is attached
if (binding.recyclerViewNotif.adapter != storeAdapter) {
Log.d(TAG, "Re-attaching storeAdapter to RecyclerView")
binding.recyclerViewNotif.adapter = storeAdapter
}
storeAdapter.submitList(notifications)
// DEBUG: Debug the RecyclerView state
Log.d(TAG, "RecyclerView visibility: ${binding.recyclerViewNotif.visibility == View.VISIBLE}")
Log.d(TAG, "RecyclerView adapter item count: ${storeAdapter.itemCount}")
}
} else if (result is Result.Error) {
Log.e(TAG, "showStoreNotifications: Error", result.exception)
showEmptyState("Gagal memuat notifikasi toko", false)
} else {
Log.d(TAG, "showStoreNotifications: No data yet, triggering fetch")
// If we don't have data yet, trigger a fetch
viewModel.getNotifStoreList()
}
}
private fun showEmptyState(message: String, showCreateStoreButton: Boolean) {
Log.d(TAG, "showEmptyState: message='$message', showCreateStoreButton=$showCreateStoreButton")
binding.swipeRefreshLayout.visibility = View.GONE
binding.emptyStateLayout.visibility = View.VISIBLE
// Set empty state message
binding.tvEmptyTitle.text = message
// Show "Create Store" button and description if user doesn't have a store
if (showCreateStoreButton) {
binding.tvEmptyDesc.visibility = View.VISIBLE
binding.btnCreateStore.visibility = View.VISIBLE
// Set up create store button click listener
binding.btnCreateStore.setOnClickListener {
Log.d(TAG, "Create store button clicked")
// Navigate to create store screen
// Intent to CreateStoreActivity
}
} else {
binding.tvEmptyDesc.visibility = View.GONE
binding.btnCreateStore.visibility = View.GONE
}
}
private fun hideEmptyState() {
Log.d(TAG, "hideEmptyState called")
binding.swipeRefreshLayout.visibility = View.VISIBLE
binding.emptyStateLayout.visibility = View.GONE
// Ensure recycler view is visible
binding.recyclerViewNotif.visibility = View.VISIBLE
Log.d(TAG, "hideEmptyState: Set RecyclerView visibility to VISIBLE")
}
}

View File

@ -0,0 +1,93 @@
package com.alya.ecommerce_serang.ui.notif
import android.util.Log
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import com.alya.ecommerce_serang.data.api.response.auth.NotifItem
import com.alya.ecommerce_serang.databinding.ItemNotificationBinding
import java.text.SimpleDateFormat
import java.util.Locale
class PersonalNotificationAdapter(
private val onNotificationClick: (NotifItem) -> Unit
) : ListAdapter<NotifItem, PersonalNotificationAdapter.ViewHolder>(NotificationDiffCallback()) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
Log.d(TAG, "onCreateViewHolder: Creating ViewHolder")
val binding = ItemNotificationBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
return ViewHolder(binding, onNotificationClick)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val item = getItem(position)
Log.d(TAG, "onBindViewHolder: Binding notification at position $position, id=${item.id}")
holder.bind(item)
}
override fun submitList(list: List<NotifItem>?) {
Log.d(TAG, "submitList: Received list with ${list?.size ?: 0} items")
super.submitList(list)
}
class ViewHolder(
private val binding: ItemNotificationBinding,
private val onNotificationClick: (NotifItem) -> Unit
) : RecyclerView.ViewHolder(binding.root) {
fun bind(notification: NotifItem) {
binding.apply {
tvNotificationType.text = notification.type
tvTitle.text = notification.title
tvDescription.text = notification.message
// Format the date to show just the time
formatTimeDisplay(notification.createdAt)
// Handle notification click
root.setOnClickListener {
Log.d(TAG, "ViewHolder: Notification clicked, id=${notification.id}")
onNotificationClick(notification)
}
}
}
private fun formatTimeDisplay(createdAt: String) {
try {
// Parse the date with the expected format from API
val inputFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.getDefault())
val outputFormat = SimpleDateFormat("HH:mm", Locale.getDefault())
val date = inputFormat.parse(createdAt)
date?.let {
binding.tvTime.text = outputFormat.format(it)
}
} catch (e: Exception) {
// If date parsing fails, just display the raw value
Log.e(TAG, "formatTimeDisplay: Error parsing date", e)
binding.tvTime.text = createdAt
}
}
}
private class NotificationDiffCallback : DiffUtil.ItemCallback<NotifItem>() {
override fun areItemsTheSame(oldItem: NotifItem, newItem: NotifItem): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: NotifItem, newItem: NotifItem): Boolean {
return oldItem == newItem
}
}
companion object {
private const val TAG = "PersonalNotifAdapter"
}
}

View File

@ -0,0 +1,94 @@
package com.alya.ecommerce_serang.ui.notif
import android.util.Log
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import com.alya.ecommerce_serang.data.api.response.auth.NotifstoreItem
import com.alya.ecommerce_serang.databinding.ItemNotificationBinding
import java.text.SimpleDateFormat
import java.util.Locale
class StoreNotificationAdapter(
private val onNotificationClick: (NotifstoreItem) -> Unit
) : ListAdapter<NotifstoreItem, StoreNotificationAdapter.ViewHolder>(NotificationDiffCallback()) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
Log.d(TAG, "onCreateViewHolder: Creating ViewHolder")
val binding = ItemNotificationBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
return ViewHolder(binding, onNotificationClick)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val item = getItem(position)
Log.d(TAG, "onBindViewHolder: Binding store notification at position $position, id=${item.id}")
holder.bind(item)
}
override fun submitList(list: List<NotifstoreItem>?) {
Log.d(TAG, "submitList: Received list with ${list?.size ?: 0} items")
super.submitList(list)
}
class ViewHolder(
private val binding: ItemNotificationBinding,
private val onNotificationClick: (NotifstoreItem) -> Unit
) : RecyclerView.ViewHolder(binding.root) {
fun bind(notification: NotifstoreItem) {
binding.apply {
tvNotificationType.text = notification.type
tvTitle.text = notification.title
tvDescription.text = notification.message
// Format the date to show just the time
formatTimeDisplay(notification.createdAt)
// Handle notification click
root.setOnClickListener {
Log.d(TAG, "ViewHolder: Store notification clicked, id=${notification.id}")
onNotificationClick(notification)
}
}
}
private fun formatTimeDisplay(createdAt: String) {
try {
// Parse the date with the expected format from API
val inputFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.getDefault())
val outputFormat = SimpleDateFormat("HH:mm", Locale.getDefault())
val date = inputFormat.parse(createdAt)
date?.let {
binding.tvTime.text = outputFormat.format(it)
}
} catch (e: Exception) {
// If date parsing fails, just display the raw value
Log.e(TAG, "formatTimeDisplay: Error parsing date", e)
binding.tvTime.text = createdAt
}
}
}
private class NotificationDiffCallback : DiffUtil.ItemCallback<NotifstoreItem>() {
override fun areItemsTheSame(oldItem: NotifstoreItem, newItem: NotifstoreItem): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: NotifstoreItem, newItem: NotifstoreItem): Boolean {
return oldItem == newItem
}
}
companion object{
private const val TAG = "StoreNotifAdapter"
}
}

View File

@ -8,22 +8,158 @@
android:layout_height="match_parent" android:layout_height="match_parent"
tools:context=".ui.notif.NotificationActivity"> tools:context=".ui.notif.NotificationActivity">
<Button <androidx.appcompat.widget.Toolbar
android:id="@+id/simple_notification" android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="@android:color/white"
android:elevation="4dp"
app:layout_constraintTop_toTopOf="parent">
<ImageButton
android:id="@+id/btnBack"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="simple notificaton"/> android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="Back"
android:src="@drawable/ic_back_24" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="32dp"
android:text="Notifikasi"
android:textColor="@android:color/black"
android:textSize="18sp"
android:textStyle="bold" />
</androidx.appcompat.widget.Toolbar>
<com.google.android.material.tabs.TabLayout
android:id="@+id/tabLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="@id/toolbar"
app:tabIndicatorColor="@color/blue_500"
app:tabSelectedTextColor="@color/blue_500"
app:tabTextColor="@android:color/darker_gray">
<com.google.android.material.tabs.TabItem
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Notifikasi Saya" />
<com.google.android.material.tabs.TabItem
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Notifikasi Toko" />
</com.google.android.material.tabs.TabLayout>
<TextView
android:id="@+id/tvMarkAllRead"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:padding="8dp"
android:text="Tandai Sudah Dibaca Semua"
android:textColor="@color/blue_500"
android:textSize="12sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/tabLayout" />
<TextView
android:id="@+id/tvNewest"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="8dp"
android:text="Terbaru"
android:textColor="@android:color/black"
android:textSize="14sp"
android:textStyle="bold"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tvMarkAllRead" />
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/swipeRefreshLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="visible"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toBottomOf="@id/tvNewest">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerViewNotif"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:padding="8dp"
android:scrollbars="vertical"
android:visibility="visible"
tools:listitem="@layout/item_notification"/>
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/emptyStateLayout"
android:layout_width="match_parent"
android:layout_height="0dp"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toBottomOf="@id/tvNewest">
<ImageView
android:id="@+id/ivEmptyState"
android:layout_width="120dp"
android:layout_height="120dp"
android:src="@drawable/outline_notifications_24"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.4" />
<TextView
android:id="@+id/tvEmptyTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="Belum Ada Notifikasi"
android:textColor="@android:color/black"
android:textSize="16sp"
android:textStyle="bold"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/ivEmptyState" />
<TextView
android:id="@+id/tvEmptyDesc"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:gravity="center"
android:padding="16dp"
android:text="Anda belum memiliki toko. Buat toko untuk menerima notifikasi toko."
android:textColor="@android:color/darker_gray"
android:textSize="14sp"
android:visibility="gone"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tvEmptyTitle" />
<Button <Button
android:id="@+id/update_notification" android:id="@+id/btnCreateStore"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="update notificaton"/> android:layout_marginTop="16dp"
android:background="@drawable/bg_button_filled"
android:padding="12dp"
android:text="Buat Toko"
android:textColor="@android:color/white"
android:visibility="gone"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tvEmptyDesc" />
</androidx.constraintlayout.widget.ConstraintLayout>
<Button
android:id="@+id/cancel_notification"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="cancel notificaton"/>
</LinearLayout> </LinearLayout>

View File

@ -0,0 +1,59 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="8dp"
android:layout_marginVertical="4dp"
app:cardCornerRadius="8dp"
app:cardElevation="2dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="12dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:id="@+id/tvNotificationType"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Penjualan"
android:textColor="@android:color/darker_gray"
android:textSize="12sp" />
<TextView
android:id="@+id/tvTime"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="09:30"
android:textColor="@android:color/darker_gray"
android:textSize="12sp" />
</LinearLayout>
<TextView
android:id="@+id/tvTitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:text="Buat Tagihan untuk Pesanan #12345678 Sekarang"
android:textColor="@android:color/black"
android:textSize="14sp"
android:textStyle="bold" />
<TextView
android:id="@+id/tvDescription"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:text="Hai, Nama Toko, yuk buat tagihan untuk pesanan #12345678 sebelum tanggal 12-12-2024 pukul 09.30 WIB!"
android:textColor="@android:color/black"
android:textSize="14sp" />
</LinearLayout>
</androidx.cardview.widget.CardView>

View File

@ -30,7 +30,6 @@
android:backgroundTint="@color/white" android:backgroundTint="@color/white"
android:src="@drawable/outline_notifications_24" android:src="@drawable/outline_notifications_24"
app:layout_constraintDimensionRatio="1:1" app:layout_constraintDimensionRatio="1:1"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="@id/search" app:layout_constraintBottom_toBottomOf="@id/search"
app:layout_constraintEnd_toStartOf="@id/btn_cart" app:layout_constraintEnd_toStartOf="@id/btn_cart"
app:layout_constraintTop_toTopOf="@id/search"/> app:layout_constraintTop_toTopOf="@id/search"/>