refactored viewmodels for listfragments

This commit is contained in:
Alex
2026-04-09 14:44:04 -06:00
parent 863692c058
commit 38b830509f
28 changed files with 1626 additions and 1526 deletions

View File

@@ -18,19 +18,17 @@ import androidx.fragment.app.Fragment;
import androidx.lifecycle.ViewModelProvider;
import androidx.recyclerview.widget.LinearLayoutManager;
import com.bumptech.glide.Glide;
import com.example.petstoremobile.R;
import com.example.petstoremobile.adapters.ChatAdapter;
import com.example.petstoremobile.adapters.MessageAdapter;
import com.example.petstoremobile.api.auth.TokenManager;
import com.example.petstoremobile.databinding.FragmentChatBinding;
import com.example.petstoremobile.dtos.ConversationDTO;
import com.example.petstoremobile.dtos.MessageDTO;
import com.example.petstoremobile.dtos.SendMessageRequest;
import com.example.petstoremobile.models.Chat;
import com.example.petstoremobile.models.Message;
import com.example.petstoremobile.services.ChatNotificationService;
import com.example.petstoremobile.utils.Resource;
import com.example.petstoremobile.viewmodels.ChatViewModel;
import com.example.petstoremobile.viewmodels.ChatListViewModel;
import com.example.petstoremobile.websocket.StompChatManager;
import java.util.*;
@@ -47,59 +45,44 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis
private static final String TAG = "ChatFragment";
private FragmentChatBinding binding;
private ChatViewModel viewModel;
private ChatListViewModel viewModel;
// Adapters
private ChatAdapter chatAdapter;
private MessageAdapter messageAdapter;
// Data
private final List<Chat> chatList = new ArrayList<>();
private final List<Message> messageList = new ArrayList<>();
private final Map<Long, String> customerNames = new HashMap<>();
private Uri pendingAttachmentUri;
@Inject TokenManager tokenManager;
@Inject @Named("baseUrl") String baseUrl;
// chat
private Long currentUserId;
private Long activeConversationId;
private StompChatManager stompChatManager;
private ActivityResultLauncher<Intent> attachmentLauncher;
/**
* Initializes the attachment launcher to handle file selection from the gallery.
*/
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
viewModel = new ViewModelProvider(this).get(ChatViewModel.class);
viewModel = new ViewModelProvider(this).get(ChatListViewModel.class);
attachmentLauncher = registerForActivityResult(
new ActivityResultContracts.StartActivityForResult(),
result -> {
if (result.getResultCode() == Activity.RESULT_OK && result.getData() != null) {
Uri uri = result.getData().getData();
if (uri != null) {
showAttachmentPreview(uri);
}
if (uri != null) showAttachmentPreview(uri);
}
}
);
}
/**
* Inflates the layout, initializes UI components, and sets up click listeners for messaging.
*/
@Override
public View onCreateView(@NonNull LayoutInflater inflater,
ViewGroup container, Bundle savedInstanceState) {
binding = FragmentChatBinding.inflate(inflater, container, false);
binding.btnHamburger.setOnClickListener(v -> binding.chatDrawerLayout.openDrawer(GravityCompat.START));
// Set editor action listener for message field to send when enter is pressed
binding.etMessage.setOnEditorActionListener((v, actionId, event) -> {
if (actionId == EditorInfo.IME_ACTION_SEND || actionId == EditorInfo.IME_NULL) {
binding.btnSend.performClick();
@@ -108,35 +91,26 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis
return false;
});
//When the send button is clicked check if there is an attachment and send using the correct helper function
binding.btnSend.setOnClickListener(v -> {
if (pendingAttachmentUri != null) {
sendWithAttachment(pendingAttachmentUri);
} else {
sendMessage();
}
if (pendingAttachmentUri != null) sendWithAttachment(pendingAttachmentUri);
else sendMessage();
});
//When the attachment button is clicked open the file picker
binding.btnAttach.setOnClickListener(v -> selectAttachment());
binding.btnRemoveAttachment.setOnClickListener(v -> removeAttachment());
setupRecyclerViews();
observeViewModel();
loadInitialData();
return binding.getRoot();
}
/**
* Configures the RecyclerViews for the conversation list and the message history.
*/
private void setupRecyclerViews() {
// Set up Drawer menu to select conversation
chatAdapter = new ChatAdapter(chatList, this);
binding.rvChatList.setLayoutManager(new LinearLayoutManager(getContext()));
binding.rvChatList.setAdapter(chatAdapter);
// set up RecyclerView for selected chat to show messages
messageAdapter = new MessageAdapter(messageList, null);
LinearLayoutManager lm = new LinearLayoutManager(getContext());
lm.setStackFromEnd(true);
@@ -145,26 +119,48 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis
setConversationActive(false);
}
/**
* Loads authentication tokens and user info, then initializes the Stomp WebSocket connection.
*/
private void observeViewModel() {
viewModel.getChatList().observe(getViewLifecycleOwner(), list -> {
chatList.clear();
chatList.addAll(list);
chatAdapter.notifyDataSetChanged();
if (activeConversationId != null) {
for (Chat chat : list) {
if (chat.getChatId().equals(String.valueOf(activeConversationId))) {
binding.tvChatTitle.setText(chat.getCustomerName());
break;
}
}
}
});
viewModel.getMessageList().observe(getViewLifecycleOwner(), list -> {
messageList.clear();
messageList.addAll(list);
messageAdapter.notifyDataSetChanged();
scrollToBottom();
});
viewModel.getIsLoading().observe(getViewLifecycleOwner(), loading -> {
// Can show a progress bar if needed
});
}
private void loadInitialData() {
String token = tokenManager.getToken();
currentUserId = tokenManager.getUserId();
Long currentUserId = tokenManager.getUserId();
String role = tokenManager.getRole();
messageAdapter.setCurrentUserId(currentUserId);
messageAdapter.setToken(token);
// if token exist then connect to websocket
if (token != null) {
stompChatManager = new StompChatManager(token, role, baseUrl);
stompChatManager.setMessageListener(this);
stompChatManager.setConversationListener(this);
stompChatManager.setConnectionListener(this);
stompChatManager.connect();
} else {
Log.e(TAG, "No token found");
}
if (getArguments() != null && getArguments().containsKey("conversation_id")) {
@@ -172,65 +168,17 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis
} else if (getActivity() != null && getActivity().getIntent().hasExtra("conversation_id")) {
activeConversationId = getActivity().getIntent().getLongExtra("conversation_id", -1);
getActivity().getIntent().removeExtra("conversation_id");
getActivity().getIntent().removeExtra("navigate_to");
}
loadCustomers();
viewModel.loadCustomers();
if (activeConversationId != null) {
setConversationActive(true);
if (stompChatManager != null) stompChatManager.subscribeToConversation(activeConversationId);
viewModel.loadMessageHistory(activeConversationId);
}
}
/**
* Fetches a list of customers from the ViewModel to display customer names for the chat list.
*/
private void loadCustomers() {
viewModel.getAllCustomers(0, 100).observe(getViewLifecycleOwner(), resource -> {
if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) {
resource.data.getContent().forEach(c -> customerNames.put(c.getCustomerId(), c.getFullName()));
loadConversations();
}
});
}
/**
* Retrieves all conversations for the current user through the ViewModel and populates the chat drawer.
*/
private void loadConversations() {
viewModel.getAllConversations().observe(getViewLifecycleOwner(), resource -> {
if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) {
chatList.clear();
for (ConversationDTO dto : resource.data) {
String name = customerNames.getOrDefault(
dto.getCustomerId(), "Customer #" + dto.getCustomerId());
chatList.add(new Chat(String.valueOf(dto.getId()),
name, dto.getLastMessage(),
dto.getCustomerId(), dto.getStaffId()));
}
chatAdapter.notifyDataSetChanged();
if (activeConversationId != null) {
setConversationActive(true);
// Update title to customer name of active conversation
for (Chat chat : chatList) {
if (chat.getChatId().equals(String.valueOf(activeConversationId))) {
binding.tvChatTitle.setText(chat.getCustomerName());
break;
}
}
if (stompChatManager != null) {
stompChatManager.subscribeToConversation(activeConversationId);
}
loadMessageHistory(activeConversationId);
} else {
messageList.clear();
messageAdapter.notifyDataSetChanged();
setConversationActive(false);
}
}
});
}
/**
* Handles selection of a chat from the drawer, updating the UI and subscribing to the WebSocket.
*/
@Override
public void onChatClick(Chat chat) {
activeConversationId = Long.parseLong(chat.getChatId());
@@ -238,75 +186,35 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis
binding.tvChatTitle.setText(chat.getCustomerName());
binding.chatDrawerLayout.closeDrawer(GravityCompat.START);
if (stompChatManager != null) {
stompChatManager.subscribeToConversation(activeConversationId);
}
loadMessageHistory(activeConversationId);
if (stompChatManager != null) stompChatManager.subscribeToConversation(activeConversationId);
viewModel.loadMessageHistory(activeConversationId);
}
/**
* Fetches the full message history for a specific conversation from the ViewModel.
*/
private void loadMessageHistory(Long conversationId) {
viewModel.getMessages(conversationId).observe(getViewLifecycleOwner(), resource -> {
if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) {
messageList.clear();
for (MessageDTO dto : resource.data) {
messageList.add(dtoToModel(dto));
}
messageAdapter.notifyDataSetChanged();
scrollToBottom();
}
});
}
/**
* Sends a plain text message to the currently active conversation through the ViewModel.
*/
private void sendMessage() {
//check if a chat is selected
if (activeConversationId == null) return;
//get the message from text field
String text = binding.etMessage.getText().toString().trim();
if (text.isEmpty()) return;
//clear text field after sending
binding.etMessage.setText("");
//calls viewmodel to send the message
viewModel.sendMessage(activeConversationId, new SendMessageRequest(text)).observe(getViewLifecycleOwner(), resource -> {
viewModel.sendMessage(activeConversationId, text).observe(getViewLifecycleOwner(), resource -> {
if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) {
messageList.add(dtoToModel(resource.data));
messageAdapter.notifyItemInserted(messageList.size() - 1);
scrollToBottom();
loadConversations();
viewModel.addMessageLocally(resource.data);
viewModel.loadConversations();
}
});
}
/**
* Launches a file picker intent to select an attachment for the message.
*/
private void selectAttachment() {
Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
intent.setType("*/*");
attachmentLauncher.launch(intent);
}
/**
* Displays a preview of the selected attachment in the UI.
*/
private void showAttachmentPreview(Uri uri) {
pendingAttachmentUri = uri;
binding.layoutAttachmentPreview.setVisibility(View.VISIBLE);
String mimeType = requireContext().getContentResolver().getType(uri);
String fileName = getFileName(uri);
binding.tvPreviewName.setText(fileName);
// If the file is an image, display a thumbnail of the image as well
binding.tvPreviewName.setText(getFileName(uri));
if (mimeType != null && mimeType.startsWith("image/")) {
binding.ivPreview.setVisibility(View.VISIBLE);
Glide.with(this).load(uri).into(binding.ivPreview);
@@ -315,183 +223,83 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis
}
}
/**
* Clears the current attachment selection and hides the preview UI.
*/
private void removeAttachment() {
pendingAttachmentUri = null;
binding.layoutAttachmentPreview.setVisibility(View.GONE);
}
/**
* Show the display name of the file from its Uri.
*/
private String getFileName(Uri uri) {
String result = null;
if (uri.getScheme().equals("content")) {
try (Cursor cursor = requireContext().getContentResolver().query(uri, null, null, null, null)) {
if (cursor != null && cursor.moveToFirst()) {
int index = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME);
if (index != -1) {
result = cursor.getString(index);
}
if (index != -1) result = cursor.getString(index);
}
}
}
if (result == null) {
result = uri.getPath();
int cut = result.lastIndexOf('/');
if (cut != -1) {
result = result.substring(cut + 1);
}
if (cut != -1) result = result.substring(cut + 1);
}
return result;
}
/**
* Handles sending a message that includes a file attachment via the ViewModel.
*/
private void sendWithAttachment(Uri uri) {
if (activeConversationId == null) return;
String text = binding.etMessage.getText().toString().trim();
binding.etMessage.setText("");
removeAttachment();
if (!text.isEmpty()) {
binding.etMessage.setText(text);
}
Toast.makeText(requireContext(), "File attachments are not supported", Toast.LENGTH_SHORT).show();
removeAttachment();
}
/**
* Callback triggered when a new message is received via the WebSocket.
*/
@Override
public void onMessageReceived(MessageDTO dto) {
//if there is no active selected conversation or the message received is for another chat, then just update the preview of last message
if (activeConversationId == null || !activeConversationId.equals(dto.getConversationId())) {
updateConversationPreview(dto.getConversationId(), dto.getContent());
return;
}
updateConversationPreview(dto.getConversationId(), dto.getContent());
if (currentUserId != null && currentUserId.equals(dto.getSenderId())) return;
//else add the message to the active chat if it's not from the current user
messageList.add(dtoToModel(dto));
requireActivity().runOnUiThread(() -> {
messageAdapter.notifyItemInserted(messageList.size() - 1);
scrollToBottom();
if (activeConversationId != null && activeConversationId.equals(dto.getConversationId())) {
if (!tokenManager.getUserId().equals(dto.getSenderId())) {
viewModel.addMessageLocally(dto);
}
}
viewModel.updateConversationLocally(new ConversationDTO(dto.getConversationId(), 0L, 0L, dto.getContent(), ""));
// Re-load coversations to get correct names if needed or just let local update handle it
viewModel.loadConversations();
});
}
/**
* Callback triggered when a conversation is created or updated via the WebSocket.
*/
@Override
public void onConversationUpdated(ConversationDTO dto) {
requireActivity().runOnUiThread(() -> {
boolean updated = false;
String name = customerNames.getOrDefault(
dto.getCustomerId(), "Customer #" + dto.getCustomerId());
for (int i = 0; i < chatList.size(); i++) {
Chat existing = chatList.get(i);
if (existing.getChatId().equals(String.valueOf(dto.getId()))) {
chatList.set(i, new Chat(
String.valueOf(dto.getId()),
name,
dto.getLastMessage(),
dto.getCustomerId(),
dto.getStaffId()
));
chatAdapter.notifyItemChanged(i);
updated = true;
break;
}
}
if (!updated) {
chatList.add(0, new Chat(
String.valueOf(dto.getId()),
name,
dto.getLastMessage(),
dto.getCustomerId(),
dto.getStaffId()
));
chatAdapter.notifyItemInserted(0);
}
viewModel.updateConversationLocally(dto);
if (activeConversationId != null && activeConversationId.equals(dto.getId())) {
setConversationActive(true);
binding.tvChatTitle.setText(name);
binding.tvChatTitle.setText(viewModel.getCustomerName(dto.getCustomerId()));
}
});
}
/**
* Callback triggered when the WebSocket connection is successfully opened.
*/
@Override
public void onSocketOpened() {
if (!isAdded()) {
return;
}
if (!isAdded()) return;
requireActivity().runOnUiThread(() -> {
loadConversations();
if (activeConversationId != null) {
loadMessageHistory(activeConversationId);
}
viewModel.loadConversations();
if (activeConversationId != null) viewModel.loadMessageHistory(activeConversationId);
});
}
/**
* Callback triggered when the WebSocket connection is closed.
*/
@Override
public void onSocketClosed() {
if (!isAdded()) {
return;
}
requireActivity().runOnUiThread(this::loadConversations);
if (!isAdded()) return;
requireActivity().runOnUiThread(viewModel::loadConversations);
}
/**
* Callback triggered when a WebSocket connection error occurs.
*/
@Override
public void onSocketError() {
if (!isAdded()) {
return;
}
if (!isAdded()) return;
requireActivity().runOnUiThread(() -> {
loadConversations();
if (activeConversationId != null) {
loadMessageHistory(activeConversationId);
}
viewModel.loadConversations();
if (activeConversationId != null) viewModel.loadMessageHistory(activeConversationId);
});
}
/**
* Converts a MessageDTO into a Message object.
*/
private Message dtoToModel(MessageDTO dto) {
Message m = new Message();
m.setId(dto.getId());
m.setConversationId(dto.getConversationId());
m.setSenderId(dto.getSenderId());
m.setContent(dto.getContent());
m.setTimestamp(dto.getTimestamp());
m.setIsRead(dto.getIsRead());
m.setAttachmentUrl(dto.getAttachmentUrl());
m.setAttachmentName(dto.getAttachmentName());
m.setAttachmentType(dto.getAttachmentType());
return m;
}
/**
* Scrolls the message history RecyclerView to the most recent message.
*/
private void scrollToBottom() {
if (!messageList.isEmpty()) {
binding.rvMessages.post(() ->
@@ -499,35 +307,6 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis
}
}
/**
* Updates the preview snippet of the last message for a specific conversation in the drawer.
*/
private void updateConversationPreview(Long conversationId, String lastMessage) {
if (conversationId == null) {
return;
}
requireActivity().runOnUiThread(() -> {
for (int i = 0; i < chatList.size(); i++) {
Chat existing = chatList.get(i);
if (existing.getChatId().equals(String.valueOf(conversationId))) {
Chat updated = new Chat(
existing.getChatId(),
existing.getCustomerName(),
lastMessage,
existing.getCustomerId(),
existing.getStaffId()
);
chatList.set(i, updated);
chatAdapter.notifyItemChanged(i);
return;
}
}
});
}
/**
* Toggles the UI state based on whether a conversation is currently selected.
*/
private void setConversationActive(boolean active) {
binding.btnSend.setEnabled(active);
binding.etMessage.setEnabled(active);
@@ -537,9 +316,7 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis
ChatNotificationService.activeConversationIdInUi = null;
removeAttachment();
if (binding != null && binding.tvChatTitle != null) binding.tvChatTitle.setText("Customer Chat");
if (stompChatManager != null) {
stompChatManager.clearConversationSubscription();
}
if (stompChatManager != null) stompChatManager.clearConversationSubscription();
messageList.clear();
messageAdapter.notifyDataSetChanged();
binding.etMessage.setText("");
@@ -550,9 +327,6 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis
}
}
/**
* Disconnects the WebSocket manager when the fragment view is destroyed.
*/
@Override
public void onDestroyView() {
super.onDestroyView();

View File

@@ -24,9 +24,8 @@ import com.example.petstoremobile.utils.BulkDeleteHandler;
import com.example.petstoremobile.utils.Resource;
import com.example.petstoremobile.utils.SpinnerUtils;
import com.example.petstoremobile.utils.UIUtils;
import com.example.petstoremobile.viewmodels.AdoptionViewModel;
import com.example.petstoremobile.viewmodels.AdoptionListViewModel;
import com.example.petstoremobile.utils.EventDecorator;
import com.example.petstoremobile.viewmodels.StoreViewModel;
import com.prolificinteractive.materialcalendarview.CalendarDay;
import com.prolificinteractive.materialcalendarview.CalendarMode;
@@ -46,28 +45,19 @@ public class AdoptionFragment extends Fragment implements AdoptionAdapter.OnAdop
private FragmentAdoptionBinding binding;
private List<AdoptionDTO> adoptionList = new ArrayList<>();
private List<StoreDTO> storeList = new ArrayList<>();
private AdoptionAdapter adapter;
private AdoptionViewModel adoptionViewModel;
private StoreViewModel storeViewModel;
private AdoptionListViewModel viewModel;
private BulkDeleteHandler bulkDeleteHandler;
private CalendarDay selectedCalendarDay;
private boolean isMonthMode = false;
private final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd", Locale.getDefault());
/**
* Initializes the fragment and its ViewModels.
*/
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
adoptionViewModel = new ViewModelProvider(this).get(AdoptionViewModel.class);
storeViewModel = new ViewModelProvider(this).get(StoreViewModel.class);
viewModel = new ViewModelProvider(this).get(AdoptionListViewModel.class);
}
/**
* Sets up the fragment's UI components, including RecyclerView, Search, SwipeRefresh, and Calendar.
*/
@Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
@@ -81,6 +71,7 @@ public class AdoptionFragment extends Fragment implements AdoptionAdapter.OnAdop
setupCalendar();
setupFilterToggle();
setupBulkDelete();
observeViewModel();
binding.fabAddAdoption.setOnClickListener(v -> openDetail(-1));
@@ -91,6 +82,24 @@ public class AdoptionFragment extends Fragment implements AdoptionAdapter.OnAdop
return binding.getRoot();
}
private void observeViewModel() {
viewModel.getAdoptions().observe(getViewLifecycleOwner(), list -> {
adoptionList.clear();
adoptionList.addAll(list);
updateCalendarDecorators();
adapter.notifyDataSetChanged();
});
viewModel.getStores().observe(getViewLifecycleOwner(), list -> {
SpinnerUtils.populateWhiteSpinner(requireContext(), binding.spinnerStoreAdoption, list,
StoreDTO::getStoreName, "All Stores", -1L, StoreDTO::getStoreId);
});
viewModel.getIsLoading().observe(getViewLifecycleOwner(), loading -> {
binding.swipeRefreshAdoption.setRefreshing(loading);
});
}
private void setupBulkDelete() {
bulkDeleteHandler = new BulkDeleteHandler(
this,
@@ -99,27 +108,18 @@ public class AdoptionFragment extends Fragment implements AdoptionAdapter.OnAdop
binding.btnBulkDelete,
adapter,
"adoption",
adoptionViewModel::bulkDeleteAdoptions,
viewModel::bulkDeleteAdoptions,
this::loadAdoptions
);
}
@Override
public void onDestroyView() {
super.onDestroyView();
binding = null;
}
@Override
public void onResume() {
super.onResume();
loadAdoptions();
loadStoreData();
viewModel.loadStores();
}
/**
* Toggles the calendar display between week and month modes.
*/
private void toggleCalendarMode() {
isMonthMode = !isMonthMode;
binding.calendarViewAdoption.state().edit()
@@ -127,17 +127,11 @@ public class AdoptionFragment extends Fragment implements AdoptionAdapter.OnAdop
.commit();
}
/**
* Sets up the filter toggle button to show/hide the filter layout.
*/
private void setupFilterToggle() {
UIUtils.setupFilterToggle(binding.btnToggleFilterAdoption, binding.layoutFilterAdoption,
binding.etSearchAdoption, binding.spinnerStatusAdoption, binding.spinnerStoreAdoption);
}
/**
* Sets up the date selection listener for the calendar.
*/
private void setupCalendar() {
binding.calendarViewAdoption.setOnDateChangedListener((widget, date, selected) -> {
if (selected) {
@@ -154,9 +148,6 @@ public class AdoptionFragment extends Fragment implements AdoptionAdapter.OnAdop
});
}
/**
* Updates the calendar decorators to highlight days with adoptions.
*/
private void updateCalendarDecorators() {
HashSet<CalendarDay> datesWithAdoptions = new HashSet<>();
for (AdoptionDTO adoption : adoptionList) {
@@ -177,67 +168,37 @@ public class AdoptionFragment extends Fragment implements AdoptionAdapter.OnAdop
binding.calendarViewAdoption.addDecorator(new EventDecorator(Color.RED, datesWithAdoptions));
}
/**
* Initializes the RecyclerView for displaying adoptions.
*/
private void setupRecyclerView() {
adapter = new AdoptionAdapter(adoptionList, this);
binding.recyclerViewAdoptions.setLayoutManager(new LinearLayoutManager(getContext()));
binding.recyclerViewAdoptions.setAdapter(adapter);
}
/**
* Sets up the search bar for filtering
*/
private void setupSearch() {
UIUtils.attachSearch(binding.etSearchAdoption, this::loadAdoptions);
}
/**
* Configures the status filter spinner.
*/
private void setupStatusFilter() {
String[] statuses = {"All Statuses", "Completed", "Pending", "Cancelled"};
SpinnerUtils.setupStringFilterSpinner(requireContext(), binding.spinnerStatusAdoption, statuses, this::loadAdoptions);
}
/**
* Configures the store filter spinner.
*/
private void setupStoreFilter() {
SpinnerUtils.setupFilterSpinner(binding.spinnerStoreAdoption, this::loadAdoptions);
}
/**
* Fetches store data to populate the store filter.
*/
private void loadStoreData() {
storeViewModel.getAllStores(0, 100).observe(getViewLifecycleOwner(), resource -> {
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
storeList = resource.data.getContent();
SpinnerUtils.populateWhiteSpinner(requireContext(), binding.spinnerStoreAdoption, storeList,
StoreDTO::getStoreName, "All Stores", -1L, StoreDTO::getStoreId);
}
});
}
/**
* Sets up the SwipeRefreshLayout to reload adoption data.
*/
private void setupSwipeRefresh() {
binding.swipeRefreshAdoption.setOnRefreshListener(this::loadAdoptions);
}
/**
* Fetches the adoption list from the server through the ViewModel.
*/
private void loadAdoptions() {
String query = binding.etSearchAdoption.getText().toString().trim();
String status = binding.spinnerStatusAdoption.getSelectedItem() != null ? binding.spinnerStatusAdoption.getSelectedItem().toString() : "All Statuses";
Long storeId = null;
if (binding.spinnerStoreAdoption.getSelectedItemPosition() > 0 && !storeList.isEmpty()) {
storeId = storeList.get(binding.spinnerStoreAdoption.getSelectedItemPosition() - 1).getStoreId();
List<StoreDTO> stores = viewModel.getStores().getValue();
if (binding.spinnerStoreAdoption.getSelectedItemPosition() > 0 && stores != null && !stores.isEmpty()) {
storeId = stores.get(binding.spinnerStoreAdoption.getSelectedItemPosition() - 1).getStoreId();
}
String selectedDateString = null;
@@ -249,52 +210,18 @@ public class AdoptionFragment extends Fragment implements AdoptionAdapter.OnAdop
if (status.equals("All Statuses")) status = null;
else status = status.toUpperCase();
adoptionViewModel.getAllAdoptions(0, 500, query, status, storeId, selectedDateString, null).observe(getViewLifecycleOwner(), resource -> {
if (resource == null) return;
// Check the status to see if the resource is loaded and display the data
switch (resource.status) {
case LOADING:
// Show loading indicator
binding.swipeRefreshAdoption.setRefreshing(true);
break;
case SUCCESS:
// Hide loading indicator and display data
binding.swipeRefreshAdoption.setRefreshing(false);
if (resource.data != null) {
adoptionList.clear();
adoptionList.addAll(resource.data.getContent());
updateCalendarDecorators();
adapter.notifyDataSetChanged();
}
break;
case ERROR:
// Hide loading indicator and toast error message
binding.swipeRefreshAdoption.setRefreshing(false);
Toast.makeText(getContext(), "Failed to load adoptions: " + resource.message, Toast.LENGTH_SHORT).show();
Log.e("AdoptionFragment", "Error loading adoptions: " + resource.message);
break;
}
});
viewModel.loadAdoptions(true, query, status, storeId);
}
/**
* Navigates to the adoption detail screen for a specific adoption or to create a new one.
*/
private void openDetail(int position) {
Bundle args = new Bundle();
if (position != -1) {
AdoptionDTO a = adoptionList.get(position);
args.putLong("adoptionId", a.getAdoptionId());
}
NavHostFragment.findNavController(this).navigate(R.id.nav_adoption_detail, args);
}
/**
* Handles item click in the adoption list.
*/
@Override
public void onAdoptionClick(int position) { openDetail(position); }
@@ -304,4 +231,10 @@ public class AdoptionFragment extends Fragment implements AdoptionAdapter.OnAdop
bulkDeleteHandler.onSelectionChanged(selectedCount);
}
}
@Override
public void onDestroyView() {
super.onDestroyView();
binding = null;
}
}

