From 6a3730ca04d9994ad84d760b0d7432fed8bf5c59 Mon Sep 17 00:00:00 2001 From: Alex <78383757+Lextical@users.noreply.github.com> Date: Thu, 9 Apr 2026 14:44:04 -0600 Subject: [PATCH] refactored viewmodels for listfragments --- .../fragments/ChatFragment.java | 368 ++++-------------- .../listfragments/AdoptionFragment.java | 135 ++----- .../listfragments/AnalyticsFragment.java | 291 +++++--------- .../listfragments/AppointmentFragment.java | 146 ++----- .../listfragments/InventoryFragment.java | 153 ++------ .../fragments/listfragments/PetFragment.java | 158 +++----- .../listfragments/ProductFragment.java | 139 +++---- .../ProductSupplierFragment.java | 148 ++----- .../listfragments/PurchaseOrderFragment.java | 119 ++---- .../fragments/listfragments/SaleFragment.java | 109 ++---- .../listfragments/ServiceFragment.java | 94 +---- .../listfragments/StaffFragment.java | 80 ++-- .../listfragments/SupplierFragment.java | 87 +---- .../PetProfileFragment.java | 33 +- .../viewmodels/AdoptionListViewModel.java | 83 ++++ .../viewmodels/AnalyticsViewModel.java | 163 ++++++++ .../viewmodels/AppointmentListViewModel.java | 65 ++++ .../viewmodels/ChatListViewModel.java | 132 +++++++ .../viewmodels/InventoryListViewModel.java | 81 ++++ .../viewmodels/PetListViewModel.java | 69 ++++ .../viewmodels/PetProfileViewModel.java | 35 ++ .../viewmodels/ProductListViewModel.java | 60 +++ .../ProductSupplierListViewModel.java | 77 ++++ .../PurchaseOrderListViewModel.java | 60 +++ .../viewmodels/SaleListViewModel.java | 77 ++++ .../viewmodels/ServiceListViewModel.java | 68 ++++ .../viewmodels/StaffListViewModel.java | 71 ++++ .../viewmodels/SupplierListViewModel.java | 51 +++ 28 files changed, 1626 insertions(+), 1526 deletions(-) create mode 100644 android/app/src/main/java/com/example/petstoremobile/viewmodels/AdoptionListViewModel.java create mode 100644 android/app/src/main/java/com/example/petstoremobile/viewmodels/AnalyticsViewModel.java create mode 100644 android/app/src/main/java/com/example/petstoremobile/viewmodels/AppointmentListViewModel.java create mode 100644 android/app/src/main/java/com/example/petstoremobile/viewmodels/ChatListViewModel.java create mode 100644 android/app/src/main/java/com/example/petstoremobile/viewmodels/InventoryListViewModel.java create mode 100644 android/app/src/main/java/com/example/petstoremobile/viewmodels/PetListViewModel.java create mode 100644 android/app/src/main/java/com/example/petstoremobile/viewmodels/PetProfileViewModel.java create mode 100644 android/app/src/main/java/com/example/petstoremobile/viewmodels/ProductListViewModel.java create mode 100644 android/app/src/main/java/com/example/petstoremobile/viewmodels/ProductSupplierListViewModel.java create mode 100644 android/app/src/main/java/com/example/petstoremobile/viewmodels/PurchaseOrderListViewModel.java create mode 100644 android/app/src/main/java/com/example/petstoremobile/viewmodels/SaleListViewModel.java create mode 100644 android/app/src/main/java/com/example/petstoremobile/viewmodels/ServiceListViewModel.java create mode 100644 android/app/src/main/java/com/example/petstoremobile/viewmodels/StaffListViewModel.java create mode 100644 android/app/src/main/java/com/example/petstoremobile/viewmodels/SupplierListViewModel.java diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/ChatFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/ChatFragment.java index b13edd67..1b858b08 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/ChatFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/ChatFragment.java @@ -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 chatList = new ArrayList<>(); private final List messageList = new ArrayList<>(); - private final Map 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 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(); diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AdoptionFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AdoptionFragment.java index f2a01b5d..39317495 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AdoptionFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AdoptionFragment.java @@ -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 adoptionList = new ArrayList<>(); - private List 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 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 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; + } } diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AnalyticsFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AnalyticsFragment.java index 2e2b2592..ef26db92 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AnalyticsFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AnalyticsFragment.java @@ -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 sales) { - // Filter out refunds for most metrics - List 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 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 revenueByProduct = new LinkedHashMap<>(); - Map 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> 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 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> 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 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 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 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 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> 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 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 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 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 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 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 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 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"); + } } } diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AppointmentFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AppointmentFragment.java index b921d867..8610790e 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AppointmentFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AppointmentFragment.java @@ -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 appointmentList = new ArrayList<>(); - private List 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 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 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; + } } diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/InventoryFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/InventoryFragment.java index 259a0a9e..6cefedb8 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/InventoryFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/InventoryFragment.java @@ -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 inventoryList = new ArrayList<>(); - private List 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 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) { diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/PetFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/PetFragment.java index 87a1a54c..bc1b9a6f 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/PetFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/PetFragment.java @@ -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 petList = new ArrayList<>(); - private List 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 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; } } diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ProductFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ProductFragment.java index 6ae3d349..84f076a2 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ProductFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ProductFragment.java @@ -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 productList = new ArrayList<>(); - private List 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 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; } } diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ProductSupplierFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ProductSupplierFragment.java index e1db78b6..6a066528 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ProductSupplierFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ProductSupplierFragment.java @@ -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 psList = new ArrayList<>(); - private List productList = new ArrayList<>(); - private List 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 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 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); } diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/PurchaseOrderFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/PurchaseOrderFragment.java index b27c9c1f..64dc0ea5 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/PurchaseOrderFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/PurchaseOrderFragment.java @@ -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 poList = new ArrayList<>(); - private List 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 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; + } } diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/SaleFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/SaleFragment.java index 850717e4..fef5d994 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/SaleFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/SaleFragment.java @@ -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 saleList = new ArrayList<>(); - private final List 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 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; + } } diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ServiceFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ServiceFragment.java index 77136d4e..3a1b45a1 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ServiceFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ServiceFragment.java @@ -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 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) { diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/StaffFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/StaffFragment.java index 383d702a..8407d3f6 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/StaffFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/StaffFragment.java @@ -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 employeeList = new ArrayList<>(); - private List filteredList = new ArrayList<>(); + private StaffListViewModel viewModel; + private List 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() : ""); diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/SupplierFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/SupplierFragment.java index eca755bb..78d43bd6 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/SupplierFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/SupplierFragment.java @@ -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 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())); diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/listprofilefragments/PetProfileFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/listprofilefragments/PetProfileFragment.java index 371e5c20..ee11fb4e 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/listprofilefragments/PetProfileFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/listprofilefragments/PetProfileFragment.java @@ -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) { diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/AdoptionListViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/AdoptionListViewModel.java new file mode 100644 index 00000000..683c79b4 --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/AdoptionListViewModel.java @@ -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> adoptions = new MutableLiveData<>(new ArrayList<>()); + private final MutableLiveData> stores = new MutableLiveData<>(new ArrayList<>()); + private final MutableLiveData 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> getAdoptions() { return adoptions; } + public LiveData> getStores() { return stores; } + public LiveData 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 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> bulkDeleteAdoptions(List ids) { + return adoptionRepository.bulkDeleteAdoptions(new BulkDeleteRequest(ids)); + } +} diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/AnalyticsViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/AnalyticsViewModel.java new file mode 100644 index 00000000..76c039ac --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/AnalyticsViewModel.java @@ -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 = new MutableLiveData<>(); + private final MutableLiveData isLoading = new MutableLiveData<>(false); + private final MutableLiveData errorMessage = new MutableLiveData<>(); + + @Inject + public AnalyticsViewModel(SaleRepository saleRepository) { + this.saleRepository = saleRepository; + } + + public LiveData getAnalyticsData() { return analyticsData; } + public LiveData getIsLoading() { return isLoading; } + public LiveData 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 sales) { + List 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 revenueByProduct = new LinkedHashMap<>(); + Map quantityByProduct = new LinkedHashMap<>(); + Map paymentCount = new LinkedHashMap<>(); + Map 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 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> topRevenueProducts; + public List> topQuantityProducts; + public List> paymentMethodStats; + public List> employeePerformance; + public List> dailyRevenue; + } +} diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/AppointmentListViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/AppointmentListViewModel.java new file mode 100644 index 00000000..8bdaf699 --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/AppointmentListViewModel.java @@ -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> appointments = new MutableLiveData<>(new ArrayList<>()); + private final MutableLiveData> stores = new MutableLiveData<>(new ArrayList<>()); + private final MutableLiveData isLoading = new MutableLiveData<>(false); + + @Inject + public AppointmentListViewModel(AppointmentRepository appointmentRepository, StoreRepository storeRepository) { + this.appointmentRepository = appointmentRepository; + this.storeRepository = storeRepository; + } + + public LiveData> getAppointments() { return appointments; } + public LiveData> getStores() { return stores; } + public LiveData 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> bulkDeleteAppointments(List ids) { + return appointmentRepository.bulkDeleteAppointments(new BulkDeleteRequest(ids)); + } +} diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/ChatListViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/ChatListViewModel.java new file mode 100644 index 00000000..0aa8021f --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/ChatListViewModel.java @@ -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> chatList = new MutableLiveData<>(new ArrayList<>()); + private final MutableLiveData> messageList = new MutableLiveData<>(new ArrayList<>()); + private final Map customerNames = new HashMap<>(); + private final MutableLiveData isLoading = new MutableLiveData<>(false); + + @Inject + public ChatListViewModel(ChatRepository chatRepository, CustomerRepository customerRepository) { + this.chatRepository = chatRepository; + this.customerRepository = customerRepository; + } + + public LiveData> getChatList() { return chatList; } + public LiveData> getMessageList() { return messageList; } + public LiveData 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 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 messages = new ArrayList<>(); + for (MessageDTO dto : resource.data) { + messages.add(dtoToModel(dto)); + } + messageList.setValue(messages); + } + }); + } + + public LiveData> sendMessage(Long conversationId, String text) { + return chatRepository.sendMessage(conversationId, new SendMessageRequest(text)); + } + + public void addMessageLocally(MessageDTO dto) { + List current = new ArrayList<>(messageList.getValue()); + current.add(dtoToModel(dto)); + messageList.setValue(current); + } + + public void updateConversationLocally(ConversationDTO dto) { + List 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); + } +} diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/InventoryListViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/InventoryListViewModel.java new file mode 100644 index 00000000..c91f8337 --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/InventoryListViewModel.java @@ -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> inventory = new MutableLiveData<>(new ArrayList<>()); + private final MutableLiveData> stores = new MutableLiveData<>(new ArrayList<>()); + private final MutableLiveData 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> getInventory() { return inventory; } + public LiveData> getStores() { return stores; } + public LiveData 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 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> bulkDeleteInventory(List ids) { + return inventoryRepository.bulkDeleteInventory(new BulkDeleteRequest(ids)); + } +} diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/PetListViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/PetListViewModel.java new file mode 100644 index 00000000..8a567450 --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/PetListViewModel.java @@ -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> pets = new MutableLiveData<>(new ArrayList<>()); + private final MutableLiveData> stores = new MutableLiveData<>(new ArrayList<>()); + private final MutableLiveData isLoading = new MutableLiveData<>(false); + + @Inject + public PetListViewModel(PetRepository petRepository, StoreRepository storeRepository) { + this.petRepository = petRepository; + this.storeRepository = storeRepository; + } + + public LiveData> getPets() { return pets; } + public LiveData> getStores() { return stores; } + public LiveData 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> bulkDeletePets(List ids) { + return petRepository.bulkDeletePets(new BulkDeleteRequest(ids)); + } +} diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/PetProfileViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/PetProfileViewModel.java new file mode 100644 index 00000000..1fb75f1c --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/PetProfileViewModel.java @@ -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> getPetById(Long id) { + return repository.getPetById(id); + } + + public LiveData> uploadPetImage(Long id, MultipartBody.Part image) { + return repository.uploadPetImage(id, image); + } + + public LiveData> deletePetImage(Long id) { + return repository.deletePetImage(id); + } +} diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/ProductListViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/ProductListViewModel.java new file mode 100644 index 00000000..ecd2d238 --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/ProductListViewModel.java @@ -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> products = new MutableLiveData<>(new ArrayList<>()); + private final MutableLiveData> categories = new MutableLiveData<>(new ArrayList<>()); + private final MutableLiveData isLoading = new MutableLiveData<>(false); + + @Inject + public ProductListViewModel(ProductRepository productRepository, CategoryRepository categoryRepository) { + this.productRepository = productRepository; + this.categoryRepository = categoryRepository; + } + + public LiveData> getProducts() { return products; } + public LiveData> getCategories() { return categories; } + public LiveData 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()); + } + }); + } +} diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/ProductSupplierListViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/ProductSupplierListViewModel.java new file mode 100644 index 00000000..cad846c4 --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/ProductSupplierListViewModel.java @@ -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> productSuppliers = new MutableLiveData<>(new ArrayList<>()); + private final MutableLiveData> products = new MutableLiveData<>(new ArrayList<>()); + private final MutableLiveData> suppliers = new MutableLiveData<>(new ArrayList<>()); + private final MutableLiveData isLoading = new MutableLiveData<>(false); + + @Inject + public ProductSupplierListViewModel(ProductSupplierRepository psRepository, ProductRepository productRepository, SupplierRepository supplierRepository) { + this.psRepository = psRepository; + this.productRepository = productRepository; + this.supplierRepository = supplierRepository; + } + + public LiveData> getProductSuppliers() { return productSuppliers; } + public LiveData> getProducts() { return products; } + public LiveData> getSuppliers() { return suppliers; } + public LiveData 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> bulkDeleteProductSuppliers(List ids) { + return psRepository.bulkDeleteProductSuppliers(new BulkDeleteRequest(ids)); + } +} diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/PurchaseOrderListViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/PurchaseOrderListViewModel.java new file mode 100644 index 00000000..438f4198 --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/PurchaseOrderListViewModel.java @@ -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> purchaseOrders = new MutableLiveData<>(new ArrayList<>()); + private final MutableLiveData> stores = new MutableLiveData<>(new ArrayList<>()); + private final MutableLiveData isLoading = new MutableLiveData<>(false); + + @Inject + public PurchaseOrderListViewModel(PurchaseOrderRepository purchaseOrderRepository, StoreRepository storeRepository) { + this.purchaseOrderRepository = purchaseOrderRepository; + this.storeRepository = storeRepository; + } + + public LiveData> getPurchaseOrders() { return purchaseOrders; } + public LiveData> getStores() { return stores; } + public LiveData 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()); + } + }); + } +} diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/SaleListViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/SaleListViewModel.java new file mode 100644 index 00000000..a364a7d8 --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/SaleListViewModel.java @@ -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> sales = new MutableLiveData<>(new ArrayList<>()); + private final MutableLiveData> stores = new MutableLiveData<>(new ArrayList<>()); + private final MutableLiveData 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> getSales() { return sales; } + public LiveData> getStores() { return stores; } + public LiveData 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 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()); + } + }); + } +} diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/ServiceListViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/ServiceListViewModel.java new file mode 100644 index 00000000..d0fa121b --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/ServiceListViewModel.java @@ -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> services = new MutableLiveData<>(new ArrayList<>()); + private final MutableLiveData 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> getServices() { return services; } + public LiveData 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 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> bulkDeleteServices(List ids) { + return repository.bulkDeleteServices(new BulkDeleteRequest(ids)); + } +} diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/StaffListViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/StaffListViewModel.java new file mode 100644 index 00000000..1bd317ca --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/StaffListViewModel.java @@ -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> employees = new MutableLiveData<>(new ArrayList<>()); + private final MutableLiveData> filteredEmployees = new MutableLiveData<>(new ArrayList<>()); + private final MutableLiveData isLoading = new MutableLiveData<>(false); + private String lastQuery = ""; + + @Inject + public StaffListViewModel(EmployeeRepository repository) { + this.repository = repository; + } + + public LiveData> getFilteredEmployees() { return filteredEmployees; } + public LiveData 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 all = employees.getValue(); + if (all == null) return; + + if (query.isEmpty()) { + filteredEmployees.setValue(new ArrayList<>(all)); + } else { + List 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); + } + } +} diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/SupplierListViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/SupplierListViewModel.java new file mode 100644 index 00000000..072ad3bd --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/SupplierListViewModel.java @@ -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> suppliers = new MutableLiveData<>(new ArrayList<>()); + private final MutableLiveData isLoading = new MutableLiveData<>(false); + + @Inject + public SupplierListViewModel(SupplierRepository repository) { + this.repository = repository; + } + + public LiveData> getSuppliers() { return suppliers; } + public LiveData 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> bulkDeleteSuppliers(List ids) { + return repository.bulkDeleteSuppliers(new BulkDeleteRequest(ids)); + } +}