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.FcmTokenResponse
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.LoginResponse
import com.alya.ecommerce_serang.data.api.response.auth.OtpResponse
@ -454,4 +456,12 @@ interface ApiService {
@GET("chat")
suspend fun getChatList(
): 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.ListStoreTypeResponse
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.RegisterResponse
import com.alya.ecommerce_serang.data.api.response.auth.RegisterStoreResponse
@ -345,6 +347,36 @@ class UserRepository(private val apiService: ApiService) {
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{
private const val TAG = "UserRepository"
}

View File

@ -1,67 +1,136 @@
package com.alya.ecommerce_serang.ui.notif
import android.content.Context
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import android.util.Log
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
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.UserRepository
import com.alya.ecommerce_serang.utils.SessionManager
import dagger.hilt.android.lifecycle.HiltViewModel
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 javax.inject.Inject
@HiltViewModel
class NotifViewModel @Inject constructor(
private val notificationBuilder: NotificationCompat.Builder,
private val notificationManager: NotificationManagerCompat,
@ApplicationContext private val context: Context,
private val userRepository: UserRepository,
private val webSocketManager: WebSocketManager,
private val sessionManager: SessionManager
) : ViewModel() {
private val _userProfile = MutableStateFlow<Result<UserProfile?>>(Result.Loading)
val userProfile: StateFlow<Result<UserProfile?>> = _userProfile.asStateFlow()
private val _notifList = MutableLiveData<Result<List<NotifItem>>>()
val notifList: LiveData<Result<List<NotifItem>>> = _notifList
init {
fetchUserProfile()
}
private val _checkStore = MutableLiveData<Boolean>()
val checkStore: LiveData<Boolean> = _checkStore
// Fetch user profile to get necessary data
fun fetchUserProfile() {
private val _notifStoreList = MutableLiveData<Result<List<NotifstoreItem>>>()
val notifStoreList: LiveData<Result<List<NotifstoreItem>>> = _notifStoreList
fun getNotifList() {
Log.d(TAG, "getNotifList: Fetching personal notifications")
viewModelScope.launch {
_userProfile.value = Result.Loading
val result = userRepository.fetchUserProfile()
_userProfile.value = result
try {
Log.d(TAG, "getNotifList: Setting state to Loading")
_notifList.value = Result.Loading
// If successful, save the user ID for WebSocket use
if (result is Result.Success && result.data != null) {
sessionManager.saveUserId(result.data.userId.toString())
Log.d(TAG, "getNotifList: Calling repository to get notifications")
val result = userRepository.getListNotif()
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 startWebSocketConnection() {
webSocketManager.startWebSocketConnection()
fun getNotifStoreList() {
Log.d(TAG, "getNotifStoreList: Fetching store notifications")
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")
}
}
_notifStoreList.value = result
} catch (e: Exception) {
Log.e(TAG, "getNotifStoreList: Unexpected error", e)
_notifStoreList.value = Result.Error(e)
}
}
}
// Stop WebSocket connection
fun stopWebSocketConnection() {
webSocketManager.stopWebSocketConnection()
fun checkStoreUser() {
Log.d(TAG, "checkStoreUser: Checking if user has a store")
viewModelScope.launch {
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")
}
}
}
// Call when ViewModel is cleared (e.g., app closing)
override fun onCleared() {
super.onCleared()
// No need to stop here - the service will manage its own lifecycle
companion object {
private const val TAG = "NotifViewModel" // Constant for logging tag
}
}

View File

@ -1,118 +1,324 @@
package com.alya.ecommerce_serang.ui.notif
import android.content.pm.PackageManager
import android.os.Build
import android.os.Bundle
import android.widget.Toast
import android.util.Log
import android.view.View
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.recyclerview.widget.LinearLayoutManager
import com.alya.ecommerce_serang.data.repository.Result
import com.alya.ecommerce_serang.databinding.ActivityNotificationBinding
import com.google.android.material.tabs.TabLayout
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch
@AndroidEntryPoint // Required for Hilt
private const val TAG = "NotificationActivity"
@AndroidEntryPoint
class NotificationActivity : AppCompatActivity() {
private lateinit var binding: ActivityNotificationBinding
private val viewModel: NotifViewModel by viewModels()
// Permission request code
private val NOTIFICATION_PERMISSION_CODE = 100
private lateinit var personalAdapter: PersonalNotificationAdapter
private lateinit var storeAdapter: StoreNotificationAdapter
private var hasStore = false
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Log.d(TAG, "onCreate: Starting NotificationActivity")
binding = ActivityNotificationBinding.inflate(layoutInflater)
setContentView(binding.root)
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
setupToolbar()
setupAdapters()
setupTabLayout()
setupSwipeRefresh()
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 setupToolbar() {
binding.btnBack.setOnClickListener {
onBackPressed()
}
}
private fun setupAdapters() {
Log.d(TAG, "setupAdapters: Creating adapters")
// Create LayoutManager explicitly
val layoutManager = LinearLayoutManager(this)
Log.d(TAG, "setupAdapters: Created LinearLayoutManager")
// Personal notifications adapter
personalAdapter = PersonalNotificationAdapter { notifItem ->
// Handle personal notification click
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()
}
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
showStoreNotifications()
}
}
}
override fun onTabUnselected(tab: TabLayout.Tab) {}
override fun onTabReselected(tab: TabLayout.Tab) {}
})
}
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 {
Log.d(TAG, "User doesn't have store, showing empty state")
showEmptyState("Anda belum memiliki toko", true)
}
}
}
// 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")
}
}
}
}
// Start WebSocket connection
// viewModel.startWebSocketConnection()
// Observe store notifications
viewModel.notifStoreList.observe(this) { result ->
Log.d(TAG, "notifStoreList observed: ${result.javaClass.simpleName}")
binding.swipeRefreshLayout.isRefreshing = false
binding = ActivityNotificationBinding.inflate(layoutInflater)
setContentView(binding.root)
// Check and request notification permission for Android 13+
requestNotificationPermissionIfNeeded()
// Set up button click listeners
// setupButtonListeners()
}
// private fun setupButtonListeners() {
// binding.simpleNotification.setOnClickListener {
// viewModel.showSimpleNotification()
// }
//
// binding.updateNotification.setOnClickListener {
// viewModel.updateSimpleNotification()
// }
//
// binding.cancelNotification.setOnClickListener {
// viewModel.cancelSimpleNotification()
// }
// }
private fun requestNotificationPermissionIfNeeded() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
if (ContextCompat.checkSelfPermission(
this,
android.Manifest.permission.POST_NOTIFICATIONS
) != PackageManager.PERMISSION_GRANTED
) {
ActivityCompat.requestPermissions(
this,
arrayOf(android.Manifest.permission.POST_NOTIFICATIONS),
NOTIFICATION_PERMISSION_CODE
)
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")
}
}
}
}
}
// Handle permission request result
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<out String>,
grantResults: IntArray
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
private fun showPersonalNotifications() {
Log.d(TAG, "showPersonalNotifications called")
val result = viewModel.notifList.value
if (requestCode == NOTIFICATION_PERMISSION_CODE) {
if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
// Permission granted
Toast.makeText(this, "Notification permission granted", Toast.LENGTH_SHORT).show()
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 {
// Permission denied
Toast.makeText(this, "Notification permission denied", Toast.LENGTH_SHORT).show()
// You might want to show a dialog explaining why notifications are important
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"
tools:context=".ui.notif.NotificationActivity">
<Button
android:id="@+id/simple_notification"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="simple notificaton"/>
<androidx.appcompat.widget.Toolbar
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">
<Button
android:id="@+id/update_notification"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="update notificaton"/>
<ImageButton
android:id="@+id/btnBack"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="Back"
android:src="@drawable/ic_back_24" />
<Button
android:id="@+id/cancel_notification"
<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:text="cancel notificaton"/>
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
android:id="@+id/btnCreateStore"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
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>
</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:src="@drawable/outline_notifications_24"
app:layout_constraintDimensionRatio="1:1"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="@id/search"
app:layout_constraintEnd_toStartOf="@id/btn_cart"
app:layout_constraintTop_toTopOf="@id/search"/>