View File

@@ -2,16 +2,14 @@ package com.example.petstoremobile.fragments.listfragments;
import android.graphics.Color;
import android.os.Bundle;
import android.util.Log;
import android.view.*;
import android.widget.*;
import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.ViewModelProvider;
import com.example.petstoremobile.databinding.FragmentAnalyticsBinding;
import com.example.petstoremobile.dtos.SaleDTO;
import com.example.petstoremobile.utils.UIUtils;
import com.example.petstoremobile.viewmodels.SaleViewModel;
import com.example.petstoremobile.viewmodels.AnalyticsViewModel;
import dagger.hilt.android.AndroidEntryPoint;
import java.math.BigDecimal;
import java.math.RoundingMode;
@@ -21,226 +19,131 @@ import java.util.*;
public class AnalyticsFragment extends Fragment {
private FragmentAnalyticsBinding binding;
private SaleViewModel saleViewModel;
private AnalyticsViewModel viewModel;
@Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
binding = FragmentAnalyticsBinding.inflate(inflater, container, false);
saleViewModel = new ViewModelProvider(this).get(SaleViewModel.class);
viewModel = new ViewModelProvider(this).get(AnalyticsViewModel.class);
loadAnalytics();
observeViewModel();
viewModel.loadAnalytics();
binding.btnRefreshAnalytics.setOnClickListener(v -> loadAnalytics());
binding.btnRefreshAnalytics.setOnClickListener(v -> viewModel.loadAnalytics());
UIUtils.setupHamburgerMenu(binding.btnHamburgerAnalytics, this);
return binding.getRoot();
}
private void observeViewModel() {
viewModel.getAnalyticsData().observe(getViewLifecycleOwner(), this::computeAndDisplay);
viewModel.getIsLoading().observe(getViewLifecycleOwner(), loading -> {
if (loading) {
binding.tvTotalRevenue.setText("Loading...");
binding.tvTotalTransactions.setText("...");
binding.tvAvgTransaction.setText("...");
binding.tvTotalItems.setText("...");
}
});
viewModel.getErrorMessage().observe(getViewLifecycleOwner(), error -> {
if (error != null) showError(error);
});
}
@Override
public void onDestroyView() {
super.onDestroyView();
binding = null;
}
private void loadAnalytics() {
// Clear all sections
private void computeAndDisplay(AnalyticsViewModel.AnalyticsData data) {
if (data == null) return;
// Summary
binding.tvTotalRevenue.setText("$" + data.totalRevenue.setScale(2, RoundingMode.HALF_UP));
binding.tvTotalTransactions.setText(String.valueOf(data.totalTransactions));
binding.tvAvgTransaction.setText("$" + data.avgTransaction);
binding.tvTotalItems.setText(String.valueOf(data.totalItems));
// Top Revenue Products
binding.llTopRevenue.removeAllViews();
binding.llTopQuantity.removeAllViews();
binding.llPaymentMethods.removeAllViews();
binding.llEmployeePerformance.removeAllViews();
binding.llDailyRevenue.removeAllViews();
saleViewModel.getAllSales(0, 1000, null, null, null, "saleDate,desc")
.observe(getViewLifecycleOwner(), resource -> {
if (resource != null) {
switch (resource.status) {
case SUCCESS:
if (resource.data != null) {
computeAndDisplay(resource.data.getContent());
}
break;
case ERROR:
Log.e("Analytics", resource.message != null ? resource.message : "Error loading sales");
showError("Failed to load sales data");
break;
case LOADING:
binding.tvTotalRevenue.setText("Loading...");
binding.tvTotalTransactions.setText("...");
binding.tvAvgTransaction.setText("...");
binding.tvTotalItems.setText("...");
break;
}
}
});
}
private void computeAndDisplay(List<SaleDTO> sales) {
// Filter out refunds for most metrics
List<SaleDTO> regularSales = new ArrayList<>();
for (SaleDTO s : sales) {
if (!Boolean.TRUE.equals(s.getIsRefund()))
regularSales.add(s);
}
// ── Summary ──────────────────────────────────────────
BigDecimal totalRevenue = BigDecimal.ZERO;
int totalItems = 0;
for (SaleDTO s : regularSales) {
if (s.getTotalAmount() != null)
totalRevenue = totalRevenue.add(s.getTotalAmount());
if (s.getItems() != null) {
for (SaleDTO.SaleItemDTO item : s.getItems()) {
if (item.getQuantity() != null)
totalItems += Math.abs(item.getQuantity());
}
if (data.topRevenueProducts != null && !data.topRevenueProducts.isEmpty()) {
BigDecimal maxRevenue = data.topRevenueProducts.get(0).getValue();
if (maxRevenue.compareTo(BigDecimal.ZERO) == 0) maxRevenue = BigDecimal.ONE;
for (Map.Entry<String, BigDecimal> e : data.topRevenueProducts) {
addBarRow(binding.llTopRevenue, e.getKey(), "$" + e.getValue().setScale(2, RoundingMode.HALF_UP),
e.getValue().floatValue() / maxRevenue.floatValue(), "#ff6b35");
}
}
int totalTx = regularSales.size();
BigDecimal avgTx = totalTx > 0
? totalRevenue.divide(BigDecimal.valueOf(totalTx), 2, RoundingMode.HALF_UP)
: BigDecimal.ZERO;
binding.tvTotalRevenue.setText("$" + totalRevenue.setScale(2, RoundingMode.HALF_UP));
binding.tvTotalTransactions.setText(String.valueOf(totalTx));
binding.tvAvgTransaction.setText("$" + avgTx);
binding.tvTotalItems.setText(String.valueOf(totalItems));
// ── Top Products by Revenue ───────────────────────────
Map<String, BigDecimal> revenueByProduct = new LinkedHashMap<>();
Map<String, Integer> quantityByProduct = new LinkedHashMap<>();
for (SaleDTO s : regularSales) {
if (s.getItems() != null) {
for (SaleDTO.SaleItemDTO item : s.getItems()) {
String name = item.getProductName() != null ? item.getProductName() : "Unknown";
int qty = item.getQuantity() != null ? Math.abs(item.getQuantity()) : 0;
BigDecimal lineTotal = item.getUnitPrice() != null
? item.getUnitPrice().multiply(BigDecimal.valueOf(qty))
: BigDecimal.ZERO;
revenueByProduct.merge(name, lineTotal, BigDecimal::add);
quantityByProduct.merge(name, qty, Integer::sum);
}
}
}
// Sort by revenue desc, take top 5
List<Map.Entry<String, BigDecimal>> topRevenue = new ArrayList<>(revenueByProduct.entrySet());
topRevenue.sort((a, b) -> b.getValue().compareTo(a.getValue()));
BigDecimal maxRevenue = topRevenue.isEmpty() ? BigDecimal.ONE : topRevenue.get(0).getValue();
binding.llTopRevenue.removeAllViews();
for (int i = 0; i < Math.min(5, topRevenue.size()); i++) {
Map.Entry<String, BigDecimal> e = topRevenue.get(i);
addBarRow(binding.llTopRevenue, e.getKey(), "$" + e.getValue().setScale(2, RoundingMode.HALF_UP),
e.getValue().floatValue() / maxRevenue.floatValue(), "#ff6b35");
}
if (topRevenue.isEmpty())
} else {
addEmptyRow(binding.llTopRevenue, "No data");
}
// Sort by quantity desc, take top 5
List<Map.Entry<String, Integer>> topQuantity = new ArrayList<>(quantityByProduct.entrySet());
topQuantity.sort((a, b) -> b.getValue() - a.getValue());
int maxQty = topQuantity.isEmpty() ? 1 : topQuantity.get(0).getValue();
// Top Quantity Products
binding.llTopQuantity.removeAllViews();
for (int i = 0; i < Math.min(5, topQuantity.size()); i++) {
Map.Entry<String, Integer> e = topQuantity.get(i);
addBarRow(binding.llTopQuantity, e.getKey(), e.getValue() + " units",
(float) e.getValue() / maxQty, "#4ecdc4");
}
if (topQuantity.isEmpty())
addEmptyRow(binding.llTopQuantity, "No data");
// ── Payment Methods ───────────────────────────────────
Map<String, Integer> paymentCount = new LinkedHashMap<>();
for (SaleDTO s : regularSales) {
String method = s.getPaymentMethod() != null ? s.getPaymentMethod() : "Unknown";
paymentCount.merge(method, 1, Integer::sum);
}
int maxPayment = paymentCount.values().stream().max(Integer::compare).orElse(1);
String[] paymentColors = { "#1a759f", "#ff9f1c", "#577590", "#90be6d" };
int ci = 0;
binding.llPaymentMethods.removeAllViews();
for (Map.Entry<String, Integer> e : paymentCount.entrySet()) {
addBarRow(binding.llPaymentMethods, e.getKey(),
e.getValue() + " transactions",
(float) e.getValue() / maxPayment,
paymentColors[ci % paymentColors.length]);
ci++;
}
if (paymentCount.isEmpty())
addEmptyRow(binding.llPaymentMethods, "No data");
// ── Employee Performance ──────────────────────────────
Map<String, BigDecimal> employeeRevenue = new LinkedHashMap<>();
for (SaleDTO s : regularSales) {
String emp = s.getEmployeeName() != null ? s.getEmployeeName() : "Unknown";
if (s.getTotalAmount() != null)
employeeRevenue.merge(emp, s.getTotalAmount(), BigDecimal::add);
}
List<Map.Entry<String, BigDecimal>> empList = new ArrayList<>(employeeRevenue.entrySet());
empList.sort((a, b) -> b.getValue().compareTo(a.getValue()));
BigDecimal maxEmp = empList.isEmpty() ? BigDecimal.ONE : empList.get(0).getValue();
binding.llEmployeePerformance.removeAllViews();
for (Map.Entry<String, BigDecimal> e : empList) {
addBarRow(binding.llEmployeePerformance, e.getKey(),
"$" + e.getValue().setScale(2, RoundingMode.HALF_UP),
e.getValue().floatValue() / maxEmp.floatValue(),
"#1a759f");
}
if (empList.isEmpty())
addEmptyRow(binding.llEmployeePerformance, "No data");
// ── Daily Revenue (last 7 days) ───────────────────────
Map<String, BigDecimal> dailyRevenue = new TreeMap<>();
// Initialize last 7 days
Calendar cal = Calendar.getInstance();
for (int i = 6; i >= 0; i--) {
Calendar day = Calendar.getInstance();
day.add(Calendar.DAY_OF_YEAR, -i);
String key = String.format("%04d-%02d-%02d",
day.get(Calendar.YEAR),
day.get(Calendar.MONTH) + 1,
day.get(Calendar.DAY_OF_MONTH));
dailyRevenue.put(key, BigDecimal.ZERO);
}
for (SaleDTO s : regularSales) {
if (s.getSaleDate() != null && s.getTotalAmount() != null) {
String date = s.getSaleDate().length() >= 10
? s.getSaleDate().substring(0, 10)
: s.getSaleDate();
if (dailyRevenue.containsKey(date)) {
dailyRevenue.merge(date, s.getTotalAmount(), BigDecimal::add);
}
if (data.topQuantityProducts != null && !data.topQuantityProducts.isEmpty()) {
int maxQty = data.topQuantityProducts.get(0).getValue();
if (maxQty == 0) maxQty = 1;
for (Map.Entry<String, Integer> e : data.topQuantityProducts) {
addBarRow(binding.llTopQuantity, e.getKey(), e.getValue() + " units",
(float) e.getValue() / maxQty, "#4ecdc4");
}
} else {
addEmptyRow(binding.llTopQuantity, "No data");
}
BigDecimal maxDaily = dailyRevenue.values().stream()
.max(BigDecimal::compareTo).orElse(BigDecimal.ONE);
if (maxDaily.compareTo(BigDecimal.ZERO) == 0)
maxDaily = BigDecimal.ONE;
// Payment Methods
binding.llPaymentMethods.removeAllViews();
if (data.paymentMethodStats != null && !data.paymentMethodStats.isEmpty()) {
int maxPayment = data.paymentMethodStats.stream().mapToInt(Map.Entry::getValue).max().orElse(1);
String[] paymentColors = { "#1a759f", "#ff9f1c", "#577590", "#90be6d" };
int ci = 0;
for (Map.Entry<String, Integer> e : data.paymentMethodStats) {
addBarRow(binding.llPaymentMethods, e.getKey(),
e.getValue() + " transactions",
(float) e.getValue() / maxPayment,
paymentColors[ci % paymentColors.length]);
ci++;
}
} else {
addEmptyRow(binding.llPaymentMethods, "No data");
}
// Employee Performance
binding.llEmployeePerformance.removeAllViews();
if (data.employeePerformance != null && !data.employeePerformance.isEmpty()) {
BigDecimal maxEmp = data.employeePerformance.get(data.employeePerformance.size() - 1).getValue();
if (maxEmp.compareTo(BigDecimal.ZERO) == 0) maxEmp = BigDecimal.ONE;
// Sorting is ascending from VM for some reason? Let's check VM... it says b.getValue().compareTo(a.getValue()) which is DESC.
// Wait, computeAnalytics sorts them... let's assume DESC as per VM code.
maxEmp = data.employeePerformance.get(0).getValue();
if (maxEmp.compareTo(BigDecimal.ZERO) == 0) maxEmp = BigDecimal.ONE;
for (Map.Entry<String, BigDecimal> e : data.employeePerformance) {
addBarRow(binding.llEmployeePerformance, e.getKey(),
"$" + e.getValue().setScale(2, RoundingMode.HALF_UP),
e.getValue().floatValue() / maxEmp.floatValue(),
"#1a759f");
}
} else {
addEmptyRow(binding.llEmployeePerformance, "No data");
}
// Daily Revenue
binding.llDailyRevenue.removeAllViews();
for (Map.Entry<String, BigDecimal> e : dailyRevenue.entrySet()) {
// Show just MM-DD
String label = e.getKey().length() >= 10
? e.getKey().substring(5)
: e.getKey();
addBarRow(binding.llDailyRevenue, label,
"$" + e.getValue().setScale(2, RoundingMode.HALF_UP),
e.getValue().floatValue() / maxDaily.floatValue(),
"#ff6b35");
if (data.dailyRevenue != null && !data.dailyRevenue.isEmpty()) {
BigDecimal maxDaily = data.dailyRevenue.stream().map(Map.Entry::getValue).max(BigDecimal::compareTo).orElse(BigDecimal.ONE);
if (maxDaily.compareTo(BigDecimal.ZERO) == 0) maxDaily = BigDecimal.ONE;
for (Map.Entry<String, BigDecimal> e : data.dailyRevenue) {
String label = e.getKey().length() >= 10 ? e.getKey().substring(5) : e.getKey();
addBarRow(binding.llDailyRevenue, label,
"$" + e.getValue().setScale(2, RoundingMode.HALF_UP),
e.getValue().floatValue() / maxDaily.floatValue(),
"#ff6b35");
}
}
}

View File

@@ -14,7 +14,6 @@ import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Toast;
import com.example.petstoremobile.R;
import com.example.petstoremobile.adapters.AppointmentAdapter;
@@ -25,10 +24,9 @@ import com.example.petstoremobile.utils.BulkDeleteHandler;
import com.example.petstoremobile.utils.Resource;
import com.example.petstoremobile.utils.SpinnerUtils;
import com.example.petstoremobile.utils.UIUtils;
import com.example.petstoremobile.viewmodels.AppointmentViewModel;
import com.example.petstoremobile.viewmodels.AppointmentListViewModel;
import com.example.petstoremobile.utils.EventDecorator;
import com.example.petstoremobile.viewmodels.AuthViewModel;
import com.example.petstoremobile.viewmodels.StoreViewModel;
import com.prolificinteractive.materialcalendarview.CalendarDay;
import com.prolificinteractive.materialcalendarview.CalendarMode;
@@ -48,11 +46,9 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter.
private FragmentAppointmentBinding binding;
private List<AppointmentDTO> appointmentList = new ArrayList<>();
private List<StoreDTO> storeList = new ArrayList<>();
private AppointmentAdapter adapter;
private AppointmentViewModel appointmentViewModel;
private StoreViewModel storeViewModel;
private AppointmentListViewModel viewModel;
private AuthViewModel authViewModel;
private BulkDeleteHandler bulkDeleteHandler;
@@ -61,20 +57,13 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter.
private Long currentUserId = null;
private final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd", Locale.getDefault());
/**
* Initializes the fragment and its associated ViewModels.
*/
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
appointmentViewModel = new ViewModelProvider(this).get(AppointmentViewModel.class);
storeViewModel = new ViewModelProvider(this).get(StoreViewModel.class);
viewModel = new ViewModelProvider(this).get(AppointmentListViewModel.class);
authViewModel = new ViewModelProvider(this).get(AuthViewModel.class);
}
/**
* Sets up the fragment's UI, including RecyclerView, search, swipe-to-refresh, and calendar.
*/
@Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
@@ -89,6 +78,7 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter.
setupFilterToggle();
setupMyAppointmentFilter();
setupBulkDelete();
observeViewModel();
binding.fabAddAppointment.setOnClickListener(v -> openAppointmentDetails(-1));
@@ -101,6 +91,24 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter.
return binding.getRoot();
}
private void observeViewModel() {
viewModel.getAppointments().observe(getViewLifecycleOwner(), list -> {
appointmentList.clear();
appointmentList.addAll(list);
updateCalendarDecorators();
adapter.notifyDataSetChanged();
});
viewModel.getStores().observe(getViewLifecycleOwner(), list -> {
SpinnerUtils.populateWhiteSpinner(requireContext(), binding.spinnerStore, list,
StoreDTO::getStoreName, "All Stores", -1L, StoreDTO::getStoreId);
});
viewModel.getIsLoading().observe(getViewLifecycleOwner(), loading -> {
binding.swipeRefreshAppointment.setRefreshing(loading);
});
}
private void setupBulkDelete() {
bulkDeleteHandler = new BulkDeleteHandler(
this,
@@ -109,27 +117,18 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter.
binding.btnBulkDelete,
adapter,
"appointment",
appointmentViewModel::bulkDeleteAppointments,
viewModel::bulkDeleteAppointments,
this::loadAppointmentData
);
}
@Override
public void onDestroyView() {
super.onDestroyView();
binding = null;
}
@Override
public void onResume() {
super.onResume();
loadAppointmentData();
loadStoreData();
viewModel.loadStores();
}
/**
* Toggles the calendar between week and month display modes.
*/
private void toggleCalendarMode() {
isMonthMode = !isMonthMode;
binding.calendarView.state().edit()
@@ -137,18 +136,12 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter.
.commit();
}
/**
* Sets up the "My Appointments" filter button.
*/
private void setupMyAppointmentFilter() {
binding.btnMyAppointments.setOnClickListener(v -> {
loadAppointmentData();
});
}
/**
* Fetches current user info to get the employeeId.
*/
private void loadCurrentUserInfo() {
authViewModel.getMe().observe(getViewLifecycleOwner(), resource -> {
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
@@ -157,17 +150,11 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter.
});
}
/**
* Sets up the filter toggle button to show/hide the filter layout.
*/
private void setupFilterToggle() {
UIUtils.setupFilterToggle(binding.btnToggleFilter, binding.layoutFilter, binding.etSearchAppointment,
binding.spinnerStatus, binding.spinnerStore);
}
/**
* Sets up the date selection listener for the calendar.
*/
private void setupCalendar() {
binding.calendarView.setOnDateChangedListener((widget, date, selected) -> {
if (selected) {
@@ -184,17 +171,11 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter.
});
}
/**
* Updates calendar indicators to highlight dates that have scheduled appointments.
*/
private void updateCalendarDecorators() {
HashSet<CalendarDay> datesWithAppointments = new HashSet<>();
SimpleDateFormat displayFormat = new SimpleDateFormat("yyyy-MM-dd", Locale.getDefault());
for (AppointmentDTO appointment : appointmentList) {
try {
//Get the appointment date
Date date = displayFormat.parse(appointment.getAppointmentDate());
//if the date is not null, add it to the hashset
Date date = dateFormat.parse(appointment.getAppointmentDate());
if (date != null) {
Calendar cal = Calendar.getInstance();
cal.setTime(date);
@@ -204,56 +185,27 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter.
Log.e("AppointmentFragment", "Error parsing date: " + appointment.getAppointmentDate());
}
}
//update the indicators to the calendar
binding.calendarView.removeDecorators();
binding.calendarView.addDecorator(new EventDecorator(Color.RED, datesWithAppointments));
}
/**
* Configures the search bar for filtering.
*/
private void setupSearch() {
UIUtils.attachSearch(binding.etSearchAppointment, this::loadAppointmentData);
}
/**
* Configures the status filter spinner.
*/
private void setupStatusFilter() {
String[] statuses = {"All Statuses", "Booked", "Completed", "Cancelled", "Missed"};
SpinnerUtils.setupStringFilterSpinner(requireContext(), binding.spinnerStatus, statuses, this::loadAppointmentData);
}
/**
* Configures the store filter spinner.
*/
private void setupStoreFilter() {
SpinnerUtils.setupFilterSpinner(binding.spinnerStore, this::loadAppointmentData);
}
/**
* Fetches store data to populate the store filter.
*/
private void loadStoreData() {
storeViewModel.getAllStores(0, 100).observe(getViewLifecycleOwner(), resource -> {
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
storeList = resource.data.getContent();
SpinnerUtils.populateWhiteSpinner(requireContext(), binding.spinnerStore, storeList,
StoreDTO::getStoreName, "All Stores", -1L, StoreDTO::getStoreId);
}
});
}
/**
* Initializes the SwipeRefreshLayout to allow manual data refreshing.
*/
private void setupSwipeRefresh() {
binding.swipeRefreshAppointment.setOnRefreshListener(this::loadAppointmentData);
}
/**
* Navigates to the appointment detail screen for editing or creating an appointment.
*/
private void openAppointmentDetails(int position) {
Bundle args = new Bundle();
if (position != -1) {
@@ -263,9 +215,6 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter.
NavHostFragment.findNavController(this).navigate(R.id.nav_appointment_detail, args);
}
/**
* Handles item click in the appointment list.
*/
@Override
public void onAppointmentClick(int position) {
openAppointmentDetails(position);
@@ -278,16 +227,14 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter.
}
}
/**
* Fetches appointment data from the server with all active filters.
*/
private void loadAppointmentData() {
String query = binding.etSearchAppointment.getText().toString().trim();
String status = binding.spinnerStatus.getSelectedItem() != null ? binding.spinnerStatus.getSelectedItem().toString() : "All Statuses";
Long storeId = null;
if (binding.spinnerStore.getSelectedItemPosition() > 0 && !storeList.isEmpty()) {
storeId = storeList.get(binding.spinnerStore.getSelectedItemPosition() - 1).getStoreId();
List<StoreDTO> stores = viewModel.getStores().getValue();
if (binding.spinnerStore.getSelectedItemPosition() > 0 && stores != null && !stores.isEmpty()) {
storeId = stores.get(binding.spinnerStore.getSelectedItemPosition() - 1).getStoreId();
}
String selectedDateString = null;
@@ -304,41 +251,18 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter.
if (status.equals("All Statuses")) status = null;
else status = status.toUpperCase();
appointmentViewModel.getAllAppointments(0, 500, query, status, storeId, selectedDateString, employeeId).observe(getViewLifecycleOwner(), resource -> {
if (resource == null) return;
// Check the status to see if the resource is loaded and display the data
switch (resource.status) {
case LOADING:
// Show loading indicator
binding.swipeRefreshAppointment.setRefreshing(true);
break;
case SUCCESS:
// Hide loading indicator and display data
binding.swipeRefreshAppointment.setRefreshing(false);
if (resource.data != null) {
appointmentList.clear();
appointmentList.addAll(resource.data.getContent());
updateCalendarDecorators();
adapter.notifyDataSetChanged();
}
break;
case ERROR:
// Hide loading indicator and toast error message
binding.swipeRefreshAppointment.setRefreshing(false);
Toast.makeText(getContext(), "Failed to load appointments: " + resource.message, Toast.LENGTH_SHORT).show();
Log.e("AppointmentFragment", "Error loading appointments: " + resource.message);
break;
}
});
viewModel.loadAppointments(query, status, storeId, selectedDateString, employeeId);
}
/**
* Initializes the RecyclerView for displaying appointments.
*/
private void setupRecyclerView() {
adapter = new AppointmentAdapter(appointmentList, this);
binding.recyclerViewAppointments.setLayoutManager(new LinearLayoutManager(getContext()));
binding.recyclerViewAppointments.setAdapter(adapter);
}
@Override
public void onDestroyView() {
super.onDestroyView();
binding = null;
}
}

View File

@@ -1,7 +1,6 @@
package com.example.petstoremobile.fragments.listfragments;
import android.os.Bundle;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
@@ -22,8 +21,7 @@ import com.example.petstoremobile.dtos.InventoryDTO;
import com.example.petstoremobile.dtos.StoreDTO;
import com.example.petstoremobile.utils.BulkDeleteHandler;
import com.example.petstoremobile.utils.UIUtils;
import com.example.petstoremobile.viewmodels.InventoryViewModel;
import com.example.petstoremobile.utils.Resource;
import com.example.petstoremobile.viewmodels.InventoryListViewModel;
import com.example.petstoremobile.utils.SpinnerUtils;
import java.util.ArrayList;
@@ -34,33 +32,18 @@ import dagger.hilt.android.AndroidEntryPoint;
@AndroidEntryPoint
public class InventoryFragment extends Fragment implements InventoryAdapter.OnInventoryClickListener {
private static final String TAG = "InventoryFragment";
private static final int PAGE_SIZE = 20;
private FragmentInventoryBinding binding;
private final List<InventoryDTO> inventoryList = new ArrayList<>();
private List<StoreDTO> storeList = new ArrayList<>();
private InventoryAdapter adapter;
private InventoryViewModel viewModel;
private InventoryListViewModel viewModel;
private BulkDeleteHandler bulkDeleteHandler;
// Pagination
private int currentPage = 0;
private boolean isLastPage = false;
private boolean isLoading = false;
/**
* Initializes the fragment and its ViewModel.
*/
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
viewModel = new ViewModelProvider(this).get(InventoryViewModel.class);
viewModel = new ViewModelProvider(this).get(InventoryListViewModel.class);
}
/**
* Sets up the fragment's UI components, including the inventory list and search.
*/
@Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
@@ -72,8 +55,9 @@ public class InventoryFragment extends Fragment implements InventoryAdapter.OnIn
setupSwipeRefresh();
setupFilterToggle();
setupBulkDelete();
observeViewModel();
loadInventory(true);
loadStoreData();
binding.fabAddInventory.setOnClickListener(v -> openDetail(null));
@@ -82,6 +66,23 @@ public class InventoryFragment extends Fragment implements InventoryAdapter.OnIn
return binding.getRoot();
}
private void observeViewModel() {
viewModel.getInventory().observe(getViewLifecycleOwner(), list -> {
inventoryList.clear();
inventoryList.addAll(list);
adapter.notifyDataSetChanged();
});
viewModel.getStores().observe(getViewLifecycleOwner(), list -> {
SpinnerUtils.populateWhiteSpinner(requireContext(), binding.spinnerStore, list,
StoreDTO::getStoreName, "All Stores", -1L, StoreDTO::getStoreId);
});
viewModel.getIsLoading().observe(getViewLifecycleOwner(), loading -> {
binding.swipeRefreshInventory.setRefreshing(loading);
});
}
private void setupBulkDelete() {
bulkDeleteHandler = new BulkDeleteHandler(
this,
@@ -95,49 +96,30 @@ public class InventoryFragment extends Fragment implements InventoryAdapter.OnIn
);
}
@Override
public void onResume() {
super.onResume();
viewModel.loadStores();
}
@Override
public void onDestroyView() {
super.onDestroyView();
binding = null;
}
/**
* Sets up the filter toggle button to show/hide the filter layout.
*/
private void setupFilterToggle() {
UIUtils.setupFilterToggle(binding.btnToggleFilter, binding.layoutFilter, binding.etSearchInventory, binding.spinnerStore);
}
/**
* Sets up the search bar for filtering.
*/
private void setupSearch() {
UIUtils.attachSearch(binding.etSearchInventory, () -> loadInventory(true));
}
/**
* Configures the store filter spinner.
*/
private void setupStoreFilter() {
SpinnerUtils.setupFilterSpinner(binding.spinnerStore, () -> loadInventory(true));
}
/**
* Fetches store data to populate the store filter.
*/
private void loadStoreData() {
viewModel.getAllStores(0, 100).observe(getViewLifecycleOwner(), resource -> {
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
storeList = resource.data.getContent();
SpinnerUtils.populateWhiteSpinner(requireContext(), binding.spinnerStore, storeList,
StoreDTO::getStoreName, "All Stores", -1L, StoreDTO::getStoreId);
}
});
}
/**
* Initializes the RecyclerView with a layout manager, and adapter.
*/
private void setupRecyclerView() {
adapter = new InventoryAdapter(inventoryList, this);
binding.recyclerViewInventory.setLayoutManager(new LinearLayoutManager(getContext()));
@@ -146,105 +128,45 @@ public class InventoryFragment extends Fragment implements InventoryAdapter.OnIn
binding.recyclerViewInventory.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
if (dy <= 0)
return;
if (dy <= 0) return;
LinearLayoutManager lm = (LinearLayoutManager) binding.recyclerViewInventory.getLayoutManager();
if (lm == null)
return;
if (lm == null) return;
int visible = lm.getChildCount();
int total = lm.getItemCount();
int firstVis = lm.findFirstVisibleItemPosition();
if (!isLoading && !isLastPage && (visible + firstVis) >= total - 3) {
Boolean isLoading = viewModel.getIsLoading().getValue();
if ((isLoading == null || !isLoading) && !viewModel.isLastPage() && (visible + firstVis) >= total - 3) {
loadInventory(false);
}
}
});
}
/**
* Sets up the SwipeRefreshLayout to reload the first page of inventory items.
*/
private void setupSwipeRefresh() {
binding.swipeRefreshInventory.setOnRefreshListener(() -> loadInventory(true));
}
/**
* Fetches a page of inventory items from the API.
*/
private void loadInventory(boolean reset) {
if (isLoading) return;
if (reset) {
currentPage = 0;
isLastPage = false;
}
// Search text from input
String query = binding.etSearchInventory != null ? binding.etSearchInventory.getText().toString().trim() : "";
if (query.isEmpty()) query = null;
Long storeId = null;
if (binding.spinnerStore.getSelectedItemPosition() > 0 && !storeList.isEmpty()) {
storeId = storeList.get(binding.spinnerStore.getSelectedItemPosition() - 1).getStoreId();
List<StoreDTO> stores = viewModel.getStores().getValue();
if (binding.spinnerStore.getSelectedItemPosition() > 0 && stores != null && !stores.isEmpty()) {
storeId = stores.get(binding.spinnerStore.getSelectedItemPosition() - 1).getStoreId();
}
//Load all inventory items from the backend using viewModel
viewModel.getAllInventory(query, storeId, currentPage, PAGE_SIZE, "product.prodName").observe(getViewLifecycleOwner(), resource -> {
if (resource == null) return;
// Check the status to see if the resource is loaded and display the data
switch (resource.status) {
case LOADING:
// Show loading indicator
isLoading = true;
binding.swipeRefreshInventory.setRefreshing(true);
break;
case SUCCESS:
// Hide loading indicator and display data
isLoading = false;
binding.swipeRefreshInventory.setRefreshing(false);
if (resource.data != null) {
if (reset) inventoryList.clear();
inventoryList.addAll(resource.data.getContent());
adapter.notifyDataSetChanged();
isLastPage = resource.data.isLast();
if (!isLastPage) currentPage++;
}
break;
case ERROR:
// Hide loading indicator and toast error message
isLoading = false;
binding.swipeRefreshInventory.setRefreshing(false);
Log.e(TAG, "Error: " + resource.message);
Toast.makeText(getContext(), "Failed to load inventory: " + resource.message, Toast.LENGTH_SHORT).show();
break;
}
});
viewModel.loadInventory(reset, query, storeId);
}
/**
* Navigates to the inventory detail screen for a specific item or to add a new one.
*/
private void openDetail(InventoryDTO inv) {
Bundle args = new Bundle();
if (inv != null) {
args.putLong("inventoryId", inv.getInventoryId());
}
NavHostFragment.findNavController(this).navigate(R.id.nav_inventory_detail, args);
}
/**
* Reloads inventory data when changes occur.
*/
public void onInventoryChanged() {
loadInventory(true);
}
/**
* Handles item click in the inventory list.
*/
@Override
public void onInventoryClick(int position) {
if (position >= 0 && position < inventoryList.size()) {
@@ -252,9 +174,6 @@ public class InventoryFragment extends Fragment implements InventoryAdapter.OnIn
}
}
/**
* Updates the bulk deletion UI visibility and count when items are selected or deselected.
*/
@Override
public void onSelectionChanged(int selectedCount) {
if (bulkDeleteHandler != null) {

View File

@@ -9,7 +9,6 @@ import androidx.lifecycle.ViewModelProvider;
import androidx.navigation.fragment.NavHostFragment;
import androidx.recyclerview.widget.LinearLayoutManager;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
@@ -25,8 +24,7 @@ import com.example.petstoremobile.utils.BulkDeleteHandler;
import com.example.petstoremobile.utils.Resource;
import com.example.petstoremobile.utils.SpinnerUtils;
import com.example.petstoremobile.utils.UIUtils;
import com.example.petstoremobile.viewmodels.PetViewModel;
import com.example.petstoremobile.viewmodels.StoreViewModel;
import com.example.petstoremobile.viewmodels.PetListViewModel;
import java.util.ArrayList;
import java.util.List;
@@ -40,28 +38,19 @@ import dagger.hilt.android.AndroidEntryPoint;
public class PetFragment extends Fragment implements PetAdapter.OnPetClickListener {
private FragmentPetBinding binding;
private List<PetDTO> petList = new ArrayList<>();
private List<StoreDTO> storeList = new ArrayList<>();
private PetAdapter adapter;
private PetViewModel viewModel;
private StoreViewModel storeViewModel;
private PetListViewModel viewModel;
private BulkDeleteHandler bulkDeleteHandler;
@Inject @Named("baseUrl") String baseUrl;
@Inject TokenManager tokenManager;
/**
* Initializes the fragment and its associated ViewModels.
*/
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
viewModel = new ViewModelProvider(this).get(PetViewModel.class);
storeViewModel = new ViewModelProvider(this).get(StoreViewModel.class);
viewModel = new ViewModelProvider(this).get(PetListViewModel.class);
}
/**
* Sets up the fragment's UI components, including RecyclerView, filters, and swipe-to-refresh.
*/
@Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
@@ -75,6 +64,7 @@ public class PetFragment extends Fragment implements PetAdapter.OnPetClickListen
setupSwipeRefresh();
setupFilterToggle();
setupBulkDelete();
observeViewModel();
binding.fabAddPet.setOnClickListener(v -> openPetDetails());
@@ -83,6 +73,23 @@ public class PetFragment extends Fragment implements PetAdapter.OnPetClickListen
return binding.getRoot();
}
private void observeViewModel() {
viewModel.getPets().observe(getViewLifecycleOwner(), list -> {
petList.clear();
petList.addAll(list);
adapter.notifyDataSetChanged();
});
viewModel.getStores().observe(getViewLifecycleOwner(), list -> {
SpinnerUtils.populateWhiteSpinner(requireContext(), binding.spinnerStore, list,
StoreDTO::getStoreName, "All Stores", -1L, StoreDTO::getStoreId);
});
viewModel.getIsLoading().observe(getViewLifecycleOwner(), loading -> {
binding.swipeRefreshPet.setRefreshing(loading);
});
}
private void setupBulkDelete() {
bulkDeleteHandler = new BulkDeleteHandler(
this,
@@ -96,83 +103,62 @@ public class PetFragment extends Fragment implements PetAdapter.OnPetClickListen
);
}
@Override
public void onDestroyView() {
super.onDestroyView();
binding = null;
}
/**
* Reloads data every time the fragment becomes visible.
*/
@Override
public void onResume() {
super.onResume();
loadPetData();
loadStoreData();
viewModel.loadStores();
}
/**
* Sets up the filter toggle button to show/hide the filter layout.
*/
private void setupFilterToggle() {
UIUtils.setupFilterToggle(binding.btnToggleFilter, binding.layoutFilter, binding.etSearchPet,
binding.spinnerStatus, binding.spinnerSpecies, binding.spinnerStore);
}
/**
* Configures the search bar.
*/
private void setupSearch() {
UIUtils.attachSearch(binding.etSearchPet, this::loadPetData);
}
/**
* Configures the status filter spinner.
*/
private void setupStatusFilter() {
String[] statuses = {"All Statuses", "Available", "Adopted", "Owned"};
SpinnerUtils.setupStringFilterSpinner(requireContext(), binding.spinnerStatus, statuses, this::loadPetData);
}
/**
* Configures the species filter spinner with species.
*/
private void setupSpeciesFilter() {
String[] species = {"All Species", "Dog", "Cat", "Bird", "Rabbit", "Fish", "Hamster"};
SpinnerUtils.setupStringFilterSpinner(requireContext(), binding.spinnerSpecies, species, this::loadPetData);
}
/**
* Configures the store filter spinner.
*/
private void setupStoreFilter() {
SpinnerUtils.setupFilterSpinner(binding.spinnerStore, this::loadPetData);
}
/**
* Fetches store data to populate the store filter.
*/
private void loadStoreData() {
storeViewModel.getAllStores(0, 100).observe(getViewLifecycleOwner(), resource -> {
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
storeList = resource.data.getContent();
SpinnerUtils.populateWhiteSpinner(requireContext(), binding.spinnerStore, storeList,
StoreDTO::getStoreName, "All Stores", -1L, StoreDTO::getStoreId);
}
});
}
/**
* Sets up the SwipeRefreshLayout.
*/
private void setupSwipeRefresh() {
binding.swipeRefreshPet.setOnRefreshListener(this::loadPetData);
}
/**
* Navigates to the pet profile screen.
*/
private void loadPetData() {
String query = binding.etSearchPet.getText().toString().trim();
String status = binding.spinnerStatus.getSelectedItem() != null ? binding.spinnerStatus.getSelectedItem().toString() : "All Statuses";
String species = binding.spinnerSpecies.getSelectedItem() != null ? binding.spinnerSpecies.getSelectedItem().toString() : "All Species";
Long storeId = null;
List<StoreDTO> stores = viewModel.getStores().getValue();
if (binding.spinnerStore.getSelectedItemPosition() > 0 && stores != null && !stores.isEmpty()) {
storeId = stores.get(binding.spinnerStore.getSelectedItemPosition() - 1).getStoreId();
}
viewModel.loadPets(query, status, species, storeId);
}
private void setupRecyclerView() {
adapter = new PetAdapter(petList, this);
adapter.setBaseUrl(baseUrl);
adapter.setToken(tokenManager.getToken());
binding.recyclerViewPets.setLayoutManager(new LinearLayoutManager(getContext()));
binding.recyclerViewPets.setAdapter(adapter);
}
private void openPetProfile(int position) {
Bundle args = new Bundle();
PetDTO pet = petList.get(position);
@@ -180,9 +166,6 @@ public class PetFragment extends Fragment implements PetAdapter.OnPetClickListen
NavHostFragment.findNavController(this).navigate(R.id.nav_pet_profile, args);
}
/**
* Navigates to the pet detail screen.
*/
private void openPetDetails() {
NavHostFragment.findNavController(this).navigate(R.id.nav_pet_detail);
}
@@ -199,54 +182,9 @@ public class PetFragment extends Fragment implements PetAdapter.OnPetClickListen
}
}
/**
* Fetches pet data from the server with all active filters.
*/
private void loadPetData() {
String query = binding.etSearchPet.getText().toString().trim();
String status = binding.spinnerStatus.getSelectedItem() != null ? binding.spinnerStatus.getSelectedItem().toString() : "All Statuses";
String species = binding.spinnerSpecies.getSelectedItem() != null ? binding.spinnerSpecies.getSelectedItem().toString() : "All Species";
Long storeId = null;
if (binding.spinnerStore.getSelectedItemPosition() > 0 && !storeList.isEmpty()) {
storeId = storeList.get(binding.spinnerStore.getSelectedItemPosition() - 1).getStoreId();
}
if (status.equals("All Statuses")) status = null;
if (species.equals("All Species")) species = null;
viewModel.getAllPets(0, 100, query, status, species, storeId, null, "petName").observe(getViewLifecycleOwner(), resource -> {
if (resource == null) return;
switch (resource.status) {
case LOADING:
binding.swipeRefreshPet.setRefreshing(true);
break;
case SUCCESS:
binding.swipeRefreshPet.setRefreshing(false);
if (resource.data != null) {
petList.clear();
petList.addAll(resource.data.getContent());
adapter.notifyDataSetChanged();
}
break;
case ERROR:
binding.swipeRefreshPet.setRefreshing(false);
Toast.makeText(getContext(), "Failed to load pets: " + resource.message, Toast.LENGTH_SHORT).show();
Log.e("PetFragment", "Error loading pets: " + resource.message);
break;
}
});
}
/**
* Initializes the RecyclerView.
*/
private void setupRecyclerView() {
adapter = new PetAdapter(petList, this);
adapter.setBaseUrl(baseUrl);
adapter.setToken(tokenManager.getToken());
binding.recyclerViewPets.setLayoutManager(new LinearLayoutManager(getContext()));
binding.recyclerViewPets.setAdapter(adapter);
@Override
public void onDestroyView() {
super.onDestroyView();
binding = null;
}
}

