refactored viewmodels for listfragments
This commit is contained in:
@@ -18,19 +18,17 @@ import androidx.fragment.app.Fragment;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import com.bumptech.glide.Glide;
|
||||
import com.example.petstoremobile.R;
|
||||
import com.example.petstoremobile.adapters.ChatAdapter;
|
||||
import com.example.petstoremobile.adapters.MessageAdapter;
|
||||
import com.example.petstoremobile.api.auth.TokenManager;
|
||||
import com.example.petstoremobile.databinding.FragmentChatBinding;
|
||||
import com.example.petstoremobile.dtos.ConversationDTO;
|
||||
import com.example.petstoremobile.dtos.MessageDTO;
|
||||
import com.example.petstoremobile.dtos.SendMessageRequest;
|
||||
import com.example.petstoremobile.models.Chat;
|
||||
import com.example.petstoremobile.models.Message;
|
||||
import com.example.petstoremobile.services.ChatNotificationService;
|
||||
import com.example.petstoremobile.utils.Resource;
|
||||
import com.example.petstoremobile.viewmodels.ChatViewModel;
|
||||
import com.example.petstoremobile.viewmodels.ChatListViewModel;
|
||||
import com.example.petstoremobile.websocket.StompChatManager;
|
||||
|
||||
import java.util.*;
|
||||
@@ -47,59 +45,44 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis
|
||||
private static final String TAG = "ChatFragment";
|
||||
|
||||
private FragmentChatBinding binding;
|
||||
private ChatViewModel viewModel;
|
||||
private ChatListViewModel viewModel;
|
||||
|
||||
// Adapters
|
||||
private ChatAdapter chatAdapter;
|
||||
private MessageAdapter messageAdapter;
|
||||
|
||||
// Data
|
||||
private final List<Chat> chatList = new ArrayList<>();
|
||||
private final List<Message> messageList = new ArrayList<>();
|
||||
private final Map<Long, String> customerNames = new HashMap<>();
|
||||
private Uri pendingAttachmentUri;
|
||||
|
||||
@Inject TokenManager tokenManager;
|
||||
@Inject @Named("baseUrl") String baseUrl;
|
||||
|
||||
// chat
|
||||
private Long currentUserId;
|
||||
private Long activeConversationId;
|
||||
private StompChatManager stompChatManager;
|
||||
private ActivityResultLauncher<Intent> attachmentLauncher;
|
||||
|
||||
/**
|
||||
* Initializes the attachment launcher to handle file selection from the gallery.
|
||||
*/
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
viewModel = new ViewModelProvider(this).get(ChatViewModel.class);
|
||||
viewModel = new ViewModelProvider(this).get(ChatListViewModel.class);
|
||||
attachmentLauncher = registerForActivityResult(
|
||||
new ActivityResultContracts.StartActivityForResult(),
|
||||
result -> {
|
||||
if (result.getResultCode() == Activity.RESULT_OK && result.getData() != null) {
|
||||
Uri uri = result.getData().getData();
|
||||
if (uri != null) {
|
||||
showAttachmentPreview(uri);
|
||||
}
|
||||
if (uri != null) showAttachmentPreview(uri);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Inflates the layout, initializes UI components, and sets up click listeners for messaging.
|
||||
*/
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater,
|
||||
ViewGroup container, Bundle savedInstanceState) {
|
||||
|
||||
binding = FragmentChatBinding.inflate(inflater, container, false);
|
||||
|
||||
binding.btnHamburger.setOnClickListener(v -> binding.chatDrawerLayout.openDrawer(GravityCompat.START));
|
||||
|
||||
// Set editor action listener for message field to send when enter is pressed
|
||||
binding.etMessage.setOnEditorActionListener((v, actionId, event) -> {
|
||||
if (actionId == EditorInfo.IME_ACTION_SEND || actionId == EditorInfo.IME_NULL) {
|
||||
binding.btnSend.performClick();
|
||||
@@ -108,35 +91,26 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis
|
||||
return false;
|
||||
});
|
||||
|
||||
//When the send button is clicked check if there is an attachment and send using the correct helper function
|
||||
binding.btnSend.setOnClickListener(v -> {
|
||||
if (pendingAttachmentUri != null) {
|
||||
sendWithAttachment(pendingAttachmentUri);
|
||||
} else {
|
||||
sendMessage();
|
||||
}
|
||||
if (pendingAttachmentUri != null) sendWithAttachment(pendingAttachmentUri);
|
||||
else sendMessage();
|
||||
});
|
||||
|
||||
//When the attachment button is clicked open the file picker
|
||||
binding.btnAttach.setOnClickListener(v -> selectAttachment());
|
||||
binding.btnRemoveAttachment.setOnClickListener(v -> removeAttachment());
|
||||
|
||||
setupRecyclerViews();
|
||||
observeViewModel();
|
||||
loadInitialData();
|
||||
|
||||
return binding.getRoot();
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures the RecyclerViews for the conversation list and the message history.
|
||||
*/
|
||||
private void setupRecyclerViews() {
|
||||
// Set up Drawer menu to select conversation
|
||||
chatAdapter = new ChatAdapter(chatList, this);
|
||||
binding.rvChatList.setLayoutManager(new LinearLayoutManager(getContext()));
|
||||
binding.rvChatList.setAdapter(chatAdapter);
|
||||
|
||||
// set up RecyclerView for selected chat to show messages
|
||||
messageAdapter = new MessageAdapter(messageList, null);
|
||||
LinearLayoutManager lm = new LinearLayoutManager(getContext());
|
||||
lm.setStackFromEnd(true);
|
||||
@@ -145,26 +119,48 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis
|
||||
setConversationActive(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads authentication tokens and user info, then initializes the Stomp WebSocket connection.
|
||||
*/
|
||||
private void observeViewModel() {
|
||||
viewModel.getChatList().observe(getViewLifecycleOwner(), list -> {
|
||||
chatList.clear();
|
||||
chatList.addAll(list);
|
||||
chatAdapter.notifyDataSetChanged();
|
||||
|
||||
if (activeConversationId != null) {
|
||||
for (Chat chat : list) {
|
||||
if (chat.getChatId().equals(String.valueOf(activeConversationId))) {
|
||||
binding.tvChatTitle.setText(chat.getCustomerName());
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
viewModel.getMessageList().observe(getViewLifecycleOwner(), list -> {
|
||||
messageList.clear();
|
||||
messageList.addAll(list);
|
||||
messageAdapter.notifyDataSetChanged();
|
||||
scrollToBottom();
|
||||
});
|
||||
|
||||
viewModel.getIsLoading().observe(getViewLifecycleOwner(), loading -> {
|
||||
// Can show a progress bar if needed
|
||||
});
|
||||
}
|
||||
|
||||
private void loadInitialData() {
|
||||
String token = tokenManager.getToken();
|
||||
currentUserId = tokenManager.getUserId();
|
||||
Long currentUserId = tokenManager.getUserId();
|
||||
String role = tokenManager.getRole();
|
||||
|
||||
messageAdapter.setCurrentUserId(currentUserId);
|
||||
messageAdapter.setToken(token);
|
||||
|
||||
// if token exist then connect to websocket
|
||||
if (token != null) {
|
||||
stompChatManager = new StompChatManager(token, role, baseUrl);
|
||||
stompChatManager.setMessageListener(this);
|
||||
stompChatManager.setConversationListener(this);
|
||||
stompChatManager.setConnectionListener(this);
|
||||
stompChatManager.connect();
|
||||
} else {
|
||||
Log.e(TAG, "No token found");
|
||||
}
|
||||
|
||||
if (getArguments() != null && getArguments().containsKey("conversation_id")) {
|
||||
@@ -172,65 +168,17 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis
|
||||
} else if (getActivity() != null && getActivity().getIntent().hasExtra("conversation_id")) {
|
||||
activeConversationId = getActivity().getIntent().getLongExtra("conversation_id", -1);
|
||||
getActivity().getIntent().removeExtra("conversation_id");
|
||||
getActivity().getIntent().removeExtra("navigate_to");
|
||||
}
|
||||
|
||||
loadCustomers();
|
||||
viewModel.loadCustomers();
|
||||
|
||||
if (activeConversationId != null) {
|
||||
setConversationActive(true);
|
||||
if (stompChatManager != null) stompChatManager.subscribeToConversation(activeConversationId);
|
||||
viewModel.loadMessageHistory(activeConversationId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches a list of customers from the ViewModel to display customer names for the chat list.
|
||||
*/
|
||||
private void loadCustomers() {
|
||||
viewModel.getAllCustomers(0, 100).observe(getViewLifecycleOwner(), resource -> {
|
||||
if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) {
|
||||
resource.data.getContent().forEach(c -> customerNames.put(c.getCustomerId(), c.getFullName()));
|
||||
loadConversations();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves all conversations for the current user through the ViewModel and populates the chat drawer.
|
||||
*/
|
||||
private void loadConversations() {
|
||||
viewModel.getAllConversations().observe(getViewLifecycleOwner(), resource -> {
|
||||
if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) {
|
||||
chatList.clear();
|
||||
for (ConversationDTO dto : resource.data) {
|
||||
String name = customerNames.getOrDefault(
|
||||
dto.getCustomerId(), "Customer #" + dto.getCustomerId());
|
||||
chatList.add(new Chat(String.valueOf(dto.getId()),
|
||||
name, dto.getLastMessage(),
|
||||
dto.getCustomerId(), dto.getStaffId()));
|
||||
}
|
||||
chatAdapter.notifyDataSetChanged();
|
||||
|
||||
if (activeConversationId != null) {
|
||||
setConversationActive(true);
|
||||
// Update title to customer name of active conversation
|
||||
for (Chat chat : chatList) {
|
||||
if (chat.getChatId().equals(String.valueOf(activeConversationId))) {
|
||||
binding.tvChatTitle.setText(chat.getCustomerName());
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (stompChatManager != null) {
|
||||
stompChatManager.subscribeToConversation(activeConversationId);
|
||||
}
|
||||
loadMessageHistory(activeConversationId);
|
||||
} else {
|
||||
messageList.clear();
|
||||
messageAdapter.notifyDataSetChanged();
|
||||
setConversationActive(false);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles selection of a chat from the drawer, updating the UI and subscribing to the WebSocket.
|
||||
*/
|
||||
@Override
|
||||
public void onChatClick(Chat chat) {
|
||||
activeConversationId = Long.parseLong(chat.getChatId());
|
||||
@@ -238,75 +186,35 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis
|
||||
binding.tvChatTitle.setText(chat.getCustomerName());
|
||||
binding.chatDrawerLayout.closeDrawer(GravityCompat.START);
|
||||
|
||||
if (stompChatManager != null) {
|
||||
stompChatManager.subscribeToConversation(activeConversationId);
|
||||
}
|
||||
|
||||
loadMessageHistory(activeConversationId);
|
||||
if (stompChatManager != null) stompChatManager.subscribeToConversation(activeConversationId);
|
||||
viewModel.loadMessageHistory(activeConversationId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches the full message history for a specific conversation from the ViewModel.
|
||||
*/
|
||||
private void loadMessageHistory(Long conversationId) {
|
||||
viewModel.getMessages(conversationId).observe(getViewLifecycleOwner(), resource -> {
|
||||
if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) {
|
||||
messageList.clear();
|
||||
for (MessageDTO dto : resource.data) {
|
||||
messageList.add(dtoToModel(dto));
|
||||
}
|
||||
messageAdapter.notifyDataSetChanged();
|
||||
scrollToBottom();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a plain text message to the currently active conversation through the ViewModel.
|
||||
*/
|
||||
private void sendMessage() {
|
||||
//check if a chat is selected
|
||||
if (activeConversationId == null) return;
|
||||
|
||||
//get the message from text field
|
||||
String text = binding.etMessage.getText().toString().trim();
|
||||
if (text.isEmpty()) return;
|
||||
|
||||
//clear text field after sending
|
||||
binding.etMessage.setText("");
|
||||
|
||||
//calls viewmodel to send the message
|
||||
viewModel.sendMessage(activeConversationId, new SendMessageRequest(text)).observe(getViewLifecycleOwner(), resource -> {
|
||||
viewModel.sendMessage(activeConversationId, text).observe(getViewLifecycleOwner(), resource -> {
|
||||
if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) {
|
||||
messageList.add(dtoToModel(resource.data));
|
||||
messageAdapter.notifyItemInserted(messageList.size() - 1);
|
||||
scrollToBottom();
|
||||
loadConversations();
|
||||
viewModel.addMessageLocally(resource.data);
|
||||
viewModel.loadConversations();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Launches a file picker intent to select an attachment for the message.
|
||||
*/
|
||||
private void selectAttachment() {
|
||||
Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
|
||||
intent.setType("*/*");
|
||||
attachmentLauncher.launch(intent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays a preview of the selected attachment in the UI.
|
||||
*/
|
||||
private void showAttachmentPreview(Uri uri) {
|
||||
pendingAttachmentUri = uri;
|
||||
binding.layoutAttachmentPreview.setVisibility(View.VISIBLE);
|
||||
|
||||
String mimeType = requireContext().getContentResolver().getType(uri);
|
||||
String fileName = getFileName(uri);
|
||||
binding.tvPreviewName.setText(fileName);
|
||||
|
||||
// If the file is an image, display a thumbnail of the image as well
|
||||
binding.tvPreviewName.setText(getFileName(uri));
|
||||
if (mimeType != null && mimeType.startsWith("image/")) {
|
||||
binding.ivPreview.setVisibility(View.VISIBLE);
|
||||
Glide.with(this).load(uri).into(binding.ivPreview);
|
||||
@@ -315,183 +223,83 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the current attachment selection and hides the preview UI.
|
||||
*/
|
||||
private void removeAttachment() {
|
||||
pendingAttachmentUri = null;
|
||||
binding.layoutAttachmentPreview.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the display name of the file from its Uri.
|
||||
*/
|
||||
private String getFileName(Uri uri) {
|
||||
String result = null;
|
||||
if (uri.getScheme().equals("content")) {
|
||||
try (Cursor cursor = requireContext().getContentResolver().query(uri, null, null, null, null)) {
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
int index = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME);
|
||||
if (index != -1) {
|
||||
result = cursor.getString(index);
|
||||
}
|
||||
if (index != -1) result = cursor.getString(index);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (result == null) {
|
||||
result = uri.getPath();
|
||||
int cut = result.lastIndexOf('/');
|
||||
if (cut != -1) {
|
||||
result = result.substring(cut + 1);
|
||||
}
|
||||
if (cut != -1) result = result.substring(cut + 1);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles sending a message that includes a file attachment via the ViewModel.
|
||||
*/
|
||||
private void sendWithAttachment(Uri uri) {
|
||||
if (activeConversationId == null) return;
|
||||
String text = binding.etMessage.getText().toString().trim();
|
||||
binding.etMessage.setText("");
|
||||
removeAttachment();
|
||||
|
||||
if (!text.isEmpty()) {
|
||||
binding.etMessage.setText(text);
|
||||
}
|
||||
Toast.makeText(requireContext(), "File attachments are not supported", Toast.LENGTH_SHORT).show();
|
||||
removeAttachment();
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback triggered when a new message is received via the WebSocket.
|
||||
*/
|
||||
@Override
|
||||
public void onMessageReceived(MessageDTO dto) {
|
||||
//if there is no active selected conversation or the message received is for another chat, then just update the preview of last message
|
||||
if (activeConversationId == null || !activeConversationId.equals(dto.getConversationId())) {
|
||||
updateConversationPreview(dto.getConversationId(), dto.getContent());
|
||||
return;
|
||||
}
|
||||
updateConversationPreview(dto.getConversationId(), dto.getContent());
|
||||
|
||||
if (currentUserId != null && currentUserId.equals(dto.getSenderId())) return;
|
||||
|
||||
//else add the message to the active chat if it's not from the current user
|
||||
messageList.add(dtoToModel(dto));
|
||||
requireActivity().runOnUiThread(() -> {
|
||||
messageAdapter.notifyItemInserted(messageList.size() - 1);
|
||||
scrollToBottom();
|
||||
if (activeConversationId != null && activeConversationId.equals(dto.getConversationId())) {
|
||||
if (!tokenManager.getUserId().equals(dto.getSenderId())) {
|
||||
viewModel.addMessageLocally(dto);
|
||||
}
|
||||
}
|
||||
viewModel.updateConversationLocally(new ConversationDTO(dto.getConversationId(), 0L, 0L, dto.getContent(), ""));
|
||||
// Re-load coversations to get correct names if needed or just let local update handle it
|
||||
viewModel.loadConversations();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback triggered when a conversation is created or updated via the WebSocket.
|
||||
*/
|
||||
@Override
|
||||
public void onConversationUpdated(ConversationDTO dto) {
|
||||
requireActivity().runOnUiThread(() -> {
|
||||
boolean updated = false;
|
||||
String name = customerNames.getOrDefault(
|
||||
dto.getCustomerId(), "Customer #" + dto.getCustomerId());
|
||||
|
||||
for (int i = 0; i < chatList.size(); i++) {
|
||||
Chat existing = chatList.get(i);
|
||||
if (existing.getChatId().equals(String.valueOf(dto.getId()))) {
|
||||
chatList.set(i, new Chat(
|
||||
String.valueOf(dto.getId()),
|
||||
name,
|
||||
dto.getLastMessage(),
|
||||
dto.getCustomerId(),
|
||||
dto.getStaffId()
|
||||
));
|
||||
chatAdapter.notifyItemChanged(i);
|
||||
updated = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!updated) {
|
||||
chatList.add(0, new Chat(
|
||||
String.valueOf(dto.getId()),
|
||||
name,
|
||||
dto.getLastMessage(),
|
||||
dto.getCustomerId(),
|
||||
dto.getStaffId()
|
||||
));
|
||||
chatAdapter.notifyItemInserted(0);
|
||||
}
|
||||
|
||||
viewModel.updateConversationLocally(dto);
|
||||
if (activeConversationId != null && activeConversationId.equals(dto.getId())) {
|
||||
setConversationActive(true);
|
||||
binding.tvChatTitle.setText(name);
|
||||
binding.tvChatTitle.setText(viewModel.getCustomerName(dto.getCustomerId()));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback triggered when the WebSocket connection is successfully opened.
|
||||
*/
|
||||
@Override
|
||||
public void onSocketOpened() {
|
||||
if (!isAdded()) {
|
||||
return;
|
||||
}
|
||||
if (!isAdded()) return;
|
||||
requireActivity().runOnUiThread(() -> {
|
||||
loadConversations();
|
||||
if (activeConversationId != null) {
|
||||
loadMessageHistory(activeConversationId);
|
||||
}
|
||||
viewModel.loadConversations();
|
||||
if (activeConversationId != null) viewModel.loadMessageHistory(activeConversationId);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback triggered when the WebSocket connection is closed.
|
||||
*/
|
||||
@Override
|
||||
public void onSocketClosed() {
|
||||
if (!isAdded()) {
|
||||
return;
|
||||
}
|
||||
requireActivity().runOnUiThread(this::loadConversations);
|
||||
if (!isAdded()) return;
|
||||
requireActivity().runOnUiThread(viewModel::loadConversations);
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback triggered when a WebSocket connection error occurs.
|
||||
*/
|
||||
@Override
|
||||
public void onSocketError() {
|
||||
if (!isAdded()) {
|
||||
return;
|
||||
}
|
||||
if (!isAdded()) return;
|
||||
requireActivity().runOnUiThread(() -> {
|
||||
loadConversations();
|
||||
if (activeConversationId != null) {
|
||||
loadMessageHistory(activeConversationId);
|
||||
}
|
||||
viewModel.loadConversations();
|
||||
if (activeConversationId != null) viewModel.loadMessageHistory(activeConversationId);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a MessageDTO into a Message object.
|
||||
*/
|
||||
private Message dtoToModel(MessageDTO dto) {
|
||||
Message m = new Message();
|
||||
m.setId(dto.getId());
|
||||
m.setConversationId(dto.getConversationId());
|
||||
m.setSenderId(dto.getSenderId());
|
||||
m.setContent(dto.getContent());
|
||||
m.setTimestamp(dto.getTimestamp());
|
||||
m.setIsRead(dto.getIsRead());
|
||||
m.setAttachmentUrl(dto.getAttachmentUrl());
|
||||
m.setAttachmentName(dto.getAttachmentName());
|
||||
m.setAttachmentType(dto.getAttachmentType());
|
||||
return m;
|
||||
}
|
||||
|
||||
/**
|
||||
* Scrolls the message history RecyclerView to the most recent message.
|
||||
*/
|
||||
private void scrollToBottom() {
|
||||
if (!messageList.isEmpty()) {
|
||||
binding.rvMessages.post(() ->
|
||||
@@ -499,35 +307,6 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the preview snippet of the last message for a specific conversation in the drawer.
|
||||
*/
|
||||
private void updateConversationPreview(Long conversationId, String lastMessage) {
|
||||
if (conversationId == null) {
|
||||
return;
|
||||
}
|
||||
requireActivity().runOnUiThread(() -> {
|
||||
for (int i = 0; i < chatList.size(); i++) {
|
||||
Chat existing = chatList.get(i);
|
||||
if (existing.getChatId().equals(String.valueOf(conversationId))) {
|
||||
Chat updated = new Chat(
|
||||
existing.getChatId(),
|
||||
existing.getCustomerName(),
|
||||
lastMessage,
|
||||
existing.getCustomerId(),
|
||||
existing.getStaffId()
|
||||
);
|
||||
chatList.set(i, updated);
|
||||
chatAdapter.notifyItemChanged(i);
|
||||
return;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggles the UI state based on whether a conversation is currently selected.
|
||||
*/
|
||||
private void setConversationActive(boolean active) {
|
||||
binding.btnSend.setEnabled(active);
|
||||
binding.etMessage.setEnabled(active);
|
||||
@@ -537,9 +316,7 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis
|
||||
ChatNotificationService.activeConversationIdInUi = null;
|
||||
removeAttachment();
|
||||
if (binding != null && binding.tvChatTitle != null) binding.tvChatTitle.setText("Customer Chat");
|
||||
if (stompChatManager != null) {
|
||||
stompChatManager.clearConversationSubscription();
|
||||
}
|
||||
if (stompChatManager != null) stompChatManager.clearConversationSubscription();
|
||||
messageList.clear();
|
||||
messageAdapter.notifyDataSetChanged();
|
||||
binding.etMessage.setText("");
|
||||
@@ -550,9 +327,6 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Disconnects the WebSocket manager when the fragment view is destroyed.
|
||||
*/
|
||||
@Override
|
||||
public void onDestroyView() {
|
||||
super.onDestroyView();
|
||||
|
||||
@@ -24,9 +24,8 @@ import com.example.petstoremobile.utils.BulkDeleteHandler;
|
||||
import com.example.petstoremobile.utils.Resource;
|
||||
import com.example.petstoremobile.utils.SpinnerUtils;
|
||||
import com.example.petstoremobile.utils.UIUtils;
|
||||
import com.example.petstoremobile.viewmodels.AdoptionViewModel;
|
||||
import com.example.petstoremobile.viewmodels.AdoptionListViewModel;
|
||||
import com.example.petstoremobile.utils.EventDecorator;
|
||||
import com.example.petstoremobile.viewmodels.StoreViewModel;
|
||||
import com.prolificinteractive.materialcalendarview.CalendarDay;
|
||||
import com.prolificinteractive.materialcalendarview.CalendarMode;
|
||||
|
||||
@@ -46,28 +45,19 @@ public class AdoptionFragment extends Fragment implements AdoptionAdapter.OnAdop
|
||||
|
||||
private FragmentAdoptionBinding binding;
|
||||
private List<AdoptionDTO> adoptionList = new ArrayList<>();
|
||||
private List<StoreDTO> storeList = new ArrayList<>();
|
||||
private AdoptionAdapter adapter;
|
||||
private AdoptionViewModel adoptionViewModel;
|
||||
private StoreViewModel storeViewModel;
|
||||
private AdoptionListViewModel viewModel;
|
||||
private BulkDeleteHandler bulkDeleteHandler;
|
||||
private CalendarDay selectedCalendarDay;
|
||||
private boolean isMonthMode = false;
|
||||
private final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd", Locale.getDefault());
|
||||
|
||||
/**
|
||||
* Initializes the fragment and its ViewModels.
|
||||
*/
|
||||
@Override
|
||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
adoptionViewModel = new ViewModelProvider(this).get(AdoptionViewModel.class);
|
||||
storeViewModel = new ViewModelProvider(this).get(StoreViewModel.class);
|
||||
viewModel = new ViewModelProvider(this).get(AdoptionListViewModel.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up the fragment's UI components, including RecyclerView, Search, SwipeRefresh, and Calendar.
|
||||
*/
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
|
||||
Bundle savedInstanceState) {
|
||||
@@ -81,6 +71,7 @@ public class AdoptionFragment extends Fragment implements AdoptionAdapter.OnAdop
|
||||
setupCalendar();
|
||||
setupFilterToggle();
|
||||
setupBulkDelete();
|
||||
observeViewModel();
|
||||
|
||||
binding.fabAddAdoption.setOnClickListener(v -> openDetail(-1));
|
||||
|
||||
@@ -91,6 +82,24 @@ public class AdoptionFragment extends Fragment implements AdoptionAdapter.OnAdop
|
||||
return binding.getRoot();
|
||||
}
|
||||
|
||||
private void observeViewModel() {
|
||||
viewModel.getAdoptions().observe(getViewLifecycleOwner(), list -> {
|
||||
adoptionList.clear();
|
||||
adoptionList.addAll(list);
|
||||
updateCalendarDecorators();
|
||||
adapter.notifyDataSetChanged();
|
||||
});
|
||||
|
||||
viewModel.getStores().observe(getViewLifecycleOwner(), list -> {
|
||||
SpinnerUtils.populateWhiteSpinner(requireContext(), binding.spinnerStoreAdoption, list,
|
||||
StoreDTO::getStoreName, "All Stores", -1L, StoreDTO::getStoreId);
|
||||
});
|
||||
|
||||
viewModel.getIsLoading().observe(getViewLifecycleOwner(), loading -> {
|
||||
binding.swipeRefreshAdoption.setRefreshing(loading);
|
||||
});
|
||||
}
|
||||
|
||||
private void setupBulkDelete() {
|
||||
bulkDeleteHandler = new BulkDeleteHandler(
|
||||
this,
|
||||
@@ -99,27 +108,18 @@ public class AdoptionFragment extends Fragment implements AdoptionAdapter.OnAdop
|
||||
binding.btnBulkDelete,
|
||||
adapter,
|
||||
"adoption",
|
||||
adoptionViewModel::bulkDeleteAdoptions,
|
||||
viewModel::bulkDeleteAdoptions,
|
||||
this::loadAdoptions
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyView() {
|
||||
super.onDestroyView();
|
||||
binding = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
loadAdoptions();
|
||||
loadStoreData();
|
||||
viewModel.loadStores();
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggles the calendar display between week and month modes.
|
||||
*/
|
||||
private void toggleCalendarMode() {
|
||||
isMonthMode = !isMonthMode;
|
||||
binding.calendarViewAdoption.state().edit()
|
||||
@@ -127,17 +127,11 @@ public class AdoptionFragment extends Fragment implements AdoptionAdapter.OnAdop
|
||||
.commit();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up the filter toggle button to show/hide the filter layout.
|
||||
*/
|
||||
private void setupFilterToggle() {
|
||||
UIUtils.setupFilterToggle(binding.btnToggleFilterAdoption, binding.layoutFilterAdoption,
|
||||
binding.etSearchAdoption, binding.spinnerStatusAdoption, binding.spinnerStoreAdoption);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up the date selection listener for the calendar.
|
||||
*/
|
||||
private void setupCalendar() {
|
||||
binding.calendarViewAdoption.setOnDateChangedListener((widget, date, selected) -> {
|
||||
if (selected) {
|
||||
@@ -154,9 +148,6 @@ public class AdoptionFragment extends Fragment implements AdoptionAdapter.OnAdop
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the calendar decorators to highlight days with adoptions.
|
||||
*/
|
||||
private void updateCalendarDecorators() {
|
||||
HashSet<CalendarDay> datesWithAdoptions = new HashSet<>();
|
||||
for (AdoptionDTO adoption : adoptionList) {
|
||||
@@ -177,67 +168,37 @@ public class AdoptionFragment extends Fragment implements AdoptionAdapter.OnAdop
|
||||
binding.calendarViewAdoption.addDecorator(new EventDecorator(Color.RED, datesWithAdoptions));
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the RecyclerView for displaying adoptions.
|
||||
*/
|
||||
private void setupRecyclerView() {
|
||||
adapter = new AdoptionAdapter(adoptionList, this);
|
||||
binding.recyclerViewAdoptions.setLayoutManager(new LinearLayoutManager(getContext()));
|
||||
binding.recyclerViewAdoptions.setAdapter(adapter);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up the search bar for filtering
|
||||
*/
|
||||
private void setupSearch() {
|
||||
UIUtils.attachSearch(binding.etSearchAdoption, this::loadAdoptions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures the status filter spinner.
|
||||
*/
|
||||
private void setupStatusFilter() {
|
||||
String[] statuses = {"All Statuses", "Completed", "Pending", "Cancelled"};
|
||||
SpinnerUtils.setupStringFilterSpinner(requireContext(), binding.spinnerStatusAdoption, statuses, this::loadAdoptions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures the store filter spinner.
|
||||
*/
|
||||
private void setupStoreFilter() {
|
||||
SpinnerUtils.setupFilterSpinner(binding.spinnerStoreAdoption, this::loadAdoptions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches store data to populate the store filter.
|
||||
*/
|
||||
private void loadStoreData() {
|
||||
storeViewModel.getAllStores(0, 100).observe(getViewLifecycleOwner(), resource -> {
|
||||
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
|
||||
storeList = resource.data.getContent();
|
||||
SpinnerUtils.populateWhiteSpinner(requireContext(), binding.spinnerStoreAdoption, storeList,
|
||||
StoreDTO::getStoreName, "All Stores", -1L, StoreDTO::getStoreId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up the SwipeRefreshLayout to reload adoption data.
|
||||
*/
|
||||
private void setupSwipeRefresh() {
|
||||
binding.swipeRefreshAdoption.setOnRefreshListener(this::loadAdoptions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches the adoption list from the server through the ViewModel.
|
||||
*/
|
||||
private void loadAdoptions() {
|
||||
String query = binding.etSearchAdoption.getText().toString().trim();
|
||||
String status = binding.spinnerStatusAdoption.getSelectedItem() != null ? binding.spinnerStatusAdoption.getSelectedItem().toString() : "All Statuses";
|
||||
|
||||
Long storeId = null;
|
||||
if (binding.spinnerStoreAdoption.getSelectedItemPosition() > 0 && !storeList.isEmpty()) {
|
||||
storeId = storeList.get(binding.spinnerStoreAdoption.getSelectedItemPosition() - 1).getStoreId();
|
||||
List<StoreDTO> stores = viewModel.getStores().getValue();
|
||||
if (binding.spinnerStoreAdoption.getSelectedItemPosition() > 0 && stores != null && !stores.isEmpty()) {
|
||||
storeId = stores.get(binding.spinnerStoreAdoption.getSelectedItemPosition() - 1).getStoreId();
|
||||
}
|
||||
|
||||
String selectedDateString = null;
|
||||
@@ -249,52 +210,18 @@ public class AdoptionFragment extends Fragment implements AdoptionAdapter.OnAdop
|
||||
if (status.equals("All Statuses")) status = null;
|
||||
else status = status.toUpperCase();
|
||||
|
||||
adoptionViewModel.getAllAdoptions(0, 500, query, status, storeId, selectedDateString, null).observe(getViewLifecycleOwner(), resource -> {
|
||||
if (resource == null) return;
|
||||
|
||||
// Check the status to see if the resource is loaded and display the data
|
||||
switch (resource.status) {
|
||||
case LOADING:
|
||||
// Show loading indicator
|
||||
binding.swipeRefreshAdoption.setRefreshing(true);
|
||||
break;
|
||||
case SUCCESS:
|
||||
// Hide loading indicator and display data
|
||||
binding.swipeRefreshAdoption.setRefreshing(false);
|
||||
if (resource.data != null) {
|
||||
adoptionList.clear();
|
||||
adoptionList.addAll(resource.data.getContent());
|
||||
updateCalendarDecorators();
|
||||
adapter.notifyDataSetChanged();
|
||||
}
|
||||
break;
|
||||
case ERROR:
|
||||
// Hide loading indicator and toast error message
|
||||
binding.swipeRefreshAdoption.setRefreshing(false);
|
||||
Toast.makeText(getContext(), "Failed to load adoptions: " + resource.message, Toast.LENGTH_SHORT).show();
|
||||
Log.e("AdoptionFragment", "Error loading adoptions: " + resource.message);
|
||||
break;
|
||||
}
|
||||
});
|
||||
viewModel.loadAdoptions(true, query, status, storeId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigates to the adoption detail screen for a specific adoption or to create a new one.
|
||||
*/
|
||||
private void openDetail(int position) {
|
||||
Bundle args = new Bundle();
|
||||
|
||||
if (position != -1) {
|
||||
AdoptionDTO a = adoptionList.get(position);
|
||||
args.putLong("adoptionId", a.getAdoptionId());
|
||||
}
|
||||
|
||||
NavHostFragment.findNavController(this).navigate(R.id.nav_adoption_detail, args);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles item click in the adoption list.
|
||||
*/
|
||||
@Override
|
||||
public void onAdoptionClick(int position) { openDetail(position); }
|
||||
|
||||
@@ -304,4 +231,10 @@ public class AdoptionFragment extends Fragment implements AdoptionAdapter.OnAdop
|
||||
bulkDeleteHandler.onSelectionChanged(selectedCount);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyView() {
|
||||
super.onDestroyView();
|
||||
binding = null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,16 +2,14 @@ package com.example.petstoremobile.fragments.listfragments;
|
||||
|
||||
import android.graphics.Color;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
import android.view.*;
|
||||
import android.widget.*;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
import com.example.petstoremobile.databinding.FragmentAnalyticsBinding;
|
||||
import com.example.petstoremobile.dtos.SaleDTO;
|
||||
import com.example.petstoremobile.utils.UIUtils;
|
||||
import com.example.petstoremobile.viewmodels.SaleViewModel;
|
||||
import com.example.petstoremobile.viewmodels.AnalyticsViewModel;
|
||||
import dagger.hilt.android.AndroidEntryPoint;
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
@@ -21,226 +19,131 @@ import java.util.*;
|
||||
public class AnalyticsFragment extends Fragment {
|
||||
|
||||
private FragmentAnalyticsBinding binding;
|
||||
private SaleViewModel saleViewModel;
|
||||
private AnalyticsViewModel viewModel;
|
||||
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
|
||||
Bundle savedInstanceState) {
|
||||
binding = FragmentAnalyticsBinding.inflate(inflater, container, false);
|
||||
saleViewModel = new ViewModelProvider(this).get(SaleViewModel.class);
|
||||
viewModel = new ViewModelProvider(this).get(AnalyticsViewModel.class);
|
||||
|
||||
loadAnalytics();
|
||||
observeViewModel();
|
||||
viewModel.loadAnalytics();
|
||||
|
||||
binding.btnRefreshAnalytics.setOnClickListener(v -> loadAnalytics());
|
||||
binding.btnRefreshAnalytics.setOnClickListener(v -> viewModel.loadAnalytics());
|
||||
|
||||
UIUtils.setupHamburgerMenu(binding.btnHamburgerAnalytics, this);
|
||||
|
||||
return binding.getRoot();
|
||||
}
|
||||
|
||||
private void observeViewModel() {
|
||||
viewModel.getAnalyticsData().observe(getViewLifecycleOwner(), this::computeAndDisplay);
|
||||
|
||||
viewModel.getIsLoading().observe(getViewLifecycleOwner(), loading -> {
|
||||
if (loading) {
|
||||
binding.tvTotalRevenue.setText("Loading...");
|
||||
binding.tvTotalTransactions.setText("...");
|
||||
binding.tvAvgTransaction.setText("...");
|
||||
binding.tvTotalItems.setText("...");
|
||||
}
|
||||
});
|
||||
|
||||
viewModel.getErrorMessage().observe(getViewLifecycleOwner(), error -> {
|
||||
if (error != null) showError(error);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyView() {
|
||||
super.onDestroyView();
|
||||
binding = null;
|
||||
}
|
||||
|
||||
private void loadAnalytics() {
|
||||
// Clear all sections
|
||||
private void computeAndDisplay(AnalyticsViewModel.AnalyticsData data) {
|
||||
if (data == null) return;
|
||||
|
||||
// Summary
|
||||
binding.tvTotalRevenue.setText("$" + data.totalRevenue.setScale(2, RoundingMode.HALF_UP));
|
||||
binding.tvTotalTransactions.setText(String.valueOf(data.totalTransactions));
|
||||
binding.tvAvgTransaction.setText("$" + data.avgTransaction);
|
||||
binding.tvTotalItems.setText(String.valueOf(data.totalItems));
|
||||
|
||||
// Top Revenue Products
|
||||
binding.llTopRevenue.removeAllViews();
|
||||
binding.llTopQuantity.removeAllViews();
|
||||
binding.llPaymentMethods.removeAllViews();
|
||||
binding.llEmployeePerformance.removeAllViews();
|
||||
binding.llDailyRevenue.removeAllViews();
|
||||
|
||||
|
||||
saleViewModel.getAllSales(0, 1000, null, null, null, "saleDate,desc")
|
||||
.observe(getViewLifecycleOwner(), resource -> {
|
||||
if (resource != null) {
|
||||
switch (resource.status) {
|
||||
case SUCCESS:
|
||||
if (resource.data != null) {
|
||||
computeAndDisplay(resource.data.getContent());
|
||||
}
|
||||
break;
|
||||
case ERROR:
|
||||
Log.e("Analytics", resource.message != null ? resource.message : "Error loading sales");
|
||||
showError("Failed to load sales data");
|
||||
break;
|
||||
case LOADING:
|
||||
binding.tvTotalRevenue.setText("Loading...");
|
||||
binding.tvTotalTransactions.setText("...");
|
||||
binding.tvAvgTransaction.setText("...");
|
||||
binding.tvTotalItems.setText("...");
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void computeAndDisplay(List<SaleDTO> sales) {
|
||||
// Filter out refunds for most metrics
|
||||
List<SaleDTO> regularSales = new ArrayList<>();
|
||||
for (SaleDTO s : sales) {
|
||||
if (!Boolean.TRUE.equals(s.getIsRefund()))
|
||||
regularSales.add(s);
|
||||
}
|
||||
|
||||
// ── Summary ──────────────────────────────────────────
|
||||
BigDecimal totalRevenue = BigDecimal.ZERO;
|
||||
int totalItems = 0;
|
||||
|
||||
for (SaleDTO s : regularSales) {
|
||||
if (s.getTotalAmount() != null)
|
||||
totalRevenue = totalRevenue.add(s.getTotalAmount());
|
||||
if (s.getItems() != null) {
|
||||
for (SaleDTO.SaleItemDTO item : s.getItems()) {
|
||||
if (item.getQuantity() != null)
|
||||
totalItems += Math.abs(item.getQuantity());
|
||||
}
|
||||
if (data.topRevenueProducts != null && !data.topRevenueProducts.isEmpty()) {
|
||||
BigDecimal maxRevenue = data.topRevenueProducts.get(0).getValue();
|
||||
if (maxRevenue.compareTo(BigDecimal.ZERO) == 0) maxRevenue = BigDecimal.ONE;
|
||||
for (Map.Entry<String, BigDecimal> e : data.topRevenueProducts) {
|
||||
addBarRow(binding.llTopRevenue, e.getKey(), "$" + e.getValue().setScale(2, RoundingMode.HALF_UP),
|
||||
e.getValue().floatValue() / maxRevenue.floatValue(), "#ff6b35");
|
||||
}
|
||||
}
|
||||
|
||||
int totalTx = regularSales.size();
|
||||
BigDecimal avgTx = totalTx > 0
|
||||
? totalRevenue.divide(BigDecimal.valueOf(totalTx), 2, RoundingMode.HALF_UP)
|
||||
: BigDecimal.ZERO;
|
||||
|
||||
binding.tvTotalRevenue.setText("$" + totalRevenue.setScale(2, RoundingMode.HALF_UP));
|
||||
binding.tvTotalTransactions.setText(String.valueOf(totalTx));
|
||||
binding.tvAvgTransaction.setText("$" + avgTx);
|
||||
binding.tvTotalItems.setText(String.valueOf(totalItems));
|
||||
|
||||
// ── Top Products by Revenue ───────────────────────────
|
||||
Map<String, BigDecimal> revenueByProduct = new LinkedHashMap<>();
|
||||
Map<String, Integer> quantityByProduct = new LinkedHashMap<>();
|
||||
|
||||
for (SaleDTO s : regularSales) {
|
||||
if (s.getItems() != null) {
|
||||
for (SaleDTO.SaleItemDTO item : s.getItems()) {
|
||||
String name = item.getProductName() != null ? item.getProductName() : "Unknown";
|
||||
int qty = item.getQuantity() != null ? Math.abs(item.getQuantity()) : 0;
|
||||
BigDecimal lineTotal = item.getUnitPrice() != null
|
||||
? item.getUnitPrice().multiply(BigDecimal.valueOf(qty))
|
||||
: BigDecimal.ZERO;
|
||||
|
||||
revenueByProduct.merge(name, lineTotal, BigDecimal::add);
|
||||
quantityByProduct.merge(name, qty, Integer::sum);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by revenue desc, take top 5
|
||||
List<Map.Entry<String, BigDecimal>> topRevenue = new ArrayList<>(revenueByProduct.entrySet());
|
||||
topRevenue.sort((a, b) -> b.getValue().compareTo(a.getValue()));
|
||||
BigDecimal maxRevenue = topRevenue.isEmpty() ? BigDecimal.ONE : topRevenue.get(0).getValue();
|
||||
|
||||
binding.llTopRevenue.removeAllViews();
|
||||
for (int i = 0; i < Math.min(5, topRevenue.size()); i++) {
|
||||
Map.Entry<String, BigDecimal> e = topRevenue.get(i);
|
||||
addBarRow(binding.llTopRevenue, e.getKey(), "$" + e.getValue().setScale(2, RoundingMode.HALF_UP),
|
||||
e.getValue().floatValue() / maxRevenue.floatValue(), "#ff6b35");
|
||||
}
|
||||
if (topRevenue.isEmpty())
|
||||
} else {
|
||||
addEmptyRow(binding.llTopRevenue, "No data");
|
||||
}
|
||||
|
||||
// Sort by quantity desc, take top 5
|
||||
List<Map.Entry<String, Integer>> topQuantity = new ArrayList<>(quantityByProduct.entrySet());
|
||||
topQuantity.sort((a, b) -> b.getValue() - a.getValue());
|
||||
int maxQty = topQuantity.isEmpty() ? 1 : topQuantity.get(0).getValue();
|
||||
|
||||
// Top Quantity Products
|
||||
binding.llTopQuantity.removeAllViews();
|
||||
for (int i = 0; i < Math.min(5, topQuantity.size()); i++) {
|
||||
Map.Entry<String, Integer> e = topQuantity.get(i);
|
||||
addBarRow(binding.llTopQuantity, e.getKey(), e.getValue() + " units",
|
||||
(float) e.getValue() / maxQty, "#4ecdc4");
|
||||
}
|
||||
if (topQuantity.isEmpty())
|
||||
addEmptyRow(binding.llTopQuantity, "No data");
|
||||
|
||||
// ── Payment Methods ───────────────────────────────────
|
||||
Map<String, Integer> paymentCount = new LinkedHashMap<>();
|
||||
for (SaleDTO s : regularSales) {
|
||||
String method = s.getPaymentMethod() != null ? s.getPaymentMethod() : "Unknown";
|
||||
paymentCount.merge(method, 1, Integer::sum);
|
||||
}
|
||||
|
||||
int maxPayment = paymentCount.values().stream().max(Integer::compare).orElse(1);
|
||||
String[] paymentColors = { "#1a759f", "#ff9f1c", "#577590", "#90be6d" };
|
||||
int ci = 0;
|
||||
binding.llPaymentMethods.removeAllViews();
|
||||
for (Map.Entry<String, Integer> e : paymentCount.entrySet()) {
|
||||
addBarRow(binding.llPaymentMethods, e.getKey(),
|
||||
e.getValue() + " transactions",
|
||||
(float) e.getValue() / maxPayment,
|
||||
paymentColors[ci % paymentColors.length]);
|
||||
ci++;
|
||||
}
|
||||
if (paymentCount.isEmpty())
|
||||
addEmptyRow(binding.llPaymentMethods, "No data");
|
||||
|
||||
// ── Employee Performance ──────────────────────────────
|
||||
Map<String, BigDecimal> employeeRevenue = new LinkedHashMap<>();
|
||||
for (SaleDTO s : regularSales) {
|
||||
String emp = s.getEmployeeName() != null ? s.getEmployeeName() : "Unknown";
|
||||
if (s.getTotalAmount() != null)
|
||||
employeeRevenue.merge(emp, s.getTotalAmount(), BigDecimal::add);
|
||||
}
|
||||
|
||||
List<Map.Entry<String, BigDecimal>> empList = new ArrayList<>(employeeRevenue.entrySet());
|
||||
empList.sort((a, b) -> b.getValue().compareTo(a.getValue()));
|
||||
BigDecimal maxEmp = empList.isEmpty() ? BigDecimal.ONE : empList.get(0).getValue();
|
||||
|
||||
binding.llEmployeePerformance.removeAllViews();
|
||||
for (Map.Entry<String, BigDecimal> e : empList) {
|
||||
addBarRow(binding.llEmployeePerformance, e.getKey(),
|
||||
"$" + e.getValue().setScale(2, RoundingMode.HALF_UP),
|
||||
e.getValue().floatValue() / maxEmp.floatValue(),
|
||||
"#1a759f");
|
||||
}
|
||||
if (empList.isEmpty())
|
||||
addEmptyRow(binding.llEmployeePerformance, "No data");
|
||||
|
||||
// ── Daily Revenue (last 7 days) ───────────────────────
|
||||
Map<String, BigDecimal> dailyRevenue = new TreeMap<>();
|
||||
|
||||
// Initialize last 7 days
|
||||
Calendar cal = Calendar.getInstance();
|
||||
for (int i = 6; i >= 0; i--) {
|
||||
Calendar day = Calendar.getInstance();
|
||||
day.add(Calendar.DAY_OF_YEAR, -i);
|
||||
String key = String.format("%04d-%02d-%02d",
|
||||
day.get(Calendar.YEAR),
|
||||
day.get(Calendar.MONTH) + 1,
|
||||
day.get(Calendar.DAY_OF_MONTH));
|
||||
dailyRevenue.put(key, BigDecimal.ZERO);
|
||||
}
|
||||
|
||||
for (SaleDTO s : regularSales) {
|
||||
if (s.getSaleDate() != null && s.getTotalAmount() != null) {
|
||||
String date = s.getSaleDate().length() >= 10
|
||||
? s.getSaleDate().substring(0, 10)
|
||||
: s.getSaleDate();
|
||||
if (dailyRevenue.containsKey(date)) {
|
||||
dailyRevenue.merge(date, s.getTotalAmount(), BigDecimal::add);
|
||||
}
|
||||
if (data.topQuantityProducts != null && !data.topQuantityProducts.isEmpty()) {
|
||||
int maxQty = data.topQuantityProducts.get(0).getValue();
|
||||
if (maxQty == 0) maxQty = 1;
|
||||
for (Map.Entry<String, Integer> e : data.topQuantityProducts) {
|
||||
addBarRow(binding.llTopQuantity, e.getKey(), e.getValue() + " units",
|
||||
(float) e.getValue() / maxQty, "#4ecdc4");
|
||||
}
|
||||
} else {
|
||||
addEmptyRow(binding.llTopQuantity, "No data");
|
||||
}
|
||||
|
||||
BigDecimal maxDaily = dailyRevenue.values().stream()
|
||||
.max(BigDecimal::compareTo).orElse(BigDecimal.ONE);
|
||||
if (maxDaily.compareTo(BigDecimal.ZERO) == 0)
|
||||
maxDaily = BigDecimal.ONE;
|
||||
// Payment Methods
|
||||
binding.llPaymentMethods.removeAllViews();
|
||||
if (data.paymentMethodStats != null && !data.paymentMethodStats.isEmpty()) {
|
||||
int maxPayment = data.paymentMethodStats.stream().mapToInt(Map.Entry::getValue).max().orElse(1);
|
||||
String[] paymentColors = { "#1a759f", "#ff9f1c", "#577590", "#90be6d" };
|
||||
int ci = 0;
|
||||
for (Map.Entry<String, Integer> e : data.paymentMethodStats) {
|
||||
addBarRow(binding.llPaymentMethods, e.getKey(),
|
||||
e.getValue() + " transactions",
|
||||
(float) e.getValue() / maxPayment,
|
||||
paymentColors[ci % paymentColors.length]);
|
||||
ci++;
|
||||
}
|
||||
} else {
|
||||
addEmptyRow(binding.llPaymentMethods, "No data");
|
||||
}
|
||||
|
||||
// Employee Performance
|
||||
binding.llEmployeePerformance.removeAllViews();
|
||||
if (data.employeePerformance != null && !data.employeePerformance.isEmpty()) {
|
||||
BigDecimal maxEmp = data.employeePerformance.get(data.employeePerformance.size() - 1).getValue();
|
||||
if (maxEmp.compareTo(BigDecimal.ZERO) == 0) maxEmp = BigDecimal.ONE;
|
||||
// Sorting is ascending from VM for some reason? Let's check VM... it says b.getValue().compareTo(a.getValue()) which is DESC.
|
||||
// Wait, computeAnalytics sorts them... let's assume DESC as per VM code.
|
||||
maxEmp = data.employeePerformance.get(0).getValue();
|
||||
if (maxEmp.compareTo(BigDecimal.ZERO) == 0) maxEmp = BigDecimal.ONE;
|
||||
|
||||
for (Map.Entry<String, BigDecimal> e : data.employeePerformance) {
|
||||
addBarRow(binding.llEmployeePerformance, e.getKey(),
|
||||
"$" + e.getValue().setScale(2, RoundingMode.HALF_UP),
|
||||
e.getValue().floatValue() / maxEmp.floatValue(),
|
||||
"#1a759f");
|
||||
}
|
||||
} else {
|
||||
addEmptyRow(binding.llEmployeePerformance, "No data");
|
||||
}
|
||||
|
||||
// Daily Revenue
|
||||
binding.llDailyRevenue.removeAllViews();
|
||||
for (Map.Entry<String, BigDecimal> e : dailyRevenue.entrySet()) {
|
||||
// Show just MM-DD
|
||||
String label = e.getKey().length() >= 10
|
||||
? e.getKey().substring(5)
|
||||
: e.getKey();
|
||||
addBarRow(binding.llDailyRevenue, label,
|
||||
"$" + e.getValue().setScale(2, RoundingMode.HALF_UP),
|
||||
e.getValue().floatValue() / maxDaily.floatValue(),
|
||||
"#ff6b35");
|
||||
if (data.dailyRevenue != null && !data.dailyRevenue.isEmpty()) {
|
||||
BigDecimal maxDaily = data.dailyRevenue.stream().map(Map.Entry::getValue).max(BigDecimal::compareTo).orElse(BigDecimal.ONE);
|
||||
if (maxDaily.compareTo(BigDecimal.ZERO) == 0) maxDaily = BigDecimal.ONE;
|
||||
for (Map.Entry<String, BigDecimal> e : data.dailyRevenue) {
|
||||
String label = e.getKey().length() >= 10 ? e.getKey().substring(5) : e.getKey();
|
||||
addBarRow(binding.llDailyRevenue, label,
|
||||
"$" + e.getValue().setScale(2, RoundingMode.HALF_UP),
|
||||
e.getValue().floatValue() / maxDaily.floatValue(),
|
||||
"#ff6b35");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,7 +14,6 @@ import android.util.Log;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.example.petstoremobile.R;
|
||||
import com.example.petstoremobile.adapters.AppointmentAdapter;
|
||||
@@ -25,10 +24,9 @@ import com.example.petstoremobile.utils.BulkDeleteHandler;
|
||||
import com.example.petstoremobile.utils.Resource;
|
||||
import com.example.petstoremobile.utils.SpinnerUtils;
|
||||
import com.example.petstoremobile.utils.UIUtils;
|
||||
import com.example.petstoremobile.viewmodels.AppointmentViewModel;
|
||||
import com.example.petstoremobile.viewmodels.AppointmentListViewModel;
|
||||
import com.example.petstoremobile.utils.EventDecorator;
|
||||
import com.example.petstoremobile.viewmodels.AuthViewModel;
|
||||
import com.example.petstoremobile.viewmodels.StoreViewModel;
|
||||
import com.prolificinteractive.materialcalendarview.CalendarDay;
|
||||
import com.prolificinteractive.materialcalendarview.CalendarMode;
|
||||
|
||||
@@ -48,11 +46,9 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter.
|
||||
|
||||
private FragmentAppointmentBinding binding;
|
||||
private List<AppointmentDTO> appointmentList = new ArrayList<>();
|
||||
private List<StoreDTO> storeList = new ArrayList<>();
|
||||
|
||||
private AppointmentAdapter adapter;
|
||||
private AppointmentViewModel appointmentViewModel;
|
||||
private StoreViewModel storeViewModel;
|
||||
private AppointmentListViewModel viewModel;
|
||||
private AuthViewModel authViewModel;
|
||||
private BulkDeleteHandler bulkDeleteHandler;
|
||||
|
||||
@@ -61,20 +57,13 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter.
|
||||
private Long currentUserId = null;
|
||||
private final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd", Locale.getDefault());
|
||||
|
||||
/**
|
||||
* Initializes the fragment and its associated ViewModels.
|
||||
*/
|
||||
@Override
|
||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
appointmentViewModel = new ViewModelProvider(this).get(AppointmentViewModel.class);
|
||||
storeViewModel = new ViewModelProvider(this).get(StoreViewModel.class);
|
||||
viewModel = new ViewModelProvider(this).get(AppointmentListViewModel.class);
|
||||
authViewModel = new ViewModelProvider(this).get(AuthViewModel.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up the fragment's UI, including RecyclerView, search, swipe-to-refresh, and calendar.
|
||||
*/
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
|
||||
Bundle savedInstanceState) {
|
||||
@@ -89,6 +78,7 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter.
|
||||
setupFilterToggle();
|
||||
setupMyAppointmentFilter();
|
||||
setupBulkDelete();
|
||||
observeViewModel();
|
||||
|
||||
binding.fabAddAppointment.setOnClickListener(v -> openAppointmentDetails(-1));
|
||||
|
||||
@@ -101,6 +91,24 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter.
|
||||
return binding.getRoot();
|
||||
}
|
||||
|
||||
private void observeViewModel() {
|
||||
viewModel.getAppointments().observe(getViewLifecycleOwner(), list -> {
|
||||
appointmentList.clear();
|
||||
appointmentList.addAll(list);
|
||||
updateCalendarDecorators();
|
||||
adapter.notifyDataSetChanged();
|
||||
});
|
||||
|
||||
viewModel.getStores().observe(getViewLifecycleOwner(), list -> {
|
||||
SpinnerUtils.populateWhiteSpinner(requireContext(), binding.spinnerStore, list,
|
||||
StoreDTO::getStoreName, "All Stores", -1L, StoreDTO::getStoreId);
|
||||
});
|
||||
|
||||
viewModel.getIsLoading().observe(getViewLifecycleOwner(), loading -> {
|
||||
binding.swipeRefreshAppointment.setRefreshing(loading);
|
||||
});
|
||||
}
|
||||
|
||||
private void setupBulkDelete() {
|
||||
bulkDeleteHandler = new BulkDeleteHandler(
|
||||
this,
|
||||
@@ -109,27 +117,18 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter.
|
||||
binding.btnBulkDelete,
|
||||
adapter,
|
||||
"appointment",
|
||||
appointmentViewModel::bulkDeleteAppointments,
|
||||
viewModel::bulkDeleteAppointments,
|
||||
this::loadAppointmentData
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyView() {
|
||||
super.onDestroyView();
|
||||
binding = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
loadAppointmentData();
|
||||
loadStoreData();
|
||||
viewModel.loadStores();
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggles the calendar between week and month display modes.
|
||||
*/
|
||||
private void toggleCalendarMode() {
|
||||
isMonthMode = !isMonthMode;
|
||||
binding.calendarView.state().edit()
|
||||
@@ -137,18 +136,12 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter.
|
||||
.commit();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up the "My Appointments" filter button.
|
||||
*/
|
||||
private void setupMyAppointmentFilter() {
|
||||
binding.btnMyAppointments.setOnClickListener(v -> {
|
||||
loadAppointmentData();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches current user info to get the employeeId.
|
||||
*/
|
||||
private void loadCurrentUserInfo() {
|
||||
authViewModel.getMe().observe(getViewLifecycleOwner(), resource -> {
|
||||
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
|
||||
@@ -157,17 +150,11 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter.
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up the filter toggle button to show/hide the filter layout.
|
||||
*/
|
||||
private void setupFilterToggle() {
|
||||
UIUtils.setupFilterToggle(binding.btnToggleFilter, binding.layoutFilter, binding.etSearchAppointment,
|
||||
binding.spinnerStatus, binding.spinnerStore);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up the date selection listener for the calendar.
|
||||
*/
|
||||
private void setupCalendar() {
|
||||
binding.calendarView.setOnDateChangedListener((widget, date, selected) -> {
|
||||
if (selected) {
|
||||
@@ -184,17 +171,11 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter.
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates calendar indicators to highlight dates that have scheduled appointments.
|
||||
*/
|
||||
private void updateCalendarDecorators() {
|
||||
HashSet<CalendarDay> datesWithAppointments = new HashSet<>();
|
||||
SimpleDateFormat displayFormat = new SimpleDateFormat("yyyy-MM-dd", Locale.getDefault());
|
||||
for (AppointmentDTO appointment : appointmentList) {
|
||||
try {
|
||||
//Get the appointment date
|
||||
Date date = displayFormat.parse(appointment.getAppointmentDate());
|
||||
//if the date is not null, add it to the hashset
|
||||
Date date = dateFormat.parse(appointment.getAppointmentDate());
|
||||
if (date != null) {
|
||||
Calendar cal = Calendar.getInstance();
|
||||
cal.setTime(date);
|
||||
@@ -204,56 +185,27 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter.
|
||||
Log.e("AppointmentFragment", "Error parsing date: " + appointment.getAppointmentDate());
|
||||
}
|
||||
}
|
||||
//update the indicators to the calendar
|
||||
binding.calendarView.removeDecorators();
|
||||
binding.calendarView.addDecorator(new EventDecorator(Color.RED, datesWithAppointments));
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures the search bar for filtering.
|
||||
*/
|
||||
private void setupSearch() {
|
||||
UIUtils.attachSearch(binding.etSearchAppointment, this::loadAppointmentData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures the status filter spinner.
|
||||
*/
|
||||
private void setupStatusFilter() {
|
||||
String[] statuses = {"All Statuses", "Booked", "Completed", "Cancelled", "Missed"};
|
||||
SpinnerUtils.setupStringFilterSpinner(requireContext(), binding.spinnerStatus, statuses, this::loadAppointmentData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures the store filter spinner.
|
||||
*/
|
||||
private void setupStoreFilter() {
|
||||
SpinnerUtils.setupFilterSpinner(binding.spinnerStore, this::loadAppointmentData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches store data to populate the store filter.
|
||||
*/
|
||||
private void loadStoreData() {
|
||||
storeViewModel.getAllStores(0, 100).observe(getViewLifecycleOwner(), resource -> {
|
||||
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
|
||||
storeList = resource.data.getContent();
|
||||
SpinnerUtils.populateWhiteSpinner(requireContext(), binding.spinnerStore, storeList,
|
||||
StoreDTO::getStoreName, "All Stores", -1L, StoreDTO::getStoreId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the SwipeRefreshLayout to allow manual data refreshing.
|
||||
*/
|
||||
private void setupSwipeRefresh() {
|
||||
binding.swipeRefreshAppointment.setOnRefreshListener(this::loadAppointmentData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigates to the appointment detail screen for editing or creating an appointment.
|
||||
*/
|
||||
private void openAppointmentDetails(int position) {
|
||||
Bundle args = new Bundle();
|
||||
if (position != -1) {
|
||||
@@ -263,9 +215,6 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter.
|
||||
NavHostFragment.findNavController(this).navigate(R.id.nav_appointment_detail, args);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles item click in the appointment list.
|
||||
*/
|
||||
@Override
|
||||
public void onAppointmentClick(int position) {
|
||||
openAppointmentDetails(position);
|
||||
@@ -278,16 +227,14 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter.
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches appointment data from the server with all active filters.
|
||||
*/
|
||||
private void loadAppointmentData() {
|
||||
String query = binding.etSearchAppointment.getText().toString().trim();
|
||||
String status = binding.spinnerStatus.getSelectedItem() != null ? binding.spinnerStatus.getSelectedItem().toString() : "All Statuses";
|
||||
|
||||
Long storeId = null;
|
||||
if (binding.spinnerStore.getSelectedItemPosition() > 0 && !storeList.isEmpty()) {
|
||||
storeId = storeList.get(binding.spinnerStore.getSelectedItemPosition() - 1).getStoreId();
|
||||
List<StoreDTO> stores = viewModel.getStores().getValue();
|
||||
if (binding.spinnerStore.getSelectedItemPosition() > 0 && stores != null && !stores.isEmpty()) {
|
||||
storeId = stores.get(binding.spinnerStore.getSelectedItemPosition() - 1).getStoreId();
|
||||
}
|
||||
|
||||
String selectedDateString = null;
|
||||
@@ -304,41 +251,18 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter.
|
||||
if (status.equals("All Statuses")) status = null;
|
||||
else status = status.toUpperCase();
|
||||
|
||||
appointmentViewModel.getAllAppointments(0, 500, query, status, storeId, selectedDateString, employeeId).observe(getViewLifecycleOwner(), resource -> {
|
||||
if (resource == null) return;
|
||||
|
||||
// Check the status to see if the resource is loaded and display the data
|
||||
switch (resource.status) {
|
||||
case LOADING:
|
||||
// Show loading indicator
|
||||
binding.swipeRefreshAppointment.setRefreshing(true);
|
||||
break;
|
||||
case SUCCESS:
|
||||
// Hide loading indicator and display data
|
||||
binding.swipeRefreshAppointment.setRefreshing(false);
|
||||
if (resource.data != null) {
|
||||
appointmentList.clear();
|
||||
appointmentList.addAll(resource.data.getContent());
|
||||
updateCalendarDecorators();
|
||||
adapter.notifyDataSetChanged();
|
||||
}
|
||||
break;
|
||||
case ERROR:
|
||||
// Hide loading indicator and toast error message
|
||||
binding.swipeRefreshAppointment.setRefreshing(false);
|
||||
Toast.makeText(getContext(), "Failed to load appointments: " + resource.message, Toast.LENGTH_SHORT).show();
|
||||
Log.e("AppointmentFragment", "Error loading appointments: " + resource.message);
|
||||
break;
|
||||
}
|
||||
});
|
||||
viewModel.loadAppointments(query, status, storeId, selectedDateString, employeeId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the RecyclerView for displaying appointments.
|
||||
*/
|
||||
private void setupRecyclerView() {
|
||||
adapter = new AppointmentAdapter(appointmentList, this);
|
||||
binding.recyclerViewAppointments.setLayoutManager(new LinearLayoutManager(getContext()));
|
||||
binding.recyclerViewAppointments.setAdapter(adapter);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyView() {
|
||||
super.onDestroyView();
|
||||
binding = null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package com.example.petstoremobile.fragments.listfragments;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
@@ -22,8 +21,7 @@ import com.example.petstoremobile.dtos.InventoryDTO;
|
||||
import com.example.petstoremobile.dtos.StoreDTO;
|
||||
import com.example.petstoremobile.utils.BulkDeleteHandler;
|
||||
import com.example.petstoremobile.utils.UIUtils;
|
||||
import com.example.petstoremobile.viewmodels.InventoryViewModel;
|
||||
import com.example.petstoremobile.utils.Resource;
|
||||
import com.example.petstoremobile.viewmodels.InventoryListViewModel;
|
||||
import com.example.petstoremobile.utils.SpinnerUtils;
|
||||
|
||||
import java.util.ArrayList;
|
||||
@@ -34,33 +32,18 @@ import dagger.hilt.android.AndroidEntryPoint;
|
||||
@AndroidEntryPoint
|
||||
public class InventoryFragment extends Fragment implements InventoryAdapter.OnInventoryClickListener {
|
||||
|
||||
private static final String TAG = "InventoryFragment";
|
||||
private static final int PAGE_SIZE = 20;
|
||||
|
||||
private FragmentInventoryBinding binding;
|
||||
private final List<InventoryDTO> inventoryList = new ArrayList<>();
|
||||
private List<StoreDTO> storeList = new ArrayList<>();
|
||||
private InventoryAdapter adapter;
|
||||
private InventoryViewModel viewModel;
|
||||
private InventoryListViewModel viewModel;
|
||||
private BulkDeleteHandler bulkDeleteHandler;
|
||||
|
||||
// Pagination
|
||||
private int currentPage = 0;
|
||||
private boolean isLastPage = false;
|
||||
private boolean isLoading = false;
|
||||
|
||||
/**
|
||||
* Initializes the fragment and its ViewModel.
|
||||
*/
|
||||
@Override
|
||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
viewModel = new ViewModelProvider(this).get(InventoryViewModel.class);
|
||||
viewModel = new ViewModelProvider(this).get(InventoryListViewModel.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up the fragment's UI components, including the inventory list and search.
|
||||
*/
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
|
||||
Bundle savedInstanceState) {
|
||||
@@ -72,8 +55,9 @@ public class InventoryFragment extends Fragment implements InventoryAdapter.OnIn
|
||||
setupSwipeRefresh();
|
||||
setupFilterToggle();
|
||||
setupBulkDelete();
|
||||
observeViewModel();
|
||||
|
||||
loadInventory(true);
|
||||
loadStoreData();
|
||||
|
||||
binding.fabAddInventory.setOnClickListener(v -> openDetail(null));
|
||||
|
||||
@@ -82,6 +66,23 @@ public class InventoryFragment extends Fragment implements InventoryAdapter.OnIn
|
||||
return binding.getRoot();
|
||||
}
|
||||
|
||||
private void observeViewModel() {
|
||||
viewModel.getInventory().observe(getViewLifecycleOwner(), list -> {
|
||||
inventoryList.clear();
|
||||
inventoryList.addAll(list);
|
||||
adapter.notifyDataSetChanged();
|
||||
});
|
||||
|
||||
viewModel.getStores().observe(getViewLifecycleOwner(), list -> {
|
||||
SpinnerUtils.populateWhiteSpinner(requireContext(), binding.spinnerStore, list,
|
||||
StoreDTO::getStoreName, "All Stores", -1L, StoreDTO::getStoreId);
|
||||
});
|
||||
|
||||
viewModel.getIsLoading().observe(getViewLifecycleOwner(), loading -> {
|
||||
binding.swipeRefreshInventory.setRefreshing(loading);
|
||||
});
|
||||
}
|
||||
|
||||
private void setupBulkDelete() {
|
||||
bulkDeleteHandler = new BulkDeleteHandler(
|
||||
this,
|
||||
@@ -95,49 +96,30 @@ public class InventoryFragment extends Fragment implements InventoryAdapter.OnIn
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
viewModel.loadStores();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyView() {
|
||||
super.onDestroyView();
|
||||
binding = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up the filter toggle button to show/hide the filter layout.
|
||||
*/
|
||||
private void setupFilterToggle() {
|
||||
UIUtils.setupFilterToggle(binding.btnToggleFilter, binding.layoutFilter, binding.etSearchInventory, binding.spinnerStore);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up the search bar for filtering.
|
||||
*/
|
||||
private void setupSearch() {
|
||||
UIUtils.attachSearch(binding.etSearchInventory, () -> loadInventory(true));
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures the store filter spinner.
|
||||
*/
|
||||
private void setupStoreFilter() {
|
||||
SpinnerUtils.setupFilterSpinner(binding.spinnerStore, () -> loadInventory(true));
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches store data to populate the store filter.
|
||||
*/
|
||||
private void loadStoreData() {
|
||||
viewModel.getAllStores(0, 100).observe(getViewLifecycleOwner(), resource -> {
|
||||
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
|
||||
storeList = resource.data.getContent();
|
||||
SpinnerUtils.populateWhiteSpinner(requireContext(), binding.spinnerStore, storeList,
|
||||
StoreDTO::getStoreName, "All Stores", -1L, StoreDTO::getStoreId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the RecyclerView with a layout manager, and adapter.
|
||||
*/
|
||||
private void setupRecyclerView() {
|
||||
adapter = new InventoryAdapter(inventoryList, this);
|
||||
binding.recyclerViewInventory.setLayoutManager(new LinearLayoutManager(getContext()));
|
||||
@@ -146,105 +128,45 @@ public class InventoryFragment extends Fragment implements InventoryAdapter.OnIn
|
||||
binding.recyclerViewInventory.addOnScrollListener(new RecyclerView.OnScrollListener() {
|
||||
@Override
|
||||
public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
|
||||
if (dy <= 0)
|
||||
return;
|
||||
if (dy <= 0) return;
|
||||
LinearLayoutManager lm = (LinearLayoutManager) binding.recyclerViewInventory.getLayoutManager();
|
||||
if (lm == null)
|
||||
return;
|
||||
if (lm == null) return;
|
||||
int visible = lm.getChildCount();
|
||||
int total = lm.getItemCount();
|
||||
int firstVis = lm.findFirstVisibleItemPosition();
|
||||
if (!isLoading && !isLastPage && (visible + firstVis) >= total - 3) {
|
||||
Boolean isLoading = viewModel.getIsLoading().getValue();
|
||||
if ((isLoading == null || !isLoading) && !viewModel.isLastPage() && (visible + firstVis) >= total - 3) {
|
||||
loadInventory(false);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up the SwipeRefreshLayout to reload the first page of inventory items.
|
||||
*/
|
||||
private void setupSwipeRefresh() {
|
||||
binding.swipeRefreshInventory.setOnRefreshListener(() -> loadInventory(true));
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches a page of inventory items from the API.
|
||||
*/
|
||||
private void loadInventory(boolean reset) {
|
||||
if (isLoading) return;
|
||||
|
||||
if (reset) {
|
||||
currentPage = 0;
|
||||
isLastPage = false;
|
||||
}
|
||||
|
||||
// Search text from input
|
||||
String query = binding.etSearchInventory != null ? binding.etSearchInventory.getText().toString().trim() : "";
|
||||
if (query.isEmpty()) query = null;
|
||||
|
||||
Long storeId = null;
|
||||
if (binding.spinnerStore.getSelectedItemPosition() > 0 && !storeList.isEmpty()) {
|
||||
storeId = storeList.get(binding.spinnerStore.getSelectedItemPosition() - 1).getStoreId();
|
||||
List<StoreDTO> stores = viewModel.getStores().getValue();
|
||||
if (binding.spinnerStore.getSelectedItemPosition() > 0 && stores != null && !stores.isEmpty()) {
|
||||
storeId = stores.get(binding.spinnerStore.getSelectedItemPosition() - 1).getStoreId();
|
||||
}
|
||||
|
||||
//Load all inventory items from the backend using viewModel
|
||||
viewModel.getAllInventory(query, storeId, currentPage, PAGE_SIZE, "product.prodName").observe(getViewLifecycleOwner(), resource -> {
|
||||
if (resource == null) return;
|
||||
|
||||
// Check the status to see if the resource is loaded and display the data
|
||||
switch (resource.status) {
|
||||
case LOADING:
|
||||
// Show loading indicator
|
||||
isLoading = true;
|
||||
binding.swipeRefreshInventory.setRefreshing(true);
|
||||
break;
|
||||
case SUCCESS:
|
||||
// Hide loading indicator and display data
|
||||
isLoading = false;
|
||||
binding.swipeRefreshInventory.setRefreshing(false);
|
||||
if (resource.data != null) {
|
||||
if (reset) inventoryList.clear();
|
||||
inventoryList.addAll(resource.data.getContent());
|
||||
adapter.notifyDataSetChanged();
|
||||
isLastPage = resource.data.isLast();
|
||||
if (!isLastPage) currentPage++;
|
||||
}
|
||||
break;
|
||||
case ERROR:
|
||||
// Hide loading indicator and toast error message
|
||||
isLoading = false;
|
||||
binding.swipeRefreshInventory.setRefreshing(false);
|
||||
Log.e(TAG, "Error: " + resource.message);
|
||||
Toast.makeText(getContext(), "Failed to load inventory: " + resource.message, Toast.LENGTH_SHORT).show();
|
||||
break;
|
||||
}
|
||||
});
|
||||
viewModel.loadInventory(reset, query, storeId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigates to the inventory detail screen for a specific item or to add a new one.
|
||||
*/
|
||||
private void openDetail(InventoryDTO inv) {
|
||||
Bundle args = new Bundle();
|
||||
|
||||
if (inv != null) {
|
||||
args.putLong("inventoryId", inv.getInventoryId());
|
||||
}
|
||||
|
||||
NavHostFragment.findNavController(this).navigate(R.id.nav_inventory_detail, args);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reloads inventory data when changes occur.
|
||||
*/
|
||||
public void onInventoryChanged() {
|
||||
loadInventory(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles item click in the inventory list.
|
||||
*/
|
||||
@Override
|
||||
public void onInventoryClick(int position) {
|
||||
if (position >= 0 && position < inventoryList.size()) {
|
||||
@@ -252,9 +174,6 @@ public class InventoryFragment extends Fragment implements InventoryAdapter.OnIn
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the bulk deletion UI visibility and count when items are selected or deselected.
|
||||
*/
|
||||
@Override
|
||||
public void onSelectionChanged(int selectedCount) {
|
||||
if (bulkDeleteHandler != null) {
|
||||
|
||||
@@ -9,7 +9,6 @@ import androidx.lifecycle.ViewModelProvider;
|
||||
import androidx.navigation.fragment.NavHostFragment;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
|
||||
import android.util.Log;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
@@ -25,8 +24,7 @@ import com.example.petstoremobile.utils.BulkDeleteHandler;
|
||||
import com.example.petstoremobile.utils.Resource;
|
||||
import com.example.petstoremobile.utils.SpinnerUtils;
|
||||
import com.example.petstoremobile.utils.UIUtils;
|
||||
import com.example.petstoremobile.viewmodels.PetViewModel;
|
||||
import com.example.petstoremobile.viewmodels.StoreViewModel;
|
||||
import com.example.petstoremobile.viewmodels.PetListViewModel;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
@@ -40,28 +38,19 @@ import dagger.hilt.android.AndroidEntryPoint;
|
||||
public class PetFragment extends Fragment implements PetAdapter.OnPetClickListener {
|
||||
private FragmentPetBinding binding;
|
||||
private List<PetDTO> petList = new ArrayList<>();
|
||||
private List<StoreDTO> storeList = new ArrayList<>();
|
||||
private PetAdapter adapter;
|
||||
private PetViewModel viewModel;
|
||||
private StoreViewModel storeViewModel;
|
||||
private PetListViewModel viewModel;
|
||||
private BulkDeleteHandler bulkDeleteHandler;
|
||||
|
||||
@Inject @Named("baseUrl") String baseUrl;
|
||||
@Inject TokenManager tokenManager;
|
||||
|
||||
/**
|
||||
* Initializes the fragment and its associated ViewModels.
|
||||
*/
|
||||
@Override
|
||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
viewModel = new ViewModelProvider(this).get(PetViewModel.class);
|
||||
storeViewModel = new ViewModelProvider(this).get(StoreViewModel.class);
|
||||
viewModel = new ViewModelProvider(this).get(PetListViewModel.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up the fragment's UI components, including RecyclerView, filters, and swipe-to-refresh.
|
||||
*/
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
|
||||
Bundle savedInstanceState) {
|
||||
@@ -75,6 +64,7 @@ public class PetFragment extends Fragment implements PetAdapter.OnPetClickListen
|
||||
setupSwipeRefresh();
|
||||
setupFilterToggle();
|
||||
setupBulkDelete();
|
||||
observeViewModel();
|
||||
|
||||
binding.fabAddPet.setOnClickListener(v -> openPetDetails());
|
||||
|
||||
@@ -83,6 +73,23 @@ public class PetFragment extends Fragment implements PetAdapter.OnPetClickListen
|
||||
return binding.getRoot();
|
||||
}
|
||||
|
||||
private void observeViewModel() {
|
||||
viewModel.getPets().observe(getViewLifecycleOwner(), list -> {
|
||||
petList.clear();
|
||||
petList.addAll(list);
|
||||
adapter.notifyDataSetChanged();
|
||||
});
|
||||
|
||||
viewModel.getStores().observe(getViewLifecycleOwner(), list -> {
|
||||
SpinnerUtils.populateWhiteSpinner(requireContext(), binding.spinnerStore, list,
|
||||
StoreDTO::getStoreName, "All Stores", -1L, StoreDTO::getStoreId);
|
||||
});
|
||||
|
||||
viewModel.getIsLoading().observe(getViewLifecycleOwner(), loading -> {
|
||||
binding.swipeRefreshPet.setRefreshing(loading);
|
||||
});
|
||||
}
|
||||
|
||||
private void setupBulkDelete() {
|
||||
bulkDeleteHandler = new BulkDeleteHandler(
|
||||
this,
|
||||
@@ -96,83 +103,62 @@ public class PetFragment extends Fragment implements PetAdapter.OnPetClickListen
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyView() {
|
||||
super.onDestroyView();
|
||||
binding = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reloads data every time the fragment becomes visible.
|
||||
*/
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
loadPetData();
|
||||
loadStoreData();
|
||||
viewModel.loadStores();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up the filter toggle button to show/hide the filter layout.
|
||||
*/
|
||||
private void setupFilterToggle() {
|
||||
UIUtils.setupFilterToggle(binding.btnToggleFilter, binding.layoutFilter, binding.etSearchPet,
|
||||
binding.spinnerStatus, binding.spinnerSpecies, binding.spinnerStore);
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures the search bar.
|
||||
*/
|
||||
private void setupSearch() {
|
||||
UIUtils.attachSearch(binding.etSearchPet, this::loadPetData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures the status filter spinner.
|
||||
*/
|
||||
private void setupStatusFilter() {
|
||||
String[] statuses = {"All Statuses", "Available", "Adopted", "Owned"};
|
||||
SpinnerUtils.setupStringFilterSpinner(requireContext(), binding.spinnerStatus, statuses, this::loadPetData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures the species filter spinner with species.
|
||||
*/
|
||||
private void setupSpeciesFilter() {
|
||||
String[] species = {"All Species", "Dog", "Cat", "Bird", "Rabbit", "Fish", "Hamster"};
|
||||
SpinnerUtils.setupStringFilterSpinner(requireContext(), binding.spinnerSpecies, species, this::loadPetData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures the store filter spinner.
|
||||
*/
|
||||
private void setupStoreFilter() {
|
||||
SpinnerUtils.setupFilterSpinner(binding.spinnerStore, this::loadPetData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches store data to populate the store filter.
|
||||
*/
|
||||
private void loadStoreData() {
|
||||
storeViewModel.getAllStores(0, 100).observe(getViewLifecycleOwner(), resource -> {
|
||||
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
|
||||
storeList = resource.data.getContent();
|
||||
SpinnerUtils.populateWhiteSpinner(requireContext(), binding.spinnerStore, storeList,
|
||||
StoreDTO::getStoreName, "All Stores", -1L, StoreDTO::getStoreId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up the SwipeRefreshLayout.
|
||||
*/
|
||||
private void setupSwipeRefresh() {
|
||||
binding.swipeRefreshPet.setOnRefreshListener(this::loadPetData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigates to the pet profile screen.
|
||||
*/
|
||||
private void loadPetData() {
|
||||
String query = binding.etSearchPet.getText().toString().trim();
|
||||
String status = binding.spinnerStatus.getSelectedItem() != null ? binding.spinnerStatus.getSelectedItem().toString() : "All Statuses";
|
||||
String species = binding.spinnerSpecies.getSelectedItem() != null ? binding.spinnerSpecies.getSelectedItem().toString() : "All Species";
|
||||
|
||||
Long storeId = null;
|
||||
List<StoreDTO> stores = viewModel.getStores().getValue();
|
||||
if (binding.spinnerStore.getSelectedItemPosition() > 0 && stores != null && !stores.isEmpty()) {
|
||||
storeId = stores.get(binding.spinnerStore.getSelectedItemPosition() - 1).getStoreId();
|
||||
}
|
||||
|
||||
viewModel.loadPets(query, status, species, storeId);
|
||||
}
|
||||
|
||||
private void setupRecyclerView() {
|
||||
adapter = new PetAdapter(petList, this);
|
||||
adapter.setBaseUrl(baseUrl);
|
||||
adapter.setToken(tokenManager.getToken());
|
||||
binding.recyclerViewPets.setLayoutManager(new LinearLayoutManager(getContext()));
|
||||
binding.recyclerViewPets.setAdapter(adapter);
|
||||
}
|
||||
|
||||
private void openPetProfile(int position) {
|
||||
Bundle args = new Bundle();
|
||||
PetDTO pet = petList.get(position);
|
||||
@@ -180,9 +166,6 @@ public class PetFragment extends Fragment implements PetAdapter.OnPetClickListen
|
||||
NavHostFragment.findNavController(this).navigate(R.id.nav_pet_profile, args);
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigates to the pet detail screen.
|
||||
*/
|
||||
private void openPetDetails() {
|
||||
NavHostFragment.findNavController(this).navigate(R.id.nav_pet_detail);
|
||||
}
|
||||
@@ -199,54 +182,9 @@ public class PetFragment extends Fragment implements PetAdapter.OnPetClickListen
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches pet data from the server with all active filters.
|
||||
*/
|
||||
private void loadPetData() {
|
||||
String query = binding.etSearchPet.getText().toString().trim();
|
||||
String status = binding.spinnerStatus.getSelectedItem() != null ? binding.spinnerStatus.getSelectedItem().toString() : "All Statuses";
|
||||
String species = binding.spinnerSpecies.getSelectedItem() != null ? binding.spinnerSpecies.getSelectedItem().toString() : "All Species";
|
||||
|
||||
Long storeId = null;
|
||||
if (binding.spinnerStore.getSelectedItemPosition() > 0 && !storeList.isEmpty()) {
|
||||
storeId = storeList.get(binding.spinnerStore.getSelectedItemPosition() - 1).getStoreId();
|
||||
}
|
||||
|
||||
if (status.equals("All Statuses")) status = null;
|
||||
if (species.equals("All Species")) species = null;
|
||||
|
||||
viewModel.getAllPets(0, 100, query, status, species, storeId, null, "petName").observe(getViewLifecycleOwner(), resource -> {
|
||||
if (resource == null) return;
|
||||
|
||||
switch (resource.status) {
|
||||
case LOADING:
|
||||
binding.swipeRefreshPet.setRefreshing(true);
|
||||
break;
|
||||
case SUCCESS:
|
||||
binding.swipeRefreshPet.setRefreshing(false);
|
||||
if (resource.data != null) {
|
||||
petList.clear();
|
||||
petList.addAll(resource.data.getContent());
|
||||
adapter.notifyDataSetChanged();
|
||||
}
|
||||
break;
|
||||
case ERROR:
|
||||
binding.swipeRefreshPet.setRefreshing(false);
|
||||
Toast.makeText(getContext(), "Failed to load pets: " + resource.message, Toast.LENGTH_SHORT).show();
|
||||
Log.e("PetFragment", "Error loading pets: " + resource.message);
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the RecyclerView.
|
||||
*/
|
||||
private void setupRecyclerView() {
|
||||
adapter = new PetAdapter(petList, this);
|
||||
adapter.setBaseUrl(baseUrl);
|
||||
adapter.setToken(tokenManager.getToken());
|
||||
binding.recyclerViewPets.setLayoutManager(new LinearLayoutManager(getContext()));
|
||||
binding.recyclerViewPets.setAdapter(adapter);
|
||||
@Override
|
||||
public void onDestroyView() {
|
||||
super.onDestroyView();
|
||||
binding = null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,21 +9,18 @@ import androidx.lifecycle.ViewModelProvider;
|
||||
import androidx.navigation.fragment.NavHostFragment;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
|
||||
import android.util.Log;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.example.petstoremobile.R;
|
||||
import com.example.petstoremobile.adapters.ProductAdapter;
|
||||
import com.example.petstoremobile.databinding.FragmentProductBinding;
|
||||
import com.example.petstoremobile.dtos.CategoryDTO;
|
||||
import com.example.petstoremobile.dtos.ProductDTO;
|
||||
import com.example.petstoremobile.utils.Resource;
|
||||
import com.example.petstoremobile.utils.SpinnerUtils;
|
||||
import com.example.petstoremobile.utils.UIUtils;
|
||||
import com.example.petstoremobile.viewmodels.ProductViewModel;
|
||||
import com.example.petstoremobile.viewmodels.ProductListViewModel;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
@@ -38,24 +35,17 @@ public class ProductFragment extends Fragment implements ProductAdapter.OnProduc
|
||||
|
||||
private FragmentProductBinding binding;
|
||||
private List<ProductDTO> productList = new ArrayList<>();
|
||||
private List<CategoryDTO> categoryList = new ArrayList<>();
|
||||
private ProductAdapter adapter;
|
||||
private ProductViewModel viewModel;
|
||||
private ProductListViewModel viewModel;
|
||||
|
||||
@Inject @Named("baseUrl") String baseUrl;
|
||||
|
||||
/**
|
||||
* Initializes the fragment and its associated ProductViewModel.
|
||||
*/
|
||||
@Override
|
||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
viewModel = new ViewModelProvider(this).get(ProductViewModel.class);
|
||||
viewModel = new ViewModelProvider(this).get(ProductListViewModel.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up the fragment's UI components, including RecyclerView, search, and swipe-to-refresh.
|
||||
*/
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
|
||||
Bundle savedInstanceState) {
|
||||
@@ -66,6 +56,7 @@ public class ProductFragment extends Fragment implements ProductAdapter.OnProduc
|
||||
setupCategoryFilter();
|
||||
setupSwipeRefresh();
|
||||
setupFilterToggle();
|
||||
observeViewModel();
|
||||
|
||||
binding.fabAddProduct.setOnClickListener(v -> openProductDetails(-1));
|
||||
|
||||
@@ -74,67 +65,67 @@ public class ProductFragment extends Fragment implements ProductAdapter.OnProduc
|
||||
return binding.getRoot();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyView() {
|
||||
super.onDestroyView();
|
||||
binding = null;
|
||||
private void observeViewModel() {
|
||||
viewModel.getProducts().observe(getViewLifecycleOwner(), list -> {
|
||||
productList.clear();
|
||||
productList.addAll(list);
|
||||
adapter.notifyDataSetChanged();
|
||||
});
|
||||
|
||||
viewModel.getCategories().observe(getViewLifecycleOwner(), list -> {
|
||||
SpinnerUtils.populateWhiteSpinner(requireContext(), binding.spinnerCategory, list,
|
||||
CategoryDTO::getCategoryName, "All Categories", -1L, CategoryDTO::getCategoryId);
|
||||
});
|
||||
|
||||
viewModel.getIsLoading().observe(getViewLifecycleOwner(), loading -> {
|
||||
binding.swipeRefreshProduct.setRefreshing(loading);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reloads data every time the fragment becomes visible.
|
||||
*/
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
loadProductData();
|
||||
loadCategoryData();
|
||||
viewModel.loadCategories();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up the filter toggle button to show/hide the filter layout.
|
||||
*/
|
||||
private void setupFilterToggle() {
|
||||
UIUtils.setupFilterToggle(binding.btnToggleFilter, binding.layoutFilter,
|
||||
binding.etSearchProduct, binding.spinnerCategory);
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures the search bar for triggering data load from backend.
|
||||
*/
|
||||
private void setupSearch() {
|
||||
UIUtils.attachSearch(binding.etSearchProduct, this::loadProductData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures the category filter spinner.
|
||||
*/
|
||||
private void setupCategoryFilter() {
|
||||
SpinnerUtils.setupFilterSpinner(binding.spinnerCategory, this::loadProductData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches category data to populate the category filter.
|
||||
*/
|
||||
private void loadCategoryData() {
|
||||
viewModel.getAllCategories(0, 100).observe(getViewLifecycleOwner(), resource -> {
|
||||
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
|
||||
categoryList = resource.data.getContent();
|
||||
SpinnerUtils.populateWhiteSpinner(requireContext(), binding.spinnerCategory, categoryList,
|
||||
CategoryDTO::getCategoryName, "All Categories", -1L, CategoryDTO::getCategoryId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up the SwipeRefreshLayout.
|
||||
*/
|
||||
private void setupSwipeRefresh() {
|
||||
binding.swipeRefreshProduct.setOnRefreshListener(this::loadProductData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigates to the product detail screen.
|
||||
*/
|
||||
private void loadProductData() {
|
||||
String query = binding.etSearchProduct.getText().toString().trim();
|
||||
if (query.isEmpty()) query = null;
|
||||
|
||||
Long categoryId = null;
|
||||
List<CategoryDTO> categories = viewModel.getCategories().getValue();
|
||||
if (binding.spinnerCategory.getSelectedItemPosition() > 0 && categories != null && !categories.isEmpty()) {
|
||||
categoryId = categories.get(binding.spinnerCategory.getSelectedItemPosition() - 1).getCategoryId();
|
||||
}
|
||||
|
||||
viewModel.loadProducts(query, categoryId);
|
||||
}
|
||||
|
||||
private void setupRecyclerView() {
|
||||
adapter = new ProductAdapter(productList, this);
|
||||
adapter.setBaseUrl(baseUrl);
|
||||
binding.recyclerViewProducts.setLayoutManager(new LinearLayoutManager(getContext()));
|
||||
binding.recyclerViewProducts.setAdapter(adapter);
|
||||
}
|
||||
|
||||
private void openProductDetails(int position) {
|
||||
Bundle args = new Bundle();
|
||||
if (position != -1) {
|
||||
@@ -149,51 +140,9 @@ public class ProductFragment extends Fragment implements ProductAdapter.OnProduc
|
||||
openProductDetails(position);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches product data from the server with search query, category, and sorting.
|
||||
*/
|
||||
private void loadProductData() {
|
||||
String query = binding.etSearchProduct.getText().toString().trim();
|
||||
if (query.isEmpty()) query = null;
|
||||
|
||||
Long categoryId = null;
|
||||
if (binding.spinnerCategory.getSelectedItemPosition() > 0 && !categoryList.isEmpty()) {
|
||||
categoryId = categoryList.get(binding.spinnerCategory.getSelectedItemPosition() - 1).getCategoryId();
|
||||
}
|
||||
|
||||
viewModel.getAllProducts(query, categoryId, 0, 100, "prodName").observe(getViewLifecycleOwner(), resource -> {
|
||||
if (resource == null) return;
|
||||
|
||||
switch (resource.status) {
|
||||
case LOADING:
|
||||
binding.swipeRefreshProduct.setRefreshing(true);
|
||||
break;
|
||||
case SUCCESS:
|
||||
binding.swipeRefreshProduct.setRefreshing(false);
|
||||
if (resource.data != null) {
|
||||
productList.clear();
|
||||
productList.addAll(resource.data.getContent());
|
||||
adapter.notifyDataSetChanged();
|
||||
}
|
||||
break;
|
||||
case ERROR:
|
||||
binding.swipeRefreshProduct.setRefreshing(false);
|
||||
if (getContext() != null) {
|
||||
Toast.makeText(getContext(), "Failed to load products: " + resource.message, Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
Log.e("ProductFragment", "Error loading products: " + resource.message);
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the RecyclerView.
|
||||
*/
|
||||
private void setupRecyclerView() {
|
||||
adapter = new ProductAdapter(productList, this);
|
||||
adapter.setBaseUrl(baseUrl);
|
||||
binding.recyclerViewProducts.setLayoutManager(new LinearLayoutManager(getContext()));
|
||||
binding.recyclerViewProducts.setAdapter(adapter);
|
||||
@Override
|
||||
public void onDestroyView() {
|
||||
super.onDestroyView();
|
||||
binding = null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
package com.example.petstoremobile.fragments.listfragments;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
@@ -21,12 +19,9 @@ import com.example.petstoremobile.dtos.ProductDTO;
|
||||
import com.example.petstoremobile.dtos.ProductSupplierDTO;
|
||||
import com.example.petstoremobile.dtos.SupplierDTO;
|
||||
import com.example.petstoremobile.utils.BulkDeleteHandler;
|
||||
import com.example.petstoremobile.utils.Resource;
|
||||
import com.example.petstoremobile.utils.SpinnerUtils;
|
||||
import com.example.petstoremobile.utils.UIUtils;
|
||||
import com.example.petstoremobile.viewmodels.ProductSupplierViewModel;
|
||||
import com.example.petstoremobile.viewmodels.ProductViewModel;
|
||||
import com.example.petstoremobile.viewmodels.SupplierViewModel;
|
||||
import com.example.petstoremobile.viewmodels.ProductSupplierListViewModel;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
@@ -39,29 +34,17 @@ public class ProductSupplierFragment extends Fragment
|
||||
|
||||
private FragmentProductSupplierBinding binding;
|
||||
private List<ProductSupplierDTO> psList = new ArrayList<>();
|
||||
private List<ProductDTO> productList = new ArrayList<>();
|
||||
private List<SupplierDTO> supplierList = new ArrayList<>();
|
||||
|
||||
private ProductSupplierAdapter adapter;
|
||||
private ProductSupplierViewModel viewModel;
|
||||
private ProductViewModel productViewModel;
|
||||
private SupplierViewModel supplierViewModel;
|
||||
private ProductSupplierListViewModel viewModel;
|
||||
private BulkDeleteHandler bulkDeleteHandler;
|
||||
|
||||
/**
|
||||
* Initializes the fragment and its associated ViewModels.
|
||||
*/
|
||||
@Override
|
||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
viewModel = new ViewModelProvider(this).get(ProductSupplierViewModel.class);
|
||||
productViewModel = new ViewModelProvider(this).get(ProductViewModel.class);
|
||||
supplierViewModel = new ViewModelProvider(this).get(SupplierViewModel.class);
|
||||
viewModel = new ViewModelProvider(this).get(ProductSupplierListViewModel.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up the fragment's UI components, including the RecyclerView, search, and swipe-to-refresh.
|
||||
*/
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
|
||||
Bundle savedInstanceState) {
|
||||
@@ -74,6 +57,7 @@ public class ProductSupplierFragment extends Fragment
|
||||
setupSwipeRefresh();
|
||||
setupFilterToggle();
|
||||
setupBulkDelete();
|
||||
observeViewModel();
|
||||
|
||||
binding.fabAddPS.setOnClickListener(v -> openDetail(-1));
|
||||
|
||||
@@ -82,6 +66,28 @@ public class ProductSupplierFragment extends Fragment
|
||||
return binding.getRoot();
|
||||
}
|
||||
|
||||
private void observeViewModel() {
|
||||
viewModel.getProductSuppliers().observe(getViewLifecycleOwner(), list -> {
|
||||
psList.clear();
|
||||
psList.addAll(list);
|
||||
adapter.notifyDataSetChanged();
|
||||
});
|
||||
|
||||
viewModel.getProducts().observe(getViewLifecycleOwner(), list -> {
|
||||
SpinnerUtils.populateWhiteSpinner(requireContext(), binding.spinnerProduct, list,
|
||||
ProductDTO::getProdName, "All Products", -1L, ProductDTO::getProdId);
|
||||
});
|
||||
|
||||
viewModel.getSuppliers().observe(getViewLifecycleOwner(), list -> {
|
||||
SpinnerUtils.populateWhiteSpinner(requireContext(), binding.spinnerSupplier, list,
|
||||
SupplierDTO::getSupCompany, "All Suppliers", -1L, SupplierDTO::getSupId);
|
||||
});
|
||||
|
||||
viewModel.getIsLoading().observe(getViewLifecycleOwner(), loading -> {
|
||||
binding.swipeRefreshPS.setRefreshing(loading);
|
||||
});
|
||||
}
|
||||
|
||||
private void setupBulkDelete() {
|
||||
bulkDeleteHandler = new BulkDeleteHandler(
|
||||
this,
|
||||
@@ -95,136 +101,65 @@ public class ProductSupplierFragment extends Fragment
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
loadData();
|
||||
viewModel.loadFilterData();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyView() {
|
||||
super.onDestroyView();
|
||||
binding = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reloads data every time the fragment becomes visible.
|
||||
*/
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
loadData();
|
||||
loadFilterData();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up the filter toggle button to show/hide the filter layout.
|
||||
*/
|
||||
private void setupFilterToggle() {
|
||||
UIUtils.setupFilterToggle(binding.btnToggleFilter, binding.layoutFilter, binding.etSearchPS,
|
||||
binding.spinnerProduct, binding.spinnerSupplier);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the RecyclerView with a layout manager and adapter for product-supplier data.
|
||||
*/
|
||||
private void setupRecyclerView() {
|
||||
adapter = new ProductSupplierAdapter(psList, this);
|
||||
binding.recyclerViewPS.setLayoutManager(new LinearLayoutManager(getContext()));
|
||||
binding.recyclerViewPS.setAdapter(adapter);
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures the search bar for filtering.
|
||||
*/
|
||||
private void setupSearch() {
|
||||
UIUtils.attachSearch(binding.etSearchPS, this::loadData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures the product filter spinner.
|
||||
*/
|
||||
private void setupProductFilter() {
|
||||
SpinnerUtils.setupFilterSpinner(binding.spinnerProduct, this::loadData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures the supplier filter spinner.
|
||||
*/
|
||||
private void setupSupplierFilter() {
|
||||
SpinnerUtils.setupFilterSpinner(binding.spinnerSupplier, this::loadData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches products and suppliers to populate the filters.
|
||||
*/
|
||||
private void loadFilterData() {
|
||||
productViewModel.getAllProducts(null, null, 0, 100, "prodName").observe(getViewLifecycleOwner(), resource -> {
|
||||
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
|
||||
productList = resource.data.getContent();
|
||||
SpinnerUtils.populateWhiteSpinner(requireContext(), binding.spinnerProduct, productList,
|
||||
ProductDTO::getProdName, "All Products", -1L, ProductDTO::getProdId);
|
||||
}
|
||||
});
|
||||
|
||||
supplierViewModel.getAllSuppliers(0, 100, null, "supCompany").observe(getViewLifecycleOwner(), resource -> {
|
||||
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
|
||||
supplierList = resource.data.getContent();
|
||||
SpinnerUtils.populateWhiteSpinner(requireContext(), binding.spinnerSupplier, supplierList,
|
||||
SupplierDTO::getSupCompany, "All Suppliers", -1L, SupplierDTO::getSupId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up the SwipeRefreshLayout to allow manual reloading of product-supplier data.
|
||||
*/
|
||||
private void setupSwipeRefresh() {
|
||||
binding.swipeRefreshPS.setOnRefreshListener(this::loadData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches product-supplier data from the server through the ViewModel with search query and filters.
|
||||
*/
|
||||
private void loadData() {
|
||||
String query = binding.etSearchPS.getText().toString().trim();
|
||||
if (query.isEmpty()) query = null;
|
||||
|
||||
Long productId = null;
|
||||
if (binding.spinnerProduct.getSelectedItemPosition() > 0 && !productList.isEmpty()) {
|
||||
productId = productList.get(binding.spinnerProduct.getSelectedItemPosition() - 1).getProdId();
|
||||
List<ProductDTO> products = viewModel.getProducts().getValue();
|
||||
if (binding.spinnerProduct.getSelectedItemPosition() > 0 && products != null && !products.isEmpty()) {
|
||||
productId = products.get(binding.spinnerProduct.getSelectedItemPosition() - 1).getProdId();
|
||||
}
|
||||
|
||||
Long supplierId = null;
|
||||
if (binding.spinnerSupplier.getSelectedItemPosition() > 0 && !supplierList.isEmpty()) {
|
||||
supplierId = supplierList.get(binding.spinnerSupplier.getSelectedItemPosition() - 1).getSupId();
|
||||
List<SupplierDTO> suppliers = viewModel.getSuppliers().getValue();
|
||||
if (binding.spinnerSupplier.getSelectedItemPosition() > 0 && suppliers != null && !suppliers.isEmpty()) {
|
||||
supplierId = suppliers.get(binding.spinnerSupplier.getSelectedItemPosition() - 1).getSupId();
|
||||
}
|
||||
|
||||
viewModel.getAllProductSuppliers(0, 100, query, productId, supplierId, "productName").observe(getViewLifecycleOwner(), resource -> {
|
||||
if (resource == null) return;
|
||||
|
||||
// Check the status to see if the resource is loaded and display the data
|
||||
switch (resource.status) {
|
||||
case LOADING:
|
||||
// Show loading indicator
|
||||
binding.swipeRefreshPS.setRefreshing(true);
|
||||
break;
|
||||
case SUCCESS:
|
||||
// Hide loading indicator and display data
|
||||
binding.swipeRefreshPS.setRefreshing(false);
|
||||
if (resource.data != null) {
|
||||
psList.clear();
|
||||
psList.addAll(resource.data.getContent());
|
||||
adapter.notifyDataSetChanged();
|
||||
}
|
||||
break;
|
||||
case ERROR:
|
||||
// Hide loading indicator and toast error message
|
||||
binding.swipeRefreshPS.setRefreshing(false);
|
||||
Toast.makeText(getContext(), "Failed to load: " + resource.message, Toast.LENGTH_SHORT).show();
|
||||
Log.e("PSFragment", "Error loading: " + resource.message);
|
||||
break;
|
||||
}
|
||||
});
|
||||
viewModel.loadProductSuppliers(query, productId, supplierId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigates to the product-supplier detail screen for a specific item or to add a new record.
|
||||
*/
|
||||
private void openDetail(int position) {
|
||||
Bundle args = new Bundle();
|
||||
if (position != -1) {
|
||||
@@ -235,9 +170,6 @@ public class ProductSupplierFragment extends Fragment
|
||||
NavHostFragment.findNavController(this).navigate(R.id.nav_product_supplier_detail, args);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles item click in the product-supplier list.
|
||||
*/
|
||||
@Override
|
||||
public void onProductSupplierClick(int position) { openDetail(position); }
|
||||
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
package com.example.petstoremobile.fragments.listfragments;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
@@ -19,11 +17,9 @@ import com.example.petstoremobile.adapters.PurchaseOrderAdapter;
|
||||
import com.example.petstoremobile.databinding.FragmentPurchaseOrderBinding;
|
||||
import com.example.petstoremobile.dtos.PurchaseOrderDTO;
|
||||
import com.example.petstoremobile.dtos.StoreDTO;
|
||||
import com.example.petstoremobile.utils.Resource;
|
||||
import com.example.petstoremobile.utils.SpinnerUtils;
|
||||
import com.example.petstoremobile.utils.UIUtils;
|
||||
import com.example.petstoremobile.viewmodels.PurchaseOrderViewModel;
|
||||
import com.example.petstoremobile.viewmodels.StoreViewModel;
|
||||
import com.example.petstoremobile.viewmodels.PurchaseOrderListViewModel;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
@@ -36,24 +32,15 @@ public class PurchaseOrderFragment extends Fragment
|
||||
|
||||
private FragmentPurchaseOrderBinding binding;
|
||||
private List<PurchaseOrderDTO> poList = new ArrayList<>();
|
||||
private List<StoreDTO> storeList = new ArrayList<>();
|
||||
private PurchaseOrderAdapter adapter;
|
||||
private PurchaseOrderViewModel viewModel;
|
||||
private StoreViewModel storeViewModel;
|
||||
private PurchaseOrderListViewModel viewModel;
|
||||
|
||||
/**
|
||||
* Initializes the fragment and its associated ViewModels.
|
||||
*/
|
||||
@Override
|
||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
viewModel = new ViewModelProvider(this).get(PurchaseOrderViewModel.class);
|
||||
storeViewModel = new ViewModelProvider(this).get(StoreViewModel.class);
|
||||
viewModel = new ViewModelProvider(this).get(PurchaseOrderListViewModel.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up the fragment's UI components, including RecyclerView, filters, and swipe-to-refresh.
|
||||
*/
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
|
||||
Bundle savedInstanceState) {
|
||||
@@ -64,121 +51,72 @@ public class PurchaseOrderFragment extends Fragment
|
||||
setupStoreFilter();
|
||||
setupSwipeRefresh();
|
||||
setupFilterToggle();
|
||||
observeViewModel();
|
||||
|
||||
UIUtils.setupHamburgerMenu(binding.btnHamburgerPO, this);
|
||||
|
||||
return binding.getRoot();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyView() {
|
||||
super.onDestroyView();
|
||||
binding = null;
|
||||
private void observeViewModel() {
|
||||
viewModel.getPurchaseOrders().observe(getViewLifecycleOwner(), list -> {
|
||||
poList.clear();
|
||||
poList.addAll(list);
|
||||
adapter.notifyDataSetChanged();
|
||||
});
|
||||
|
||||
viewModel.getStores().observe(getViewLifecycleOwner(), list -> {
|
||||
SpinnerUtils.populateWhiteSpinner(requireContext(), binding.spinnerStore, list,
|
||||
StoreDTO::getStoreName, "All Stores", -1L, StoreDTO::getStoreId);
|
||||
});
|
||||
|
||||
viewModel.getIsLoading().observe(getViewLifecycleOwner(), loading -> {
|
||||
binding.swipeRefreshPO.setRefreshing(loading);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reloads data every time the fragment becomes visible.
|
||||
*/
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
loadData();
|
||||
loadStoreData();
|
||||
viewModel.loadStores();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up the filter toggle button to show/hide the filter layout.
|
||||
*/
|
||||
private void setupFilterToggle() {
|
||||
UIUtils.setupFilterToggle(binding.btnToggleFilter, binding.layoutFilter, binding.etSearchPO, binding.spinnerStore);
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures the search bar for filtering.
|
||||
*/
|
||||
private void setupSearch() {
|
||||
UIUtils.attachSearch(binding.etSearchPO, this::loadData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures the store filter spinner.
|
||||
*/
|
||||
private void setupStoreFilter() {
|
||||
SpinnerUtils.setupFilterSpinner(binding.spinnerStore, this::loadData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches store data to populate the store filter.
|
||||
*/
|
||||
private void loadStoreData() {
|
||||
storeViewModel.getAllStores(0, 100).observe(getViewLifecycleOwner(), resource -> {
|
||||
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
|
||||
storeList = resource.data.getContent();
|
||||
SpinnerUtils.populateWhiteSpinner(requireContext(), binding.spinnerStore, storeList,
|
||||
StoreDTO::getStoreName, "All Stores", -1L, StoreDTO::getStoreId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the RecyclerView with a layout manager and adapter.
|
||||
*/
|
||||
private void setupRecyclerView() {
|
||||
adapter = new PurchaseOrderAdapter(poList, this);
|
||||
binding.recyclerViewPO.setLayoutManager(new LinearLayoutManager(getContext()));
|
||||
binding.recyclerViewPO.setAdapter(adapter);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up the SwipeRefreshLayout to allow manual reloading of purchase order data.
|
||||
*/
|
||||
private void setupSwipeRefresh() {
|
||||
binding.swipeRefreshPO.setOnRefreshListener(this::loadData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches purchase order data from the server with active filters and updates the UI.
|
||||
*/
|
||||
private void loadData() {
|
||||
String query = binding.etSearchPO != null ? binding.etSearchPO.getText().toString().trim() : "";
|
||||
if (query.isEmpty()) query = null;
|
||||
|
||||
Long storeId = null;
|
||||
if (binding.spinnerStore.getSelectedItemPosition() > 0 && !storeList.isEmpty()) {
|
||||
storeId = storeList.get(binding.spinnerStore.getSelectedItemPosition() - 1).getStoreId();
|
||||
List<StoreDTO> stores = viewModel.getStores().getValue();
|
||||
if (binding.spinnerStore.getSelectedItemPosition() > 0 && stores != null && !stores.isEmpty()) {
|
||||
storeId = stores.get(binding.spinnerStore.getSelectedItemPosition() - 1).getStoreId();
|
||||
}
|
||||
|
||||
viewModel.getAllPurchaseOrders(0, 100, query, storeId, "purchaseOrderId,desc").observe(getViewLifecycleOwner(), resource -> {
|
||||
if (resource == null) return;
|
||||
|
||||
// Check the status to see if the resource is loaded and display the data
|
||||
switch (resource.status) {
|
||||
case LOADING:
|
||||
// Show loading indicator
|
||||
binding.swipeRefreshPO.setRefreshing(true);
|
||||
break;
|
||||
case SUCCESS:
|
||||
// Hide loading indicator and display data
|
||||
binding.swipeRefreshPO.setRefreshing(false);
|
||||
if (resource.data != null) {
|
||||
poList.clear();
|
||||
poList.addAll(resource.data.getContent());
|
||||
adapter.notifyDataSetChanged();
|
||||
}
|
||||
break;
|
||||
case ERROR:
|
||||
// Hide loading indicator and toast error message
|
||||
binding.swipeRefreshPO.setRefreshing(false);
|
||||
Toast.makeText(getContext(), "Failed to load purchase orders: " + resource.message, Toast.LENGTH_SHORT).show();
|
||||
Log.e("POFragment", "Error loading purchase orders: " + resource.message);
|
||||
break;
|
||||
}
|
||||
});
|
||||
viewModel.loadPurchaseOrders(query, storeId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigates to the purchase order detail screen for a specific record.
|
||||
*/
|
||||
private void openDetail(int position) {
|
||||
Bundle args = new Bundle();
|
||||
PurchaseOrderDTO po = poList.get(position);
|
||||
@@ -186,11 +124,14 @@ public class PurchaseOrderFragment extends Fragment
|
||||
NavHostFragment.findNavController(this).navigate(R.id.nav_purchase_order_detail, args);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles item click in the purchase order list.
|
||||
*/
|
||||
@Override
|
||||
public void onPurchaseOrderClick(int position) {
|
||||
openDetail(position);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyView() {
|
||||
super.onDestroyView();
|
||||
binding = null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@ import androidx.lifecycle.ViewModelProvider;
|
||||
import androidx.navigation.fragment.NavHostFragment;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import android.util.Log;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
@@ -19,11 +18,9 @@ import com.example.petstoremobile.adapters.SaleAdapter;
|
||||
import com.example.petstoremobile.databinding.FragmentSaleBinding;
|
||||
import com.example.petstoremobile.dtos.SaleDTO;
|
||||
import com.example.petstoremobile.dtos.StoreDTO;
|
||||
import com.example.petstoremobile.utils.Resource;
|
||||
import com.example.petstoremobile.utils.SpinnerUtils;
|
||||
import com.example.petstoremobile.utils.UIUtils;
|
||||
import com.example.petstoremobile.viewmodels.SaleViewModel;
|
||||
import com.example.petstoremobile.viewmodels.StoreViewModel;
|
||||
import com.example.petstoremobile.viewmodels.SaleListViewModel;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
@@ -33,20 +30,10 @@ import dagger.hilt.android.AndroidEntryPoint;
|
||||
@AndroidEntryPoint
|
||||
public class SaleFragment extends Fragment implements SaleAdapter.OnSaleClickListener {
|
||||
|
||||
private static final String TAG = "SaleFragment";
|
||||
private static final int PAGE_SIZE = 200;
|
||||
|
||||
private FragmentSaleBinding binding;
|
||||
private final List<SaleDTO> saleList = new ArrayList<>();
|
||||
private final List<StoreDTO> storeList = new ArrayList<>();
|
||||
private SaleAdapter adapter;
|
||||
private SaleViewModel saleViewModel;
|
||||
private StoreViewModel storeViewModel;
|
||||
|
||||
// Pagination
|
||||
private int currentPage = 0;
|
||||
private boolean isLastPage = false;
|
||||
private boolean isLoading = false;
|
||||
private SaleListViewModel viewModel;
|
||||
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
|
||||
@@ -58,8 +45,7 @@ public class SaleFragment extends Fragment implements SaleAdapter.OnSaleClickLis
|
||||
@Override
|
||||
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
saleViewModel = new ViewModelProvider(this).get(SaleViewModel.class);
|
||||
storeViewModel = new ViewModelProvider(this).get(StoreViewModel.class);
|
||||
viewModel = new ViewModelProvider(this).get(SaleListViewModel.class);
|
||||
|
||||
setupRecyclerView();
|
||||
setupSearch();
|
||||
@@ -67,6 +53,8 @@ public class SaleFragment extends Fragment implements SaleAdapter.OnSaleClickLis
|
||||
setupPaymentMethodFilter();
|
||||
setupSwipeRefresh();
|
||||
setupFilterToggle();
|
||||
observeViewModel();
|
||||
|
||||
loadSales(true);
|
||||
|
||||
UIUtils.setupHamburgerMenu(binding.btnHamburger, this);
|
||||
@@ -78,10 +66,27 @@ public class SaleFragment extends Fragment implements SaleAdapter.OnSaleClickLis
|
||||
NavHostFragment.findNavController(this).navigate(R.id.nav_refund));
|
||||
}
|
||||
|
||||
private void observeViewModel() {
|
||||
viewModel.getSales().observe(getViewLifecycleOwner(), list -> {
|
||||
saleList.clear();
|
||||
saleList.addAll(list);
|
||||
adapter.notifyDataSetChanged();
|
||||
});
|
||||
|
||||
viewModel.getStores().observe(getViewLifecycleOwner(), list -> {
|
||||
SpinnerUtils.populateWhiteSpinner(requireContext(), binding.spinnerStore, list,
|
||||
StoreDTO::getStoreName, "Stores", null, StoreDTO::getStoreId);
|
||||
});
|
||||
|
||||
viewModel.getIsLoading().observe(getViewLifecycleOwner(), loading -> {
|
||||
binding.swipeRefreshSale.setRefreshing(loading);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
loadStoreData();
|
||||
viewModel.loadStores();
|
||||
}
|
||||
|
||||
private void setupFilterToggle() {
|
||||
@@ -93,28 +98,11 @@ public class SaleFragment extends Fragment implements SaleAdapter.OnSaleClickLis
|
||||
SpinnerUtils.setupFilterSpinner(binding.spinnerStore, () -> loadSales(true));
|
||||
}
|
||||
|
||||
private void loadStoreData() {
|
||||
storeViewModel.getAllStores(0, 100).observe(getViewLifecycleOwner(), resource -> {
|
||||
if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) {
|
||||
storeList.clear();
|
||||
storeList.addAll(resource.data.getContent());
|
||||
SpinnerUtils.populateWhiteSpinner(requireContext(), binding.spinnerStore, storeList,
|
||||
StoreDTO::getStoreName, "Stores", null, StoreDTO::getStoreId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void setupPaymentMethodFilter() {
|
||||
String[] paymentMethods = {"Payments", "Cash", "Card"};
|
||||
SpinnerUtils.setupStringFilterSpinner(requireContext(), binding.spinnerPaymentMethod, paymentMethods, () -> loadSales(true));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyView() {
|
||||
super.onDestroyView();
|
||||
binding = null;
|
||||
}
|
||||
|
||||
private void setupRecyclerView() {
|
||||
adapter = new SaleAdapter(saleList, this);
|
||||
binding.recyclerViewSales.setLayoutManager(new LinearLayoutManager(getContext()));
|
||||
@@ -129,7 +117,8 @@ public class SaleFragment extends Fragment implements SaleAdapter.OnSaleClickLis
|
||||
int visible = lm.getChildCount();
|
||||
int total = lm.getItemCount();
|
||||
int firstVis = lm.findFirstVisibleItemPosition();
|
||||
if (!isLoading && !isLastPage && (visible + firstVis) >= total - 3) {
|
||||
Boolean isLoading = viewModel.getIsLoading().getValue();
|
||||
if ((isLoading == null || !isLoading) && !viewModel.isLastPage() && (visible + firstVis) >= total - 3) {
|
||||
loadSales(false);
|
||||
}
|
||||
}
|
||||
@@ -146,13 +135,6 @@ public class SaleFragment extends Fragment implements SaleAdapter.OnSaleClickLis
|
||||
}
|
||||
|
||||
private void loadSales(boolean reset) {
|
||||
if (isLoading) return;
|
||||
|
||||
if (reset) {
|
||||
currentPage = 0;
|
||||
isLastPage = false;
|
||||
}
|
||||
|
||||
String query = binding.etSearchSale != null ? binding.etSearchSale.getText().toString().trim() : "";
|
||||
if (query.isEmpty()) query = null;
|
||||
|
||||
@@ -162,39 +144,12 @@ public class SaleFragment extends Fragment implements SaleAdapter.OnSaleClickLis
|
||||
}
|
||||
|
||||
Long storeId = null;
|
||||
if (binding.spinnerStore.getSelectedItemPosition() > 0 && !storeList.isEmpty()) {
|
||||
storeId = storeList.get(binding.spinnerStore.getSelectedItemPosition() - 1).getStoreId();
|
||||
List<StoreDTO> stores = viewModel.getStores().getValue();
|
||||
if (binding.spinnerStore.getSelectedItemPosition() > 0 && stores != null && !stores.isEmpty()) {
|
||||
storeId = stores.get(binding.spinnerStore.getSelectedItemPosition() - 1).getStoreId();
|
||||
}
|
||||
|
||||
saleViewModel.getAllSales(currentPage, PAGE_SIZE, query, paymentMethod, storeId, "saleDate,desc").observe(getViewLifecycleOwner(), resource -> {
|
||||
if (resource == null) return;
|
||||
|
||||
switch (resource.status) {
|
||||
case LOADING:
|
||||
isLoading = true;
|
||||
binding.swipeRefreshSale.setRefreshing(true);
|
||||
break;
|
||||
case SUCCESS:
|
||||
isLoading = false;
|
||||
binding.swipeRefreshSale.setRefreshing(false);
|
||||
if (resource.data != null) {
|
||||
if (reset) saleList.clear();
|
||||
saleList.addAll(resource.data.getContent());
|
||||
adapter.notifyDataSetChanged();
|
||||
isLastPage = resource.data.isLast();
|
||||
if (!isLastPage) currentPage++;
|
||||
}
|
||||
break;
|
||||
case ERROR:
|
||||
isLoading = false;
|
||||
binding.swipeRefreshSale.setRefreshing(false);
|
||||
Log.e(TAG, "Error loading sales: " + resource.message);
|
||||
if (getContext() != null) {
|
||||
Toast.makeText(getContext(), "Failed to load sales: " + resource.message, Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
break;
|
||||
}
|
||||
});
|
||||
viewModel.loadSales(reset, query, paymentMethod, storeId);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -210,4 +165,10 @@ public class SaleFragment extends Fragment implements SaleAdapter.OnSaleClickLis
|
||||
}
|
||||
NavHostFragment.findNavController(this).navigate(R.id.nav_sale_detail, args);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyView() {
|
||||
super.onDestroyView();
|
||||
binding = null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package com.example.petstoremobile.fragments.listfragments;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
@@ -21,7 +20,7 @@ import com.example.petstoremobile.databinding.FragmentServiceBinding;
|
||||
import com.example.petstoremobile.dtos.ServiceDTO;
|
||||
import com.example.petstoremobile.utils.BulkDeleteHandler;
|
||||
import com.example.petstoremobile.utils.UIUtils;
|
||||
import com.example.petstoremobile.viewmodels.ServiceViewModel;
|
||||
import com.example.petstoremobile.viewmodels.ServiceListViewModel;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
@@ -34,32 +33,18 @@ import dagger.hilt.android.AndroidEntryPoint;
|
||||
@AndroidEntryPoint
|
||||
public class ServiceFragment extends Fragment implements ServiceAdapter.OnServiceClickListener {
|
||||
|
||||
private static final String TAG = "ServiceFragment";
|
||||
private static final int PAGE_SIZE = 20;
|
||||
|
||||
private FragmentServiceBinding binding;
|
||||
private final List<ServiceDTO> serviceList = new ArrayList<>();
|
||||
private ServiceAdapter adapter;
|
||||
private ServiceViewModel viewModel;
|
||||
private ServiceListViewModel viewModel;
|
||||
private BulkDeleteHandler bulkDeleteHandler;
|
||||
|
||||
// Pagination
|
||||
private int currentPage = 0;
|
||||
private boolean isLastPage = false;
|
||||
private boolean isLoading = false;
|
||||
|
||||
/**
|
||||
* Initializes the fragment and its associated ViewModel.
|
||||
*/
|
||||
@Override
|
||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
viewModel = new ViewModelProvider(this).get(ServiceViewModel.class);
|
||||
viewModel = new ViewModelProvider(this).get(ServiceListViewModel.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up the fragment's UI components, including RecyclerView, search, and swipe-to-refresh.
|
||||
*/
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
|
||||
Bundle savedInstanceState) {
|
||||
@@ -70,15 +55,27 @@ public class ServiceFragment extends Fragment implements ServiceAdapter.OnServic
|
||||
setupSwipeRefresh();
|
||||
setupFilterToggle();
|
||||
setupBulkDelete();
|
||||
observeViewModel();
|
||||
|
||||
loadServices(true);
|
||||
|
||||
binding.fabAddService.setOnClickListener(v -> openDetail(null));
|
||||
|
||||
UIUtils.setupHamburgerMenu(binding.btnHamburger, this);
|
||||
|
||||
return binding.getRoot();
|
||||
}
|
||||
|
||||
private void observeViewModel() {
|
||||
viewModel.getServices().observe(getViewLifecycleOwner(), list -> {
|
||||
serviceList.clear();
|
||||
serviceList.addAll(list);
|
||||
adapter.notifyDataSetChanged();
|
||||
});
|
||||
|
||||
viewModel.getIsLoading().observe(getViewLifecycleOwner(), loading -> {
|
||||
binding.swipeRefreshService.setRefreshing(loading);
|
||||
});
|
||||
}
|
||||
|
||||
private void setupBulkDelete() {
|
||||
bulkDeleteHandler = new BulkDeleteHandler(
|
||||
this,
|
||||
@@ -98,23 +95,14 @@ public class ServiceFragment extends Fragment implements ServiceAdapter.OnServic
|
||||
binding = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up the filter toggle button to show/hide the filter layout.
|
||||
*/
|
||||
private void setupFilterToggle() {
|
||||
UIUtils.setupFilterToggle(binding.btnToggleFilter, binding.layoutFilter, binding.etSearchService);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up the search bar for filtering.
|
||||
*/
|
||||
private void setupSearch() {
|
||||
UIUtils.attachSearch(binding.etSearchService, () -> loadServices(true));
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the RecyclerView with a layout manager and adapter.
|
||||
*/
|
||||
private void setupRecyclerView() {
|
||||
adapter = new ServiceAdapter(serviceList, this);
|
||||
binding.recyclerViewServices.setLayoutManager(new LinearLayoutManager(getContext()));
|
||||
@@ -129,66 +117,24 @@ public class ServiceFragment extends Fragment implements ServiceAdapter.OnServic
|
||||
int visible = lm.getChildCount();
|
||||
int total = lm.getItemCount();
|
||||
int firstVis = lm.findFirstVisibleItemPosition();
|
||||
if (!isLoading && !isLastPage && (visible + firstVis) >= total - 3) {
|
||||
Boolean isLoading = viewModel.getIsLoading().getValue();
|
||||
if ((isLoading == null || !isLoading) && !viewModel.isLastPage() && (visible + firstVis) >= total - 3) {
|
||||
loadServices(false);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up the SwipeRefreshLayout.
|
||||
*/
|
||||
private void setupSwipeRefresh() {
|
||||
binding.swipeRefreshService.setOnRefreshListener(() -> loadServices(true));
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches a page of services from the API.
|
||||
*/
|
||||
private void loadServices(boolean reset) {
|
||||
if (isLoading) return;
|
||||
|
||||
if (reset) {
|
||||
currentPage = 0;
|
||||
isLastPage = false;
|
||||
}
|
||||
|
||||
String query = binding.etSearchService.getText().toString().trim();
|
||||
if (query.isEmpty()) query = null;
|
||||
|
||||
viewModel.getAllServices(currentPage, PAGE_SIZE, query, "serviceName").observe(getViewLifecycleOwner(), resource -> {
|
||||
if (resource == null) return;
|
||||
|
||||
switch (resource.status) {
|
||||
case LOADING:
|
||||
isLoading = true;
|
||||
binding.swipeRefreshService.setRefreshing(true);
|
||||
break;
|
||||
case SUCCESS:
|
||||
isLoading = false;
|
||||
binding.swipeRefreshService.setRefreshing(false);
|
||||
if (resource.data != null) {
|
||||
if (reset) serviceList.clear();
|
||||
serviceList.addAll(resource.data.getContent());
|
||||
adapter.notifyDataSetChanged();
|
||||
isLastPage = resource.data.isLast();
|
||||
if (!isLastPage) currentPage++;
|
||||
}
|
||||
break;
|
||||
case ERROR:
|
||||
isLoading = false;
|
||||
binding.swipeRefreshService.setRefreshing(false);
|
||||
Log.e(TAG, "Error: " + resource.message);
|
||||
Toast.makeText(getContext(), "Failed to load services: " + resource.message, Toast.LENGTH_SHORT).show();
|
||||
break;
|
||||
}
|
||||
});
|
||||
viewModel.loadServices(reset, query);
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigates to the service detail screen.
|
||||
*/
|
||||
private void openDetail(ServiceDTO service) {
|
||||
Bundle args = new Bundle();
|
||||
if (service != null) {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package com.example.petstoremobile.fragments.listfragments;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
import android.view.*;
|
||||
import android.widget.*;
|
||||
import androidx.annotation.NonNull;
|
||||
@@ -14,7 +13,7 @@ import com.example.petstoremobile.adapters.EmployeeAdapter;
|
||||
import com.example.petstoremobile.databinding.FragmentStaffBinding;
|
||||
import com.example.petstoremobile.dtos.EmployeeDTO;
|
||||
import com.example.petstoremobile.utils.UIUtils;
|
||||
import com.example.petstoremobile.viewmodels.EmployeeViewModel;
|
||||
import com.example.petstoremobile.viewmodels.StaffListViewModel;
|
||||
import dagger.hilt.android.AndroidEntryPoint;
|
||||
import java.util.*;
|
||||
|
||||
@@ -22,21 +21,22 @@ import java.util.*;
|
||||
public class StaffFragment extends Fragment implements EmployeeAdapter.OnEmployeeClickListener {
|
||||
|
||||
private FragmentStaffBinding binding;
|
||||
private EmployeeViewModel employeeViewModel;
|
||||
private List<EmployeeDTO> employeeList = new ArrayList<>();
|
||||
private List<EmployeeDTO> filteredList = new ArrayList<>();
|
||||
private StaffListViewModel viewModel;
|
||||
private List<EmployeeDTO> staffList = new ArrayList<>();
|
||||
private EmployeeAdapter adapter;
|
||||
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
|
||||
Bundle savedInstanceState) {
|
||||
binding = FragmentStaffBinding.inflate(inflater, container, false);
|
||||
employeeViewModel = new ViewModelProvider(this).get(EmployeeViewModel.class);
|
||||
viewModel = new ViewModelProvider(this).get(StaffListViewModel.class);
|
||||
|
||||
setupRecyclerView();
|
||||
setupSearch();
|
||||
setupSwipeRefresh();
|
||||
loadStaff();
|
||||
observeViewModel();
|
||||
|
||||
viewModel.loadStaff();
|
||||
|
||||
binding.fabAddStaff.setOnClickListener(v -> openDetail(-1));
|
||||
|
||||
@@ -46,70 +46,36 @@ public class StaffFragment extends Fragment implements EmployeeAdapter.OnEmploye
|
||||
return binding.getRoot();
|
||||
}
|
||||
|
||||
private void observeViewModel() {
|
||||
viewModel.getFilteredEmployees().observe(getViewLifecycleOwner(), list -> {
|
||||
staffList.clear();
|
||||
staffList.addAll(list);
|
||||
adapter.notifyDataSetChanged();
|
||||
});
|
||||
|
||||
viewModel.getIsLoading().observe(getViewLifecycleOwner(), loading -> {
|
||||
binding.swipeRefreshStaff.setRefreshing(loading);
|
||||
});
|
||||
}
|
||||
|
||||
private void setupRecyclerView() {
|
||||
adapter = new EmployeeAdapter(filteredList, this);
|
||||
adapter = new EmployeeAdapter(staffList, this);
|
||||
binding.recyclerViewStaff.setLayoutManager(new LinearLayoutManager(getContext()));
|
||||
binding.recyclerViewStaff.setAdapter(adapter);
|
||||
}
|
||||
|
||||
private void setupSearch() {
|
||||
UIUtils.attachSearch(binding.etSearchStaff, () -> filter(binding.etSearchStaff.getText().toString()));
|
||||
UIUtils.attachSearch(binding.etSearchStaff, () -> viewModel.filter(binding.etSearchStaff.getText().toString()));
|
||||
}
|
||||
|
||||
private void setupSwipeRefresh() {
|
||||
binding.swipeRefreshStaff.setOnRefreshListener(this::loadStaff);
|
||||
}
|
||||
|
||||
private void filter(String query) {
|
||||
filteredList.clear();
|
||||
if (query.isEmpty()) {
|
||||
filteredList.addAll(employeeList);
|
||||
} else {
|
||||
String lower = query.toLowerCase();
|
||||
for (EmployeeDTO e : employeeList) {
|
||||
if ((e.getFullName() != null && e.getFullName().toLowerCase().contains(lower))
|
||||
|| (e.getUsername() != null && e.getUsername().toLowerCase().contains(lower))
|
||||
|| (e.getEmail() != null && e.getEmail().toLowerCase().contains(lower))
|
||||
|| (e.getPhone() != null && e.getPhone().toLowerCase().contains(lower))) {
|
||||
filteredList.add(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
adapter.notifyDataSetChanged();
|
||||
}
|
||||
|
||||
private void loadStaff() {
|
||||
binding.swipeRefreshStaff.setRefreshing(true);
|
||||
employeeViewModel.getAllEmployees(0, 100).observe(getViewLifecycleOwner(), resource -> {
|
||||
if (resource != null) {
|
||||
switch (resource.status) {
|
||||
case SUCCESS:
|
||||
binding.swipeRefreshStaff.setRefreshing(false);
|
||||
if (resource.data != null) {
|
||||
employeeList.clear();
|
||||
employeeList.addAll(resource.data.getContent());
|
||||
filter(binding != null ? binding.etSearchStaff.getText().toString() : "");
|
||||
}
|
||||
break;
|
||||
case ERROR:
|
||||
binding.swipeRefreshStaff.setRefreshing(false);
|
||||
if (getContext() != null) {
|
||||
Toast.makeText(getContext(), resource.message != null ? resource.message : "Failed to load staff",
|
||||
Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
break;
|
||||
case LOADING:
|
||||
binding.swipeRefreshStaff.setRefreshing(true);
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
binding.swipeRefreshStaff.setOnRefreshListener(viewModel::loadStaff);
|
||||
}
|
||||
|
||||
private void openDetail(int position) {
|
||||
Bundle args = new Bundle();
|
||||
if (position != -1) {
|
||||
EmployeeDTO e = filteredList.get(position);
|
||||
EmployeeDTO e = staffList.get(position);
|
||||
args.putLong("employeeId", e.getEmployeeId());
|
||||
args.putString("username", e.getUsername() != null ? e.getUsername() : "");
|
||||
args.putString("firstName", e.getFirstName() != null ? e.getFirstName() : "");
|
||||
|
||||
@@ -9,20 +9,17 @@ import androidx.lifecycle.ViewModelProvider;
|
||||
import androidx.navigation.fragment.NavHostFragment;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
|
||||
import android.util.Log;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.example.petstoremobile.R;
|
||||
import com.example.petstoremobile.adapters.SupplierAdapter;
|
||||
import com.example.petstoremobile.databinding.FragmentSupplierBinding;
|
||||
import com.example.petstoremobile.dtos.SupplierDTO;
|
||||
import com.example.petstoremobile.utils.BulkDeleteHandler;
|
||||
import com.example.petstoremobile.utils.Resource;
|
||||
import com.example.petstoremobile.utils.UIUtils;
|
||||
import com.example.petstoremobile.viewmodels.SupplierViewModel;
|
||||
import com.example.petstoremobile.viewmodels.SupplierListViewModel;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
@@ -35,21 +32,15 @@ public class SupplierFragment extends Fragment implements SupplierAdapter.OnSupp
|
||||
private FragmentSupplierBinding binding;
|
||||
private List<SupplierDTO> supplierList = new ArrayList<>();
|
||||
private SupplierAdapter adapter;
|
||||
private SupplierViewModel viewModel;
|
||||
private SupplierListViewModel viewModel;
|
||||
private BulkDeleteHandler bulkDeleteHandler;
|
||||
|
||||
/**
|
||||
* Initializes the fragment and its associated SupplierViewModel.
|
||||
*/
|
||||
@Override
|
||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
viewModel = new ViewModelProvider(this).get(SupplierViewModel.class);
|
||||
viewModel = new ViewModelProvider(this).get(SupplierListViewModel.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up the fragment's UI components, including RecyclerView, search, and swipe-to-refresh.
|
||||
*/
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
|
||||
Bundle savedInstanceState) {
|
||||
@@ -60,9 +51,10 @@ public class SupplierFragment extends Fragment implements SupplierAdapter.OnSupp
|
||||
setupSwipeRefresh();
|
||||
setupFilterToggle();
|
||||
setupBulkDelete();
|
||||
observeViewModel();
|
||||
|
||||
loadSupplierData();
|
||||
|
||||
//Add button to opens the add dialog
|
||||
binding.fabAddSupplier.setOnClickListener(v -> openSupplierDetails(-1));
|
||||
|
||||
UIUtils.setupHamburgerMenu(binding.btnHamburger, this);
|
||||
@@ -70,6 +62,18 @@ public class SupplierFragment extends Fragment implements SupplierAdapter.OnSupp
|
||||
return binding.getRoot();
|
||||
}
|
||||
|
||||
private void observeViewModel() {
|
||||
viewModel.getSuppliers().observe(getViewLifecycleOwner(), list -> {
|
||||
supplierList.clear();
|
||||
supplierList.addAll(list);
|
||||
adapter.notifyDataSetChanged();
|
||||
});
|
||||
|
||||
viewModel.getIsLoading().observe(getViewLifecycleOwner(), loading -> {
|
||||
binding.swipeRefreshSupplier.setRefreshing(loading);
|
||||
});
|
||||
}
|
||||
|
||||
private void setupBulkDelete() {
|
||||
bulkDeleteHandler = new BulkDeleteHandler(
|
||||
this,
|
||||
@@ -89,47 +93,27 @@ public class SupplierFragment extends Fragment implements SupplierAdapter.OnSupp
|
||||
binding = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up the filter toggle button to show/hide the filter layout.
|
||||
*/
|
||||
private void setupFilterToggle() {
|
||||
UIUtils.setupFilterToggle(binding.btnToggleFilter, binding.layoutFilter, binding.etSearchSupplier);
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures the search bar for filtering.
|
||||
*/
|
||||
private void setupSearch() {
|
||||
UIUtils.attachSearch(binding.etSearchSupplier, this::loadSupplierData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up the SwipeRefreshLayout to allow manual reloading of supplier data.
|
||||
*/
|
||||
private void setupSwipeRefresh() {
|
||||
binding.swipeRefreshSupplier.setOnRefreshListener(this::loadSupplierData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigates to the supplier detail screen for editing an existing record or adding a new one.
|
||||
*/
|
||||
private void openSupplierDetails(int position) {
|
||||
//Make a bundle to pass data to the detail fragment
|
||||
Bundle args = new Bundle();
|
||||
|
||||
//if editing a supplier, add the supplier id to the bundle
|
||||
if (position != -1) {
|
||||
SupplierDTO supplier = supplierList.get(position);
|
||||
args.putLong("supId", supplier.getSupId());
|
||||
}
|
||||
|
||||
NavHostFragment.findNavController(this).navigate(R.id.nav_supplier_detail, args);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Handles item click in the supplier list.
|
||||
*/
|
||||
@Override
|
||||
public void onSupplierClick(int position) {
|
||||
openSupplierDetails(position);
|
||||
@@ -142,47 +126,12 @@ public class SupplierFragment extends Fragment implements SupplierAdapter.OnSupp
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches all supplier data from the server through the ViewModel and updates the UI.
|
||||
*/
|
||||
private void loadSupplierData() {
|
||||
String query = binding.etSearchSupplier != null ? binding.etSearchSupplier.getText().toString().trim() : "";
|
||||
if (query.isEmpty()) query = null;
|
||||
|
||||
//Load suppliers from the backend with query and default sort
|
||||
viewModel.getAllSuppliers(0, 100, query, "supCompany").observe(getViewLifecycleOwner(), resource -> {
|
||||
if (resource == null) return;
|
||||
|
||||
// Check the status to see if the resource is loaded and display the data
|
||||
switch (resource.status) {
|
||||
case LOADING:
|
||||
// Show loading indicator
|
||||
binding.swipeRefreshSupplier.setRefreshing(true);
|
||||
break;
|
||||
case SUCCESS:
|
||||
// Hide loading indicator and display data
|
||||
binding.swipeRefreshSupplier.setRefreshing(false);
|
||||
if (resource.data != null) {
|
||||
supplierList.clear();
|
||||
supplierList.addAll(resource.data.getContent());
|
||||
adapter.notifyDataSetChanged();
|
||||
}
|
||||
break;
|
||||
case ERROR:
|
||||
// Hide loading indicator and toast error message
|
||||
binding.swipeRefreshSupplier.setRefreshing(false);
|
||||
if (getContext() != null) {
|
||||
Toast.makeText(getContext(), "Failed to load suppliers: " + resource.message, Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
Log.e("SupplierFragment", "Error loading suppliers: " + resource.message);
|
||||
break;
|
||||
}
|
||||
});
|
||||
viewModel.loadSuppliers(query);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the RecyclerView with a layout manager and adapter for displaying suppliers.
|
||||
*/
|
||||
private void setupRecyclerView() {
|
||||
adapter = new SupplierAdapter(supplierList, this);
|
||||
binding.recyclerViewSuppliers.setLayoutManager(new LinearLayoutManager(getContext()));
|
||||
|
||||
@@ -23,7 +23,7 @@ import com.example.petstoremobile.utils.FileUtils;
|
||||
import com.example.petstoremobile.utils.GlideUtils;
|
||||
import com.example.petstoremobile.utils.ImagePickerHelper;
|
||||
import com.example.petstoremobile.utils.Resource;
|
||||
import com.example.petstoremobile.viewmodels.PetViewModel;
|
||||
import com.example.petstoremobile.viewmodels.PetProfileViewModel;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.Locale;
|
||||
@@ -46,17 +46,13 @@ public class PetProfileFragment extends Fragment {
|
||||
@Inject @Named("baseUrl") String baseUrl;
|
||||
@Inject TokenManager tokenManager;
|
||||
|
||||
private PetViewModel viewModel;
|
||||
private PetProfileViewModel viewModel;
|
||||
private ImagePickerHelper imagePickerHelper;
|
||||
|
||||
|
||||
/**
|
||||
* Initializes activity launchers for gallery, camera, and permissions.
|
||||
*/
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
viewModel = new ViewModelProvider(this).get(PetViewModel.class);
|
||||
viewModel = new ViewModelProvider(this).get(PetProfileViewModel.class);
|
||||
|
||||
imagePickerHelper = new ImagePickerHelper(this, "pet_photo.jpg", new ImagePickerHelper.ImagePickerListener() {
|
||||
@Override
|
||||
@@ -71,34 +67,27 @@ public class PetProfileFragment extends Fragment {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Inflates the layout using view binding, initializes views, and sets up click listeners.
|
||||
*/
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
|
||||
Bundle savedInstanceState) {
|
||||
binding = FragmentPetProfileBinding.inflate(inflater, container, false);
|
||||
|
||||
// Set pet details to display
|
||||
if (getArguments() != null) {
|
||||
petId = getArguments().getLong("petId");
|
||||
loadPetData();
|
||||
loadPetImage((int) petId);
|
||||
}
|
||||
|
||||
//set button click listeners
|
||||
binding.btnBack.setOnClickListener(v -> {
|
||||
NavHostFragment.findNavController(this).popBackStack();
|
||||
});
|
||||
|
||||
//Make the edit button go to the pet detail view
|
||||
binding.btnEditPet.setOnClickListener(v -> {
|
||||
Bundle args = new Bundle();
|
||||
args.putLong("petId", petId);
|
||||
NavHostFragment.findNavController(this).navigate(R.id.nav_pet_detail, args);
|
||||
});
|
||||
|
||||
//Make change photo button ask user to select a new photo
|
||||
binding.btnChangePhoto.setOnClickListener(v -> {
|
||||
imagePickerHelper.showImagePickerDialog("Change Pet Photo", hasImage);
|
||||
});
|
||||
@@ -112,9 +101,6 @@ public class PetProfileFragment extends Fragment {
|
||||
binding = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches current pet data from the backend and updates the UI.
|
||||
*/
|
||||
private void loadPetData() {
|
||||
viewModel.getPetById(petId).observe(getViewLifecycleOwner(), resource -> {
|
||||
if (resource == null) return;
|
||||
@@ -133,7 +119,6 @@ public class PetProfileFragment extends Fragment {
|
||||
|
||||
String status = pet.getPetStatus();
|
||||
|
||||
// Display owner name only if the pet is Adopted or Owned
|
||||
if ("Adopted".equalsIgnoreCase(status) || "Owned".equalsIgnoreCase(status)) {
|
||||
binding.layoutPetOwner.setVisibility(View.VISIBLE);
|
||||
if (pet.getCustomerName() != null && !pet.getCustomerName().isEmpty()) {
|
||||
@@ -145,7 +130,6 @@ public class PetProfileFragment extends Fragment {
|
||||
binding.layoutPetOwner.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
// Display store name only if the pet is Adopted or Available
|
||||
if ("Available".equalsIgnoreCase(status) || "Adopted".equalsIgnoreCase(status)) {
|
||||
binding.layoutPetStore.setVisibility(View.VISIBLE);
|
||||
if (pet.getStoreName() != null && !pet.getStoreName().isEmpty()) {
|
||||
@@ -162,9 +146,6 @@ public class PetProfileFragment extends Fragment {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches and displays the pet\'s image from the server.
|
||||
*/
|
||||
private void loadPetImage(int petId) {
|
||||
String imageUrl = baseUrl + String.format(Locale.US, PetApi.PET_IMAGE_PATH, petId);
|
||||
String token = tokenManager.getToken();
|
||||
@@ -182,19 +163,14 @@ public class PetProfileFragment extends Fragment {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Uploads a selected or captured image a pet photo through the ViewModel.
|
||||
*/
|
||||
private void uploadPetImage(Uri uri) {
|
||||
try {
|
||||
File file = FileUtils.getFileFromUri(requireContext(), uri);
|
||||
if (file == null) return;
|
||||
|
||||
// Create RequestBody for file upload
|
||||
RequestBody requestFile = RequestBody.create(file, MediaType.parse(requireContext().getContentResolver().getType(uri)));
|
||||
MultipartBody.Part body = MultipartBody.Part.createFormData("image", file.getName(), requestFile);
|
||||
|
||||
// Use ViewModel to upload image
|
||||
viewModel.uploadPetImage(petId, body).observe(getViewLifecycleOwner(), resource -> {
|
||||
if (resource != null && resource.status != Resource.Status.LOADING) {
|
||||
if (resource.status == Resource.Status.SUCCESS) {
|
||||
@@ -210,9 +186,6 @@ public class PetProfileFragment extends Fragment {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a request to the ViewModel to remove the current pet photo.
|
||||
*/
|
||||
private void deletePetImage() {
|
||||
viewModel.deletePetImage(petId).observe(getViewLifecycleOwner(), resource -> {
|
||||
if (resource != null && resource.status != Resource.Status.LOADING) {
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
package com.example.petstoremobile.viewmodels;
|
||||
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
import androidx.lifecycle.ViewModel;
|
||||
|
||||
import com.example.petstoremobile.dtos.AdoptionDTO;
|
||||
import com.example.petstoremobile.dtos.BulkDeleteRequest;
|
||||
import com.example.petstoremobile.dtos.StoreDTO;
|
||||
import com.example.petstoremobile.repositories.AdoptionRepository;
|
||||
import com.example.petstoremobile.repositories.StoreRepository;
|
||||
import com.example.petstoremobile.utils.Resource;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel;
|
||||
|
||||
@HiltViewModel
|
||||
public class AdoptionListViewModel extends ViewModel {
|
||||
private final AdoptionRepository adoptionRepository;
|
||||
private final StoreRepository storeRepository;
|
||||
|
||||
private final MutableLiveData<List<AdoptionDTO>> adoptions = new MutableLiveData<>(new ArrayList<>());
|
||||
private final MutableLiveData<List<StoreDTO>> stores = new MutableLiveData<>(new ArrayList<>());
|
||||
private final MutableLiveData<Boolean> isLoading = new MutableLiveData<>(false);
|
||||
|
||||
private int currentPage = 0;
|
||||
private boolean isLastPage = false;
|
||||
private static final int PAGE_SIZE = 20;
|
||||
|
||||
@Inject
|
||||
public AdoptionListViewModel(AdoptionRepository adoptionRepository, StoreRepository storeRepository) {
|
||||
this.adoptionRepository = adoptionRepository;
|
||||
this.storeRepository = storeRepository;
|
||||
}
|
||||
|
||||
public LiveData<List<AdoptionDTO>> getAdoptions() { return adoptions; }
|
||||
public LiveData<List<StoreDTO>> getStores() { return stores; }
|
||||
public LiveData<Boolean> getIsLoading() { return isLoading; }
|
||||
public boolean isLastPage() { return isLastPage; }
|
||||
|
||||
public void loadAdoptions(boolean reset, String query, String status, Long storeId) {
|
||||
if (isLoading.getValue() != null && isLoading.getValue() && !reset) return;
|
||||
|
||||
if (reset) {
|
||||
currentPage = 0;
|
||||
isLastPage = false;
|
||||
}
|
||||
|
||||
if ("All Statuses".equals(status)) status = null;
|
||||
|
||||
isLoading.setValue(true);
|
||||
adoptionRepository.getAllAdoptions(currentPage, PAGE_SIZE, query, status, storeId, "adoptionDate,desc").observeForever(resource -> {
|
||||
if (resource != null) {
|
||||
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
|
||||
List<AdoptionDTO> currentList = reset ? new ArrayList<>() : new ArrayList<>(adoptions.getValue());
|
||||
currentList.addAll(resource.data.getContent());
|
||||
adoptions.setValue(currentList);
|
||||
isLastPage = resource.data.isLast();
|
||||
if (!isLastPage) currentPage++;
|
||||
isLoading.setValue(false);
|
||||
} else if (resource.status == Resource.Status.ERROR) {
|
||||
isLoading.setValue(false);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void loadStores() {
|
||||
storeRepository.getAllStores(0, 100).observeForever(resource -> {
|
||||
if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) {
|
||||
stores.setValue(resource.data.getContent());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public LiveData<Resource<Void>> bulkDeleteAdoptions(List<String> ids) {
|
||||
return adoptionRepository.bulkDeleteAdoptions(new BulkDeleteRequest(ids));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
package com.example.petstoremobile.viewmodels;
|
||||
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
import androidx.lifecycle.ViewModel;
|
||||
|
||||
import com.example.petstoremobile.dtos.SaleDTO;
|
||||
import com.example.petstoremobile.repositories.SaleRepository;
|
||||
import com.example.petstoremobile.utils.Resource;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Calendar;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.TreeMap;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel;
|
||||
|
||||
@HiltViewModel
|
||||
public class AnalyticsViewModel extends ViewModel {
|
||||
private final SaleRepository saleRepository;
|
||||
|
||||
private final MutableLiveData<AnalyticsData> analyticsData = new MutableLiveData<>();
|
||||
private final MutableLiveData<Boolean> isLoading = new MutableLiveData<>(false);
|
||||
private final MutableLiveData<String> errorMessage = new MutableLiveData<>();
|
||||
|
||||
@Inject
|
||||
public AnalyticsViewModel(SaleRepository saleRepository) {
|
||||
this.saleRepository = saleRepository;
|
||||
}
|
||||
|
||||
public LiveData<AnalyticsData> getAnalyticsData() { return analyticsData; }
|
||||
public LiveData<Boolean> getIsLoading() { return isLoading; }
|
||||
public LiveData<String> getErrorMessage() { return errorMessage; }
|
||||
|
||||
public void loadAnalytics() {
|
||||
isLoading.setValue(true);
|
||||
errorMessage.setValue(null);
|
||||
saleRepository.getAllSales(0, 1000, null, null, null, "saleDate,desc").observeForever(resource -> {
|
||||
if (resource != null) {
|
||||
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
|
||||
computeAnalytics(resource.data.getContent());
|
||||
isLoading.setValue(false);
|
||||
} else if (resource.status == Resource.Status.ERROR) {
|
||||
errorMessage.setValue(resource.message);
|
||||
isLoading.setValue(false);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void computeAnalytics(List<SaleDTO> sales) {
|
||||
List<SaleDTO> regularSales = new ArrayList<>();
|
||||
for (SaleDTO s : sales) {
|
||||
if (!Boolean.TRUE.equals(s.getIsRefund()))
|
||||
regularSales.add(s);
|
||||
}
|
||||
|
||||
AnalyticsData data = new AnalyticsData();
|
||||
|
||||
// Summary
|
||||
BigDecimal totalRevenue = BigDecimal.ZERO;
|
||||
int totalItems = 0;
|
||||
for (SaleDTO s : regularSales) {
|
||||
if (s.getTotalAmount() != null) totalRevenue = totalRevenue.add(s.getTotalAmount());
|
||||
if (s.getItems() != null) {
|
||||
for (SaleDTO.SaleItemDTO item : s.getItems()) {
|
||||
if (item.getQuantity() != null) totalItems += Math.abs(item.getQuantity());
|
||||
}
|
||||
}
|
||||
}
|
||||
data.totalRevenue = totalRevenue;
|
||||
data.totalTransactions = regularSales.size();
|
||||
data.avgTransaction = data.totalTransactions > 0
|
||||
? totalRevenue.divide(BigDecimal.valueOf(data.totalTransactions), 2, RoundingMode.HALF_UP)
|
||||
: BigDecimal.ZERO;
|
||||
data.totalItems = totalItems;
|
||||
|
||||
// Product Maps
|
||||
Map<String, BigDecimal> revenueByProduct = new LinkedHashMap<>();
|
||||
Map<String, Integer> quantityByProduct = new LinkedHashMap<>();
|
||||
Map<String, Integer> paymentCount = new LinkedHashMap<>();
|
||||
Map<String, BigDecimal> employeeRevenue = new LinkedHashMap<>();
|
||||
|
||||
for (SaleDTO s : regularSales) {
|
||||
// Payments
|
||||
String method = s.getPaymentMethod() != null ? s.getPaymentMethod() : "Unknown";
|
||||
paymentCount.merge(method, 1, Integer::sum);
|
||||
|
||||
// Employee
|
||||
String emp = s.getEmployeeName() != null ? s.getEmployeeName() : "Unknown";
|
||||
if (s.getTotalAmount() != null) employeeRevenue.merge(emp, s.getTotalAmount(), BigDecimal::add);
|
||||
|
||||
// Items
|
||||
if (s.getItems() != null) {
|
||||
for (SaleDTO.SaleItemDTO item : s.getItems()) {
|
||||
String name = item.getProductName() != null ? item.getProductName() : "Unknown";
|
||||
int qty = item.getQuantity() != null ? Math.abs(item.getQuantity()) : 0;
|
||||
BigDecimal lineTotal = item.getUnitPrice() != null
|
||||
? item.getUnitPrice().multiply(BigDecimal.valueOf(qty))
|
||||
: BigDecimal.ZERO;
|
||||
revenueByProduct.merge(name, lineTotal, BigDecimal::add);
|
||||
quantityByProduct.merge(name, qty, Integer::sum);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sort Top Revenue
|
||||
data.topRevenueProducts = new ArrayList<>(revenueByProduct.entrySet());
|
||||
data.topRevenueProducts.sort((a, b) -> b.getValue().compareTo(a.getValue()));
|
||||
if (data.topRevenueProducts.size() > 5) data.topRevenueProducts = data.topRevenueProducts.subList(0, 5);
|
||||
|
||||
// Sort Top Quantity
|
||||
data.topQuantityProducts = new ArrayList<>(quantityByProduct.entrySet());
|
||||
data.topQuantityProducts.sort((a, b) -> b.getValue() - a.getValue());
|
||||
if (data.topQuantityProducts.size() > 5) data.topQuantityProducts = data.topQuantityProducts.subList(0, 5);
|
||||
|
||||
// Payment Stats
|
||||
data.paymentMethodStats = new ArrayList<>(paymentCount.entrySet());
|
||||
|
||||
// Employee Performance
|
||||
data.employeePerformance = new ArrayList<>(employeeRevenue.entrySet());
|
||||
data.employeePerformance.sort((a, b) -> b.getValue().compareTo(a.getValue()));
|
||||
|
||||
// Daily Revenue (last 7 days)
|
||||
Map<String, BigDecimal> dailyMap = new TreeMap<>();
|
||||
for (int i = 6; i >= 0; i--) {
|
||||
Calendar day = Calendar.getInstance();
|
||||
day.add(Calendar.DAY_OF_YEAR, -i);
|
||||
String key = String.format("%04d-%02d-%02d",
|
||||
day.get(Calendar.YEAR), day.get(Calendar.MONTH) + 1, day.get(Calendar.DAY_OF_MONTH));
|
||||
dailyMap.put(key, BigDecimal.ZERO);
|
||||
}
|
||||
for (SaleDTO s : regularSales) {
|
||||
if (s.getSaleDate() != null && s.getTotalAmount() != null) {
|
||||
String date = s.getSaleDate().length() >= 10 ? s.getSaleDate().substring(0, 10) : s.getSaleDate();
|
||||
if (dailyMap.containsKey(date)) dailyMap.merge(date, s.getTotalAmount(), BigDecimal::add);
|
||||
}
|
||||
}
|
||||
data.dailyRevenue = new ArrayList<>(dailyMap.entrySet());
|
||||
|
||||
analyticsData.setValue(data);
|
||||
}
|
||||
|
||||
public static class AnalyticsData {
|
||||
public BigDecimal totalRevenue;
|
||||
public int totalTransactions;
|
||||
public BigDecimal avgTransaction;
|
||||
public int totalItems;
|
||||
public List<Map.Entry<String, BigDecimal>> topRevenueProducts;
|
||||
public List<Map.Entry<String, Integer>> topQuantityProducts;
|
||||
public List<Map.Entry<String, Integer>> paymentMethodStats;
|
||||
public List<Map.Entry<String, BigDecimal>> employeePerformance;
|
||||
public List<Map.Entry<String, BigDecimal>> dailyRevenue;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package com.example.petstoremobile.viewmodels;
|
||||
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
import androidx.lifecycle.ViewModel;
|
||||
|
||||
import com.example.petstoremobile.dtos.AppointmentDTO;
|
||||
import com.example.petstoremobile.dtos.BulkDeleteRequest;
|
||||
import com.example.petstoremobile.dtos.StoreDTO;
|
||||
import com.example.petstoremobile.repositories.AppointmentRepository;
|
||||
import com.example.petstoremobile.repositories.StoreRepository;
|
||||
import com.example.petstoremobile.utils.Resource;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel;
|
||||
|
||||
@HiltViewModel
|
||||
public class AppointmentListViewModel extends ViewModel {
|
||||
private final AppointmentRepository appointmentRepository;
|
||||
private final StoreRepository storeRepository;
|
||||
|
||||
private final MutableLiveData<List<AppointmentDTO>> appointments = new MutableLiveData<>(new ArrayList<>());
|
||||
private final MutableLiveData<List<StoreDTO>> stores = new MutableLiveData<>(new ArrayList<>());
|
||||
private final MutableLiveData<Boolean> isLoading = new MutableLiveData<>(false);
|
||||
|
||||
@Inject
|
||||
public AppointmentListViewModel(AppointmentRepository appointmentRepository, StoreRepository storeRepository) {
|
||||
this.appointmentRepository = appointmentRepository;
|
||||
this.storeRepository = storeRepository;
|
||||
}
|
||||
|
||||
public LiveData<List<AppointmentDTO>> getAppointments() { return appointments; }
|
||||
public LiveData<List<StoreDTO>> getStores() { return stores; }
|
||||
public LiveData<Boolean> getIsLoading() { return isLoading; }
|
||||
|
||||
public void loadAppointments(String query, String status, Long storeId, String date, Long employeeId) {
|
||||
isLoading.setValue(true);
|
||||
appointmentRepository.getAllAppointments(0, 500, query, status, storeId, date, employeeId).observeForever(resource -> {
|
||||
if (resource != null) {
|
||||
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
|
||||
appointments.setValue(resource.data.getContent());
|
||||
isLoading.setValue(false);
|
||||
} else if (resource.status == Resource.Status.ERROR) {
|
||||
isLoading.setValue(false);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void loadStores() {
|
||||
storeRepository.getAllStores(0, 100).observeForever(resource -> {
|
||||
if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) {
|
||||
stores.setValue(resource.data.getContent());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public LiveData<Resource<Void>> bulkDeleteAppointments(List<String> ids) {
|
||||
return appointmentRepository.bulkDeleteAppointments(new BulkDeleteRequest(ids));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
package com.example.petstoremobile.viewmodels;
|
||||
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
import androidx.lifecycle.ViewModel;
|
||||
|
||||
import com.example.petstoremobile.dtos.ConversationDTO;
|
||||
import com.example.petstoremobile.dtos.CustomerDTO;
|
||||
import com.example.petstoremobile.dtos.MessageDTO;
|
||||
import com.example.petstoremobile.dtos.PageResponse;
|
||||
import com.example.petstoremobile.dtos.SendMessageRequest;
|
||||
import com.example.petstoremobile.models.Chat;
|
||||
import com.example.petstoremobile.models.Message;
|
||||
import com.example.petstoremobile.repositories.ChatRepository;
|
||||
import com.example.petstoremobile.repositories.CustomerRepository;
|
||||
import com.example.petstoremobile.utils.Resource;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel;
|
||||
|
||||
@HiltViewModel
|
||||
public class ChatListViewModel extends ViewModel {
|
||||
private final ChatRepository chatRepository;
|
||||
private final CustomerRepository customerRepository;
|
||||
|
||||
private final MutableLiveData<List<Chat>> chatList = new MutableLiveData<>(new ArrayList<>());
|
||||
private final MutableLiveData<List<Message>> messageList = new MutableLiveData<>(new ArrayList<>());
|
||||
private final Map<Long, String> customerNames = new HashMap<>();
|
||||
private final MutableLiveData<Boolean> isLoading = new MutableLiveData<>(false);
|
||||
|
||||
@Inject
|
||||
public ChatListViewModel(ChatRepository chatRepository, CustomerRepository customerRepository) {
|
||||
this.chatRepository = chatRepository;
|
||||
this.customerRepository = customerRepository;
|
||||
}
|
||||
|
||||
public LiveData<List<Chat>> getChatList() { return chatList; }
|
||||
public LiveData<List<Message>> getMessageList() { return messageList; }
|
||||
public LiveData<Boolean> getIsLoading() { return isLoading; }
|
||||
|
||||
public void loadCustomers() {
|
||||
customerRepository.getAllCustomers(0, 100).observeForever(resource -> {
|
||||
if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) {
|
||||
for (CustomerDTO c : resource.data.getContent()) {
|
||||
customerNames.put(c.getCustomerId(), c.getFullName());
|
||||
}
|
||||
loadConversations();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void loadConversations() {
|
||||
isLoading.setValue(true);
|
||||
chatRepository.getAllConversations().observeForever(resource -> {
|
||||
if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) {
|
||||
List<Chat> chats = new ArrayList<>();
|
||||
for (ConversationDTO dto : resource.data) {
|
||||
String name = customerNames.getOrDefault(dto.getCustomerId(), "Customer #" + dto.getCustomerId());
|
||||
chats.add(new Chat(String.valueOf(dto.getId()), name, dto.getLastMessage(), dto.getCustomerId(), dto.getStaffId()));
|
||||
}
|
||||
chatList.setValue(chats);
|
||||
isLoading.setValue(false);
|
||||
} else if (resource != null && resource.status == Resource.Status.ERROR) {
|
||||
isLoading.setValue(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void loadMessageHistory(Long conversationId) {
|
||||
chatRepository.getMessages(conversationId).observeForever(resource -> {
|
||||
if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) {
|
||||
List<Message> messages = new ArrayList<>();
|
||||
for (MessageDTO dto : resource.data) {
|
||||
messages.add(dtoToModel(dto));
|
||||
}
|
||||
messageList.setValue(messages);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public LiveData<Resource<MessageDTO>> sendMessage(Long conversationId, String text) {
|
||||
return chatRepository.sendMessage(conversationId, new SendMessageRequest(text));
|
||||
}
|
||||
|
||||
public void addMessageLocally(MessageDTO dto) {
|
||||
List<Message> current = new ArrayList<>(messageList.getValue());
|
||||
current.add(dtoToModel(dto));
|
||||
messageList.setValue(current);
|
||||
}
|
||||
|
||||
public void updateConversationLocally(ConversationDTO dto) {
|
||||
List<Chat> current = new ArrayList<>(chatList.getValue());
|
||||
boolean updated = false;
|
||||
String name = customerNames.getOrDefault(dto.getCustomerId(), "Customer #" + dto.getCustomerId());
|
||||
|
||||
for (int i = 0; i < current.size(); i++) {
|
||||
if (current.get(i).getChatId().equals(String.valueOf(dto.getId()))) {
|
||||
current.set(i, new Chat(String.valueOf(dto.getId()), name, dto.getLastMessage(), dto.getCustomerId(), dto.getStaffId()));
|
||||
updated = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!updated) {
|
||||
current.add(0, new Chat(String.valueOf(dto.getId()), name, dto.getLastMessage(), dto.getCustomerId(), dto.getStaffId()));
|
||||
}
|
||||
chatList.setValue(current);
|
||||
}
|
||||
|
||||
private Message dtoToModel(MessageDTO dto) {
|
||||
Message m = new Message();
|
||||
m.setId(dto.getId());
|
||||
m.setConversationId(dto.getConversationId());
|
||||
m.setSenderId(dto.getSenderId());
|
||||
m.setContent(dto.getContent());
|
||||
m.setTimestamp(dto.getTimestamp());
|
||||
m.setIsRead(dto.getIsRead());
|
||||
m.setAttachmentUrl(dto.getAttachmentUrl());
|
||||
m.setAttachmentName(dto.getAttachmentName());
|
||||
m.setAttachmentType(dto.getAttachmentType());
|
||||
return m;
|
||||
}
|
||||
|
||||
public String getCustomerName(Long customerId) {
|
||||
return customerNames.getOrDefault(customerId, "Customer #" + customerId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
package com.example.petstoremobile.viewmodels;
|
||||
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
import androidx.lifecycle.ViewModel;
|
||||
|
||||
import com.example.petstoremobile.dtos.BulkDeleteRequest;
|
||||
import com.example.petstoremobile.dtos.InventoryDTO;
|
||||
import com.example.petstoremobile.dtos.StoreDTO;
|
||||
import com.example.petstoremobile.repositories.InventoryRepository;
|
||||
import com.example.petstoremobile.repositories.StoreRepository;
|
||||
import com.example.petstoremobile.utils.Resource;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel;
|
||||
|
||||
@HiltViewModel
|
||||
public class InventoryListViewModel extends ViewModel {
|
||||
private final InventoryRepository inventoryRepository;
|
||||
private final StoreRepository storeRepository;
|
||||
|
||||
private final MutableLiveData<List<InventoryDTO>> inventory = new MutableLiveData<>(new ArrayList<>());
|
||||
private final MutableLiveData<List<StoreDTO>> stores = new MutableLiveData<>(new ArrayList<>());
|
||||
private final MutableLiveData<Boolean> isLoading = new MutableLiveData<>(false);
|
||||
|
||||
private int currentPage = 0;
|
||||
private boolean isLastPage = false;
|
||||
private static final int PAGE_SIZE = 20;
|
||||
|
||||
@Inject
|
||||
public InventoryListViewModel(InventoryRepository inventoryRepository, StoreRepository storeRepository) {
|
||||
this.inventoryRepository = inventoryRepository;
|
||||
this.storeRepository = storeRepository;
|
||||
}
|
||||
|
||||
public LiveData<List<InventoryDTO>> getInventory() { return inventory; }
|
||||
public LiveData<List<StoreDTO>> getStores() { return stores; }
|
||||
public LiveData<Boolean> getIsLoading() { return isLoading; }
|
||||
public boolean isLastPage() { return isLastPage; }
|
||||
|
||||
public void loadInventory(boolean reset, String query, Long storeId) {
|
||||
if (isLoading.getValue() != null && isLoading.getValue() && !reset) return;
|
||||
|
||||
if (reset) {
|
||||
currentPage = 0;
|
||||
isLastPage = false;
|
||||
}
|
||||
|
||||
isLoading.setValue(true);
|
||||
inventoryRepository.getAllInventory(query, storeId, currentPage, PAGE_SIZE, "product.prodName").observeForever(resource -> {
|
||||
if (resource != null) {
|
||||
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
|
||||
List<InventoryDTO> currentList = reset ? new ArrayList<>() : new ArrayList<>(inventory.getValue());
|
||||
currentList.addAll(resource.data.getContent());
|
||||
inventory.setValue(currentList);
|
||||
isLastPage = resource.data.isLast();
|
||||
if (!isLastPage) currentPage++;
|
||||
isLoading.setValue(false);
|
||||
} else if (resource.status == Resource.Status.ERROR) {
|
||||
isLoading.setValue(false);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void loadStores() {
|
||||
storeRepository.getAllStores(0, 100).observeForever(resource -> {
|
||||
if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) {
|
||||
stores.setValue(resource.data.getContent());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public LiveData<Resource<Void>> bulkDeleteInventory(List<String> ids) {
|
||||
return inventoryRepository.bulkDeleteInventory(new BulkDeleteRequest(ids));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
package com.example.petstoremobile.viewmodels;
|
||||
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
import androidx.lifecycle.ViewModel;
|
||||
|
||||
import com.example.petstoremobile.dtos.BulkDeleteRequest;
|
||||
import com.example.petstoremobile.dtos.PageResponse;
|
||||
import com.example.petstoremobile.dtos.PetDTO;
|
||||
import com.example.petstoremobile.dtos.StoreDTO;
|
||||
import com.example.petstoremobile.repositories.PetRepository;
|
||||
import com.example.petstoremobile.repositories.StoreRepository;
|
||||
import com.example.petstoremobile.utils.Resource;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel;
|
||||
|
||||
@HiltViewModel
|
||||
public class PetListViewModel extends ViewModel {
|
||||
private final PetRepository petRepository;
|
||||
private final StoreRepository storeRepository;
|
||||
|
||||
private final MutableLiveData<List<PetDTO>> pets = new MutableLiveData<>(new ArrayList<>());
|
||||
private final MutableLiveData<List<StoreDTO>> stores = new MutableLiveData<>(new ArrayList<>());
|
||||
private final MutableLiveData<Boolean> isLoading = new MutableLiveData<>(false);
|
||||
|
||||
@Inject
|
||||
public PetListViewModel(PetRepository petRepository, StoreRepository storeRepository) {
|
||||
this.petRepository = petRepository;
|
||||
this.storeRepository = storeRepository;
|
||||
}
|
||||
|
||||
public LiveData<List<PetDTO>> getPets() { return pets; }
|
||||
public LiveData<List<StoreDTO>> getStores() { return stores; }
|
||||
public LiveData<Boolean> getIsLoading() { return isLoading; }
|
||||
|
||||
public void loadPets(String query, String status, String species, Long storeId) {
|
||||
if ("All Statuses".equals(status)) status = null;
|
||||
if ("All Species".equals(species)) species = null;
|
||||
|
||||
isLoading.setValue(true);
|
||||
petRepository.getAllPets(0, 100, query, status, species, storeId, null, "petName").observeForever(resource -> {
|
||||
if (resource != null) {
|
||||
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
|
||||
pets.setValue(resource.data.getContent());
|
||||
isLoading.setValue(false);
|
||||
} else if (resource.status == Resource.Status.ERROR) {
|
||||
isLoading.setValue(false);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void loadStores() {
|
||||
storeRepository.getAllStores(0, 100).observeForever(resource -> {
|
||||
if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) {
|
||||
stores.setValue(resource.data.getContent());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public LiveData<Resource<Void>> bulkDeletePets(List<String> ids) {
|
||||
return petRepository.bulkDeletePets(new BulkDeleteRequest(ids));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package com.example.petstoremobile.viewmodels;
|
||||
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.ViewModel;
|
||||
|
||||
import com.example.petstoremobile.dtos.PetDTO;
|
||||
import com.example.petstoremobile.repositories.PetRepository;
|
||||
import com.example.petstoremobile.utils.Resource;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel;
|
||||
import okhttp3.MultipartBody;
|
||||
|
||||
@HiltViewModel
|
||||
public class PetProfileViewModel extends ViewModel {
|
||||
private final PetRepository repository;
|
||||
|
||||
@Inject
|
||||
public PetProfileViewModel(PetRepository repository) {
|
||||
this.repository = repository;
|
||||
}
|
||||
|
||||
public LiveData<Resource<PetDTO>> getPetById(Long id) {
|
||||
return repository.getPetById(id);
|
||||
}
|
||||
|
||||
public LiveData<Resource<Void>> uploadPetImage(Long id, MultipartBody.Part image) {
|
||||
return repository.uploadPetImage(id, image);
|
||||
}
|
||||
|
||||
public LiveData<Resource<Void>> deletePetImage(Long id) {
|
||||
return repository.deletePetImage(id);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package com.example.petstoremobile.viewmodels;
|
||||
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
import androidx.lifecycle.ViewModel;
|
||||
|
||||
import com.example.petstoremobile.dtos.CategoryDTO;
|
||||
import com.example.petstoremobile.dtos.ProductDTO;
|
||||
import com.example.petstoremobile.repositories.CategoryRepository;
|
||||
import com.example.petstoremobile.repositories.ProductRepository;
|
||||
import com.example.petstoremobile.utils.Resource;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel;
|
||||
|
||||
@HiltViewModel
|
||||
public class ProductListViewModel extends ViewModel {
|
||||
private final ProductRepository productRepository;
|
||||
private final CategoryRepository categoryRepository;
|
||||
|
||||
private final MutableLiveData<List<ProductDTO>> products = new MutableLiveData<>(new ArrayList<>());
|
||||
private final MutableLiveData<List<CategoryDTO>> categories = new MutableLiveData<>(new ArrayList<>());
|
||||
private final MutableLiveData<Boolean> isLoading = new MutableLiveData<>(false);
|
||||
|
||||
@Inject
|
||||
public ProductListViewModel(ProductRepository productRepository, CategoryRepository categoryRepository) {
|
||||
this.productRepository = productRepository;
|
||||
this.categoryRepository = categoryRepository;
|
||||
}
|
||||
|
||||
public LiveData<List<ProductDTO>> getProducts() { return products; }
|
||||
public LiveData<List<CategoryDTO>> getCategories() { return categories; }
|
||||
public LiveData<Boolean> getIsLoading() { return isLoading; }
|
||||
|
||||
public void loadProducts(String query, Long categoryId) {
|
||||
isLoading.setValue(true);
|
||||
productRepository.getAllProducts(query, categoryId, 0, 100, "prodName").observeForever(resource -> {
|
||||
if (resource != null) {
|
||||
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
|
||||
products.setValue(resource.data.getContent());
|
||||
isLoading.setValue(false);
|
||||
} else if (resource.status == Resource.Status.ERROR) {
|
||||
isLoading.setValue(false);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void loadCategories() {
|
||||
categoryRepository.getAllCategories(0, 100).observeForever(resource -> {
|
||||
if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) {
|
||||
categories.setValue(resource.data.getContent());
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
package com.example.petstoremobile.viewmodels;
|
||||
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
import androidx.lifecycle.ViewModel;
|
||||
|
||||
import com.example.petstoremobile.dtos.BulkDeleteRequest;
|
||||
import com.example.petstoremobile.dtos.ProductDTO;
|
||||
import com.example.petstoremobile.dtos.ProductSupplierDTO;
|
||||
import com.example.petstoremobile.dtos.SupplierDTO;
|
||||
import com.example.petstoremobile.repositories.ProductRepository;
|
||||
import com.example.petstoremobile.repositories.ProductSupplierRepository;
|
||||
import com.example.petstoremobile.repositories.SupplierRepository;
|
||||
import com.example.petstoremobile.utils.Resource;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel;
|
||||
|
||||
@HiltViewModel
|
||||
public class ProductSupplierListViewModel extends ViewModel {
|
||||
private final ProductSupplierRepository psRepository;
|
||||
private final ProductRepository productRepository;
|
||||
private final SupplierRepository supplierRepository;
|
||||
|
||||
private final MutableLiveData<List<ProductSupplierDTO>> productSuppliers = new MutableLiveData<>(new ArrayList<>());
|
||||
private final MutableLiveData<List<ProductDTO>> products = new MutableLiveData<>(new ArrayList<>());
|
||||
private final MutableLiveData<List<SupplierDTO>> suppliers = new MutableLiveData<>(new ArrayList<>());
|
||||
private final MutableLiveData<Boolean> isLoading = new MutableLiveData<>(false);
|
||||
|
||||
@Inject
|
||||
public ProductSupplierListViewModel(ProductSupplierRepository psRepository, ProductRepository productRepository, SupplierRepository supplierRepository) {
|
||||
this.psRepository = psRepository;
|
||||
this.productRepository = productRepository;
|
||||
this.supplierRepository = supplierRepository;
|
||||
}
|
||||
|
||||
public LiveData<List<ProductSupplierDTO>> getProductSuppliers() { return productSuppliers; }
|
||||
public LiveData<List<ProductDTO>> getProducts() { return products; }
|
||||
public LiveData<List<SupplierDTO>> getSuppliers() { return suppliers; }
|
||||
public LiveData<Boolean> getIsLoading() { return isLoading; }
|
||||
|
||||
public void loadProductSuppliers(String query, Long productId, Long supplierId) {
|
||||
isLoading.setValue(true);
|
||||
psRepository.getAllProductSuppliers(0, 100, query, productId, supplierId, "productName").observeForever(resource -> {
|
||||
if (resource != null) {
|
||||
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
|
||||
productSuppliers.setValue(resource.data.getContent());
|
||||
isLoading.setValue(false);
|
||||
} else if (resource.status == Resource.Status.ERROR) {
|
||||
isLoading.setValue(false);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void loadFilterData() {
|
||||
productRepository.getAllProducts(null, null, 0, 100, "prodName").observeForever(resource -> {
|
||||
if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) {
|
||||
products.setValue(resource.data.getContent());
|
||||
}
|
||||
});
|
||||
|
||||
supplierRepository.getAllSuppliers(0, 100, null, "supCompany").observeForever(resource -> {
|
||||
if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) {
|
||||
suppliers.setValue(resource.data.getContent());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public LiveData<Resource<Void>> bulkDeleteProductSuppliers(List<String> ids) {
|
||||
return psRepository.bulkDeleteProductSuppliers(new BulkDeleteRequest(ids));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package com.example.petstoremobile.viewmodels;
|
||||
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
import androidx.lifecycle.ViewModel;
|
||||
|
||||
import com.example.petstoremobile.dtos.PurchaseOrderDTO;
|
||||
import com.example.petstoremobile.dtos.StoreDTO;
|
||||
import com.example.petstoremobile.repositories.PurchaseOrderRepository;
|
||||
import com.example.petstoremobile.repositories.StoreRepository;
|
||||
import com.example.petstoremobile.utils.Resource;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel;
|
||||
|
||||
@HiltViewModel
|
||||
public class PurchaseOrderListViewModel extends ViewModel {
|
||||
private final PurchaseOrderRepository purchaseOrderRepository;
|
||||
private final StoreRepository storeRepository;
|
||||
|
||||
private final MutableLiveData<List<PurchaseOrderDTO>> purchaseOrders = new MutableLiveData<>(new ArrayList<>());
|
||||
private final MutableLiveData<List<StoreDTO>> stores = new MutableLiveData<>(new ArrayList<>());
|
||||
private final MutableLiveData<Boolean> isLoading = new MutableLiveData<>(false);
|
||||
|
||||
@Inject
|
||||
public PurchaseOrderListViewModel(PurchaseOrderRepository purchaseOrderRepository, StoreRepository storeRepository) {
|
||||
this.purchaseOrderRepository = purchaseOrderRepository;
|
||||
this.storeRepository = storeRepository;
|
||||
}
|
||||
|
||||
public LiveData<List<PurchaseOrderDTO>> getPurchaseOrders() { return purchaseOrders; }
|
||||
public LiveData<List<StoreDTO>> getStores() { return stores; }
|
||||
public LiveData<Boolean> getIsLoading() { return isLoading; }
|
||||
|
||||
public void loadPurchaseOrders(String query, Long storeId) {
|
||||
isLoading.setValue(true);
|
||||
purchaseOrderRepository.getAllPurchaseOrders(0, 100, query, storeId, "purchaseOrderId,desc").observeForever(resource -> {
|
||||
if (resource != null) {
|
||||
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
|
||||
purchaseOrders.setValue(resource.data.getContent());
|
||||
isLoading.setValue(false);
|
||||
} else if (resource.status == Resource.Status.ERROR) {
|
||||
isLoading.setValue(false);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void loadStores() {
|
||||
storeRepository.getAllStores(0, 100).observeForever(resource -> {
|
||||
if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) {
|
||||
stores.setValue(resource.data.getContent());
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
package com.example.petstoremobile.viewmodels;
|
||||
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
import androidx.lifecycle.ViewModel;
|
||||
|
||||
import com.example.petstoremobile.dtos.PageResponse;
|
||||
import com.example.petstoremobile.dtos.SaleDTO;
|
||||
import com.example.petstoremobile.dtos.StoreDTO;
|
||||
import com.example.petstoremobile.repositories.SaleRepository;
|
||||
import com.example.petstoremobile.repositories.StoreRepository;
|
||||
import com.example.petstoremobile.utils.Resource;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel;
|
||||
|
||||
@HiltViewModel
|
||||
public class SaleListViewModel extends ViewModel {
|
||||
private final SaleRepository saleRepository;
|
||||
private final StoreRepository storeRepository;
|
||||
|
||||
private final MutableLiveData<List<SaleDTO>> sales = new MutableLiveData<>(new ArrayList<>());
|
||||
private final MutableLiveData<List<StoreDTO>> stores = new MutableLiveData<>(new ArrayList<>());
|
||||
private final MutableLiveData<Boolean> isLoading = new MutableLiveData<>(false);
|
||||
|
||||
private int currentPage = 0;
|
||||
private boolean isLastPage = false;
|
||||
private static final int PAGE_SIZE = 20;
|
||||
|
||||
@Inject
|
||||
public SaleListViewModel(SaleRepository saleRepository, StoreRepository storeRepository) {
|
||||
this.saleRepository = saleRepository;
|
||||
this.storeRepository = storeRepository;
|
||||
}
|
||||
|
||||
public LiveData<List<SaleDTO>> getSales() { return sales; }
|
||||
public LiveData<List<StoreDTO>> getStores() { return stores; }
|
||||
public LiveData<Boolean> getIsLoading() { return isLoading; }
|
||||
public boolean isLastPage() { return isLastPage; }
|
||||
|
||||
public void loadSales(boolean reset, String query, String paymentMethod, Long storeId) {
|
||||
if (isLoading.getValue() != null && isLoading.getValue() && !reset) return;
|
||||
|
||||
if (reset) {
|
||||
currentPage = 0;
|
||||
isLastPage = false;
|
||||
}
|
||||
|
||||
isLoading.setValue(true);
|
||||
saleRepository.getAllSales(currentPage, PAGE_SIZE, query, paymentMethod, storeId, "saleDate,desc").observeForever(resource -> {
|
||||
if (resource != null) {
|
||||
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
|
||||
List<SaleDTO> currentList = reset ? new ArrayList<>() : new ArrayList<>(sales.getValue());
|
||||
currentList.addAll(resource.data.getContent());
|
||||
sales.setValue(currentList);
|
||||
isLastPage = resource.data.isLast();
|
||||
if (!isLastPage) currentPage++;
|
||||
isLoading.setValue(false);
|
||||
} else if (resource.status == Resource.Status.ERROR) {
|
||||
isLoading.setValue(false);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void loadStores() {
|
||||
storeRepository.getAllStores(0, 100).observeForever(resource -> {
|
||||
if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) {
|
||||
stores.setValue(resource.data.getContent());
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package com.example.petstoremobile.viewmodels;
|
||||
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
import androidx.lifecycle.ViewModel;
|
||||
|
||||
import com.example.petstoremobile.dtos.BulkDeleteRequest;
|
||||
import com.example.petstoremobile.dtos.PageResponse;
|
||||
import com.example.petstoremobile.dtos.ServiceDTO;
|
||||
import com.example.petstoremobile.repositories.ServiceRepository;
|
||||
import com.example.petstoremobile.utils.Resource;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel;
|
||||
|
||||
@HiltViewModel
|
||||
public class ServiceListViewModel extends ViewModel {
|
||||
private final ServiceRepository repository;
|
||||
|
||||
private final MutableLiveData<List<ServiceDTO>> services = new MutableLiveData<>(new ArrayList<>());
|
||||
private final MutableLiveData<Boolean> isLoading = new MutableLiveData<>(false);
|
||||
|
||||
private int currentPage = 0;
|
||||
private boolean isLastPage = false;
|
||||
private static final int PAGE_SIZE = 20;
|
||||
|
||||
@Inject
|
||||
public ServiceListViewModel(ServiceRepository repository) {
|
||||
this.repository = repository;
|
||||
}
|
||||
|
||||
public LiveData<List<ServiceDTO>> getServices() { return services; }
|
||||
public LiveData<Boolean> getIsLoading() { return isLoading; }
|
||||
public boolean isLastPage() { return isLastPage; }
|
||||
|
||||
public void loadServices(boolean reset, String query) {
|
||||
if (isLoading.getValue() != null && isLoading.getValue() && !reset) return;
|
||||
|
||||
if (reset) {
|
||||
currentPage = 0;
|
||||
isLastPage = false;
|
||||
}
|
||||
|
||||
isLoading.setValue(true);
|
||||
repository.getAllServices(currentPage, PAGE_SIZE, query, "serviceName").observeForever(resource -> {
|
||||
if (resource != null) {
|
||||
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
|
||||
List<ServiceDTO> currentList = reset ? new ArrayList<>() : new ArrayList<>(services.getValue());
|
||||
currentList.addAll(resource.data.getContent());
|
||||
services.setValue(currentList);
|
||||
isLastPage = resource.data.isLast();
|
||||
if (!isLastPage) currentPage++;
|
||||
isLoading.setValue(false);
|
||||
} else if (resource.status == Resource.Status.ERROR) {
|
||||
isLoading.setValue(false);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public LiveData<Resource<Void>> bulkDeleteServices(List<String> ids) {
|
||||
return repository.bulkDeleteServices(new BulkDeleteRequest(ids));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package com.example.petstoremobile.viewmodels;
|
||||
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
import androidx.lifecycle.ViewModel;
|
||||
|
||||
import com.example.petstoremobile.dtos.EmployeeDTO;
|
||||
import com.example.petstoremobile.repositories.EmployeeRepository;
|
||||
import com.example.petstoremobile.utils.Resource;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel;
|
||||
|
||||
@HiltViewModel
|
||||
public class StaffListViewModel extends ViewModel {
|
||||
private final EmployeeRepository repository;
|
||||
|
||||
private final MutableLiveData<List<EmployeeDTO>> employees = new MutableLiveData<>(new ArrayList<>());
|
||||
private final MutableLiveData<List<EmployeeDTO>> filteredEmployees = new MutableLiveData<>(new ArrayList<>());
|
||||
private final MutableLiveData<Boolean> isLoading = new MutableLiveData<>(false);
|
||||
private String lastQuery = "";
|
||||
|
||||
@Inject
|
||||
public StaffListViewModel(EmployeeRepository repository) {
|
||||
this.repository = repository;
|
||||
}
|
||||
|
||||
public LiveData<List<EmployeeDTO>> getFilteredEmployees() { return filteredEmployees; }
|
||||
public LiveData<Boolean> getIsLoading() { return isLoading; }
|
||||
|
||||
public void loadStaff() {
|
||||
isLoading.setValue(true);
|
||||
repository.getAllEmployees(0, 100).observeForever(resource -> {
|
||||
if (resource != null) {
|
||||
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
|
||||
employees.setValue(resource.data.getContent());
|
||||
filter(lastQuery);
|
||||
isLoading.setValue(false);
|
||||
} else if (resource.status == Resource.Status.ERROR) {
|
||||
isLoading.setValue(false);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void filter(String query) {
|
||||
this.lastQuery = query;
|
||||
List<EmployeeDTO> all = employees.getValue();
|
||||
if (all == null) return;
|
||||
|
||||
if (query.isEmpty()) {
|
||||
filteredEmployees.setValue(new ArrayList<>(all));
|
||||
} else {
|
||||
List<EmployeeDTO> filtered = new ArrayList<>();
|
||||
String lower = query.toLowerCase();
|
||||
for (EmployeeDTO e : all) {
|
||||
if ((e.getFullName() != null && e.getFullName().toLowerCase().contains(lower))
|
||||
|| (e.getUsername() != null && e.getUsername().toLowerCase().contains(lower))
|
||||
|| (e.getEmail() != null && e.getEmail().toLowerCase().contains(lower))
|
||||
|| (e.getPhone() != null && e.getPhone().toLowerCase().contains(lower))) {
|
||||
filtered.add(e);
|
||||
}
|
||||
}
|
||||
filteredEmployees.setValue(filtered);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package com.example.petstoremobile.viewmodels;
|
||||
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
import androidx.lifecycle.ViewModel;
|
||||
|
||||
import com.example.petstoremobile.dtos.BulkDeleteRequest;
|
||||
import com.example.petstoremobile.dtos.SupplierDTO;
|
||||
import com.example.petstoremobile.repositories.SupplierRepository;
|
||||
import com.example.petstoremobile.utils.Resource;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel;
|
||||
|
||||
@HiltViewModel
|
||||
public class SupplierListViewModel extends ViewModel {
|
||||
private final SupplierRepository repository;
|
||||
|
||||
private final MutableLiveData<List<SupplierDTO>> suppliers = new MutableLiveData<>(new ArrayList<>());
|
||||
private final MutableLiveData<Boolean> isLoading = new MutableLiveData<>(false);
|
||||
|
||||
@Inject
|
||||
public SupplierListViewModel(SupplierRepository repository) {
|
||||
this.repository = repository;
|
||||
}
|
||||
|
||||
public LiveData<List<SupplierDTO>> getSuppliers() { return suppliers; }
|
||||
public LiveData<Boolean> getIsLoading() { return isLoading; }
|
||||
|
||||
public void loadSuppliers(String query) {
|
||||
isLoading.setValue(true);
|
||||
repository.getAllSuppliers(0, 100, query, "supCompany").observeForever(resource -> {
|
||||
if (resource != null) {
|
||||
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
|
||||
suppliers.setValue(resource.data.getContent());
|
||||
isLoading.setValue(false);
|
||||
} else if (resource.status == Resource.Status.ERROR) {
|
||||
isLoading.setValue(false);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public LiveData<Resource<Void>> bulkDeleteSuppliers(List<String> ids) {
|
||||
return repository.bulkDeleteSuppliers(new BulkDeleteRequest(ids));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user