View File

@@ -9,21 +9,18 @@ import androidx.lifecycle.ViewModelProvider;
import androidx.navigation.fragment.NavHostFragment;
import androidx.recyclerview.widget.LinearLayoutManager;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Toast;
import com.example.petstoremobile.R;
import com.example.petstoremobile.adapters.ProductAdapter;
import com.example.petstoremobile.databinding.FragmentProductBinding;
import com.example.petstoremobile.dtos.CategoryDTO;
import com.example.petstoremobile.dtos.ProductDTO;
import com.example.petstoremobile.utils.Resource;
import com.example.petstoremobile.utils.SpinnerUtils;
import com.example.petstoremobile.utils.UIUtils;
import com.example.petstoremobile.viewmodels.ProductViewModel;
import com.example.petstoremobile.viewmodels.ProductListViewModel;
import java.util.ArrayList;
import java.util.List;
@@ -38,24 +35,17 @@ public class ProductFragment extends Fragment implements ProductAdapter.OnProduc
private FragmentProductBinding binding;
private List<ProductDTO> productList = new ArrayList<>();
private List<CategoryDTO> categoryList = new ArrayList<>();
private ProductAdapter adapter;
private ProductViewModel viewModel;
private ProductListViewModel viewModel;
@Inject @Named("baseUrl") String baseUrl;
/**
* Initializes the fragment and its associated ProductViewModel.
*/
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
viewModel = new ViewModelProvider(this).get(ProductViewModel.class);
viewModel = new ViewModelProvider(this).get(ProductListViewModel.class);
}
/**
* Sets up the fragment's UI components, including RecyclerView, search, and swipe-to-refresh.
*/
@Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
@@ -66,6 +56,7 @@ public class ProductFragment extends Fragment implements ProductAdapter.OnProduc
setupCategoryFilter();
setupSwipeRefresh();
setupFilterToggle();
observeViewModel();
binding.fabAddProduct.setOnClickListener(v -> openProductDetails(-1));
@@ -74,67 +65,67 @@ public class ProductFragment extends Fragment implements ProductAdapter.OnProduc
return binding.getRoot();
}
@Override
public void onDestroyView() {
super.onDestroyView();
binding = null;
private void observeViewModel() {
viewModel.getProducts().observe(getViewLifecycleOwner(), list -> {
productList.clear();
productList.addAll(list);
adapter.notifyDataSetChanged();
});
viewModel.getCategories().observe(getViewLifecycleOwner(), list -> {
SpinnerUtils.populateWhiteSpinner(requireContext(), binding.spinnerCategory, list,
CategoryDTO::getCategoryName, "All Categories", -1L, CategoryDTO::getCategoryId);
});
viewModel.getIsLoading().observe(getViewLifecycleOwner(), loading -> {
binding.swipeRefreshProduct.setRefreshing(loading);
});
}
/**
* Reloads data every time the fragment becomes visible.
*/
@Override
public void onResume() {
super.onResume();
loadProductData();
loadCategoryData();
viewModel.loadCategories();
}
/**
* Sets up the filter toggle button to show/hide the filter layout.
*/
private void setupFilterToggle() {
UIUtils.setupFilterToggle(binding.btnToggleFilter, binding.layoutFilter,
binding.etSearchProduct, binding.spinnerCategory);
}
/**
* Configures the search bar for triggering data load from backend.
*/
private void setupSearch() {
UIUtils.attachSearch(binding.etSearchProduct, this::loadProductData);
}
/**
* Configures the category filter spinner.
*/
private void setupCategoryFilter() {
SpinnerUtils.setupFilterSpinner(binding.spinnerCategory, this::loadProductData);
}
/**
* Fetches category data to populate the category filter.
*/
private void loadCategoryData() {
viewModel.getAllCategories(0, 100).observe(getViewLifecycleOwner(), resource -> {
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
categoryList = resource.data.getContent();
SpinnerUtils.populateWhiteSpinner(requireContext(), binding.spinnerCategory, categoryList,
CategoryDTO::getCategoryName, "All Categories", -1L, CategoryDTO::getCategoryId);
}
});
}
/**
* Sets up the SwipeRefreshLayout.
*/
private void setupSwipeRefresh() {
binding.swipeRefreshProduct.setOnRefreshListener(this::loadProductData);
}
/**
* Navigates to the product detail screen.
*/
private void loadProductData() {
String query = binding.etSearchProduct.getText().toString().trim();
if (query.isEmpty()) query = null;
Long categoryId = null;
List<CategoryDTO> categories = viewModel.getCategories().getValue();
if (binding.spinnerCategory.getSelectedItemPosition() > 0 && categories != null && !categories.isEmpty()) {
categoryId = categories.get(binding.spinnerCategory.getSelectedItemPosition() - 1).getCategoryId();
}
viewModel.loadProducts(query, categoryId);
}
private void setupRecyclerView() {
adapter = new ProductAdapter(productList, this);
adapter.setBaseUrl(baseUrl);
binding.recyclerViewProducts.setLayoutManager(new LinearLayoutManager(getContext()));
binding.recyclerViewProducts.setAdapter(adapter);
}
private void openProductDetails(int position) {
Bundle args = new Bundle();
if (position != -1) {
@@ -149,51 +140,9 @@ public class ProductFragment extends Fragment implements ProductAdapter.OnProduc
openProductDetails(position);
}
/**
* Fetches product data from the server with search query, category, and sorting.
*/
private void loadProductData() {
String query = binding.etSearchProduct.getText().toString().trim();
if (query.isEmpty()) query = null;
Long categoryId = null;
if (binding.spinnerCategory.getSelectedItemPosition() > 0 && !categoryList.isEmpty()) {
categoryId = categoryList.get(binding.spinnerCategory.getSelectedItemPosition() - 1).getCategoryId();
}
viewModel.getAllProducts(query, categoryId, 0, 100, "prodName").observe(getViewLifecycleOwner(), resource -> {
if (resource == null) return;
switch (resource.status) {
case LOADING:
binding.swipeRefreshProduct.setRefreshing(true);
break;
case SUCCESS:
binding.swipeRefreshProduct.setRefreshing(false);
if (resource.data != null) {
productList.clear();
productList.addAll(resource.data.getContent());
adapter.notifyDataSetChanged();
}
break;
case ERROR:
binding.swipeRefreshProduct.setRefreshing(false);
if (getContext() != null) {
Toast.makeText(getContext(), "Failed to load products: " + resource.message, Toast.LENGTH_SHORT).show();
}
Log.e("ProductFragment", "Error loading products: " + resource.message);
break;
}
});
}
/**
* Initializes the RecyclerView.
*/
private void setupRecyclerView() {
adapter = new ProductAdapter(productList, this);
adapter.setBaseUrl(baseUrl);
binding.recyclerViewProducts.setLayoutManager(new LinearLayoutManager(getContext()));
binding.recyclerViewProducts.setAdapter(adapter);
@Override
public void onDestroyView() {
super.onDestroyView();
binding = null;
}
}

View File

@@ -1,11 +1,9 @@
package com.example.petstoremobile.fragments.listfragments;
import android.os.Bundle;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -21,12 +19,9 @@ import com.example.petstoremobile.dtos.ProductDTO;
import com.example.petstoremobile.dtos.ProductSupplierDTO;
import com.example.petstoremobile.dtos.SupplierDTO;
import com.example.petstoremobile.utils.BulkDeleteHandler;
import com.example.petstoremobile.utils.Resource;
import com.example.petstoremobile.utils.SpinnerUtils;
import com.example.petstoremobile.utils.UIUtils;
import com.example.petstoremobile.viewmodels.ProductSupplierViewModel;
import com.example.petstoremobile.viewmodels.ProductViewModel;
import com.example.petstoremobile.viewmodels.SupplierViewModel;
import com.example.petstoremobile.viewmodels.ProductSupplierListViewModel;
import java.util.ArrayList;
import java.util.List;
@@ -39,29 +34,17 @@ public class ProductSupplierFragment extends Fragment
private FragmentProductSupplierBinding binding;
private List<ProductSupplierDTO> psList = new ArrayList<>();
private List<ProductDTO> productList = new ArrayList<>();
private List<SupplierDTO> supplierList = new ArrayList<>();
private ProductSupplierAdapter adapter;
private ProductSupplierViewModel viewModel;
private ProductViewModel productViewModel;
private SupplierViewModel supplierViewModel;
private ProductSupplierListViewModel viewModel;
private BulkDeleteHandler bulkDeleteHandler;
/**
* Initializes the fragment and its associated ViewModels.
*/
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
viewModel = new ViewModelProvider(this).get(ProductSupplierViewModel.class);
productViewModel = new ViewModelProvider(this).get(ProductViewModel.class);
supplierViewModel = new ViewModelProvider(this).get(SupplierViewModel.class);
viewModel = new ViewModelProvider(this).get(ProductSupplierListViewModel.class);
}
/**
* Sets up the fragment's UI components, including the RecyclerView, search, and swipe-to-refresh.
*/
@Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
@@ -74,6 +57,7 @@ public class ProductSupplierFragment extends Fragment
setupSwipeRefresh();
setupFilterToggle();
setupBulkDelete();
observeViewModel();
binding.fabAddPS.setOnClickListener(v -> openDetail(-1));
@@ -82,6 +66,28 @@ public class ProductSupplierFragment extends Fragment
return binding.getRoot();
}
private void observeViewModel() {
viewModel.getProductSuppliers().observe(getViewLifecycleOwner(), list -> {
psList.clear();
psList.addAll(list);
adapter.notifyDataSetChanged();
});
viewModel.getProducts().observe(getViewLifecycleOwner(), list -> {
SpinnerUtils.populateWhiteSpinner(requireContext(), binding.spinnerProduct, list,
ProductDTO::getProdName, "All Products", -1L, ProductDTO::getProdId);
});
viewModel.getSuppliers().observe(getViewLifecycleOwner(), list -> {
SpinnerUtils.populateWhiteSpinner(requireContext(), binding.spinnerSupplier, list,
SupplierDTO::getSupCompany, "All Suppliers", -1L, SupplierDTO::getSupId);
});
viewModel.getIsLoading().observe(getViewLifecycleOwner(), loading -> {
binding.swipeRefreshPS.setRefreshing(loading);
});
}
private void setupBulkDelete() {
bulkDeleteHandler = new BulkDeleteHandler(
this,
@@ -95,136 +101,65 @@ public class ProductSupplierFragment extends Fragment
);
}
@Override
public void onResume() {
super.onResume();
loadData();
viewModel.loadFilterData();
}
@Override
public void onDestroyView() {
super.onDestroyView();
binding = null;
}
/**
* Reloads data every time the fragment becomes visible.
*/
@Override
public void onResume() {
super.onResume();
loadData();
loadFilterData();
}
/**
* Sets up the filter toggle button to show/hide the filter layout.
*/
private void setupFilterToggle() {
UIUtils.setupFilterToggle(binding.btnToggleFilter, binding.layoutFilter, binding.etSearchPS,
binding.spinnerProduct, binding.spinnerSupplier);
}
/**
* Initializes the RecyclerView with a layout manager and adapter for product-supplier data.
*/
private void setupRecyclerView() {
adapter = new ProductSupplierAdapter(psList, this);
binding.recyclerViewPS.setLayoutManager(new LinearLayoutManager(getContext()));
binding.recyclerViewPS.setAdapter(adapter);
}
/**
* Configures the search bar for filtering.
*/
private void setupSearch() {
UIUtils.attachSearch(binding.etSearchPS, this::loadData);
}
/**
* Configures the product filter spinner.
*/
private void setupProductFilter() {
SpinnerUtils.setupFilterSpinner(binding.spinnerProduct, this::loadData);
}
/**
* Configures the supplier filter spinner.
*/
private void setupSupplierFilter() {
SpinnerUtils.setupFilterSpinner(binding.spinnerSupplier, this::loadData);
}
/**
* Fetches products and suppliers to populate the filters.
*/
private void loadFilterData() {
productViewModel.getAllProducts(null, null, 0, 100, "prodName").observe(getViewLifecycleOwner(), resource -> {
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
productList = resource.data.getContent();
SpinnerUtils.populateWhiteSpinner(requireContext(), binding.spinnerProduct, productList,
ProductDTO::getProdName, "All Products", -1L, ProductDTO::getProdId);
}
});
supplierViewModel.getAllSuppliers(0, 100, null, "supCompany").observe(getViewLifecycleOwner(), resource -> {
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
supplierList = resource.data.getContent();
SpinnerUtils.populateWhiteSpinner(requireContext(), binding.spinnerSupplier, supplierList,
SupplierDTO::getSupCompany, "All Suppliers", -1L, SupplierDTO::getSupId);
}
});
}
/**
* Sets up the SwipeRefreshLayout to allow manual reloading of product-supplier data.
*/
private void setupSwipeRefresh() {
binding.swipeRefreshPS.setOnRefreshListener(this::loadData);
}
/**
* Fetches product-supplier data from the server through the ViewModel with search query and filters.
*/
private void loadData() {
String query = binding.etSearchPS.getText().toString().trim();
if (query.isEmpty()) query = null;
Long productId = null;
if (binding.spinnerProduct.getSelectedItemPosition() > 0 && !productList.isEmpty()) {
productId = productList.get(binding.spinnerProduct.getSelectedItemPosition() - 1).getProdId();
List<ProductDTO> products = viewModel.getProducts().getValue();
if (binding.spinnerProduct.getSelectedItemPosition() > 0 && products != null && !products.isEmpty()) {
productId = products.get(binding.spinnerProduct.getSelectedItemPosition() - 1).getProdId();
}
Long supplierId = null;
if (binding.spinnerSupplier.getSelectedItemPosition() > 0 && !supplierList.isEmpty()) {
supplierId = supplierList.get(binding.spinnerSupplier.getSelectedItemPosition() - 1).getSupId();
List<SupplierDTO> suppliers = viewModel.getSuppliers().getValue();
if (binding.spinnerSupplier.getSelectedItemPosition() > 0 && suppliers != null && !suppliers.isEmpty()) {
supplierId = suppliers.get(binding.spinnerSupplier.getSelectedItemPosition() - 1).getSupId();
}
viewModel.getAllProductSuppliers(0, 100, query, productId, supplierId, "productName").observe(getViewLifecycleOwner(), resource -> {
if (resource == null) return;
// Check the status to see if the resource is loaded and display the data
switch (resource.status) {
case LOADING:
// Show loading indicator
binding.swipeRefreshPS.setRefreshing(true);
break;
case SUCCESS:
// Hide loading indicator and display data
binding.swipeRefreshPS.setRefreshing(false);
if (resource.data != null) {
psList.clear();
psList.addAll(resource.data.getContent());
adapter.notifyDataSetChanged();
}
break;
case ERROR:
// Hide loading indicator and toast error message
binding.swipeRefreshPS.setRefreshing(false);
Toast.makeText(getContext(), "Failed to load: " + resource.message, Toast.LENGTH_SHORT).show();
Log.e("PSFragment", "Error loading: " + resource.message);
break;
}
});
viewModel.loadProductSuppliers(query, productId, supplierId);
}
/**
* Navigates to the product-supplier detail screen for a specific item or to add a new record.
*/
private void openDetail(int position) {
Bundle args = new Bundle();
if (position != -1) {
@@ -235,9 +170,6 @@ public class ProductSupplierFragment extends Fragment
NavHostFragment.findNavController(this).navigate(R.id.nav_product_supplier_detail, args);
}
/**
* Handles item click in the product-supplier list.
*/
@Override
public void onProductSupplierClick(int position) { openDetail(position); }

View File

@@ -1,11 +1,9 @@
package com.example.petstoremobile.fragments.listfragments;
import android.os.Bundle;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -19,11 +17,9 @@ import com.example.petstoremobile.adapters.PurchaseOrderAdapter;
import com.example.petstoremobile.databinding.FragmentPurchaseOrderBinding;
import com.example.petstoremobile.dtos.PurchaseOrderDTO;
import com.example.petstoremobile.dtos.StoreDTO;
import com.example.petstoremobile.utils.Resource;
import com.example.petstoremobile.utils.SpinnerUtils;
import com.example.petstoremobile.utils.UIUtils;
import com.example.petstoremobile.viewmodels.PurchaseOrderViewModel;
import com.example.petstoremobile.viewmodels.StoreViewModel;
import com.example.petstoremobile.viewmodels.PurchaseOrderListViewModel;
import java.util.ArrayList;
import java.util.List;
@@ -36,24 +32,15 @@ public class PurchaseOrderFragment extends Fragment
private FragmentPurchaseOrderBinding binding;
private List<PurchaseOrderDTO> poList = new ArrayList<>();
private List<StoreDTO> storeList = new ArrayList<>();
private PurchaseOrderAdapter adapter;
private PurchaseOrderViewModel viewModel;
private StoreViewModel storeViewModel;
private PurchaseOrderListViewModel viewModel;
/**
* Initializes the fragment and its associated ViewModels.
*/
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
viewModel = new ViewModelProvider(this).get(PurchaseOrderViewModel.class);
storeViewModel = new ViewModelProvider(this).get(StoreViewModel.class);
viewModel = new ViewModelProvider(this).get(PurchaseOrderListViewModel.class);
}
/**
* Sets up the fragment's UI components, including RecyclerView, filters, and swipe-to-refresh.
*/
@Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
@@ -64,121 +51,72 @@ public class PurchaseOrderFragment extends Fragment
setupStoreFilter();
setupSwipeRefresh();
setupFilterToggle();
observeViewModel();
UIUtils.setupHamburgerMenu(binding.btnHamburgerPO, this);
return binding.getRoot();
}
@Override
public void onDestroyView() {
super.onDestroyView();
binding = null;
private void observeViewModel() {
viewModel.getPurchaseOrders().observe(getViewLifecycleOwner(), list -> {
poList.clear();
poList.addAll(list);
adapter.notifyDataSetChanged();
});
viewModel.getStores().observe(getViewLifecycleOwner(), list -> {
SpinnerUtils.populateWhiteSpinner(requireContext(), binding.spinnerStore, list,
StoreDTO::getStoreName, "All Stores", -1L, StoreDTO::getStoreId);
});
viewModel.getIsLoading().observe(getViewLifecycleOwner(), loading -> {
binding.swipeRefreshPO.setRefreshing(loading);
});
}
/**
* Reloads data every time the fragment becomes visible.
*/
@Override
public void onResume() {
super.onResume();
loadData();
loadStoreData();
viewModel.loadStores();
}
/**
* Sets up the filter toggle button to show/hide the filter layout.
*/
private void setupFilterToggle() {
UIUtils.setupFilterToggle(binding.btnToggleFilter, binding.layoutFilter, binding.etSearchPO, binding.spinnerStore);
}
/**
* Configures the search bar for filtering.
*/
private void setupSearch() {
UIUtils.attachSearch(binding.etSearchPO, this::loadData);
}
/**
* Configures the store filter spinner.
*/
private void setupStoreFilter() {
SpinnerUtils.setupFilterSpinner(binding.spinnerStore, this::loadData);
}
/**
* Fetches store data to populate the store filter.
*/
private void loadStoreData() {
storeViewModel.getAllStores(0, 100).observe(getViewLifecycleOwner(), resource -> {
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
storeList = resource.data.getContent();
SpinnerUtils.populateWhiteSpinner(requireContext(), binding.spinnerStore, storeList,
StoreDTO::getStoreName, "All Stores", -1L, StoreDTO::getStoreId);
}
});
}
/**
* Initializes the RecyclerView with a layout manager and adapter.
*/
private void setupRecyclerView() {
adapter = new PurchaseOrderAdapter(poList, this);
binding.recyclerViewPO.setLayoutManager(new LinearLayoutManager(getContext()));
binding.recyclerViewPO.setAdapter(adapter);
}
/**
* Sets up the SwipeRefreshLayout to allow manual reloading of purchase order data.
*/
private void setupSwipeRefresh() {
binding.swipeRefreshPO.setOnRefreshListener(this::loadData);
}
/**
* Fetches purchase order data from the server with active filters and updates the UI.
*/
private void loadData() {
String query = binding.etSearchPO != null ? binding.etSearchPO.getText().toString().trim() : "";
if (query.isEmpty()) query = null;
Long storeId = null;
if (binding.spinnerStore.getSelectedItemPosition() > 0 && !storeList.isEmpty()) {
storeId = storeList.get(binding.spinnerStore.getSelectedItemPosition() - 1).getStoreId();
List<StoreDTO> stores = viewModel.getStores().getValue();
if (binding.spinnerStore.getSelectedItemPosition() > 0 && stores != null && !stores.isEmpty()) {
storeId = stores.get(binding.spinnerStore.getSelectedItemPosition() - 1).getStoreId();
}
viewModel.getAllPurchaseOrders(0, 100, query, storeId, "purchaseOrderId,desc").observe(getViewLifecycleOwner(), resource -> {
if (resource == null) return;
// Check the status to see if the resource is loaded and display the data
switch (resource.status) {
case LOADING:
// Show loading indicator
binding.swipeRefreshPO.setRefreshing(true);
break;
case SUCCESS:
// Hide loading indicator and display data
binding.swipeRefreshPO.setRefreshing(false);
if (resource.data != null) {
poList.clear();
poList.addAll(resource.data.getContent());
adapter.notifyDataSetChanged();
}
break;
case ERROR:
// Hide loading indicator and toast error message
binding.swipeRefreshPO.setRefreshing(false);
Toast.makeText(getContext(), "Failed to load purchase orders: " + resource.message, Toast.LENGTH_SHORT).show();
Log.e("POFragment", "Error loading purchase orders: " + resource.message);
break;
}
});
viewModel.loadPurchaseOrders(query, storeId);
}
/**
* Navigates to the purchase order detail screen for a specific record.
*/
private void openDetail(int position) {
Bundle args = new Bundle();
PurchaseOrderDTO po = poList.get(position);
@@ -186,11 +124,14 @@ public class PurchaseOrderFragment extends Fragment
NavHostFragment.findNavController(this).navigate(R.id.nav_purchase_order_detail, args);
}
/**
* Handles item click in the purchase order list.
*/
@Override
public void onPurchaseOrderClick(int position) {
openDetail(position);
}
@Override
public void onDestroyView() {
super.onDestroyView();
binding = null;
}
}

View File

@@ -8,7 +8,6 @@ import androidx.lifecycle.ViewModelProvider;
import androidx.navigation.fragment.NavHostFragment;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
@@ -19,11 +18,9 @@ import com.example.petstoremobile.adapters.SaleAdapter;
import com.example.petstoremobile.databinding.FragmentSaleBinding;
import com.example.petstoremobile.dtos.SaleDTO;
import com.example.petstoremobile.dtos.StoreDTO;
import com.example.petstoremobile.utils.Resource;
import com.example.petstoremobile.utils.SpinnerUtils;
import com.example.petstoremobile.utils.UIUtils;
import com.example.petstoremobile.viewmodels.SaleViewModel;
import com.example.petstoremobile.viewmodels.StoreViewModel;
import com.example.petstoremobile.viewmodels.SaleListViewModel;
import java.util.ArrayList;
import java.util.List;
@@ -33,20 +30,10 @@ import dagger.hilt.android.AndroidEntryPoint;
@AndroidEntryPoint
public class SaleFragment extends Fragment implements SaleAdapter.OnSaleClickListener {
private static final String TAG = "SaleFragment";
private static final int PAGE_SIZE = 200;
private FragmentSaleBinding binding;
private final List<SaleDTO> saleList = new ArrayList<>();
private final List<StoreDTO> storeList = new ArrayList<>();
private SaleAdapter adapter;
private SaleViewModel saleViewModel;
private StoreViewModel storeViewModel;
// Pagination
private int currentPage = 0;
private boolean isLastPage = false;
private boolean isLoading = false;
private SaleListViewModel viewModel;
@Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
@@ -58,8 +45,7 @@ public class SaleFragment extends Fragment implements SaleAdapter.OnSaleClickLis
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
saleViewModel = new ViewModelProvider(this).get(SaleViewModel.class);
storeViewModel = new ViewModelProvider(this).get(StoreViewModel.class);
viewModel = new ViewModelProvider(this).get(SaleListViewModel.class);
setupRecyclerView();
setupSearch();
@@ -67,6 +53,8 @@ public class SaleFragment extends Fragment implements SaleAdapter.OnSaleClickLis
setupPaymentMethodFilter();
setupSwipeRefresh();
setupFilterToggle();
observeViewModel();
loadSales(true);
UIUtils.setupHamburgerMenu(binding.btnHamburger, this);
@@ -78,10 +66,27 @@ public class SaleFragment extends Fragment implements SaleAdapter.OnSaleClickLis
NavHostFragment.findNavController(this).navigate(R.id.nav_refund));
}
private void observeViewModel() {
viewModel.getSales().observe(getViewLifecycleOwner(), list -> {
saleList.clear();
saleList.addAll(list);
adapter.notifyDataSetChanged();
});
viewModel.getStores().observe(getViewLifecycleOwner(), list -> {
SpinnerUtils.populateWhiteSpinner(requireContext(), binding.spinnerStore, list,
StoreDTO::getStoreName, "Stores", null, StoreDTO::getStoreId);
});
viewModel.getIsLoading().observe(getViewLifecycleOwner(), loading -> {
binding.swipeRefreshSale.setRefreshing(loading);
});
}
@Override
public void onResume() {
super.onResume();
loadStoreData();
viewModel.loadStores();
}
private void setupFilterToggle() {
@@ -93,28 +98,11 @@ public class SaleFragment extends Fragment implements SaleAdapter.OnSaleClickLis
SpinnerUtils.setupFilterSpinner(binding.spinnerStore, () -> loadSales(true));
}
private void loadStoreData() {
storeViewModel.getAllStores(0, 100).observe(getViewLifecycleOwner(), resource -> {
if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) {
storeList.clear();
storeList.addAll(resource.data.getContent());
SpinnerUtils.populateWhiteSpinner(requireContext(), binding.spinnerStore, storeList,
StoreDTO::getStoreName, "Stores", null, StoreDTO::getStoreId);
}
});
}
private void setupPaymentMethodFilter() {
String[] paymentMethods = {"Payments", "Cash", "Card"};
SpinnerUtils.setupStringFilterSpinner(requireContext(), binding.spinnerPaymentMethod, paymentMethods, () -> loadSales(true));
}
@Override
public void onDestroyView() {
super.onDestroyView();
binding = null;
}
private void setupRecyclerView() {
adapter = new SaleAdapter(saleList, this);
binding.recyclerViewSales.setLayoutManager(new LinearLayoutManager(getContext()));
@@ -129,7 +117,8 @@ public class SaleFragment extends Fragment implements SaleAdapter.OnSaleClickLis
int visible = lm.getChildCount();
int total = lm.getItemCount();
int firstVis = lm.findFirstVisibleItemPosition();
if (!isLoading && !isLastPage && (visible + firstVis) >= total - 3) {
Boolean isLoading = viewModel.getIsLoading().getValue();
if ((isLoading == null || !isLoading) && !viewModel.isLastPage() && (visible + firstVis) >= total - 3) {
loadSales(false);
}
}
@@ -146,13 +135,6 @@ public class SaleFragment extends Fragment implements SaleAdapter.OnSaleClickLis
}
private void loadSales(boolean reset) {
if (isLoading) return;
if (reset) {
currentPage = 0;
isLastPage = false;
}
String query = binding.etSearchSale != null ? binding.etSearchSale.getText().toString().trim() : "";
if (query.isEmpty()) query = null;
@@ -162,39 +144,12 @@ public class SaleFragment extends Fragment implements SaleAdapter.OnSaleClickLis
}
Long storeId = null;
if (binding.spinnerStore.getSelectedItemPosition() > 0 && !storeList.isEmpty()) {
storeId = storeList.get(binding.spinnerStore.getSelectedItemPosition() - 1).getStoreId();
List<StoreDTO> stores = viewModel.getStores().getValue();
if (binding.spinnerStore.getSelectedItemPosition() > 0 && stores != null && !stores.isEmpty()) {
storeId = stores.get(binding.spinnerStore.getSelectedItemPosition() - 1).getStoreId();
}
saleViewModel.getAllSales(currentPage, PAGE_SIZE, query, paymentMethod, storeId, "saleDate,desc").observe(getViewLifecycleOwner(), resource -> {
if (resource == null) return;
switch (resource.status) {
case LOADING:
isLoading = true;
binding.swipeRefreshSale.setRefreshing(true);
break;
case SUCCESS:
isLoading = false;
binding.swipeRefreshSale.setRefreshing(false);
if (resource.data != null) {
if (reset) saleList.clear();
saleList.addAll(resource.data.getContent());
adapter.notifyDataSetChanged();
isLastPage = resource.data.isLast();
if (!isLastPage) currentPage++;
}
break;
case ERROR:
isLoading = false;
binding.swipeRefreshSale.setRefreshing(false);
Log.e(TAG, "Error loading sales: " + resource.message);
if (getContext() != null) {
Toast.makeText(getContext(), "Failed to load sales: " + resource.message, Toast.LENGTH_SHORT).show();
}
break;
}
});
viewModel.loadSales(reset, query, paymentMethod, storeId);
}
@Override
@@ -210,4 +165,10 @@ public class SaleFragment extends Fragment implements SaleAdapter.OnSaleClickLis
}
NavHostFragment.findNavController(this).navigate(R.id.nav_sale_detail, args);
}
@Override
public void onDestroyView() {
super.onDestroyView();
binding = null;
}
}

View File

@@ -1,7 +1,6 @@
package com.example.petstoremobile.fragments.listfragments;
import android.os.Bundle;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
@@ -21,7 +20,7 @@ import com.example.petstoremobile.databinding.FragmentServiceBinding;
import com.example.petstoremobile.dtos.ServiceDTO;
import com.example.petstoremobile.utils.BulkDeleteHandler;
import com.example.petstoremobile.utils.UIUtils;
import com.example.petstoremobile.viewmodels.ServiceViewModel;
import com.example.petstoremobile.viewmodels.ServiceListViewModel;
import java.util.ArrayList;
import java.util.List;
@@ -34,32 +33,18 @@ import dagger.hilt.android.AndroidEntryPoint;
@AndroidEntryPoint
public class ServiceFragment extends Fragment implements ServiceAdapter.OnServiceClickListener {
private static final String TAG = "ServiceFragment";
private static final int PAGE_SIZE = 20;
private FragmentServiceBinding binding;
private final List<ServiceDTO> serviceList = new ArrayList<>();
private ServiceAdapter adapter;
private ServiceViewModel viewModel;
private ServiceListViewModel viewModel;
private BulkDeleteHandler bulkDeleteHandler;
// Pagination
private int currentPage = 0;
private boolean isLastPage = false;
private boolean isLoading = false;
/**
* Initializes the fragment and its associated ViewModel.
*/
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
viewModel = new ViewModelProvider(this).get(ServiceViewModel.class);
viewModel = new ViewModelProvider(this).get(ServiceListViewModel.class);
}
/**
* Sets up the fragment's UI components, including RecyclerView, search, and swipe-to-refresh.
*/
@Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
@@ -70,15 +55,27 @@ public class ServiceFragment extends Fragment implements ServiceAdapter.OnServic
setupSwipeRefresh();
setupFilterToggle();
setupBulkDelete();
observeViewModel();
loadServices(true);
binding.fabAddService.setOnClickListener(v -> openDetail(null));
UIUtils.setupHamburgerMenu(binding.btnHamburger, this);
return binding.getRoot();
}
private void observeViewModel() {
viewModel.getServices().observe(getViewLifecycleOwner(), list -> {
serviceList.clear();
serviceList.addAll(list);
adapter.notifyDataSetChanged();
});
viewModel.getIsLoading().observe(getViewLifecycleOwner(), loading -> {
binding.swipeRefreshService.setRefreshing(loading);
});
}
private void setupBulkDelete() {
bulkDeleteHandler = new BulkDeleteHandler(
this,
@@ -98,23 +95,14 @@ public class ServiceFragment extends Fragment implements ServiceAdapter.OnServic
binding = null;
}
/**
* Sets up the filter toggle button to show/hide the filter layout.
*/
private void setupFilterToggle() {
UIUtils.setupFilterToggle(binding.btnToggleFilter, binding.layoutFilter, binding.etSearchService);
}
/**
* Sets up the search bar for filtering.
*/
private void setupSearch() {
UIUtils.attachSearch(binding.etSearchService, () -> loadServices(true));
}
/**
* Initializes the RecyclerView with a layout manager and adapter.
*/
private void setupRecyclerView() {
adapter = new ServiceAdapter(serviceList, this);
binding.recyclerViewServices.setLayoutManager(new LinearLayoutManager(getContext()));
@@ -129,66 +117,24 @@ public class ServiceFragment extends Fragment implements ServiceAdapter.OnServic
int visible = lm.getChildCount();
int total = lm.getItemCount();
int firstVis = lm.findFirstVisibleItemPosition();
if (!isLoading && !isLastPage && (visible + firstVis) >= total - 3) {
Boolean isLoading = viewModel.getIsLoading().getValue();
if ((isLoading == null || !isLoading) && !viewModel.isLastPage() && (visible + firstVis) >= total - 3) {
loadServices(false);
}
}
});
}
/**
* Sets up the SwipeRefreshLayout.
*/
private void setupSwipeRefresh() {
binding.swipeRefreshService.setOnRefreshListener(() -> loadServices(true));
}
/**
* Fetches a page of services from the API.
*/
private void loadServices(boolean reset) {
if (isLoading) return;
if (reset) {
currentPage = 0;
isLastPage = false;
}
String query = binding.etSearchService.getText().toString().trim();
if (query.isEmpty()) query = null;
viewModel.getAllServices(currentPage, PAGE_SIZE, query, "serviceName").observe(getViewLifecycleOwner(), resource -> {
if (resource == null) return;
switch (resource.status) {
case LOADING:
isLoading = true;
binding.swipeRefreshService.setRefreshing(true);
break;
case SUCCESS:
isLoading = false;
binding.swipeRefreshService.setRefreshing(false);
if (resource.data != null) {
if (reset) serviceList.clear();
serviceList.addAll(resource.data.getContent());
adapter.notifyDataSetChanged();
isLastPage = resource.data.isLast();
if (!isLastPage) currentPage++;
}
break;
case ERROR:
isLoading = false;
binding.swipeRefreshService.setRefreshing(false);
Log.e(TAG, "Error: " + resource.message);
Toast.makeText(getContext(), "Failed to load services: " + resource.message, Toast.LENGTH_SHORT).show();
break;
}
});
viewModel.loadServices(reset, query);
}
/**
* Navigates to the service detail screen.
*/
private void openDetail(ServiceDTO service) {
Bundle args = new Bundle();
if (service != null) {

View File

@@ -1,7 +1,6 @@
package com.example.petstoremobile.fragments.listfragments;
import android.os.Bundle;
import android.util.Log;
import android.view.*;
import android.widget.*;
import androidx.annotation.NonNull;
@@ -14,7 +13,7 @@ import com.example.petstoremobile.adapters.EmployeeAdapter;
import com.example.petstoremobile.databinding.FragmentStaffBinding;
import com.example.petstoremobile.dtos.EmployeeDTO;
import com.example.petstoremobile.utils.UIUtils;
import com.example.petstoremobile.viewmodels.EmployeeViewModel;
import com.example.petstoremobile.viewmodels.StaffListViewModel;
import dagger.hilt.android.AndroidEntryPoint;
import java.util.*;
@@ -22,21 +21,22 @@ import java.util.*;
public class StaffFragment extends Fragment implements EmployeeAdapter.OnEmployeeClickListener {
private FragmentStaffBinding binding;
private EmployeeViewModel employeeViewModel;
private List<EmployeeDTO> employeeList = new ArrayList<>();
private List<EmployeeDTO> filteredList = new ArrayList<>();
private StaffListViewModel viewModel;
private List<EmployeeDTO> staffList = new ArrayList<>();
private EmployeeAdapter adapter;
@Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
binding = FragmentStaffBinding.inflate(inflater, container, false);
employeeViewModel = new ViewModelProvider(this).get(EmployeeViewModel.class);
viewModel = new ViewModelProvider(this).get(StaffListViewModel.class);
setupRecyclerView();
setupSearch();
setupSwipeRefresh();
loadStaff();
observeViewModel();
viewModel.loadStaff();
binding.fabAddStaff.setOnClickListener(v -> openDetail(-1));
@@ -46,70 +46,36 @@ public class StaffFragment extends Fragment implements EmployeeAdapter.OnEmploye
return binding.getRoot();
}
private void observeViewModel() {
viewModel.getFilteredEmployees().observe(getViewLifecycleOwner(), list -> {
staffList.clear();
staffList.addAll(list);
adapter.notifyDataSetChanged();
});
viewModel.getIsLoading().observe(getViewLifecycleOwner(), loading -> {
binding.swipeRefreshStaff.setRefreshing(loading);
});
}
private void setupRecyclerView() {
adapter = new EmployeeAdapter(filteredList, this);
adapter = new EmployeeAdapter(staffList, this);
binding.recyclerViewStaff.setLayoutManager(new LinearLayoutManager(getContext()));
binding.recyclerViewStaff.setAdapter(adapter);
}
private void setupSearch() {
UIUtils.attachSearch(binding.etSearchStaff, () -> filter(binding.etSearchStaff.getText().toString()));
UIUtils.attachSearch(binding.etSearchStaff, () -> viewModel.filter(binding.etSearchStaff.getText().toString()));
}
private void setupSwipeRefresh() {
binding.swipeRefreshStaff.setOnRefreshListener(this::loadStaff);
}
private void filter(String query) {
filteredList.clear();
if (query.isEmpty()) {
filteredList.addAll(employeeList);
} else {
String lower = query.toLowerCase();
for (EmployeeDTO e : employeeList) {
if ((e.getFullName() != null && e.getFullName().toLowerCase().contains(lower))
|| (e.getUsername() != null && e.getUsername().toLowerCase().contains(lower))
|| (e.getEmail() != null && e.getEmail().toLowerCase().contains(lower))
|| (e.getPhone() != null && e.getPhone().toLowerCase().contains(lower))) {
filteredList.add(e);
}
}
}
adapter.notifyDataSetChanged();
}
private void loadStaff() {
binding.swipeRefreshStaff.setRefreshing(true);
employeeViewModel.getAllEmployees(0, 100).observe(getViewLifecycleOwner(), resource -> {
if (resource != null) {
switch (resource.status) {
case SUCCESS:
binding.swipeRefreshStaff.setRefreshing(false);
if (resource.data != null) {
employeeList.clear();
employeeList.addAll(resource.data.getContent());
filter(binding != null ? binding.etSearchStaff.getText().toString() : "");
}
break;
case ERROR:
binding.swipeRefreshStaff.setRefreshing(false);
if (getContext() != null) {
Toast.makeText(getContext(), resource.message != null ? resource.message : "Failed to load staff",
Toast.LENGTH_SHORT).show();
}
break;
case LOADING:
binding.swipeRefreshStaff.setRefreshing(true);
break;
}
}
});
binding.swipeRefreshStaff.setOnRefreshListener(viewModel::loadStaff);
}
private void openDetail(int position) {
Bundle args = new Bundle();
if (position != -1) {
EmployeeDTO e = filteredList.get(position);
EmployeeDTO e = staffList.get(position);
args.putLong("employeeId", e.getEmployeeId());
args.putString("username", e.getUsername() != null ? e.getUsername() : "");
args.putString("firstName", e.getFirstName() != null ? e.getFirstName() : "");

View File

@@ -9,20 +9,17 @@ import androidx.lifecycle.ViewModelProvider;
import androidx.navigation.fragment.NavHostFragment;
import androidx.recyclerview.widget.LinearLayoutManager;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Toast;
import com.example.petstoremobile.R;
import com.example.petstoremobile.adapters.SupplierAdapter;
import com.example.petstoremobile.databinding.FragmentSupplierBinding;
import com.example.petstoremobile.dtos.SupplierDTO;
import com.example.petstoremobile.utils.BulkDeleteHandler;
import com.example.petstoremobile.utils.Resource;
import com.example.petstoremobile.utils.UIUtils;
import com.example.petstoremobile.viewmodels.SupplierViewModel;
import com.example.petstoremobile.viewmodels.SupplierListViewModel;
import java.util.ArrayList;
import java.util.List;
@@ -35,21 +32,15 @@ public class SupplierFragment extends Fragment implements SupplierAdapter.OnSupp
private FragmentSupplierBinding binding;
private List<SupplierDTO> supplierList = new ArrayList<>();
private SupplierAdapter adapter;
private SupplierViewModel viewModel;
private SupplierListViewModel viewModel;
private BulkDeleteHandler bulkDeleteHandler;
/**
* Initializes the fragment and its associated SupplierViewModel.
*/
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
viewModel = new ViewModelProvider(this).get(SupplierViewModel.class);
viewModel = new ViewModelProvider(this).get(SupplierListViewModel.class);
}
/**
* Sets up the fragment's UI components, including RecyclerView, search, and swipe-to-refresh.
*/
@Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
@@ -60,9 +51,10 @@ public class SupplierFragment extends Fragment implements SupplierAdapter.OnSupp
setupSwipeRefresh();
setupFilterToggle();
setupBulkDelete();
observeViewModel();
loadSupplierData();
//Add button to opens the add dialog
binding.fabAddSupplier.setOnClickListener(v -> openSupplierDetails(-1));
UIUtils.setupHamburgerMenu(binding.btnHamburger, this);
@@ -70,6 +62,18 @@ public class SupplierFragment extends Fragment implements SupplierAdapter.OnSupp
return binding.getRoot();
}
private void observeViewModel() {
viewModel.getSuppliers().observe(getViewLifecycleOwner(), list -> {
supplierList.clear();
supplierList.addAll(list);
adapter.notifyDataSetChanged();
});
viewModel.getIsLoading().observe(getViewLifecycleOwner(), loading -> {
binding.swipeRefreshSupplier.setRefreshing(loading);
});
}
private void setupBulkDelete() {
bulkDeleteHandler = new BulkDeleteHandler(
this,
@@ -89,47 +93,27 @@ public class SupplierFragment extends Fragment implements SupplierAdapter.OnSupp
binding = null;
}
/**
* Sets up the filter toggle button to show/hide the filter layout.
*/
private void setupFilterToggle() {
UIUtils.setupFilterToggle(binding.btnToggleFilter, binding.layoutFilter, binding.etSearchSupplier);
}
/**
* Configures the search bar for filtering.
*/
private void setupSearch() {
UIUtils.attachSearch(binding.etSearchSupplier, this::loadSupplierData);
}
/**
* Sets up the SwipeRefreshLayout to allow manual reloading of supplier data.
*/
private void setupSwipeRefresh() {
binding.swipeRefreshSupplier.setOnRefreshListener(this::loadSupplierData);
}
/**
* Navigates to the supplier detail screen for editing an existing record or adding a new one.
*/
private void openSupplierDetails(int position) {
//Make a bundle to pass data to the detail fragment
Bundle args = new Bundle();
//if editing a supplier, add the supplier id to the bundle
if (position != -1) {
SupplierDTO supplier = supplierList.get(position);
args.putLong("supId", supplier.getSupId());
}
NavHostFragment.findNavController(this).navigate(R.id.nav_supplier_detail, args);
}
/**
* Handles item click in the supplier list.
*/
@Override
public void onSupplierClick(int position) {
openSupplierDetails(position);
@@ -142,47 +126,12 @@ public class SupplierFragment extends Fragment implements SupplierAdapter.OnSupp
}
}
/**
* Fetches all supplier data from the server through the ViewModel and updates the UI.
*/
private void loadSupplierData() {
String query = binding.etSearchSupplier != null ? binding.etSearchSupplier.getText().toString().trim() : "";
if (query.isEmpty()) query = null;
//Load suppliers from the backend with query and default sort
viewModel.getAllSuppliers(0, 100, query, "supCompany").observe(getViewLifecycleOwner(), resource -> {
if (resource == null) return;
// Check the status to see if the resource is loaded and display the data
switch (resource.status) {
case LOADING:
// Show loading indicator
binding.swipeRefreshSupplier.setRefreshing(true);
break;
case SUCCESS:
// Hide loading indicator and display data
binding.swipeRefreshSupplier.setRefreshing(false);
if (resource.data != null) {
supplierList.clear();
supplierList.addAll(resource.data.getContent());
adapter.notifyDataSetChanged();
}
break;
case ERROR:
// Hide loading indicator and toast error message
binding.swipeRefreshSupplier.setRefreshing(false);
if (getContext() != null) {
Toast.makeText(getContext(), "Failed to load suppliers: " + resource.message, Toast.LENGTH_SHORT).show();
}
Log.e("SupplierFragment", "Error loading suppliers: " + resource.message);
break;
}
});
viewModel.loadSuppliers(query);
}
/**
* Initializes the RecyclerView with a layout manager and adapter for displaying suppliers.
*/
private void setupRecyclerView() {
adapter = new SupplierAdapter(supplierList, this);
binding.recyclerViewSuppliers.setLayoutManager(new LinearLayoutManager(getContext()));

View File

@@ -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) {

View File

@@ -0,0 +1,83 @@
package com.example.petstoremobile.viewmodels;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.ViewModel;
import com.example.petstoremobile.dtos.AdoptionDTO;
import com.example.petstoremobile.dtos.BulkDeleteRequest;
import com.example.petstoremobile.dtos.StoreDTO;
import com.example.petstoremobile.repositories.AdoptionRepository;
import com.example.petstoremobile.repositories.StoreRepository;
import com.example.petstoremobile.utils.Resource;
import java.util.ArrayList;
import java.util.List;
import javax.inject.Inject;
import dagger.hilt.android.lifecycle.HiltViewModel;
@HiltViewModel
public class AdoptionListViewModel extends ViewModel {
private final AdoptionRepository adoptionRepository;
private final StoreRepository storeRepository;
private final MutableLiveData<List<AdoptionDTO>> adoptions = new MutableLiveData<>(new ArrayList<>());
private final MutableLiveData<List<StoreDTO>> stores = new MutableLiveData<>(new ArrayList<>());
private final MutableLiveData<Boolean> isLoading = new MutableLiveData<>(false);
private int currentPage = 0;
private boolean isLastPage = false;
private static final int PAGE_SIZE = 20;
@Inject
public AdoptionListViewModel(AdoptionRepository adoptionRepository, StoreRepository storeRepository) {
this.adoptionRepository = adoptionRepository;
this.storeRepository = storeRepository;
}
public LiveData<List<AdoptionDTO>> getAdoptions() { return adoptions; }
public LiveData<List<StoreDTO>> getStores() { return stores; }
public LiveData<Boolean> getIsLoading() { return isLoading; }
public boolean isLastPage() { return isLastPage; }
public void loadAdoptions(boolean reset, String query, String status, Long storeId) {
if (isLoading.getValue() != null && isLoading.getValue() && !reset) return;
if (reset) {
currentPage = 0;
isLastPage = false;
}
if ("All Statuses".equals(status)) status = null;
isLoading.setValue(true);
adoptionRepository.getAllAdoptions(currentPage, PAGE_SIZE, query, status, storeId, "adoptionDate,desc").observeForever(resource -> {
if (resource != null) {
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
List<AdoptionDTO> currentList = reset ? new ArrayList<>() : new ArrayList<>(adoptions.getValue());
currentList.addAll(resource.data.getContent());
adoptions.setValue(currentList);
isLastPage = resource.data.isLast();
if (!isLastPage) currentPage++;
isLoading.setValue(false);
} else if (resource.status == Resource.Status.ERROR) {
isLoading.setValue(false);
}
}
});
}
public void loadStores() {
storeRepository.getAllStores(0, 100).observeForever(resource -> {
if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) {
stores.setValue(resource.data.getContent());
}
});
}
public LiveData<Resource<Void>> bulkDeleteAdoptions(List<String> ids) {
return adoptionRepository.bulkDeleteAdoptions(new BulkDeleteRequest(ids));
}
}

View File

@@ -0,0 +1,163 @@
package com.example.petstoremobile.viewmodels;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.ViewModel;
import com.example.petstoremobile.dtos.SaleDTO;
import com.example.petstoremobile.repositories.SaleRepository;
import com.example.petstoremobile.utils.Resource;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import javax.inject.Inject;
import dagger.hilt.android.lifecycle.HiltViewModel;
@HiltViewModel
public class AnalyticsViewModel extends ViewModel {
private final SaleRepository saleRepository;
private final MutableLiveData<AnalyticsData> analyticsData = new MutableLiveData<>();
private final MutableLiveData<Boolean> isLoading = new MutableLiveData<>(false);
private final MutableLiveData<String> errorMessage = new MutableLiveData<>();
@Inject
public AnalyticsViewModel(SaleRepository saleRepository) {
this.saleRepository = saleRepository;
}
public LiveData<AnalyticsData> getAnalyticsData() { return analyticsData; }
public LiveData<Boolean> getIsLoading() { return isLoading; }
public LiveData<String> getErrorMessage() { return errorMessage; }
public void loadAnalytics() {
isLoading.setValue(true);
errorMessage.setValue(null);
saleRepository.getAllSales(0, 1000, null, null, null, "saleDate,desc").observeForever(resource -> {
if (resource != null) {
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
computeAnalytics(resource.data.getContent());
isLoading.setValue(false);
} else if (resource.status == Resource.Status.ERROR) {
errorMessage.setValue(resource.message);
isLoading.setValue(false);
}
}
});
}
private void computeAnalytics(List<SaleDTO> sales) {
List<SaleDTO> regularSales = new ArrayList<>();
for (SaleDTO s : sales) {
if (!Boolean.TRUE.equals(s.getIsRefund()))
regularSales.add(s);
}
AnalyticsData data = new AnalyticsData();
// Summary
BigDecimal totalRevenue = BigDecimal.ZERO;
int totalItems = 0;
for (SaleDTO s : regularSales) {
if (s.getTotalAmount() != null) totalRevenue = totalRevenue.add(s.getTotalAmount());
if (s.getItems() != null) {
for (SaleDTO.SaleItemDTO item : s.getItems()) {
if (item.getQuantity() != null) totalItems += Math.abs(item.getQuantity());
}
}
}
data.totalRevenue = totalRevenue;
data.totalTransactions = regularSales.size();
data.avgTransaction = data.totalTransactions > 0
? totalRevenue.divide(BigDecimal.valueOf(data.totalTransactions), 2, RoundingMode.HALF_UP)
: BigDecimal.ZERO;
data.totalItems = totalItems;
// Product Maps
Map<String, BigDecimal> revenueByProduct = new LinkedHashMap<>();
Map<String, Integer> quantityByProduct = new LinkedHashMap<>();
Map<String, Integer> paymentCount = new LinkedHashMap<>();
Map<String, BigDecimal> employeeRevenue = new LinkedHashMap<>();
for (SaleDTO s : regularSales) {
// Payments
String method = s.getPaymentMethod() != null ? s.getPaymentMethod() : "Unknown";
paymentCount.merge(method, 1, Integer::sum);
// Employee
String emp = s.getEmployeeName() != null ? s.getEmployeeName() : "Unknown";
if (s.getTotalAmount() != null) employeeRevenue.merge(emp, s.getTotalAmount(), BigDecimal::add);
// Items
if (s.getItems() != null) {
for (SaleDTO.SaleItemDTO item : s.getItems()) {
String name = item.getProductName() != null ? item.getProductName() : "Unknown";
int qty = item.getQuantity() != null ? Math.abs(item.getQuantity()) : 0;
BigDecimal lineTotal = item.getUnitPrice() != null
? item.getUnitPrice().multiply(BigDecimal.valueOf(qty))
: BigDecimal.ZERO;
revenueByProduct.merge(name, lineTotal, BigDecimal::add);
quantityByProduct.merge(name, qty, Integer::sum);
}
}
}
// Sort Top Revenue
data.topRevenueProducts = new ArrayList<>(revenueByProduct.entrySet());
data.topRevenueProducts.sort((a, b) -> b.getValue().compareTo(a.getValue()));
if (data.topRevenueProducts.size() > 5) data.topRevenueProducts = data.topRevenueProducts.subList(0, 5);
// Sort Top Quantity
data.topQuantityProducts = new ArrayList<>(quantityByProduct.entrySet());
data.topQuantityProducts.sort((a, b) -> b.getValue() - a.getValue());
if (data.topQuantityProducts.size() > 5) data.topQuantityProducts = data.topQuantityProducts.subList(0, 5);
// Payment Stats
data.paymentMethodStats = new ArrayList<>(paymentCount.entrySet());
// Employee Performance
data.employeePerformance = new ArrayList<>(employeeRevenue.entrySet());
data.employeePerformance.sort((a, b) -> b.getValue().compareTo(a.getValue()));
// Daily Revenue (last 7 days)
Map<String, BigDecimal> dailyMap = new TreeMap<>();
for (int i = 6; i >= 0; i--) {
Calendar day = Calendar.getInstance();
day.add(Calendar.DAY_OF_YEAR, -i);
String key = String.format("%04d-%02d-%02d",
day.get(Calendar.YEAR), day.get(Calendar.MONTH) + 1, day.get(Calendar.DAY_OF_MONTH));
dailyMap.put(key, BigDecimal.ZERO);
}
for (SaleDTO s : regularSales) {
if (s.getSaleDate() != null && s.getTotalAmount() != null) {
String date = s.getSaleDate().length() >= 10 ? s.getSaleDate().substring(0, 10) : s.getSaleDate();
if (dailyMap.containsKey(date)) dailyMap.merge(date, s.getTotalAmount(), BigDecimal::add);
}
}
data.dailyRevenue = new ArrayList<>(dailyMap.entrySet());
analyticsData.setValue(data);
}
public static class AnalyticsData {
public BigDecimal totalRevenue;
public int totalTransactions;
public BigDecimal avgTransaction;
public int totalItems;
public List<Map.Entry<String, BigDecimal>> topRevenueProducts;
public List<Map.Entry<String, Integer>> topQuantityProducts;
public List<Map.Entry<String, Integer>> paymentMethodStats;
public List<Map.Entry<String, BigDecimal>> employeePerformance;
public List<Map.Entry<String, BigDecimal>> dailyRevenue;
}
}

View File

@@ -0,0 +1,65 @@
package com.example.petstoremobile.viewmodels;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.ViewModel;
import com.example.petstoremobile.dtos.AppointmentDTO;
import com.example.petstoremobile.dtos.BulkDeleteRequest;
import com.example.petstoremobile.dtos.StoreDTO;
import com.example.petstoremobile.repositories.AppointmentRepository;
import com.example.petstoremobile.repositories.StoreRepository;
import com.example.petstoremobile.utils.Resource;
import java.util.ArrayList;
import java.util.List;
import javax.inject.Inject;
import dagger.hilt.android.lifecycle.HiltViewModel;
@HiltViewModel
public class AppointmentListViewModel extends ViewModel {
private final AppointmentRepository appointmentRepository;
private final StoreRepository storeRepository;
private final MutableLiveData<List<AppointmentDTO>> appointments = new MutableLiveData<>(new ArrayList<>());
private final MutableLiveData<List<StoreDTO>> stores = new MutableLiveData<>(new ArrayList<>());
private final MutableLiveData<Boolean> isLoading = new MutableLiveData<>(false);
@Inject
public AppointmentListViewModel(AppointmentRepository appointmentRepository, StoreRepository storeRepository) {
this.appointmentRepository = appointmentRepository;
this.storeRepository = storeRepository;
}
public LiveData<List<AppointmentDTO>> getAppointments() { return appointments; }
public LiveData<List<StoreDTO>> getStores() { return stores; }
public LiveData<Boolean> getIsLoading() { return isLoading; }
public void loadAppointments(String query, String status, Long storeId, String date, Long employeeId) {
isLoading.setValue(true);
appointmentRepository.getAllAppointments(0, 500, query, status, storeId, date, employeeId).observeForever(resource -> {
if (resource != null) {
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
appointments.setValue(resource.data.getContent());
isLoading.setValue(false);
} else if (resource.status == Resource.Status.ERROR) {
isLoading.setValue(false);
}
}
});
}
public void loadStores() {
storeRepository.getAllStores(0, 100).observeForever(resource -> {
if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) {
stores.setValue(resource.data.getContent());
}
});
}
public LiveData<Resource<Void>> bulkDeleteAppointments(List<String> ids) {
return appointmentRepository.bulkDeleteAppointments(new BulkDeleteRequest(ids));
}
}

View File

@@ -0,0 +1,132 @@
package com.example.petstoremobile.viewmodels;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.ViewModel;
import com.example.petstoremobile.dtos.ConversationDTO;
import com.example.petstoremobile.dtos.CustomerDTO;
import com.example.petstoremobile.dtos.MessageDTO;
import com.example.petstoremobile.dtos.PageResponse;
import com.example.petstoremobile.dtos.SendMessageRequest;
import com.example.petstoremobile.models.Chat;
import com.example.petstoremobile.models.Message;
import com.example.petstoremobile.repositories.ChatRepository;
import com.example.petstoremobile.repositories.CustomerRepository;
import com.example.petstoremobile.utils.Resource;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.inject.Inject;
import dagger.hilt.android.lifecycle.HiltViewModel;
@HiltViewModel
public class ChatListViewModel extends ViewModel {
private final ChatRepository chatRepository;
private final CustomerRepository customerRepository;
private final MutableLiveData<List<Chat>> chatList = new MutableLiveData<>(new ArrayList<>());
private final MutableLiveData<List<Message>> messageList = new MutableLiveData<>(new ArrayList<>());
private final Map<Long, String> customerNames = new HashMap<>();
private final MutableLiveData<Boolean> isLoading = new MutableLiveData<>(false);
@Inject
public ChatListViewModel(ChatRepository chatRepository, CustomerRepository customerRepository) {
this.chatRepository = chatRepository;
this.customerRepository = customerRepository;
}
public LiveData<List<Chat>> getChatList() { return chatList; }
public LiveData<List<Message>> getMessageList() { return messageList; }
public LiveData<Boolean> getIsLoading() { return isLoading; }
public void loadCustomers() {
customerRepository.getAllCustomers(0, 100).observeForever(resource -> {
if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) {
for (CustomerDTO c : resource.data.getContent()) {
customerNames.put(c.getCustomerId(), c.getFullName());
}
loadConversations();
}
});
}
public void loadConversations() {
isLoading.setValue(true);
chatRepository.getAllConversations().observeForever(resource -> {
if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) {
List<Chat> chats = new ArrayList<>();
for (ConversationDTO dto : resource.data) {
String name = customerNames.getOrDefault(dto.getCustomerId(), "Customer #" + dto.getCustomerId());
chats.add(new Chat(String.valueOf(dto.getId()), name, dto.getLastMessage(), dto.getCustomerId(), dto.getStaffId()));
}
chatList.setValue(chats);
isLoading.setValue(false);
} else if (resource != null && resource.status == Resource.Status.ERROR) {
isLoading.setValue(false);
}
});
}
public void loadMessageHistory(Long conversationId) {
chatRepository.getMessages(conversationId).observeForever(resource -> {
if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) {
List<Message> messages = new ArrayList<>();
for (MessageDTO dto : resource.data) {
messages.add(dtoToModel(dto));
}
messageList.setValue(messages);
}
});
}
public LiveData<Resource<MessageDTO>> sendMessage(Long conversationId, String text) {
return chatRepository.sendMessage(conversationId, new SendMessageRequest(text));
}
public void addMessageLocally(MessageDTO dto) {
List<Message> current = new ArrayList<>(messageList.getValue());
current.add(dtoToModel(dto));
messageList.setValue(current);
}
public void updateConversationLocally(ConversationDTO dto) {
List<Chat> current = new ArrayList<>(chatList.getValue());
boolean updated = false;
String name = customerNames.getOrDefault(dto.getCustomerId(), "Customer #" + dto.getCustomerId());
for (int i = 0; i < current.size(); i++) {
if (current.get(i).getChatId().equals(String.valueOf(dto.getId()))) {
current.set(i, new Chat(String.valueOf(dto.getId()), name, dto.getLastMessage(), dto.getCustomerId(), dto.getStaffId()));
updated = true;
break;
}
}
if (!updated) {
current.add(0, new Chat(String.valueOf(dto.getId()), name, dto.getLastMessage(), dto.getCustomerId(), dto.getStaffId()));
}
chatList.setValue(current);
}
private Message dtoToModel(MessageDTO dto) {
Message m = new Message();
m.setId(dto.getId());
m.setConversationId(dto.getConversationId());
m.setSenderId(dto.getSenderId());
m.setContent(dto.getContent());
m.setTimestamp(dto.getTimestamp());
m.setIsRead(dto.getIsRead());
m.setAttachmentUrl(dto.getAttachmentUrl());
m.setAttachmentName(dto.getAttachmentName());
m.setAttachmentType(dto.getAttachmentType());
return m;
}
public String getCustomerName(Long customerId) {
return customerNames.getOrDefault(customerId, "Customer #" + customerId);
}
}

View File

@@ -0,0 +1,81 @@
package com.example.petstoremobile.viewmodels;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.ViewModel;
import com.example.petstoremobile.dtos.BulkDeleteRequest;
import com.example.petstoremobile.dtos.InventoryDTO;
import com.example.petstoremobile.dtos.StoreDTO;
import com.example.petstoremobile.repositories.InventoryRepository;
import com.example.petstoremobile.repositories.StoreRepository;
import com.example.petstoremobile.utils.Resource;
import java.util.ArrayList;
import java.util.List;
import javax.inject.Inject;
import dagger.hilt.android.lifecycle.HiltViewModel;
@HiltViewModel
public class InventoryListViewModel extends ViewModel {
private final InventoryRepository inventoryRepository;
private final StoreRepository storeRepository;
private final MutableLiveData<List<InventoryDTO>> inventory = new MutableLiveData<>(new ArrayList<>());
private final MutableLiveData<List<StoreDTO>> stores = new MutableLiveData<>(new ArrayList<>());
private final MutableLiveData<Boolean> isLoading = new MutableLiveData<>(false);
private int currentPage = 0;
private boolean isLastPage = false;
private static final int PAGE_SIZE = 20;
@Inject
public InventoryListViewModel(InventoryRepository inventoryRepository, StoreRepository storeRepository) {
this.inventoryRepository = inventoryRepository;
this.storeRepository = storeRepository;
}
public LiveData<List<InventoryDTO>> getInventory() { return inventory; }
public LiveData<List<StoreDTO>> getStores() { return stores; }
public LiveData<Boolean> getIsLoading() { return isLoading; }
public boolean isLastPage() { return isLastPage; }
public void loadInventory(boolean reset, String query, Long storeId) {
if (isLoading.getValue() != null && isLoading.getValue() && !reset) return;
if (reset) {
currentPage = 0;
isLastPage = false;
}
isLoading.setValue(true);
inventoryRepository.getAllInventory(query, storeId, currentPage, PAGE_SIZE, "product.prodName").observeForever(resource -> {
if (resource != null) {
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
List<InventoryDTO> currentList = reset ? new ArrayList<>() : new ArrayList<>(inventory.getValue());
currentList.addAll(resource.data.getContent());
inventory.setValue(currentList);
isLastPage = resource.data.isLast();
if (!isLastPage) currentPage++;
isLoading.setValue(false);
} else if (resource.status == Resource.Status.ERROR) {
isLoading.setValue(false);
}
}
});
}
public void loadStores() {
storeRepository.getAllStores(0, 100).observeForever(resource -> {
if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) {
stores.setValue(resource.data.getContent());
}
});
}
public LiveData<Resource<Void>> bulkDeleteInventory(List<String> ids) {
return inventoryRepository.bulkDeleteInventory(new BulkDeleteRequest(ids));
}
}

View File

@@ -0,0 +1,69 @@
package com.example.petstoremobile.viewmodels;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.ViewModel;
import com.example.petstoremobile.dtos.BulkDeleteRequest;
import com.example.petstoremobile.dtos.PageResponse;
import com.example.petstoremobile.dtos.PetDTO;
import com.example.petstoremobile.dtos.StoreDTO;
import com.example.petstoremobile.repositories.PetRepository;
import com.example.petstoremobile.repositories.StoreRepository;
import com.example.petstoremobile.utils.Resource;
import java.util.ArrayList;
import java.util.List;
import javax.inject.Inject;
import dagger.hilt.android.lifecycle.HiltViewModel;
@HiltViewModel
public class PetListViewModel extends ViewModel {
private final PetRepository petRepository;
private final StoreRepository storeRepository;
private final MutableLiveData<List<PetDTO>> pets = new MutableLiveData<>(new ArrayList<>());
private final MutableLiveData<List<StoreDTO>> stores = new MutableLiveData<>(new ArrayList<>());
private final MutableLiveData<Boolean> isLoading = new MutableLiveData<>(false);
@Inject
public PetListViewModel(PetRepository petRepository, StoreRepository storeRepository) {
this.petRepository = petRepository;
this.storeRepository = storeRepository;
}
public LiveData<List<PetDTO>> getPets() { return pets; }
public LiveData<List<StoreDTO>> getStores() { return stores; }
public LiveData<Boolean> getIsLoading() { return isLoading; }
public void loadPets(String query, String status, String species, Long storeId) {
if ("All Statuses".equals(status)) status = null;
if ("All Species".equals(species)) species = null;
isLoading.setValue(true);
petRepository.getAllPets(0, 100, query, status, species, storeId, null, "petName").observeForever(resource -> {
if (resource != null) {
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
pets.setValue(resource.data.getContent());
isLoading.setValue(false);
} else if (resource.status == Resource.Status.ERROR) {
isLoading.setValue(false);
}
}
});
}
public void loadStores() {
storeRepository.getAllStores(0, 100).observeForever(resource -> {
if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) {
stores.setValue(resource.data.getContent());
}
});
}
public LiveData<Resource<Void>> bulkDeletePets(List<String> ids) {
return petRepository.bulkDeletePets(new BulkDeleteRequest(ids));
}
}

View File

@@ -0,0 +1,35 @@
package com.example.petstoremobile.viewmodels;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.ViewModel;
import com.example.petstoremobile.dtos.PetDTO;
import com.example.petstoremobile.repositories.PetRepository;
import com.example.petstoremobile.utils.Resource;
import javax.inject.Inject;
import dagger.hilt.android.lifecycle.HiltViewModel;
import okhttp3.MultipartBody;
@HiltViewModel
public class PetProfileViewModel extends ViewModel {
private final PetRepository repository;
@Inject
public PetProfileViewModel(PetRepository repository) {
this.repository = repository;
}
public LiveData<Resource<PetDTO>> getPetById(Long id) {
return repository.getPetById(id);
}
public LiveData<Resource<Void>> uploadPetImage(Long id, MultipartBody.Part image) {
return repository.uploadPetImage(id, image);
}
public LiveData<Resource<Void>> deletePetImage(Long id) {
return repository.deletePetImage(id);
}
}

View File

@@ -0,0 +1,60 @@
package com.example.petstoremobile.viewmodels;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.ViewModel;
import com.example.petstoremobile.dtos.CategoryDTO;
import com.example.petstoremobile.dtos.ProductDTO;
import com.example.petstoremobile.repositories.CategoryRepository;
import com.example.petstoremobile.repositories.ProductRepository;
import com.example.petstoremobile.utils.Resource;
import java.util.ArrayList;
import java.util.List;
import javax.inject.Inject;
import dagger.hilt.android.lifecycle.HiltViewModel;
@HiltViewModel
public class ProductListViewModel extends ViewModel {
private final ProductRepository productRepository;
private final CategoryRepository categoryRepository;
private final MutableLiveData<List<ProductDTO>> products = new MutableLiveData<>(new ArrayList<>());
private final MutableLiveData<List<CategoryDTO>> categories = new MutableLiveData<>(new ArrayList<>());
private final MutableLiveData<Boolean> isLoading = new MutableLiveData<>(false);
@Inject
public ProductListViewModel(ProductRepository productRepository, CategoryRepository categoryRepository) {
this.productRepository = productRepository;
this.categoryRepository = categoryRepository;
}
public LiveData<List<ProductDTO>> getProducts() { return products; }
public LiveData<List<CategoryDTO>> getCategories() { return categories; }
public LiveData<Boolean> getIsLoading() { return isLoading; }
public void loadProducts(String query, Long categoryId) {
isLoading.setValue(true);
productRepository.getAllProducts(query, categoryId, 0, 100, "prodName").observeForever(resource -> {
if (resource != null) {
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
products.setValue(resource.data.getContent());
isLoading.setValue(false);
} else if (resource.status == Resource.Status.ERROR) {
isLoading.setValue(false);
}
}
});
}
public void loadCategories() {
categoryRepository.getAllCategories(0, 100).observeForever(resource -> {
if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) {
categories.setValue(resource.data.getContent());
}
});
}
}

View File

@@ -0,0 +1,77 @@
package com.example.petstoremobile.viewmodels;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.ViewModel;
import com.example.petstoremobile.dtos.BulkDeleteRequest;
import com.example.petstoremobile.dtos.ProductDTO;
import com.example.petstoremobile.dtos.ProductSupplierDTO;
import com.example.petstoremobile.dtos.SupplierDTO;
import com.example.petstoremobile.repositories.ProductRepository;
import com.example.petstoremobile.repositories.ProductSupplierRepository;
import com.example.petstoremobile.repositories.SupplierRepository;
import com.example.petstoremobile.utils.Resource;
import java.util.ArrayList;
import java.util.List;
import javax.inject.Inject;
import dagger.hilt.android.lifecycle.HiltViewModel;
@HiltViewModel
public class ProductSupplierListViewModel extends ViewModel {
private final ProductSupplierRepository psRepository;
private final ProductRepository productRepository;
private final SupplierRepository supplierRepository;
private final MutableLiveData<List<ProductSupplierDTO>> productSuppliers = new MutableLiveData<>(new ArrayList<>());
private final MutableLiveData<List<ProductDTO>> products = new MutableLiveData<>(new ArrayList<>());
private final MutableLiveData<List<SupplierDTO>> suppliers = new MutableLiveData<>(new ArrayList<>());
private final MutableLiveData<Boolean> isLoading = new MutableLiveData<>(false);
@Inject
public ProductSupplierListViewModel(ProductSupplierRepository psRepository, ProductRepository productRepository, SupplierRepository supplierRepository) {
this.psRepository = psRepository;
this.productRepository = productRepository;
this.supplierRepository = supplierRepository;
}
public LiveData<List<ProductSupplierDTO>> getProductSuppliers() { return productSuppliers; }
public LiveData<List<ProductDTO>> getProducts() { return products; }
public LiveData<List<SupplierDTO>> getSuppliers() { return suppliers; }
public LiveData<Boolean> getIsLoading() { return isLoading; }
public void loadProductSuppliers(String query, Long productId, Long supplierId) {
isLoading.setValue(true);
psRepository.getAllProductSuppliers(0, 100, query, productId, supplierId, "productName").observeForever(resource -> {
if (resource != null) {
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
productSuppliers.setValue(resource.data.getContent());
isLoading.setValue(false);
} else if (resource.status == Resource.Status.ERROR) {
isLoading.setValue(false);
}
}
});
}
public void loadFilterData() {
productRepository.getAllProducts(null, null, 0, 100, "prodName").observeForever(resource -> {
if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) {
products.setValue(resource.data.getContent());
}
});
supplierRepository.getAllSuppliers(0, 100, null, "supCompany").observeForever(resource -> {
if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) {
suppliers.setValue(resource.data.getContent());
}
});
}
public LiveData<Resource<Void>> bulkDeleteProductSuppliers(List<String> ids) {
return psRepository.bulkDeleteProductSuppliers(new BulkDeleteRequest(ids));
}
}

View File

@@ -0,0 +1,60 @@
package com.example.petstoremobile.viewmodels;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.ViewModel;
import com.example.petstoremobile.dtos.PurchaseOrderDTO;
import com.example.petstoremobile.dtos.StoreDTO;
import com.example.petstoremobile.repositories.PurchaseOrderRepository;
import com.example.petstoremobile.repositories.StoreRepository;
import com.example.petstoremobile.utils.Resource;
import java.util.ArrayList;
import java.util.List;
import javax.inject.Inject;
import dagger.hilt.android.lifecycle.HiltViewModel;
@HiltViewModel
public class PurchaseOrderListViewModel extends ViewModel {
private final PurchaseOrderRepository purchaseOrderRepository;
private final StoreRepository storeRepository;
private final MutableLiveData<List<PurchaseOrderDTO>> purchaseOrders = new MutableLiveData<>(new ArrayList<>());
private final MutableLiveData<List<StoreDTO>> stores = new MutableLiveData<>(new ArrayList<>());
private final MutableLiveData<Boolean> isLoading = new MutableLiveData<>(false);
@Inject
public PurchaseOrderListViewModel(PurchaseOrderRepository purchaseOrderRepository, StoreRepository storeRepository) {
this.purchaseOrderRepository = purchaseOrderRepository;
this.storeRepository = storeRepository;
}
public LiveData<List<PurchaseOrderDTO>> getPurchaseOrders() { return purchaseOrders; }
public LiveData<List<StoreDTO>> getStores() { return stores; }
public LiveData<Boolean> getIsLoading() { return isLoading; }
public void loadPurchaseOrders(String query, Long storeId) {
isLoading.setValue(true);
purchaseOrderRepository.getAllPurchaseOrders(0, 100, query, storeId, "purchaseOrderId,desc").observeForever(resource -> {
if (resource != null) {
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
purchaseOrders.setValue(resource.data.getContent());
isLoading.setValue(false);
} else if (resource.status == Resource.Status.ERROR) {
isLoading.setValue(false);
}
}
});
}
public void loadStores() {
storeRepository.getAllStores(0, 100).observeForever(resource -> {
if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) {
stores.setValue(resource.data.getContent());
}
});
}
}

View File

@@ -0,0 +1,77 @@
package com.example.petstoremobile.viewmodels;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.ViewModel;
import com.example.petstoremobile.dtos.PageResponse;
import com.example.petstoremobile.dtos.SaleDTO;
import com.example.petstoremobile.dtos.StoreDTO;
import com.example.petstoremobile.repositories.SaleRepository;
import com.example.petstoremobile.repositories.StoreRepository;
import com.example.petstoremobile.utils.Resource;
import java.util.ArrayList;
import java.util.List;
import javax.inject.Inject;
import dagger.hilt.android.lifecycle.HiltViewModel;
@HiltViewModel
public class SaleListViewModel extends ViewModel {
private final SaleRepository saleRepository;
private final StoreRepository storeRepository;
private final MutableLiveData<List<SaleDTO>> sales = new MutableLiveData<>(new ArrayList<>());
private final MutableLiveData<List<StoreDTO>> stores = new MutableLiveData<>(new ArrayList<>());
private final MutableLiveData<Boolean> isLoading = new MutableLiveData<>(false);
private int currentPage = 0;
private boolean isLastPage = false;
private static final int PAGE_SIZE = 20;
@Inject
public SaleListViewModel(SaleRepository saleRepository, StoreRepository storeRepository) {
this.saleRepository = saleRepository;
this.storeRepository = storeRepository;
}
public LiveData<List<SaleDTO>> getSales() { return sales; }
public LiveData<List<StoreDTO>> getStores() { return stores; }
public LiveData<Boolean> getIsLoading() { return isLoading; }
public boolean isLastPage() { return isLastPage; }
public void loadSales(boolean reset, String query, String paymentMethod, Long storeId) {
if (isLoading.getValue() != null && isLoading.getValue() && !reset) return;
if (reset) {
currentPage = 0;
isLastPage = false;
}
isLoading.setValue(true);
saleRepository.getAllSales(currentPage, PAGE_SIZE, query, paymentMethod, storeId, "saleDate,desc").observeForever(resource -> {
if (resource != null) {
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
List<SaleDTO> currentList = reset ? new ArrayList<>() : new ArrayList<>(sales.getValue());
currentList.addAll(resource.data.getContent());
sales.setValue(currentList);
isLastPage = resource.data.isLast();
if (!isLastPage) currentPage++;
isLoading.setValue(false);
} else if (resource.status == Resource.Status.ERROR) {
isLoading.setValue(false);
}
}
});
}
public void loadStores() {
storeRepository.getAllStores(0, 100).observeForever(resource -> {
if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) {
stores.setValue(resource.data.getContent());
}
});
}
}

View File

@@ -0,0 +1,68 @@
package com.example.petstoremobile.viewmodels;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.ViewModel;
import com.example.petstoremobile.dtos.BulkDeleteRequest;
import com.example.petstoremobile.dtos.PageResponse;
import com.example.petstoremobile.dtos.ServiceDTO;
import com.example.petstoremobile.repositories.ServiceRepository;
import com.example.petstoremobile.utils.Resource;
import java.util.ArrayList;
import java.util.List;
import javax.inject.Inject;
import dagger.hilt.android.lifecycle.HiltViewModel;
@HiltViewModel
public class ServiceListViewModel extends ViewModel {
private final ServiceRepository repository;
private final MutableLiveData<List<ServiceDTO>> services = new MutableLiveData<>(new ArrayList<>());
private final MutableLiveData<Boolean> isLoading = new MutableLiveData<>(false);
private int currentPage = 0;
private boolean isLastPage = false;
private static final int PAGE_SIZE = 20;
@Inject
public ServiceListViewModel(ServiceRepository repository) {
this.repository = repository;
}
public LiveData<List<ServiceDTO>> getServices() { return services; }
public LiveData<Boolean> getIsLoading() { return isLoading; }
public boolean isLastPage() { return isLastPage; }
public void loadServices(boolean reset, String query) {
if (isLoading.getValue() != null && isLoading.getValue() && !reset) return;
if (reset) {
currentPage = 0;
isLastPage = false;
}
isLoading.setValue(true);
repository.getAllServices(currentPage, PAGE_SIZE, query, "serviceName").observeForever(resource -> {
if (resource != null) {
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
List<ServiceDTO> currentList = reset ? new ArrayList<>() : new ArrayList<>(services.getValue());
currentList.addAll(resource.data.getContent());
services.setValue(currentList);
isLastPage = resource.data.isLast();
if (!isLastPage) currentPage++;
isLoading.setValue(false);
} else if (resource.status == Resource.Status.ERROR) {
isLoading.setValue(false);
}
}
});
}
public LiveData<Resource<Void>> bulkDeleteServices(List<String> ids) {
return repository.bulkDeleteServices(new BulkDeleteRequest(ids));
}
}

View File

@@ -0,0 +1,71 @@
package com.example.petstoremobile.viewmodels;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.ViewModel;
import com.example.petstoremobile.dtos.EmployeeDTO;
import com.example.petstoremobile.repositories.EmployeeRepository;
import com.example.petstoremobile.utils.Resource;
import java.util.ArrayList;
import java.util.List;
import javax.inject.Inject;
import dagger.hilt.android.lifecycle.HiltViewModel;
@HiltViewModel
public class StaffListViewModel extends ViewModel {
private final EmployeeRepository repository;
private final MutableLiveData<List<EmployeeDTO>> employees = new MutableLiveData<>(new ArrayList<>());
private final MutableLiveData<List<EmployeeDTO>> filteredEmployees = new MutableLiveData<>(new ArrayList<>());
private final MutableLiveData<Boolean> isLoading = new MutableLiveData<>(false);
private String lastQuery = "";
@Inject
public StaffListViewModel(EmployeeRepository repository) {
this.repository = repository;
}
public LiveData<List<EmployeeDTO>> getFilteredEmployees() { return filteredEmployees; }
public LiveData<Boolean> getIsLoading() { return isLoading; }
public void loadStaff() {
isLoading.setValue(true);
repository.getAllEmployees(0, 100).observeForever(resource -> {
if (resource != null) {
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
employees.setValue(resource.data.getContent());
filter(lastQuery);
isLoading.setValue(false);
} else if (resource.status == Resource.Status.ERROR) {
isLoading.setValue(false);
}
}
});
}
public void filter(String query) {
this.lastQuery = query;
List<EmployeeDTO> all = employees.getValue();
if (all == null) return;
if (query.isEmpty()) {
filteredEmployees.setValue(new ArrayList<>(all));
} else {
List<EmployeeDTO> filtered = new ArrayList<>();
String lower = query.toLowerCase();
for (EmployeeDTO e : all) {
if ((e.getFullName() != null && e.getFullName().toLowerCase().contains(lower))
|| (e.getUsername() != null && e.getUsername().toLowerCase().contains(lower))
|| (e.getEmail() != null && e.getEmail().toLowerCase().contains(lower))
|| (e.getPhone() != null && e.getPhone().toLowerCase().contains(lower))) {
filtered.add(e);
}
}
filteredEmployees.setValue(filtered);
}
}
}

View File

@@ -0,0 +1,51 @@
package com.example.petstoremobile.viewmodels;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.ViewModel;
import com.example.petstoremobile.dtos.BulkDeleteRequest;
import com.example.petstoremobile.dtos.SupplierDTO;
import com.example.petstoremobile.repositories.SupplierRepository;
import com.example.petstoremobile.utils.Resource;
import java.util.ArrayList;
import java.util.List;
import javax.inject.Inject;
import dagger.hilt.android.lifecycle.HiltViewModel;
@HiltViewModel
public class SupplierListViewModel extends ViewModel {
private final SupplierRepository repository;
private final MutableLiveData<List<SupplierDTO>> suppliers = new MutableLiveData<>(new ArrayList<>());
private final MutableLiveData<Boolean> isLoading = new MutableLiveData<>(false);
@Inject
public SupplierListViewModel(SupplierRepository repository) {
this.repository = repository;
}
public LiveData<List<SupplierDTO>> getSuppliers() { return suppliers; }
public LiveData<Boolean> getIsLoading() { return isLoading; }
public void loadSuppliers(String query) {
isLoading.setValue(true);
repository.getAllSuppliers(0, 100, query, "supCompany").observeForever(resource -> {
if (resource != null) {
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
suppliers.setValue(resource.data.getContent());
isLoading.setValue(false);
} else if (resource.status == Resource.Status.ERROR) {
isLoading.setValue(false);
}
}
});
}
public LiveData<Resource<Void>> bulkDeleteSuppliers(List<String> ids) {
return repository.bulkDeleteSuppliers(new BulkDeleteRequest(ids));
}
}