diff --git a/android/app/src/main/java/com/example/petstoremobile/adapters/MessageAdapter.java b/android/app/src/main/java/com/example/petstoremobile/adapters/MessageAdapter.java index ee58941a..0c3a51a0 100644 --- a/android/app/src/main/java/com/example/petstoremobile/adapters/MessageAdapter.java +++ b/android/app/src/main/java/com/example/petstoremobile/adapters/MessageAdapter.java @@ -22,9 +22,15 @@ public class MessageAdapter extends RecyclerView.Adapter messages; private Long currentUserId; private String token; + private String baseUrl; + private OnAttachmentClickListener attachmentClickListener; public MessageAdapter(List messages, Long currentUserId) { this.messages = messages; @@ -40,6 +46,14 @@ public class MessageAdapter extends RecyclerView.Adapter { + if (listener != null) listener.onAttachmentClick(m); + }; + binding.ivAttachment.setOnClickListener(click); + binding.tvAttachmentName.setOnClickListener(click); } } @@ -88,22 +116,52 @@ public class MessageAdapter extends RecyclerView.Adapter { + if (listener != null) listener.onAttachmentClick(m); + }; + binding.ivAttachment.setOnClickListener(click); + binding.tvAttachmentName.setOnClickListener(click); } } // helper function to display the attachment to the chat bubble - private static void displayAttachment(Message m, ImageView iv, TextView tvName, String token) { - if (m.getAttachmentUrl() != null) { - if (m.getAttachmentType() != null && m.getAttachmentType().startsWith("image/")) { + private static void displayAttachment(Message m, ImageView iv, TextView tvName, String token, String baseUrl) { + // Check if there's an attachment by looking at name or mime type + if (m.getAttachmentName() != null || m.getAttachmentMimeType() != null) { + // Construct the download URL using the message ID + String url; + if (baseUrl != null) { + String cleanBase = baseUrl.endsWith("/") ? baseUrl.substring(0, baseUrl.length() - 1) : baseUrl; + url = cleanBase + "/api/v1/chat/messages/" + m.getId() + "/attachment"; + } else { + url = m.getAttachmentUrl(); // Fallback + } + + if (url == null) { + iv.setVisibility(View.GONE); + tvName.setVisibility(View.GONE); + return; + } + + if (m.getAttachmentMimeType() != null && m.getAttachmentMimeType().startsWith("image/")) { iv.setVisibility(View.VISIBLE); tvName.setVisibility(View.GONE); - Object loadTarget = m.getAttachmentUrl(); - if (token != null && m.getAttachmentUrl().startsWith("http")) { - loadTarget = new GlideUrl(m.getAttachmentUrl(), new LazyHeaders.Builder() + Object loadTarget = url; + if (token != null) { + loadTarget = new GlideUrl(url, new LazyHeaders.Builder() .addHeader("Authorization", "Bearer " + token) .build()); } diff --git a/android/app/src/main/java/com/example/petstoremobile/api/CustomerApi.java b/android/app/src/main/java/com/example/petstoremobile/api/CustomerApi.java index 02700075..855ba5fa 100644 --- a/android/app/src/main/java/com/example/petstoremobile/api/CustomerApi.java +++ b/android/app/src/main/java/com/example/petstoremobile/api/CustomerApi.java @@ -1,6 +1,7 @@ package com.example.petstoremobile.api; import com.example.petstoremobile.dtos.CustomerDTO; +import com.example.petstoremobile.dtos.DropdownDTO; import com.example.petstoremobile.dtos.PageResponse; import java.util.List; @@ -18,4 +19,7 @@ public interface CustomerApi { @GET("api/v1/customers/{customerId}") Call getCustomerById(@Path("customerId") Long customerId); + + @GET("api/v1/dropdowns/customers") + Call> getCustomerDropdowns(); } \ No newline at end of file diff --git a/android/app/src/main/java/com/example/petstoremobile/api/MessageApi.java b/android/app/src/main/java/com/example/petstoremobile/api/MessageApi.java index 13df781f..e5504a03 100644 --- a/android/app/src/main/java/com/example/petstoremobile/api/MessageApi.java +++ b/android/app/src/main/java/com/example/petstoremobile/api/MessageApi.java @@ -3,11 +3,17 @@ package com.example.petstoremobile.api; import com.example.petstoremobile.dtos.MessageDTO; import com.example.petstoremobile.dtos.SendMessageRequest; import java.util.List; + +import okhttp3.MultipartBody; +import okhttp3.ResponseBody; import retrofit2.Call; import retrofit2.http.Body; import retrofit2.http.GET; +import retrofit2.http.Multipart; import retrofit2.http.POST; +import retrofit2.http.Part; import retrofit2.http.Path; +import retrofit2.http.Streaming; //api calls to get and send messages public interface MessageApi { @@ -17,4 +23,16 @@ public interface MessageApi { @POST("api/v1/chat/conversations/{id}/messages") Call sendMessage(@Path("id") Long conversationId, @Body SendMessageRequest request); + + @Multipart + @POST("api/v1/chat/conversations/{id}/attachments") + Call sendMessageWithAttachment( + @Path("id") Long conversationId, + @Part MultipartBody.Part content, + @Part MultipartBody.Part file + ); + + @GET("api/v1/chat/messages/{id}/attachment") + @Streaming + Call downloadAttachment(@Path("id") Long messageId); } \ No newline at end of file diff --git a/android/app/src/main/java/com/example/petstoremobile/api/PetApi.java b/android/app/src/main/java/com/example/petstoremobile/api/PetApi.java index acae1b51..24250c4c 100644 --- a/android/app/src/main/java/com/example/petstoremobile/api/PetApi.java +++ b/android/app/src/main/java/com/example/petstoremobile/api/PetApi.java @@ -1,9 +1,12 @@ package com.example.petstoremobile.api; import com.example.petstoremobile.dtos.BulkDeleteRequest; +import com.example.petstoremobile.dtos.DropdownDTO; import com.example.petstoremobile.dtos.PageResponse; import com.example.petstoremobile.dtos.PetDTO; +import java.util.List; + import okhttp3.MultipartBody; import retrofit2.Call; import retrofit2.http.Body; @@ -31,9 +34,16 @@ public interface PetApi { @Query("status") String status, @Query("species") String species, @Query("storeId") Long storeId, + @Query("customerId") Long customerId, @Query("sort") String sort ); + @GET("api/v1/dropdowns/customers/{customerId}/pets") + Call> getCustomerPets(@Path("customerId") Long customerId); + + @GET("api/v1/dropdowns/adoption-pets") + Call> getAdoptionPets(); + // Get pet by id @GET("api/v1/pets/{id}") Call getPetById(@Path("id") Long id); diff --git a/android/app/src/main/java/com/example/petstoremobile/api/StoreApi.java b/android/app/src/main/java/com/example/petstoremobile/api/StoreApi.java index 8fe7f9c1..f71b92b6 100644 --- a/android/app/src/main/java/com/example/petstoremobile/api/StoreApi.java +++ b/android/app/src/main/java/com/example/petstoremobile/api/StoreApi.java @@ -1,10 +1,14 @@ package com.example.petstoremobile.api; +import com.example.petstoremobile.dtos.DropdownDTO; import com.example.petstoremobile.dtos.PageResponse; import com.example.petstoremobile.dtos.StoreDTO; +import java.util.List; + import retrofit2.Call; import retrofit2.http.GET; +import retrofit2.http.Path; import retrofit2.http.Query; public interface StoreApi { @@ -13,4 +17,10 @@ public interface StoreApi { Call> getAllStores( @Query("page") int page, @Query("size") int size); + + @GET("api/v1/dropdowns/stores") + Call> getStoreDropdowns(); + + @GET("api/v1/dropdowns/stores/{storeId}/employees") + Call> getStoreEmployees(@Path("storeId") Long storeId); } diff --git a/android/app/src/main/java/com/example/petstoremobile/di/NetworkModule.java b/android/app/src/main/java/com/example/petstoremobile/di/NetworkModule.java index 21ea11b8..4191de71 100644 --- a/android/app/src/main/java/com/example/petstoremobile/di/NetworkModule.java +++ b/android/app/src/main/java/com/example/petstoremobile/di/NetworkModule.java @@ -55,7 +55,7 @@ public class NetworkModule { @Singleton public static OkHttpClient provideOkHttpClient(TokenManager tokenManager) { HttpLoggingInterceptor interceptor = new HttpLoggingInterceptor(); - interceptor.setLevel(HttpLoggingInterceptor.Level.BODY); + interceptor.setLevel(HttpLoggingInterceptor.Level.HEADERS); return new OkHttpClient.Builder() .addInterceptor(interceptor) @@ -191,4 +191,4 @@ public class NetworkModule { public static RefundApi provideRefundApi(Retrofit retrofit) { return retrofit.create(RefundApi.class); } -} +} \ No newline at end of file diff --git a/android/app/src/main/java/com/example/petstoremobile/dtos/ConversationDTO.java b/android/app/src/main/java/com/example/petstoremobile/dtos/ConversationDTO.java index 316aa467..3a7ea42e 100644 --- a/android/app/src/main/java/com/example/petstoremobile/dtos/ConversationDTO.java +++ b/android/app/src/main/java/com/example/petstoremobile/dtos/ConversationDTO.java @@ -12,6 +12,14 @@ public class ConversationDTO { public ConversationDTO() {} + public ConversationDTO(Long id, Long customerId, Long staffId, String lastMessage, String status) { + this.id = id; + this.customerId = customerId; + this.staffId = staffId; + this.lastMessage = lastMessage; + this.status = status; + } + public Long getId() { return id; } diff --git a/android/app/src/main/java/com/example/petstoremobile/dtos/DropdownDTO.java b/android/app/src/main/java/com/example/petstoremobile/dtos/DropdownDTO.java new file mode 100644 index 00000000..3174dae5 --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/dtos/DropdownDTO.java @@ -0,0 +1,29 @@ +package com.example.petstoremobile.dtos; + +public class DropdownDTO { + private Long id; + private String label; + + public DropdownDTO() {} + + public DropdownDTO(Long id, String label) { + this.id = id; + this.label = label; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getLabel() { + return label; + } + + public void setLabel(String label) { + this.label = label; + } +} diff --git a/android/app/src/main/java/com/example/petstoremobile/dtos/MessageDTO.java b/android/app/src/main/java/com/example/petstoremobile/dtos/MessageDTO.java index fea4cf66..a75d7812 100644 --- a/android/app/src/main/java/com/example/petstoremobile/dtos/MessageDTO.java +++ b/android/app/src/main/java/com/example/petstoremobile/dtos/MessageDTO.java @@ -28,8 +28,11 @@ public class MessageDTO { @SerializedName("attachmentName") private String attachmentName; - @SerializedName("attachmentType") - private String attachmentType; + @SerializedName("attachmentMimeType") + private String attachmentMimeType; + + @SerializedName("attachmentSizeBytes") + private Long attachmentSizeBytes; public MessageDTO() {} @@ -57,6 +60,9 @@ public class MessageDTO { public String getAttachmentName() { return attachmentName; } public void setAttachmentName(String attachmentName) { this.attachmentName = attachmentName; } - public String getAttachmentType() { return attachmentType; } - public void setAttachmentType(String attachmentType) { this.attachmentType = attachmentType; } + public String getAttachmentMimeType() { return attachmentMimeType; } + public void setAttachmentMimeType(String attachmentMimeType) { this.attachmentMimeType = attachmentMimeType; } + + public Long getAttachmentSizeBytes() { return attachmentSizeBytes; } + public void setAttachmentSizeBytes(Long attachmentSizeBytes) { this.attachmentSizeBytes = attachmentSizeBytes; } } \ No newline at end of file diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/ChatFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/ChatFragment.java index b13edd67..3c6a9d6c 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/ChatFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/ChatFragment.java @@ -1,15 +1,27 @@ package com.example.petstoremobile.fragments; import android.app.Activity; +import android.app.Dialog; +import android.content.ContentValues; import android.content.Intent; -import android.database.Cursor; import android.net.Uri; +import android.os.Build; import android.os.Bundle; -import android.provider.OpenableColumns; +import android.os.Environment; +import android.provider.MediaStore; import android.util.Log; -import android.view.*; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; import android.view.inputmethod.EditorInfo; +import android.widget.Button; +import android.widget.EditText; +import android.widget.ImageButton; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.TextView; import android.widget.Toast; + import androidx.activity.result.ActivityResultLauncher; import androidx.activity.result.contract.ActivityResultContracts; import androidx.annotation.NonNull; @@ -17,6 +29,7 @@ import androidx.core.view.GravityCompat; 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; @@ -25,20 +38,32 @@ 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.DialogUtils; +import com.example.petstoremobile.utils.FileUtils; +import com.example.petstoremobile.utils.GlideUtils; import com.example.petstoremobile.utils.Resource; -import com.example.petstoremobile.viewmodels.ChatViewModel; +import com.example.petstoremobile.utils.UIUtils; +import com.example.petstoremobile.viewmodels.ChatListViewModel; import com.example.petstoremobile.websocket.StompChatManager; -import java.util.*; +import java.io.File; +import java.io.FileOutputStream; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.List; import javax.inject.Inject; import javax.inject.Named; import dagger.hilt.android.AndroidEntryPoint; +import okhttp3.MediaType; +import okhttp3.MultipartBody; +import okhttp3.RequestBody; +import okhttp3.ResponseBody; @AndroidEntryPoint public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickListener, StompChatManager.MessageListener, @@ -47,59 +72,46 @@ 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 ChatAdapter activeChatAdapter; + private ChatAdapter closedChatAdapter; private MessageAdapter messageAdapter; - // Data - private final List chatList = new ArrayList<>(); + private final List activeChatList = new ArrayList<>(); + private final List closedChatList = new ArrayList<>(); private final List messageList = new ArrayList<>(); - private final Map customerNames = new HashMap<>(); private Uri pendingAttachmentUri; @Inject TokenManager tokenManager; @Inject @Named("baseUrl") String baseUrl; - // chat - private Long currentUserId; private Long activeConversationId; private StompChatManager stompChatManager; private ActivityResultLauncher attachmentLauncher; - /** - * Initializes the attachment launcher to handle file selection from the gallery. - */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - viewModel = new ViewModelProvider(this).get(ChatViewModel.class); + viewModel = new ViewModelProvider(requireActivity()).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,63 +120,208 @@ 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()); + setupDrawerToggles(); 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); + private void setupDrawerToggles() { + binding.headerActiveChats.setOnClickListener(v -> { + if (binding.rvActiveChats.getVisibility() == View.VISIBLE) { + binding.rvActiveChats.setVisibility(View.GONE); + binding.ivActiveChevron.setImageResource(android.R.drawable.arrow_down_float); + } else { + binding.rvActiveChats.setVisibility(View.VISIBLE); + binding.ivActiveChevron.setImageResource(android.R.drawable.arrow_up_float); + } + }); + + binding.headerClosedChats.setOnClickListener(v -> { + if (binding.rvClosedChats.getVisibility() == View.VISIBLE) { + binding.rvClosedChats.setVisibility(View.GONE); + binding.ivClosedChevron.setImageResource(android.R.drawable.arrow_down_float); + } else { + binding.rvClosedChats.setVisibility(View.VISIBLE); + binding.ivClosedChevron.setImageResource(android.R.drawable.arrow_up_float); + } + }); + } + + private void setupRecyclerViews() { + activeChatAdapter = new ChatAdapter(activeChatList, this); + binding.rvActiveChats.setLayoutManager(new LinearLayoutManager(getContext())); + binding.rvActiveChats.setAdapter(activeChatAdapter); + + closedChatAdapter = new ChatAdapter(closedChatList, this); + binding.rvClosedChats.setLayoutManager(new LinearLayoutManager(getContext())); + binding.rvClosedChats.setAdapter(closedChatAdapter); - // set up RecyclerView for selected chat to show messages messageAdapter = new MessageAdapter(messageList, null); + messageAdapter.setBaseUrl(baseUrl); + + messageAdapter.setOnAttachmentClickListener(message -> { + if (message.getAttachmentMimeType() != null && message.getAttachmentMimeType().startsWith("image/")) { + showFullScreenImage(message); + } else { + downloadFile(message); + } + }); + LinearLayoutManager lm = new LinearLayoutManager(getContext()); lm.setStackFromEnd(true); binding.rvMessages.setLayoutManager(lm); binding.rvMessages.setAdapter(messageAdapter); - setConversationActive(false); + setConversationActive(false, null); + } + + private void showFullScreenImage(Message message) { + if (baseUrl == null || message.getId() == null) return; + + Dialog dialog = new Dialog(requireContext(), android.R.style.Theme_Black_NoTitleBar_Fullscreen); + dialog.setContentView(R.layout.dialog_full_screen_image); + + ImageView imageView = dialog.findViewById(R.id.ivFullScreen); + ImageButton closeButton = dialog.findViewById(R.id.btnClose); + ImageButton downloadButton = dialog.findViewById(R.id.btnDownload); + + String cleanBase = baseUrl.endsWith("/") ? baseUrl.substring(0, baseUrl.length() - 1) : baseUrl; + String downloadUrl = cleanBase + "/api/v1/chat/messages/" + message.getId() + "/attachment"; + + GlideUtils.loadImageWithToken(requireContext(), imageView, downloadUrl, tokenManager.getToken(), R.drawable.placeholder); + + closeButton.setOnClickListener(v -> dialog.dismiss()); + downloadButton.setOnClickListener(v -> downloadFile(message)); + imageView.setOnClickListener(v -> dialog.dismiss()); + dialog.show(); + } + + private void downloadFile(Message message) { + if (message.getId() == null) return; + + DialogUtils.showConfirmDialog(requireContext(), "Download Attachment", + "Do you want to download \"" + message.getAttachmentName() + "\"?", () -> { + Toast.makeText(requireContext(), "Downloading " + message.getAttachmentName() + "...", Toast.LENGTH_SHORT).show(); + + viewModel.downloadAttachment(message.getId()).observe(getViewLifecycleOwner(), resource -> { + if (resource == null) return; + setLoading(resource.status == Resource.Status.LOADING); + if (resource.status == Resource.Status.SUCCESS && resource.data != null) { + saveFileToDownloads(resource.data, message.getAttachmentName(), message.getAttachmentMimeType()); + } else if (resource.status == Resource.Status.ERROR) { + Toast.makeText(requireContext(), "Download failed: " + resource.message, Toast.LENGTH_SHORT).show(); + } + }); + }); + } + + private void saveFileToDownloads(ResponseBody body, String fileName, String mimeType) { + try { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + ContentValues values = new ContentValues(); + values.put(MediaStore.Downloads.DISPLAY_NAME, fileName); + values.put(MediaStore.Downloads.MIME_TYPE, mimeType); + values.put(MediaStore.Downloads.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS); + + Uri uri = requireContext().getContentResolver().insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, values); + if (uri != null) { + try (OutputStream outputStream = requireContext().getContentResolver().openOutputStream(uri); + InputStream inputStream = body.byteStream()) { + byte[] buffer = new byte[4096]; + int bytesRead; + while ((bytesRead = inputStream.read(buffer)) != -1) { + outputStream.write(buffer, 0, bytesRead); + } + Toast.makeText(requireContext(), "File saved to Downloads", Toast.LENGTH_SHORT).show(); + } + } + } else { + File downloadsDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS); + File file = new File(downloadsDir, fileName); + try (OutputStream outputStream = new FileOutputStream(file); + InputStream inputStream = body.byteStream()) { + byte[] buffer = new byte[4096]; + int bytesRead; + while ((bytesRead = inputStream.read(buffer)) != -1) { + outputStream.write(buffer, 0, bytesRead); + } + Toast.makeText(requireContext(), "File saved to Downloads: " + file.getAbsolutePath(), Toast.LENGTH_LONG).show(); + } + } + body.close(); + } catch (Exception e) { + Log.e(TAG, "Error saving file", e); + Toast.makeText(requireContext(), "Error saving file", Toast.LENGTH_SHORT).show(); + } + } + + private void observeViewModel() { + viewModel.getActiveChats().observe(getViewLifecycleOwner(), list -> { + activeChatList.clear(); + activeChatList.addAll(list); + activeChatAdapter.notifyDataSetChanged(); + updateTitleAndStateIfActive(list); + }); + + viewModel.getClosedChats().observe(getViewLifecycleOwner(), list -> { + closedChatList.clear(); + closedChatList.addAll(list); + closedChatAdapter.notifyDataSetChanged(); + updateTitleAndStateIfActive(list); + }); + + viewModel.getMessageList().observe(getViewLifecycleOwner(), list -> { + messageList.clear(); + messageList.addAll(list); + messageAdapter.notifyDataSetChanged(); + scrollToBottom(); + }); + + viewModel.getIsLoading().observe(getViewLifecycleOwner(), this::setLoading); + } + + private void setLoading(boolean loading) { + if (binding != null && binding.progressBar != null) { + binding.progressBar.setVisibility(loading ? View.VISIBLE : View.GONE); + } + } + + private void updateTitleAndStateIfActive(List list) { + if (activeConversationId != null) { + for (Chat chat : list) { + if (chat.getChatId().equals(String.valueOf(activeConversationId))) { + binding.tvChatTitle.setText(chat.getCustomerName()); + setConversationActive(true, chat.getStatus()); + break; + } + } + } } - /** - * Loads authentication tokens and user info, then initializes the Stomp WebSocket connection. - */ 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,141 +329,60 @@ 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"); + } else { + activeConversationId = viewModel.getLastActiveConversationId(); } - loadCustomers(); + viewModel.loadCustomers(); + + if (activeConversationId != null) { + if (stompChatManager != null) stompChatManager.subscribeToConversation(activeConversationId); + viewModel.loadMessageHistory(activeConversationId); + } else { + setConversationActive(false, null); + } } - /** - * 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()); - setConversationActive(true); + viewModel.setLastActiveConversationId(activeConversationId); + + setConversationActive(true, chat.getStatus()); 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 -> { - if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) { - messageList.add(dtoToModel(resource.data)); - messageAdapter.notifyItemInserted(messageList.size() - 1); - scrollToBottom(); - loadConversations(); + viewModel.sendMessage(activeConversationId, text).observe(getViewLifecycleOwner(), resource -> { + if (resource == null) return; + setLoading(resource.status == Resource.Status.LOADING); + if (resource.status == Resource.Status.SUCCESS && resource.data != null) { + 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(FileUtils.getFileName(requireContext(), uri)); if (mimeType != null && mimeType.startsWith("image/")) { binding.ivPreview.setVisibility(View.VISIBLE); Glide.with(this).load(uri).into(binding.ivPreview); @@ -315,183 +391,94 @@ 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 (result == null) { - result = uri.getPath(); - int cut = result.lastIndexOf('/'); - 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; + + File file = FileUtils.getFileFromUri(requireContext(), uri); + if (file == null) { + Toast.makeText(requireContext(), "Failed to prepare file", Toast.LENGTH_SHORT).show(); + return; + } + String text = binding.etMessage.getText().toString().trim(); + + MultipartBody.Part contentPart = text.isEmpty() + ? null + : MultipartBody.Part.createFormData("content", text); + + String mimeType = requireContext().getContentResolver().getType(uri); + if (mimeType == null) mimeType = "application/octet-stream"; + + RequestBody filePartBody = RequestBody.create(file, MediaType.parse(mimeType)); + MultipartBody.Part filePart = MultipartBody.Part.createFormData("file", file.getName(), filePartBody); + binding.etMessage.setText(""); removeAttachment(); - if (!text.isEmpty()) { - binding.etMessage.setText(text); - } - Toast.makeText(requireContext(), "File attachments are not supported", Toast.LENGTH_SHORT).show(); - } - - /** - * 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(); + viewModel.sendMessageWithAttachment(activeConversationId, contentPart, filePart).observe(getViewLifecycleOwner(), resource -> { + if (resource == null) return; + setLoading(resource.status == Resource.Status.LOADING); + if (resource.status == Resource.Status.SUCCESS && resource.data != null) { + viewModel.addMessageLocally(resource.data); + viewModel.loadConversations(); + } else if (resource.status == Resource.Status.ERROR) { + Toast.makeText(requireContext(), "Failed to send attachment: " + resource.message, Toast.LENGTH_SHORT).show(); + } + }); + } + + @Override + public void onMessageReceived(MessageDTO dto) { + requireActivity().runOnUiThread(() -> { + 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(), "")); }); } - /** - * 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); + setConversationActive(true, dto.getStatus()); + 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,60 +486,26 @@ 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); - binding.btnAttach.setEnabled(active); + private void setConversationActive(boolean active, String status) { + boolean isClosed = "CLOSED".equalsIgnoreCase(status); + UIUtils.setViewsEnabled(active && !isClosed, binding.btnSend, binding.etMessage, binding.btnAttach); + if (!active) { activeConversationId = null; 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(""); binding.etMessage.setHint("Select a chat to start messaging"); } else { - binding.etMessage.setHint("Type a message..."); + binding.etMessage.setHint(isClosed ? "This chat is closed" : "Type a message..."); ChatNotificationService.activeConversationIdInUi = activeConversationId; } } - /** - * Disconnects the WebSocket manager when the fragment view is destroyed. - */ @Override public void onDestroyView() { super.onDestroyView(); diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/ProfileFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/ProfileFragment.java index af469295..b31243c2 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/ProfileFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/ProfileFragment.java @@ -172,6 +172,12 @@ public class ProfileFragment extends Fragment { return binding.getRoot(); } + private void setLoading(boolean loading) { + if (binding != null && binding.progressBar != null) { + binding.progressBar.setVisibility(loading ? View.VISIBLE : View.GONE); + } + } + @Override public void onDestroyView() { super.onDestroyView(); @@ -184,6 +190,7 @@ public class ProfileFragment extends Fragment { private void loadProfileData() { viewModel.getMe().observe(getViewLifecycleOwner(), resource -> { if (resource == null) return; + setLoading(resource.status == Resource.Status.LOADING); if (resource.status == Resource.Status.SUCCESS && resource.data != null) { currentUser = resource.data; @@ -229,6 +236,7 @@ public class ProfileFragment extends Fragment { //Call the backend to upload the avatar viewModel.uploadAvatar(body).observe(getViewLifecycleOwner(), resource -> { if (resource == null) return; + setLoading(resource.status == Resource.Status.LOADING); if (resource.status == Resource.Status.SUCCESS) { Toast.makeText(getContext(), "Avatar updated successfully", Toast.LENGTH_SHORT).show(); loadProfileData(); @@ -247,6 +255,7 @@ public class ProfileFragment extends Fragment { private void deleteAvatar() { viewModel.deleteAvatar().observe(getViewLifecycleOwner(), resource -> { if (resource == null) return; + setLoading(resource.status == Resource.Status.LOADING); if (resource.status == Resource.Status.SUCCESS) { hasImage = false; binding.imgProfile.setImageResource(R.drawable.placeholder); @@ -266,6 +275,7 @@ public class ProfileFragment extends Fragment { viewModel.updateMe(updates).observe(getViewLifecycleOwner(), resource -> { if (resource == null) return; + setLoading(resource.status == Resource.Status.LOADING); if (resource.status == Resource.Status.SUCCESS && resource.data != null) { currentUser = resource.data; Toast.makeText(getContext(), "Profile updated successfully", Toast.LENGTH_SHORT).show(); diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AdoptionFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AdoptionFragment.java index 577d58b1..2d687c6e 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AdoptionFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AdoptionFragment.java @@ -24,9 +24,8 @@ import com.example.petstoremobile.utils.BulkDeleteHandler; import com.example.petstoremobile.utils.Resource; import com.example.petstoremobile.utils.SpinnerUtils; import com.example.petstoremobile.utils.UIUtils; -import com.example.petstoremobile.viewmodels.AdoptionViewModel; +import com.example.petstoremobile.viewmodels.AdoptionListViewModel; import com.example.petstoremobile.utils.EventDecorator; -import com.example.petstoremobile.viewmodels.StoreViewModel; import com.prolificinteractive.materialcalendarview.CalendarDay; import com.prolificinteractive.materialcalendarview.CalendarMode; @@ -46,28 +45,19 @@ public class AdoptionFragment extends Fragment implements AdoptionAdapter.OnAdop private FragmentAdoptionBinding binding; private List adoptionList = new ArrayList<>(); - private List storeList = new ArrayList<>(); private AdoptionAdapter adapter; - private AdoptionViewModel adoptionViewModel; - private StoreViewModel storeViewModel; + private AdoptionListViewModel viewModel; private BulkDeleteHandler bulkDeleteHandler; private CalendarDay selectedCalendarDay; private boolean isMonthMode = false; private final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()); - /** - * Initializes the fragment and its ViewModels. - */ @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); - adoptionViewModel = new ViewModelProvider(this).get(AdoptionViewModel.class); - storeViewModel = new ViewModelProvider(this).get(StoreViewModel.class); + viewModel = new ViewModelProvider(this).get(AdoptionListViewModel.class); } - /** - * Sets up the fragment's UI components, including RecyclerView, Search, SwipeRefresh, and Calendar. - */ @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { @@ -81,6 +71,7 @@ public class AdoptionFragment extends Fragment implements AdoptionAdapter.OnAdop setupCalendar(); setupFilterToggle(); setupBulkDelete(); + observeViewModel(); binding.fabAddAdoption.setOnClickListener(v -> openDetail(-1)); @@ -91,6 +82,24 @@ public class AdoptionFragment extends Fragment implements AdoptionAdapter.OnAdop return binding.getRoot(); } + private void observeViewModel() { + viewModel.getAdoptions().observe(getViewLifecycleOwner(), list -> { + adoptionList.clear(); + adoptionList.addAll(list); + updateCalendarDecorators(); + adapter.notifyDataSetChanged(); + }); + + viewModel.getStores().observe(getViewLifecycleOwner(), list -> { + SpinnerUtils.populateWhiteSpinner(requireContext(), binding.spinnerStoreAdoption, list, + StoreDTO::getStoreName, "All Stores", -1L, StoreDTO::getStoreId); + }); + + viewModel.getIsLoading().observe(getViewLifecycleOwner(), loading -> { + binding.swipeRefreshAdoption.setRefreshing(loading); + }); + } + private void setupBulkDelete() { bulkDeleteHandler = new BulkDeleteHandler( this, @@ -99,27 +108,18 @@ public class AdoptionFragment extends Fragment implements AdoptionAdapter.OnAdop binding.btnBulkDelete, adapter, "adoption", - adoptionViewModel::bulkDeleteAdoptions, + viewModel::bulkDeleteAdoptions, this::loadAdoptions ); } - @Override - public void onDestroyView() { - super.onDestroyView(); - binding = null; - } - @Override public void onResume() { super.onResume(); loadAdoptions(); - loadStoreData(); + viewModel.loadStores(); } - /** - * Toggles the calendar display between week and month modes. - */ private void toggleCalendarMode() { isMonthMode = !isMonthMode; binding.calendarViewAdoption.state().edit() @@ -127,35 +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); - - binding.btnToggleFilterAdoption.setOnClickListener(v -> { - boolean isVisible = binding.layoutFilterAdoption.getVisibility() == View.VISIBLE; - binding.layoutFilterAdoption.setVisibility(isVisible ? View.GONE : View.VISIBLE); - - binding.btnToggleFilterAdoption.setImageResource(isVisible ? - android.R.drawable.ic_menu_search : - android.R.drawable.ic_menu_close_clear_cancel); - - if (isVisible) { - binding.etSearchAdoption.setText(""); - binding.spinnerStatusAdoption.setSelection(0); - binding.spinnerStoreAdoption.setSelection(0); - selectedCalendarDay = null; - binding.calendarViewAdoption.clearSelection(); - loadAdoptions(); - } - }); } - /** - * Sets up the date selection listener for the calendar. - */ private void setupCalendar() { binding.calendarViewAdoption.setOnDateChangedListener((widget, date, selected) -> { if (selected) { @@ -172,9 +148,6 @@ public class AdoptionFragment extends Fragment implements AdoptionAdapter.OnAdop }); } - /** - * Updates the calendar decorators to highlight days with adoptions. - */ private void updateCalendarDecorators() { HashSet datesWithAdoptions = new HashSet<>(); for (AdoptionDTO adoption : adoptionList) { @@ -195,67 +168,37 @@ public class AdoptionFragment extends Fragment implements AdoptionAdapter.OnAdop binding.calendarViewAdoption.addDecorator(new EventDecorator(Color.RED, datesWithAdoptions)); } - /** - * Initializes the RecyclerView for displaying adoptions. - */ private void setupRecyclerView() { adapter = new AdoptionAdapter(adoptionList, this); binding.recyclerViewAdoptions.setLayoutManager(new LinearLayoutManager(getContext())); binding.recyclerViewAdoptions.setAdapter(adapter); } - /** - * Sets up the search bar for filtering - */ private void setupSearch() { UIUtils.attachSearch(binding.etSearchAdoption, this::loadAdoptions); } - /** - * Configures the status filter spinner. - */ private void setupStatusFilter() { String[] statuses = {"All Statuses", "Completed", "Pending", "Cancelled"}; SpinnerUtils.setupStringFilterSpinner(requireContext(), binding.spinnerStatusAdoption, statuses, this::loadAdoptions); } - /** - * Configures the store filter spinner. - */ private void setupStoreFilter() { SpinnerUtils.setupFilterSpinner(binding.spinnerStoreAdoption, this::loadAdoptions); } - /** - * Fetches store data to populate the store filter. - */ - private void loadStoreData() { - storeViewModel.getAllStores(0, 100).observe(getViewLifecycleOwner(), resource -> { - if (resource.status == Resource.Status.SUCCESS && resource.data != null) { - storeList = resource.data.getContent(); - SpinnerUtils.populateWhiteSpinner(requireContext(), binding.spinnerStoreAdoption, storeList, - StoreDTO::getStoreName, "All Stores", -1L, StoreDTO::getStoreId); - } - }); - } - - /** - * Sets up the SwipeRefreshLayout to reload adoption data. - */ private void setupSwipeRefresh() { binding.swipeRefreshAdoption.setOnRefreshListener(this::loadAdoptions); } - /** - * Fetches the adoption list from the server through the ViewModel. - */ private void loadAdoptions() { String query = binding.etSearchAdoption.getText().toString().trim(); String status = binding.spinnerStatusAdoption.getSelectedItem() != null ? binding.spinnerStatusAdoption.getSelectedItem().toString() : "All Statuses"; Long storeId = null; - if (binding.spinnerStoreAdoption.getSelectedItemPosition() > 0 && !storeList.isEmpty()) { - storeId = storeList.get(binding.spinnerStoreAdoption.getSelectedItemPosition() - 1).getStoreId(); + List stores = viewModel.getStores().getValue(); + if (binding.spinnerStoreAdoption.getSelectedItemPosition() > 0 && stores != null && !stores.isEmpty()) { + storeId = stores.get(binding.spinnerStoreAdoption.getSelectedItemPosition() - 1).getStoreId(); } String selectedDateString = null; @@ -267,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, selectedDateString, null); } - /** - * 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); } @@ -322,4 +231,10 @@ public class AdoptionFragment extends Fragment implements AdoptionAdapter.OnAdop bulkDeleteHandler.onSelectionChanged(selectedCount); } } + + @Override + public void onDestroyView() { + super.onDestroyView(); + binding = null; + } } diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AnalyticsFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AnalyticsFragment.java index dbd24c75..44719127 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AnalyticsFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AnalyticsFragment.java @@ -2,16 +2,14 @@ package com.example.petstoremobile.fragments.listfragments; import android.graphics.Color; import android.os.Bundle; -import android.util.Log; import android.view.*; import android.widget.*; import androidx.annotation.NonNull; import androidx.fragment.app.Fragment; import androidx.lifecycle.ViewModelProvider; import com.example.petstoremobile.databinding.FragmentAnalyticsBinding; -import com.example.petstoremobile.dtos.SaleDTO; import com.example.petstoremobile.utils.UIUtils; -import com.example.petstoremobile.viewmodels.SaleViewModel; +import com.example.petstoremobile.viewmodels.AnalyticsViewModel; import dagger.hilt.android.AndroidEntryPoint; import java.math.BigDecimal; import java.math.RoundingMode; @@ -21,228 +19,130 @@ 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 -> { + binding.progressBar.setVisibility(loading ? View.VISIBLE : View.GONE); + 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(); - - // Show loading - binding.tvTotalRevenue.setText("Loading..."); - binding.tvTotalTransactions.setText("..."); - binding.tvAvgTransaction.setText("..."); - binding.tvTotalItems.setText("..."); - - 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: - // Already showing loading state in UI - break; - } - } - }); - } - - private void computeAndDisplay(List sales) { - // Filter out refunds for most metrics - List regularSales = new ArrayList<>(); - for (SaleDTO s : sales) { - if (!Boolean.TRUE.equals(s.getIsRefund())) - regularSales.add(s); - } - - // ── Summary ────────────────────────────────────────── - BigDecimal totalRevenue = BigDecimal.ZERO; - int totalItems = 0; - - for (SaleDTO s : regularSales) { - if (s.getTotalAmount() != null) - totalRevenue = totalRevenue.add(s.getTotalAmount()); - if (s.getItems() != null) { - for (SaleDTO.SaleItemDTO item : s.getItems()) { - if (item.getQuantity() != null) - totalItems += Math.abs(item.getQuantity()); - } + if (data.topRevenueProducts != null && !data.topRevenueProducts.isEmpty()) { + BigDecimal maxRevenue = data.topRevenueProducts.get(0).getValue(); + if (maxRevenue.compareTo(BigDecimal.ZERO) == 0) maxRevenue = BigDecimal.ONE; + for (Map.Entry e : data.topRevenueProducts) { + addBarRow(binding.llTopRevenue, e.getKey(), "$" + e.getValue().setScale(2, RoundingMode.HALF_UP), + e.getValue().floatValue() / maxRevenue.floatValue(), "#ff6b35"); } - } - - int totalTx = regularSales.size(); - BigDecimal avgTx = totalTx > 0 - ? totalRevenue.divide(BigDecimal.valueOf(totalTx), 2, RoundingMode.HALF_UP) - : BigDecimal.ZERO; - - binding.tvTotalRevenue.setText("$" + totalRevenue.setScale(2, RoundingMode.HALF_UP)); - binding.tvTotalTransactions.setText(String.valueOf(totalTx)); - binding.tvAvgTransaction.setText("$" + avgTx); - binding.tvTotalItems.setText(String.valueOf(totalItems)); - - // ── Top Products by Revenue ─────────────────────────── - Map revenueByProduct = new LinkedHashMap<>(); - Map quantityByProduct = new LinkedHashMap<>(); - - for (SaleDTO s : regularSales) { - if (s.getItems() != null) { - for (SaleDTO.SaleItemDTO item : s.getItems()) { - String name = item.getProductName() != null ? item.getProductName() : "Unknown"; - int qty = item.getQuantity() != null ? Math.abs(item.getQuantity()) : 0; - BigDecimal lineTotal = item.getUnitPrice() != null - ? item.getUnitPrice().multiply(BigDecimal.valueOf(qty)) - : BigDecimal.ZERO; - - revenueByProduct.merge(name, lineTotal, BigDecimal::add); - quantityByProduct.merge(name, qty, Integer::sum); - } - } - } - - // Sort by revenue desc, take top 5 - List> topRevenue = new ArrayList<>(revenueByProduct.entrySet()); - topRevenue.sort((a, b) -> b.getValue().compareTo(a.getValue())); - BigDecimal maxRevenue = topRevenue.isEmpty() ? BigDecimal.ONE : topRevenue.get(0).getValue(); - - binding.llTopRevenue.removeAllViews(); - for (int i = 0; i < Math.min(5, topRevenue.size()); i++) { - Map.Entry e = topRevenue.get(i); - addBarRow(binding.llTopRevenue, e.getKey(), "$" + e.getValue().setScale(2, RoundingMode.HALF_UP), - e.getValue().floatValue() / maxRevenue.floatValue(), "#ff6b35"); - } - if (topRevenue.isEmpty()) + } else { addEmptyRow(binding.llTopRevenue, "No data"); + } - // Sort by quantity desc, take top 5 - List> topQuantity = new ArrayList<>(quantityByProduct.entrySet()); - topQuantity.sort((a, b) -> b.getValue() - a.getValue()); - int maxQty = topQuantity.isEmpty() ? 1 : topQuantity.get(0).getValue(); - + // Top Quantity Products binding.llTopQuantity.removeAllViews(); - for (int i = 0; i < Math.min(5, topQuantity.size()); i++) { - Map.Entry e = topQuantity.get(i); - addBarRow(binding.llTopQuantity, e.getKey(), e.getValue() + " units", - (float) e.getValue() / maxQty, "#4ecdc4"); - } - if (topQuantity.isEmpty()) - addEmptyRow(binding.llTopQuantity, "No data"); - - // ── Payment Methods ─────────────────────────────────── - Map paymentCount = new LinkedHashMap<>(); - for (SaleDTO s : regularSales) { - String method = s.getPaymentMethod() != null ? s.getPaymentMethod() : "Unknown"; - paymentCount.merge(method, 1, Integer::sum); - } - - int maxPayment = paymentCount.values().stream().max(Integer::compare).orElse(1); - String[] paymentColors = { "#1a759f", "#ff9f1c", "#577590", "#90be6d" }; - int ci = 0; - binding.llPaymentMethods.removeAllViews(); - for (Map.Entry e : paymentCount.entrySet()) { - addBarRow(binding.llPaymentMethods, e.getKey(), - e.getValue() + " transactions", - (float) e.getValue() / maxPayment, - paymentColors[ci % paymentColors.length]); - ci++; - } - if (paymentCount.isEmpty()) - addEmptyRow(binding.llPaymentMethods, "No data"); - - // ── Employee Performance ────────────────────────────── - Map employeeRevenue = new LinkedHashMap<>(); - for (SaleDTO s : regularSales) { - String emp = s.getEmployeeName() != null ? s.getEmployeeName() : "Unknown"; - if (s.getTotalAmount() != null) - employeeRevenue.merge(emp, s.getTotalAmount(), BigDecimal::add); - } - - List> empList = new ArrayList<>(employeeRevenue.entrySet()); - empList.sort((a, b) -> b.getValue().compareTo(a.getValue())); - BigDecimal maxEmp = empList.isEmpty() ? BigDecimal.ONE : empList.get(0).getValue(); - - binding.llEmployeePerformance.removeAllViews(); - for (Map.Entry e : empList) { - addBarRow(binding.llEmployeePerformance, e.getKey(), - "$" + e.getValue().setScale(2, RoundingMode.HALF_UP), - e.getValue().floatValue() / maxEmp.floatValue(), - "#1a759f"); - } - if (empList.isEmpty()) - addEmptyRow(binding.llEmployeePerformance, "No data"); - - // ── Daily Revenue (last 7 days) ─────────────────────── - Map dailyRevenue = new TreeMap<>(); - - // Initialize last 7 days - Calendar cal = Calendar.getInstance(); - for (int i = 6; i >= 0; i--) { - Calendar day = Calendar.getInstance(); - day.add(Calendar.DAY_OF_YEAR, -i); - String key = String.format("%04d-%02d-%02d", - day.get(Calendar.YEAR), - day.get(Calendar.MONTH) + 1, - day.get(Calendar.DAY_OF_MONTH)); - dailyRevenue.put(key, BigDecimal.ZERO); - } - - for (SaleDTO s : regularSales) { - if (s.getSaleDate() != null && s.getTotalAmount() != null) { - String date = s.getSaleDate().length() >= 10 - ? s.getSaleDate().substring(0, 10) - : s.getSaleDate(); - if (dailyRevenue.containsKey(date)) { - dailyRevenue.merge(date, s.getTotalAmount(), BigDecimal::add); - } + if (data.topQuantityProducts != null && !data.topQuantityProducts.isEmpty()) { + int maxQty = data.topQuantityProducts.get(0).getValue(); + if (maxQty == 0) maxQty = 1; + for (Map.Entry e : data.topQuantityProducts) { + addBarRow(binding.llTopQuantity, e.getKey(), e.getValue() + " units", + (float) e.getValue() / maxQty, "#4ecdc4"); } + } else { + addEmptyRow(binding.llTopQuantity, "No data"); } - BigDecimal maxDaily = dailyRevenue.values().stream() - .max(BigDecimal::compareTo).orElse(BigDecimal.ONE); - if (maxDaily.compareTo(BigDecimal.ZERO) == 0) - maxDaily = BigDecimal.ONE; + // Payment Methods + binding.llPaymentMethods.removeAllViews(); + if (data.paymentMethodStats != null && !data.paymentMethodStats.isEmpty()) { + int maxPayment = data.paymentMethodStats.stream().mapToInt(Map.Entry::getValue).max().orElse(1); + String[] paymentColors = { "#1a759f", "#ff9f1c", "#577590", "#90be6d" }; + int ci = 0; + for (Map.Entry e : data.paymentMethodStats) { + addBarRow(binding.llPaymentMethods, e.getKey(), + e.getValue() + " transactions", + (float) e.getValue() / maxPayment, + paymentColors[ci % paymentColors.length]); + ci++; + } + } else { + addEmptyRow(binding.llPaymentMethods, "No data"); + } + // Employee Performance + binding.llEmployeePerformance.removeAllViews(); + if (data.employeePerformance != null && !data.employeePerformance.isEmpty()) { + BigDecimal maxEmp = data.employeePerformance.get(data.employeePerformance.size() - 1).getValue(); + if (maxEmp.compareTo(BigDecimal.ZERO) == 0) maxEmp = BigDecimal.ONE; + maxEmp = data.employeePerformance.get(0).getValue(); + if (maxEmp.compareTo(BigDecimal.ZERO) == 0) maxEmp = BigDecimal.ONE; + + for (Map.Entry e : data.employeePerformance) { + addBarRow(binding.llEmployeePerformance, e.getKey(), + "$" + e.getValue().setScale(2, RoundingMode.HALF_UP), + e.getValue().floatValue() / maxEmp.floatValue(), + "#1a759f"); + } + } else { + addEmptyRow(binding.llEmployeePerformance, "No data"); + } + + // Daily Revenue binding.llDailyRevenue.removeAllViews(); - for (Map.Entry e : dailyRevenue.entrySet()) { - // Show just MM-DD - String label = e.getKey().length() >= 10 - ? e.getKey().substring(5) - : e.getKey(); - addBarRow(binding.llDailyRevenue, label, - "$" + e.getValue().setScale(2, RoundingMode.HALF_UP), - e.getValue().floatValue() / maxDaily.floatValue(), - "#ff6b35"); + if (data.dailyRevenue != null && !data.dailyRevenue.isEmpty()) { + BigDecimal maxDaily = data.dailyRevenue.stream().map(Map.Entry::getValue).max(BigDecimal::compareTo).orElse(BigDecimal.ONE); + if (maxDaily.compareTo(BigDecimal.ZERO) == 0) maxDaily = BigDecimal.ONE; + for (Map.Entry e : data.dailyRevenue) { + String label = e.getKey().length() >= 10 ? e.getKey().substring(5) : e.getKey(); + addBarRow(binding.llDailyRevenue, label, + "$" + e.getValue().setScale(2, RoundingMode.HALF_UP), + e.getValue().floatValue() / maxDaily.floatValue(), + "#ff6b35"); + } } } diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AppointmentFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AppointmentFragment.java index 3d4d6cd6..8610790e 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AppointmentFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AppointmentFragment.java @@ -14,7 +14,6 @@ import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import android.widget.Toast; import com.example.petstoremobile.R; import com.example.petstoremobile.adapters.AppointmentAdapter; @@ -25,10 +24,9 @@ import com.example.petstoremobile.utils.BulkDeleteHandler; import com.example.petstoremobile.utils.Resource; import com.example.petstoremobile.utils.SpinnerUtils; import com.example.petstoremobile.utils.UIUtils; -import com.example.petstoremobile.viewmodels.AppointmentViewModel; +import com.example.petstoremobile.viewmodels.AppointmentListViewModel; import com.example.petstoremobile.utils.EventDecorator; import com.example.petstoremobile.viewmodels.AuthViewModel; -import com.example.petstoremobile.viewmodels.StoreViewModel; import com.prolificinteractive.materialcalendarview.CalendarDay; import com.prolificinteractive.materialcalendarview.CalendarMode; @@ -48,11 +46,9 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter. private FragmentAppointmentBinding binding; private List appointmentList = new ArrayList<>(); - private List storeList = new ArrayList<>(); private AppointmentAdapter adapter; - private AppointmentViewModel appointmentViewModel; - private StoreViewModel storeViewModel; + private AppointmentListViewModel viewModel; private AuthViewModel authViewModel; private BulkDeleteHandler bulkDeleteHandler; @@ -61,20 +57,13 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter. private Long currentUserId = null; private final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()); - /** - * Initializes the fragment and its associated ViewModels. - */ @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); - appointmentViewModel = new ViewModelProvider(this).get(AppointmentViewModel.class); - storeViewModel = new ViewModelProvider(this).get(StoreViewModel.class); + viewModel = new ViewModelProvider(this).get(AppointmentListViewModel.class); authViewModel = new ViewModelProvider(this).get(AuthViewModel.class); } - /** - * Sets up the fragment's UI, including RecyclerView, search, swipe-to-refresh, and calendar. - */ @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { @@ -89,6 +78,7 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter. setupFilterToggle(); setupMyAppointmentFilter(); setupBulkDelete(); + observeViewModel(); binding.fabAddAppointment.setOnClickListener(v -> openAppointmentDetails(-1)); @@ -101,6 +91,24 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter. return binding.getRoot(); } + private void observeViewModel() { + viewModel.getAppointments().observe(getViewLifecycleOwner(), list -> { + appointmentList.clear(); + appointmentList.addAll(list); + updateCalendarDecorators(); + adapter.notifyDataSetChanged(); + }); + + viewModel.getStores().observe(getViewLifecycleOwner(), list -> { + SpinnerUtils.populateWhiteSpinner(requireContext(), binding.spinnerStore, list, + StoreDTO::getStoreName, "All Stores", -1L, StoreDTO::getStoreId); + }); + + viewModel.getIsLoading().observe(getViewLifecycleOwner(), loading -> { + binding.swipeRefreshAppointment.setRefreshing(loading); + }); + } + private void setupBulkDelete() { bulkDeleteHandler = new BulkDeleteHandler( this, @@ -109,27 +117,18 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter. binding.btnBulkDelete, adapter, "appointment", - appointmentViewModel::bulkDeleteAppointments, + viewModel::bulkDeleteAppointments, this::loadAppointmentData ); } - @Override - public void onDestroyView() { - super.onDestroyView(); - binding = null; - } - @Override public void onResume() { super.onResume(); loadAppointmentData(); - loadStoreData(); + viewModel.loadStores(); } - /** - * Toggles the calendar between week and month display modes. - */ private void toggleCalendarMode() { isMonthMode = !isMonthMode; binding.calendarView.state().edit() @@ -137,18 +136,12 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter. .commit(); } - /** - * Sets up the "My Appointments" filter button. - */ private void setupMyAppointmentFilter() { binding.btnMyAppointments.setOnClickListener(v -> { loadAppointmentData(); }); } - /** - * Fetches current user info to get the employeeId. - */ private void loadCurrentUserInfo() { authViewModel.getMe().observe(getViewLifecycleOwner(), resource -> { if (resource.status == Resource.Status.SUCCESS && resource.data != null) { @@ -157,37 +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); - - // Add additional reset logic for elements specific to this fragment - binding.btnToggleFilter.setOnClickListener(v -> { - boolean isVisible = binding.layoutFilter.getVisibility() == View.VISIBLE; - binding.layoutFilter.setVisibility(isVisible ? View.GONE : View.VISIBLE); - - binding.btnToggleFilter.setImageResource(isVisible ? - android.R.drawable.ic_menu_search : - android.R.drawable.ic_menu_close_clear_cancel); - - if (isVisible) { - binding.etSearchAppointment.setText(""); - binding.spinnerStatus.setSelection(0); - binding.spinnerStore.setSelection(0); - binding.btnMyAppointments.setChecked(false); - selectedCalendarDay = null; - binding.calendarView.clearSelection(); - loadAppointmentData(); - } - }); } - /** - * Sets up the date selection listener for the calendar. - */ private void setupCalendar() { binding.calendarView.setOnDateChangedListener((widget, date, selected) -> { if (selected) { @@ -204,17 +171,11 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter. }); } - /** - * Updates calendar indicators to highlight dates that have scheduled appointments. - */ private void updateCalendarDecorators() { HashSet datesWithAppointments = new HashSet<>(); - SimpleDateFormat displayFormat = new SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()); for (AppointmentDTO appointment : appointmentList) { try { - //Get the appointment date - Date date = displayFormat.parse(appointment.getAppointmentDate()); - //if the date is not null, add it to the hashset + Date date = dateFormat.parse(appointment.getAppointmentDate()); if (date != null) { Calendar cal = Calendar.getInstance(); cal.setTime(date); @@ -224,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) { @@ -283,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); @@ -298,16 +227,14 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter. } } - /** - * Fetches appointment data from the server with all active filters. - */ private void loadAppointmentData() { String query = binding.etSearchAppointment.getText().toString().trim(); String status = binding.spinnerStatus.getSelectedItem() != null ? binding.spinnerStatus.getSelectedItem().toString() : "All Statuses"; Long storeId = null; - if (binding.spinnerStore.getSelectedItemPosition() > 0 && !storeList.isEmpty()) { - storeId = storeList.get(binding.spinnerStore.getSelectedItemPosition() - 1).getStoreId(); + List stores = viewModel.getStores().getValue(); + if (binding.spinnerStore.getSelectedItemPosition() > 0 && stores != null && !stores.isEmpty()) { + storeId = stores.get(binding.spinnerStore.getSelectedItemPosition() - 1).getStoreId(); } String selectedDateString = null; @@ -324,41 +251,18 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter. if (status.equals("All Statuses")) status = null; else status = status.toUpperCase(); - appointmentViewModel.getAllAppointments(0, 500, query, status, storeId, selectedDateString, employeeId).observe(getViewLifecycleOwner(), resource -> { - if (resource == null) return; - - // Check the status to see if the resource is loaded and display the data - switch (resource.status) { - case LOADING: - // Show loading indicator - binding.swipeRefreshAppointment.setRefreshing(true); - break; - case SUCCESS: - // Hide loading indicator and display data - binding.swipeRefreshAppointment.setRefreshing(false); - if (resource.data != null) { - appointmentList.clear(); - appointmentList.addAll(resource.data.getContent()); - updateCalendarDecorators(); - adapter.notifyDataSetChanged(); - } - break; - case ERROR: - // Hide loading indicator and toast error message - binding.swipeRefreshAppointment.setRefreshing(false); - Toast.makeText(getContext(), "Failed to load appointments: " + resource.message, Toast.LENGTH_SHORT).show(); - Log.e("AppointmentFragment", "Error loading appointments: " + resource.message); - break; - } - }); + viewModel.loadAppointments(query, status, storeId, selectedDateString, employeeId); } - /** - * Initializes the RecyclerView for displaying appointments. - */ private void setupRecyclerView() { adapter = new AppointmentAdapter(appointmentList, this); binding.recyclerViewAppointments.setLayoutManager(new LinearLayoutManager(getContext())); binding.recyclerViewAppointments.setAdapter(adapter); } + + @Override + public void onDestroyView() { + super.onDestroyView(); + binding = null; + } } diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/InventoryFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/InventoryFragment.java index 259a0a9e..6cefedb8 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/InventoryFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/InventoryFragment.java @@ -1,7 +1,6 @@ package com.example.petstoremobile.fragments.listfragments; import android.os.Bundle; -import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -22,8 +21,7 @@ import com.example.petstoremobile.dtos.InventoryDTO; import com.example.petstoremobile.dtos.StoreDTO; import com.example.petstoremobile.utils.BulkDeleteHandler; import com.example.petstoremobile.utils.UIUtils; -import com.example.petstoremobile.viewmodels.InventoryViewModel; -import com.example.petstoremobile.utils.Resource; +import com.example.petstoremobile.viewmodels.InventoryListViewModel; import com.example.petstoremobile.utils.SpinnerUtils; import java.util.ArrayList; @@ -34,33 +32,18 @@ import dagger.hilt.android.AndroidEntryPoint; @AndroidEntryPoint public class InventoryFragment extends Fragment implements InventoryAdapter.OnInventoryClickListener { - private static final String TAG = "InventoryFragment"; - private static final int PAGE_SIZE = 20; - private FragmentInventoryBinding binding; private final List inventoryList = new ArrayList<>(); - private List storeList = new ArrayList<>(); private InventoryAdapter adapter; - private InventoryViewModel viewModel; + private InventoryListViewModel viewModel; private BulkDeleteHandler bulkDeleteHandler; - // Pagination - private int currentPage = 0; - private boolean isLastPage = false; - private boolean isLoading = false; - - /** - * Initializes the fragment and its ViewModel. - */ @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); - viewModel = new ViewModelProvider(this).get(InventoryViewModel.class); + viewModel = new ViewModelProvider(this).get(InventoryListViewModel.class); } - /** - * Sets up the fragment's UI components, including the inventory list and search. - */ @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { @@ -72,8 +55,9 @@ public class InventoryFragment extends Fragment implements InventoryAdapter.OnIn setupSwipeRefresh(); setupFilterToggle(); setupBulkDelete(); + observeViewModel(); + loadInventory(true); - loadStoreData(); binding.fabAddInventory.setOnClickListener(v -> openDetail(null)); @@ -82,6 +66,23 @@ public class InventoryFragment extends Fragment implements InventoryAdapter.OnIn return binding.getRoot(); } + private void observeViewModel() { + viewModel.getInventory().observe(getViewLifecycleOwner(), list -> { + inventoryList.clear(); + inventoryList.addAll(list); + adapter.notifyDataSetChanged(); + }); + + viewModel.getStores().observe(getViewLifecycleOwner(), list -> { + SpinnerUtils.populateWhiteSpinner(requireContext(), binding.spinnerStore, list, + StoreDTO::getStoreName, "All Stores", -1L, StoreDTO::getStoreId); + }); + + viewModel.getIsLoading().observe(getViewLifecycleOwner(), loading -> { + binding.swipeRefreshInventory.setRefreshing(loading); + }); + } + private void setupBulkDelete() { bulkDeleteHandler = new BulkDeleteHandler( this, @@ -95,49 +96,30 @@ public class InventoryFragment extends Fragment implements InventoryAdapter.OnIn ); } + @Override + public void onResume() { + super.onResume(); + viewModel.loadStores(); + } + @Override public void onDestroyView() { super.onDestroyView(); binding = null; } - /** - * Sets up the filter toggle button to show/hide the filter layout. - */ private void setupFilterToggle() { UIUtils.setupFilterToggle(binding.btnToggleFilter, binding.layoutFilter, binding.etSearchInventory, binding.spinnerStore); } - /** - * Sets up the search bar for filtering. - */ private void setupSearch() { UIUtils.attachSearch(binding.etSearchInventory, () -> loadInventory(true)); } - /** - * Configures the store filter spinner. - */ private void setupStoreFilter() { SpinnerUtils.setupFilterSpinner(binding.spinnerStore, () -> loadInventory(true)); } - /** - * Fetches store data to populate the store filter. - */ - private void loadStoreData() { - viewModel.getAllStores(0, 100).observe(getViewLifecycleOwner(), resource -> { - if (resource.status == Resource.Status.SUCCESS && resource.data != null) { - storeList = resource.data.getContent(); - SpinnerUtils.populateWhiteSpinner(requireContext(), binding.spinnerStore, storeList, - StoreDTO::getStoreName, "All Stores", -1L, StoreDTO::getStoreId); - } - }); - } - - /** - * Initializes the RecyclerView with a layout manager, and adapter. - */ private void setupRecyclerView() { adapter = new InventoryAdapter(inventoryList, this); binding.recyclerViewInventory.setLayoutManager(new LinearLayoutManager(getContext())); @@ -146,105 +128,45 @@ public class InventoryFragment extends Fragment implements InventoryAdapter.OnIn binding.recyclerViewInventory.addOnScrollListener(new RecyclerView.OnScrollListener() { @Override public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) { - if (dy <= 0) - return; + if (dy <= 0) return; LinearLayoutManager lm = (LinearLayoutManager) binding.recyclerViewInventory.getLayoutManager(); - if (lm == null) - return; + if (lm == null) return; int visible = lm.getChildCount(); int total = lm.getItemCount(); int firstVis = lm.findFirstVisibleItemPosition(); - if (!isLoading && !isLastPage && (visible + firstVis) >= total - 3) { + Boolean isLoading = viewModel.getIsLoading().getValue(); + if ((isLoading == null || !isLoading) && !viewModel.isLastPage() && (visible + firstVis) >= total - 3) { loadInventory(false); } } }); } - /** - * Sets up the SwipeRefreshLayout to reload the first page of inventory items. - */ private void setupSwipeRefresh() { binding.swipeRefreshInventory.setOnRefreshListener(() -> loadInventory(true)); } - /** - * Fetches a page of inventory items from the API. - */ private void loadInventory(boolean reset) { - if (isLoading) return; - - if (reset) { - currentPage = 0; - isLastPage = false; - } - - // Search text from input String query = binding.etSearchInventory != null ? binding.etSearchInventory.getText().toString().trim() : ""; if (query.isEmpty()) query = null; Long storeId = null; - if (binding.spinnerStore.getSelectedItemPosition() > 0 && !storeList.isEmpty()) { - storeId = storeList.get(binding.spinnerStore.getSelectedItemPosition() - 1).getStoreId(); + List stores = viewModel.getStores().getValue(); + if (binding.spinnerStore.getSelectedItemPosition() > 0 && stores != null && !stores.isEmpty()) { + storeId = stores.get(binding.spinnerStore.getSelectedItemPosition() - 1).getStoreId(); } - //Load all inventory items from the backend using viewModel - viewModel.getAllInventory(query, storeId, currentPage, PAGE_SIZE, "product.prodName").observe(getViewLifecycleOwner(), resource -> { - if (resource == null) return; - - // Check the status to see if the resource is loaded and display the data - switch (resource.status) { - case LOADING: - // Show loading indicator - isLoading = true; - binding.swipeRefreshInventory.setRefreshing(true); - break; - case SUCCESS: - // Hide loading indicator and display data - isLoading = false; - binding.swipeRefreshInventory.setRefreshing(false); - if (resource.data != null) { - if (reset) inventoryList.clear(); - inventoryList.addAll(resource.data.getContent()); - adapter.notifyDataSetChanged(); - isLastPage = resource.data.isLast(); - if (!isLastPage) currentPage++; - } - break; - case ERROR: - // Hide loading indicator and toast error message - isLoading = false; - binding.swipeRefreshInventory.setRefreshing(false); - Log.e(TAG, "Error: " + resource.message); - Toast.makeText(getContext(), "Failed to load inventory: " + resource.message, Toast.LENGTH_SHORT).show(); - break; - } - }); + viewModel.loadInventory(reset, query, storeId); } - /** - * Navigates to the inventory detail screen for a specific item or to add a new one. - */ private void openDetail(InventoryDTO inv) { Bundle args = new Bundle(); - if (inv != null) { args.putLong("inventoryId", inv.getInventoryId()); } - NavHostFragment.findNavController(this).navigate(R.id.nav_inventory_detail, args); } - /** - * Reloads inventory data when changes occur. - */ - public void onInventoryChanged() { - loadInventory(true); - } - - /** - * Handles item click in the inventory list. - */ @Override public void onInventoryClick(int position) { if (position >= 0 && position < inventoryList.size()) { @@ -252,9 +174,6 @@ public class InventoryFragment extends Fragment implements InventoryAdapter.OnIn } } - /** - * Updates the bulk deletion UI visibility and count when items are selected or deselected. - */ @Override public void onSelectionChanged(int selectedCount) { if (bulkDeleteHandler != null) { diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/PetFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/PetFragment.java index e7e36eae..bc1b9a6f 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/PetFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/PetFragment.java @@ -9,7 +9,6 @@ import androidx.lifecycle.ViewModelProvider; import androidx.navigation.fragment.NavHostFragment; import androidx.recyclerview.widget.LinearLayoutManager; -import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -25,8 +24,7 @@ import com.example.petstoremobile.utils.BulkDeleteHandler; import com.example.petstoremobile.utils.Resource; import com.example.petstoremobile.utils.SpinnerUtils; import com.example.petstoremobile.utils.UIUtils; -import com.example.petstoremobile.viewmodels.PetViewModel; -import com.example.petstoremobile.viewmodels.StoreViewModel; +import com.example.petstoremobile.viewmodels.PetListViewModel; import java.util.ArrayList; import java.util.List; @@ -40,28 +38,19 @@ import dagger.hilt.android.AndroidEntryPoint; public class PetFragment extends Fragment implements PetAdapter.OnPetClickListener { private FragmentPetBinding binding; private List petList = new ArrayList<>(); - private List storeList = new ArrayList<>(); private PetAdapter adapter; - private PetViewModel viewModel; - private StoreViewModel storeViewModel; + private PetListViewModel viewModel; private BulkDeleteHandler bulkDeleteHandler; @Inject @Named("baseUrl") String baseUrl; @Inject TokenManager tokenManager; - /** - * Initializes the fragment and its associated ViewModels. - */ @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); - viewModel = new ViewModelProvider(this).get(PetViewModel.class); - storeViewModel = new ViewModelProvider(this).get(StoreViewModel.class); + viewModel = new ViewModelProvider(this).get(PetListViewModel.class); } - /** - * Sets up the fragment's UI components, including RecyclerView, filters, and swipe-to-refresh. - */ @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { @@ -75,6 +64,7 @@ public class PetFragment extends Fragment implements PetAdapter.OnPetClickListen setupSwipeRefresh(); setupFilterToggle(); setupBulkDelete(); + observeViewModel(); binding.fabAddPet.setOnClickListener(v -> openPetDetails()); @@ -83,6 +73,23 @@ public class PetFragment extends Fragment implements PetAdapter.OnPetClickListen return binding.getRoot(); } + private void observeViewModel() { + viewModel.getPets().observe(getViewLifecycleOwner(), list -> { + petList.clear(); + petList.addAll(list); + adapter.notifyDataSetChanged(); + }); + + viewModel.getStores().observe(getViewLifecycleOwner(), list -> { + SpinnerUtils.populateWhiteSpinner(requireContext(), binding.spinnerStore, list, + StoreDTO::getStoreName, "All Stores", -1L, StoreDTO::getStoreId); + }); + + viewModel.getIsLoading().observe(getViewLifecycleOwner(), loading -> { + binding.swipeRefreshPet.setRefreshing(loading); + }); + } + private void setupBulkDelete() { bulkDeleteHandler = new BulkDeleteHandler( this, @@ -96,83 +103,62 @@ public class PetFragment extends Fragment implements PetAdapter.OnPetClickListen ); } - @Override - public void onDestroyView() { - super.onDestroyView(); - binding = null; - } - - /** - * Reloads data every time the fragment becomes visible. - */ @Override public void onResume() { super.onResume(); loadPetData(); - loadStoreData(); + viewModel.loadStores(); } - /** - * Sets up the filter toggle button to show/hide the filter layout. - */ private void setupFilterToggle() { UIUtils.setupFilterToggle(binding.btnToggleFilter, binding.layoutFilter, binding.etSearchPet, binding.spinnerStatus, binding.spinnerSpecies, binding.spinnerStore); } - /** - * Configures the search bar. - */ private void setupSearch() { UIUtils.attachSearch(binding.etSearchPet, this::loadPetData); } - /** - * Configures the status filter spinner. - */ private void setupStatusFilter() { String[] statuses = {"All Statuses", "Available", "Adopted", "Owned"}; SpinnerUtils.setupStringFilterSpinner(requireContext(), binding.spinnerStatus, statuses, this::loadPetData); } - /** - * Configures the species filter spinner with species. - */ private void setupSpeciesFilter() { String[] species = {"All Species", "Dog", "Cat", "Bird", "Rabbit", "Fish", "Hamster"}; SpinnerUtils.setupStringFilterSpinner(requireContext(), binding.spinnerSpecies, species, this::loadPetData); } - /** - * Configures the store filter spinner. - */ private void setupStoreFilter() { SpinnerUtils.setupFilterSpinner(binding.spinnerStore, this::loadPetData); } - /** - * Fetches store data to populate the store filter. - */ - private void loadStoreData() { - storeViewModel.getAllStores(0, 100).observe(getViewLifecycleOwner(), resource -> { - if (resource.status == Resource.Status.SUCCESS && resource.data != null) { - storeList = resource.data.getContent(); - SpinnerUtils.populateWhiteSpinner(requireContext(), binding.spinnerStore, storeList, - StoreDTO::getStoreName, "All Stores", -1L, StoreDTO::getStoreId); - } - }); - } - - /** - * Sets up the SwipeRefreshLayout. - */ private void setupSwipeRefresh() { binding.swipeRefreshPet.setOnRefreshListener(this::loadPetData); } - /** - * Navigates to the pet profile screen. - */ + private void loadPetData() { + String query = binding.etSearchPet.getText().toString().trim(); + String status = binding.spinnerStatus.getSelectedItem() != null ? binding.spinnerStatus.getSelectedItem().toString() : "All Statuses"; + String species = binding.spinnerSpecies.getSelectedItem() != null ? binding.spinnerSpecies.getSelectedItem().toString() : "All Species"; + + Long storeId = null; + List stores = viewModel.getStores().getValue(); + if (binding.spinnerStore.getSelectedItemPosition() > 0 && stores != null && !stores.isEmpty()) { + storeId = stores.get(binding.spinnerStore.getSelectedItemPosition() - 1).getStoreId(); + } + + viewModel.loadPets(query, status, species, storeId); + } + + private void setupRecyclerView() { + adapter = new PetAdapter(petList, this); + adapter.setBaseUrl(baseUrl); + adapter.setToken(tokenManager.getToken()); + binding.recyclerViewPets.setLayoutManager(new LinearLayoutManager(getContext())); + binding.recyclerViewPets.setAdapter(adapter); + } + private void openPetProfile(int position) { Bundle args = new Bundle(); PetDTO pet = petList.get(position); @@ -180,9 +166,6 @@ public class PetFragment extends Fragment implements PetAdapter.OnPetClickListen NavHostFragment.findNavController(this).navigate(R.id.nav_pet_profile, args); } - /** - * Navigates to the pet detail screen. - */ private void openPetDetails() { NavHostFragment.findNavController(this).navigate(R.id.nav_pet_detail); } @@ -199,54 +182,9 @@ public class PetFragment extends Fragment implements PetAdapter.OnPetClickListen } } - /** - * Fetches pet data from the server with all active filters. - */ - private void loadPetData() { - String query = binding.etSearchPet.getText().toString().trim(); - String status = binding.spinnerStatus.getSelectedItem() != null ? binding.spinnerStatus.getSelectedItem().toString() : "All Statuses"; - String species = binding.spinnerSpecies.getSelectedItem() != null ? binding.spinnerSpecies.getSelectedItem().toString() : "All Species"; - - Long storeId = null; - if (binding.spinnerStore.getSelectedItemPosition() > 0 && !storeList.isEmpty()) { - storeId = storeList.get(binding.spinnerStore.getSelectedItemPosition() - 1).getStoreId(); - } - - if (status.equals("All Statuses")) status = null; - if (species.equals("All Species")) species = null; - - viewModel.getAllPets(0, 100, query, status, species, storeId, "petName").observe(getViewLifecycleOwner(), resource -> { - if (resource == null) return; - - switch (resource.status) { - case LOADING: - binding.swipeRefreshPet.setRefreshing(true); - break; - case SUCCESS: - binding.swipeRefreshPet.setRefreshing(false); - if (resource.data != null) { - petList.clear(); - petList.addAll(resource.data.getContent()); - adapter.notifyDataSetChanged(); - } - break; - case ERROR: - binding.swipeRefreshPet.setRefreshing(false); - Toast.makeText(getContext(), "Failed to load pets: " + resource.message, Toast.LENGTH_SHORT).show(); - Log.e("PetFragment", "Error loading pets: " + resource.message); - break; - } - }); - } - - /** - * Initializes the RecyclerView. - */ - private void setupRecyclerView() { - adapter = new PetAdapter(petList, this); - adapter.setBaseUrl(baseUrl); - adapter.setToken(tokenManager.getToken()); - binding.recyclerViewPets.setLayoutManager(new LinearLayoutManager(getContext())); - binding.recyclerViewPets.setAdapter(adapter); + @Override + public void onDestroyView() { + super.onDestroyView(); + binding = null; } } diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ProductFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ProductFragment.java index 6ae3d349..84f076a2 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ProductFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ProductFragment.java @@ -9,21 +9,18 @@ import androidx.lifecycle.ViewModelProvider; import androidx.navigation.fragment.NavHostFragment; import androidx.recyclerview.widget.LinearLayoutManager; -import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import android.widget.Toast; import com.example.petstoremobile.R; import com.example.petstoremobile.adapters.ProductAdapter; import com.example.petstoremobile.databinding.FragmentProductBinding; import com.example.petstoremobile.dtos.CategoryDTO; import com.example.petstoremobile.dtos.ProductDTO; -import com.example.petstoremobile.utils.Resource; import com.example.petstoremobile.utils.SpinnerUtils; import com.example.petstoremobile.utils.UIUtils; -import com.example.petstoremobile.viewmodels.ProductViewModel; +import com.example.petstoremobile.viewmodels.ProductListViewModel; import java.util.ArrayList; import java.util.List; @@ -38,24 +35,17 @@ public class ProductFragment extends Fragment implements ProductAdapter.OnProduc private FragmentProductBinding binding; private List productList = new ArrayList<>(); - private List categoryList = new ArrayList<>(); private ProductAdapter adapter; - private ProductViewModel viewModel; + private ProductListViewModel viewModel; @Inject @Named("baseUrl") String baseUrl; - /** - * Initializes the fragment and its associated ProductViewModel. - */ @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); - viewModel = new ViewModelProvider(this).get(ProductViewModel.class); + viewModel = new ViewModelProvider(this).get(ProductListViewModel.class); } - /** - * Sets up the fragment's UI components, including RecyclerView, search, and swipe-to-refresh. - */ @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { @@ -66,6 +56,7 @@ public class ProductFragment extends Fragment implements ProductAdapter.OnProduc setupCategoryFilter(); setupSwipeRefresh(); setupFilterToggle(); + observeViewModel(); binding.fabAddProduct.setOnClickListener(v -> openProductDetails(-1)); @@ -74,67 +65,67 @@ public class ProductFragment extends Fragment implements ProductAdapter.OnProduc return binding.getRoot(); } - @Override - public void onDestroyView() { - super.onDestroyView(); - binding = null; + private void observeViewModel() { + viewModel.getProducts().observe(getViewLifecycleOwner(), list -> { + productList.clear(); + productList.addAll(list); + adapter.notifyDataSetChanged(); + }); + + viewModel.getCategories().observe(getViewLifecycleOwner(), list -> { + SpinnerUtils.populateWhiteSpinner(requireContext(), binding.spinnerCategory, list, + CategoryDTO::getCategoryName, "All Categories", -1L, CategoryDTO::getCategoryId); + }); + + viewModel.getIsLoading().observe(getViewLifecycleOwner(), loading -> { + binding.swipeRefreshProduct.setRefreshing(loading); + }); } - /** - * Reloads data every time the fragment becomes visible. - */ @Override public void onResume() { super.onResume(); loadProductData(); - loadCategoryData(); + viewModel.loadCategories(); } - /** - * Sets up the filter toggle button to show/hide the filter layout. - */ private void setupFilterToggle() { UIUtils.setupFilterToggle(binding.btnToggleFilter, binding.layoutFilter, binding.etSearchProduct, binding.spinnerCategory); } - /** - * Configures the search bar for triggering data load from backend. - */ private void setupSearch() { UIUtils.attachSearch(binding.etSearchProduct, this::loadProductData); } - /** - * Configures the category filter spinner. - */ private void setupCategoryFilter() { SpinnerUtils.setupFilterSpinner(binding.spinnerCategory, this::loadProductData); } - /** - * Fetches category data to populate the category filter. - */ - private void loadCategoryData() { - viewModel.getAllCategories(0, 100).observe(getViewLifecycleOwner(), resource -> { - if (resource.status == Resource.Status.SUCCESS && resource.data != null) { - categoryList = resource.data.getContent(); - SpinnerUtils.populateWhiteSpinner(requireContext(), binding.spinnerCategory, categoryList, - CategoryDTO::getCategoryName, "All Categories", -1L, CategoryDTO::getCategoryId); - } - }); - } - - /** - * Sets up the SwipeRefreshLayout. - */ private void setupSwipeRefresh() { binding.swipeRefreshProduct.setOnRefreshListener(this::loadProductData); } - /** - * Navigates to the product detail screen. - */ + private void loadProductData() { + String query = binding.etSearchProduct.getText().toString().trim(); + if (query.isEmpty()) query = null; + + Long categoryId = null; + List categories = viewModel.getCategories().getValue(); + if (binding.spinnerCategory.getSelectedItemPosition() > 0 && categories != null && !categories.isEmpty()) { + categoryId = categories.get(binding.spinnerCategory.getSelectedItemPosition() - 1).getCategoryId(); + } + + viewModel.loadProducts(query, categoryId); + } + + private void setupRecyclerView() { + adapter = new ProductAdapter(productList, this); + adapter.setBaseUrl(baseUrl); + binding.recyclerViewProducts.setLayoutManager(new LinearLayoutManager(getContext())); + binding.recyclerViewProducts.setAdapter(adapter); + } + private void openProductDetails(int position) { Bundle args = new Bundle(); if (position != -1) { @@ -149,51 +140,9 @@ public class ProductFragment extends Fragment implements ProductAdapter.OnProduc openProductDetails(position); } - /** - * Fetches product data from the server with search query, category, and sorting. - */ - private void loadProductData() { - String query = binding.etSearchProduct.getText().toString().trim(); - if (query.isEmpty()) query = null; - - Long categoryId = null; - if (binding.spinnerCategory.getSelectedItemPosition() > 0 && !categoryList.isEmpty()) { - categoryId = categoryList.get(binding.spinnerCategory.getSelectedItemPosition() - 1).getCategoryId(); - } - - viewModel.getAllProducts(query, categoryId, 0, 100, "prodName").observe(getViewLifecycleOwner(), resource -> { - if (resource == null) return; - - switch (resource.status) { - case LOADING: - binding.swipeRefreshProduct.setRefreshing(true); - break; - case SUCCESS: - binding.swipeRefreshProduct.setRefreshing(false); - if (resource.data != null) { - productList.clear(); - productList.addAll(resource.data.getContent()); - adapter.notifyDataSetChanged(); - } - break; - case ERROR: - binding.swipeRefreshProduct.setRefreshing(false); - if (getContext() != null) { - Toast.makeText(getContext(), "Failed to load products: " + resource.message, Toast.LENGTH_SHORT).show(); - } - Log.e("ProductFragment", "Error loading products: " + resource.message); - break; - } - }); - } - - /** - * Initializes the RecyclerView. - */ - private void setupRecyclerView() { - adapter = new ProductAdapter(productList, this); - adapter.setBaseUrl(baseUrl); - binding.recyclerViewProducts.setLayoutManager(new LinearLayoutManager(getContext())); - binding.recyclerViewProducts.setAdapter(adapter); + @Override + public void onDestroyView() { + super.onDestroyView(); + binding = null; } } diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ProductSupplierFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ProductSupplierFragment.java index e1db78b6..6a066528 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ProductSupplierFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ProductSupplierFragment.java @@ -1,11 +1,9 @@ package com.example.petstoremobile.fragments.listfragments; import android.os.Bundle; -import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -21,12 +19,9 @@ import com.example.petstoremobile.dtos.ProductDTO; import com.example.petstoremobile.dtos.ProductSupplierDTO; import com.example.petstoremobile.dtos.SupplierDTO; import com.example.petstoremobile.utils.BulkDeleteHandler; -import com.example.petstoremobile.utils.Resource; import com.example.petstoremobile.utils.SpinnerUtils; import com.example.petstoremobile.utils.UIUtils; -import com.example.petstoremobile.viewmodels.ProductSupplierViewModel; -import com.example.petstoremobile.viewmodels.ProductViewModel; -import com.example.petstoremobile.viewmodels.SupplierViewModel; +import com.example.petstoremobile.viewmodels.ProductSupplierListViewModel; import java.util.ArrayList; import java.util.List; @@ -39,29 +34,17 @@ public class ProductSupplierFragment extends Fragment private FragmentProductSupplierBinding binding; private List psList = new ArrayList<>(); - private List productList = new ArrayList<>(); - private List supplierList = new ArrayList<>(); private ProductSupplierAdapter adapter; - private ProductSupplierViewModel viewModel; - private ProductViewModel productViewModel; - private SupplierViewModel supplierViewModel; + private ProductSupplierListViewModel viewModel; private BulkDeleteHandler bulkDeleteHandler; - /** - * Initializes the fragment and its associated ViewModels. - */ @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); - viewModel = new ViewModelProvider(this).get(ProductSupplierViewModel.class); - productViewModel = new ViewModelProvider(this).get(ProductViewModel.class); - supplierViewModel = new ViewModelProvider(this).get(SupplierViewModel.class); + viewModel = new ViewModelProvider(this).get(ProductSupplierListViewModel.class); } - /** - * Sets up the fragment's UI components, including the RecyclerView, search, and swipe-to-refresh. - */ @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { @@ -74,6 +57,7 @@ public class ProductSupplierFragment extends Fragment setupSwipeRefresh(); setupFilterToggle(); setupBulkDelete(); + observeViewModel(); binding.fabAddPS.setOnClickListener(v -> openDetail(-1)); @@ -82,6 +66,28 @@ public class ProductSupplierFragment extends Fragment return binding.getRoot(); } + private void observeViewModel() { + viewModel.getProductSuppliers().observe(getViewLifecycleOwner(), list -> { + psList.clear(); + psList.addAll(list); + adapter.notifyDataSetChanged(); + }); + + viewModel.getProducts().observe(getViewLifecycleOwner(), list -> { + SpinnerUtils.populateWhiteSpinner(requireContext(), binding.spinnerProduct, list, + ProductDTO::getProdName, "All Products", -1L, ProductDTO::getProdId); + }); + + viewModel.getSuppliers().observe(getViewLifecycleOwner(), list -> { + SpinnerUtils.populateWhiteSpinner(requireContext(), binding.spinnerSupplier, list, + SupplierDTO::getSupCompany, "All Suppliers", -1L, SupplierDTO::getSupId); + }); + + viewModel.getIsLoading().observe(getViewLifecycleOwner(), loading -> { + binding.swipeRefreshPS.setRefreshing(loading); + }); + } + private void setupBulkDelete() { bulkDeleteHandler = new BulkDeleteHandler( this, @@ -95,136 +101,65 @@ public class ProductSupplierFragment extends Fragment ); } + @Override + public void onResume() { + super.onResume(); + loadData(); + viewModel.loadFilterData(); + } + @Override public void onDestroyView() { super.onDestroyView(); binding = null; } - /** - * Reloads data every time the fragment becomes visible. - */ - @Override - public void onResume() { - super.onResume(); - loadData(); - loadFilterData(); - } - - /** - * Sets up the filter toggle button to show/hide the filter layout. - */ private void setupFilterToggle() { UIUtils.setupFilterToggle(binding.btnToggleFilter, binding.layoutFilter, binding.etSearchPS, binding.spinnerProduct, binding.spinnerSupplier); } - /** - * Initializes the RecyclerView with a layout manager and adapter for product-supplier data. - */ private void setupRecyclerView() { adapter = new ProductSupplierAdapter(psList, this); binding.recyclerViewPS.setLayoutManager(new LinearLayoutManager(getContext())); binding.recyclerViewPS.setAdapter(adapter); } - /** - * Configures the search bar for filtering. - */ private void setupSearch() { UIUtils.attachSearch(binding.etSearchPS, this::loadData); } - /** - * Configures the product filter spinner. - */ private void setupProductFilter() { SpinnerUtils.setupFilterSpinner(binding.spinnerProduct, this::loadData); } - /** - * Configures the supplier filter spinner. - */ private void setupSupplierFilter() { SpinnerUtils.setupFilterSpinner(binding.spinnerSupplier, this::loadData); } - /** - * Fetches products and suppliers to populate the filters. - */ - private void loadFilterData() { - productViewModel.getAllProducts(null, null, 0, 100, "prodName").observe(getViewLifecycleOwner(), resource -> { - if (resource.status == Resource.Status.SUCCESS && resource.data != null) { - productList = resource.data.getContent(); - SpinnerUtils.populateWhiteSpinner(requireContext(), binding.spinnerProduct, productList, - ProductDTO::getProdName, "All Products", -1L, ProductDTO::getProdId); - } - }); - - supplierViewModel.getAllSuppliers(0, 100, null, "supCompany").observe(getViewLifecycleOwner(), resource -> { - if (resource.status == Resource.Status.SUCCESS && resource.data != null) { - supplierList = resource.data.getContent(); - SpinnerUtils.populateWhiteSpinner(requireContext(), binding.spinnerSupplier, supplierList, - SupplierDTO::getSupCompany, "All Suppliers", -1L, SupplierDTO::getSupId); - } - }); - } - - /** - * Sets up the SwipeRefreshLayout to allow manual reloading of product-supplier data. - */ private void setupSwipeRefresh() { binding.swipeRefreshPS.setOnRefreshListener(this::loadData); } - /** - * Fetches product-supplier data from the server through the ViewModel with search query and filters. - */ private void loadData() { String query = binding.etSearchPS.getText().toString().trim(); if (query.isEmpty()) query = null; Long productId = null; - if (binding.spinnerProduct.getSelectedItemPosition() > 0 && !productList.isEmpty()) { - productId = productList.get(binding.spinnerProduct.getSelectedItemPosition() - 1).getProdId(); + List products = viewModel.getProducts().getValue(); + if (binding.spinnerProduct.getSelectedItemPosition() > 0 && products != null && !products.isEmpty()) { + productId = products.get(binding.spinnerProduct.getSelectedItemPosition() - 1).getProdId(); } Long supplierId = null; - if (binding.spinnerSupplier.getSelectedItemPosition() > 0 && !supplierList.isEmpty()) { - supplierId = supplierList.get(binding.spinnerSupplier.getSelectedItemPosition() - 1).getSupId(); + List suppliers = viewModel.getSuppliers().getValue(); + if (binding.spinnerSupplier.getSelectedItemPosition() > 0 && suppliers != null && !suppliers.isEmpty()) { + supplierId = suppliers.get(binding.spinnerSupplier.getSelectedItemPosition() - 1).getSupId(); } - viewModel.getAllProductSuppliers(0, 100, query, productId, supplierId, "productName").observe(getViewLifecycleOwner(), resource -> { - if (resource == null) return; - - // Check the status to see if the resource is loaded and display the data - switch (resource.status) { - case LOADING: - // Show loading indicator - binding.swipeRefreshPS.setRefreshing(true); - break; - case SUCCESS: - // Hide loading indicator and display data - binding.swipeRefreshPS.setRefreshing(false); - if (resource.data != null) { - psList.clear(); - psList.addAll(resource.data.getContent()); - adapter.notifyDataSetChanged(); - } - break; - case ERROR: - // Hide loading indicator and toast error message - binding.swipeRefreshPS.setRefreshing(false); - Toast.makeText(getContext(), "Failed to load: " + resource.message, Toast.LENGTH_SHORT).show(); - Log.e("PSFragment", "Error loading: " + resource.message); - break; - } - }); + viewModel.loadProductSuppliers(query, productId, supplierId); } - /** - * Navigates to the product-supplier detail screen for a specific item or to add a new record. - */ private void openDetail(int position) { Bundle args = new Bundle(); if (position != -1) { @@ -235,9 +170,6 @@ public class ProductSupplierFragment extends Fragment NavHostFragment.findNavController(this).navigate(R.id.nav_product_supplier_detail, args); } - /** - * Handles item click in the product-supplier list. - */ @Override public void onProductSupplierClick(int position) { openDetail(position); } diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/PurchaseOrderFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/PurchaseOrderFragment.java index b27c9c1f..64dc0ea5 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/PurchaseOrderFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/PurchaseOrderFragment.java @@ -1,11 +1,9 @@ package com.example.petstoremobile.fragments.listfragments; import android.os.Bundle; -import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -19,11 +17,9 @@ import com.example.petstoremobile.adapters.PurchaseOrderAdapter; import com.example.petstoremobile.databinding.FragmentPurchaseOrderBinding; import com.example.petstoremobile.dtos.PurchaseOrderDTO; import com.example.petstoremobile.dtos.StoreDTO; -import com.example.petstoremobile.utils.Resource; import com.example.petstoremobile.utils.SpinnerUtils; import com.example.petstoremobile.utils.UIUtils; -import com.example.petstoremobile.viewmodels.PurchaseOrderViewModel; -import com.example.petstoremobile.viewmodels.StoreViewModel; +import com.example.petstoremobile.viewmodels.PurchaseOrderListViewModel; import java.util.ArrayList; import java.util.List; @@ -36,24 +32,15 @@ public class PurchaseOrderFragment extends Fragment private FragmentPurchaseOrderBinding binding; private List poList = new ArrayList<>(); - private List storeList = new ArrayList<>(); private PurchaseOrderAdapter adapter; - private PurchaseOrderViewModel viewModel; - private StoreViewModel storeViewModel; + private PurchaseOrderListViewModel viewModel; - /** - * Initializes the fragment and its associated ViewModels. - */ @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); - viewModel = new ViewModelProvider(this).get(PurchaseOrderViewModel.class); - storeViewModel = new ViewModelProvider(this).get(StoreViewModel.class); + viewModel = new ViewModelProvider(this).get(PurchaseOrderListViewModel.class); } - /** - * Sets up the fragment's UI components, including RecyclerView, filters, and swipe-to-refresh. - */ @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { @@ -64,121 +51,72 @@ public class PurchaseOrderFragment extends Fragment setupStoreFilter(); setupSwipeRefresh(); setupFilterToggle(); + observeViewModel(); UIUtils.setupHamburgerMenu(binding.btnHamburgerPO, this); return binding.getRoot(); } - @Override - public void onDestroyView() { - super.onDestroyView(); - binding = null; + private void observeViewModel() { + viewModel.getPurchaseOrders().observe(getViewLifecycleOwner(), list -> { + poList.clear(); + poList.addAll(list); + adapter.notifyDataSetChanged(); + }); + + viewModel.getStores().observe(getViewLifecycleOwner(), list -> { + SpinnerUtils.populateWhiteSpinner(requireContext(), binding.spinnerStore, list, + StoreDTO::getStoreName, "All Stores", -1L, StoreDTO::getStoreId); + }); + + viewModel.getIsLoading().observe(getViewLifecycleOwner(), loading -> { + binding.swipeRefreshPO.setRefreshing(loading); + }); } - /** - * Reloads data every time the fragment becomes visible. - */ @Override public void onResume() { super.onResume(); loadData(); - loadStoreData(); + viewModel.loadStores(); } - /** - * Sets up the filter toggle button to show/hide the filter layout. - */ private void setupFilterToggle() { UIUtils.setupFilterToggle(binding.btnToggleFilter, binding.layoutFilter, binding.etSearchPO, binding.spinnerStore); } - /** - * Configures the search bar for filtering. - */ private void setupSearch() { UIUtils.attachSearch(binding.etSearchPO, this::loadData); } - /** - * Configures the store filter spinner. - */ private void setupStoreFilter() { SpinnerUtils.setupFilterSpinner(binding.spinnerStore, this::loadData); } - /** - * Fetches store data to populate the store filter. - */ - private void loadStoreData() { - storeViewModel.getAllStores(0, 100).observe(getViewLifecycleOwner(), resource -> { - if (resource.status == Resource.Status.SUCCESS && resource.data != null) { - storeList = resource.data.getContent(); - SpinnerUtils.populateWhiteSpinner(requireContext(), binding.spinnerStore, storeList, - StoreDTO::getStoreName, "All Stores", -1L, StoreDTO::getStoreId); - } - }); - } - - /** - * Initializes the RecyclerView with a layout manager and adapter. - */ private void setupRecyclerView() { adapter = new PurchaseOrderAdapter(poList, this); binding.recyclerViewPO.setLayoutManager(new LinearLayoutManager(getContext())); binding.recyclerViewPO.setAdapter(adapter); } - /** - * Sets up the SwipeRefreshLayout to allow manual reloading of purchase order data. - */ private void setupSwipeRefresh() { binding.swipeRefreshPO.setOnRefreshListener(this::loadData); } - /** - * Fetches purchase order data from the server with active filters and updates the UI. - */ private void loadData() { String query = binding.etSearchPO != null ? binding.etSearchPO.getText().toString().trim() : ""; if (query.isEmpty()) query = null; Long storeId = null; - if (binding.spinnerStore.getSelectedItemPosition() > 0 && !storeList.isEmpty()) { - storeId = storeList.get(binding.spinnerStore.getSelectedItemPosition() - 1).getStoreId(); + List stores = viewModel.getStores().getValue(); + if (binding.spinnerStore.getSelectedItemPosition() > 0 && stores != null && !stores.isEmpty()) { + storeId = stores.get(binding.spinnerStore.getSelectedItemPosition() - 1).getStoreId(); } - viewModel.getAllPurchaseOrders(0, 100, query, storeId, "purchaseOrderId,desc").observe(getViewLifecycleOwner(), resource -> { - if (resource == null) return; - - // Check the status to see if the resource is loaded and display the data - switch (resource.status) { - case LOADING: - // Show loading indicator - binding.swipeRefreshPO.setRefreshing(true); - break; - case SUCCESS: - // Hide loading indicator and display data - binding.swipeRefreshPO.setRefreshing(false); - if (resource.data != null) { - poList.clear(); - poList.addAll(resource.data.getContent()); - adapter.notifyDataSetChanged(); - } - break; - case ERROR: - // Hide loading indicator and toast error message - binding.swipeRefreshPO.setRefreshing(false); - Toast.makeText(getContext(), "Failed to load purchase orders: " + resource.message, Toast.LENGTH_SHORT).show(); - Log.e("POFragment", "Error loading purchase orders: " + resource.message); - break; - } - }); + viewModel.loadPurchaseOrders(query, storeId); } - /** - * Navigates to the purchase order detail screen for a specific record. - */ private void openDetail(int position) { Bundle args = new Bundle(); PurchaseOrderDTO po = poList.get(position); @@ -186,11 +124,14 @@ public class PurchaseOrderFragment extends Fragment NavHostFragment.findNavController(this).navigate(R.id.nav_purchase_order_detail, args); } - /** - * Handles item click in the purchase order list. - */ @Override public void onPurchaseOrderClick(int position) { openDetail(position); } + + @Override + public void onDestroyView() { + super.onDestroyView(); + binding = null; + } } diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/SaleFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/SaleFragment.java index 850717e4..fef5d994 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/SaleFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/SaleFragment.java @@ -8,7 +8,6 @@ import androidx.lifecycle.ViewModelProvider; import androidx.navigation.fragment.NavHostFragment; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; -import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -19,11 +18,9 @@ import com.example.petstoremobile.adapters.SaleAdapter; import com.example.petstoremobile.databinding.FragmentSaleBinding; import com.example.petstoremobile.dtos.SaleDTO; import com.example.petstoremobile.dtos.StoreDTO; -import com.example.petstoremobile.utils.Resource; import com.example.petstoremobile.utils.SpinnerUtils; import com.example.petstoremobile.utils.UIUtils; -import com.example.petstoremobile.viewmodels.SaleViewModel; -import com.example.petstoremobile.viewmodels.StoreViewModel; +import com.example.petstoremobile.viewmodels.SaleListViewModel; import java.util.ArrayList; import java.util.List; @@ -33,20 +30,10 @@ import dagger.hilt.android.AndroidEntryPoint; @AndroidEntryPoint public class SaleFragment extends Fragment implements SaleAdapter.OnSaleClickListener { - private static final String TAG = "SaleFragment"; - private static final int PAGE_SIZE = 200; - private FragmentSaleBinding binding; private final List saleList = new ArrayList<>(); - private final List storeList = new ArrayList<>(); private SaleAdapter adapter; - private SaleViewModel saleViewModel; - private StoreViewModel storeViewModel; - - // Pagination - private int currentPage = 0; - private boolean isLastPage = false; - private boolean isLoading = false; + private SaleListViewModel viewModel; @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, @@ -58,8 +45,7 @@ public class SaleFragment extends Fragment implements SaleAdapter.OnSaleClickLis @Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); - saleViewModel = new ViewModelProvider(this).get(SaleViewModel.class); - storeViewModel = new ViewModelProvider(this).get(StoreViewModel.class); + viewModel = new ViewModelProvider(this).get(SaleListViewModel.class); setupRecyclerView(); setupSearch(); @@ -67,6 +53,8 @@ public class SaleFragment extends Fragment implements SaleAdapter.OnSaleClickLis setupPaymentMethodFilter(); setupSwipeRefresh(); setupFilterToggle(); + observeViewModel(); + loadSales(true); UIUtils.setupHamburgerMenu(binding.btnHamburger, this); @@ -78,10 +66,27 @@ public class SaleFragment extends Fragment implements SaleAdapter.OnSaleClickLis NavHostFragment.findNavController(this).navigate(R.id.nav_refund)); } + private void observeViewModel() { + viewModel.getSales().observe(getViewLifecycleOwner(), list -> { + saleList.clear(); + saleList.addAll(list); + adapter.notifyDataSetChanged(); + }); + + viewModel.getStores().observe(getViewLifecycleOwner(), list -> { + SpinnerUtils.populateWhiteSpinner(requireContext(), binding.spinnerStore, list, + StoreDTO::getStoreName, "Stores", null, StoreDTO::getStoreId); + }); + + viewModel.getIsLoading().observe(getViewLifecycleOwner(), loading -> { + binding.swipeRefreshSale.setRefreshing(loading); + }); + } + @Override public void onResume() { super.onResume(); - loadStoreData(); + viewModel.loadStores(); } private void setupFilterToggle() { @@ -93,28 +98,11 @@ public class SaleFragment extends Fragment implements SaleAdapter.OnSaleClickLis SpinnerUtils.setupFilterSpinner(binding.spinnerStore, () -> loadSales(true)); } - private void loadStoreData() { - storeViewModel.getAllStores(0, 100).observe(getViewLifecycleOwner(), resource -> { - if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) { - storeList.clear(); - storeList.addAll(resource.data.getContent()); - SpinnerUtils.populateWhiteSpinner(requireContext(), binding.spinnerStore, storeList, - StoreDTO::getStoreName, "Stores", null, StoreDTO::getStoreId); - } - }); - } - private void setupPaymentMethodFilter() { String[] paymentMethods = {"Payments", "Cash", "Card"}; SpinnerUtils.setupStringFilterSpinner(requireContext(), binding.spinnerPaymentMethod, paymentMethods, () -> loadSales(true)); } - @Override - public void onDestroyView() { - super.onDestroyView(); - binding = null; - } - private void setupRecyclerView() { adapter = new SaleAdapter(saleList, this); binding.recyclerViewSales.setLayoutManager(new LinearLayoutManager(getContext())); @@ -129,7 +117,8 @@ public class SaleFragment extends Fragment implements SaleAdapter.OnSaleClickLis int visible = lm.getChildCount(); int total = lm.getItemCount(); int firstVis = lm.findFirstVisibleItemPosition(); - if (!isLoading && !isLastPage && (visible + firstVis) >= total - 3) { + Boolean isLoading = viewModel.getIsLoading().getValue(); + if ((isLoading == null || !isLoading) && !viewModel.isLastPage() && (visible + firstVis) >= total - 3) { loadSales(false); } } @@ -146,13 +135,6 @@ public class SaleFragment extends Fragment implements SaleAdapter.OnSaleClickLis } private void loadSales(boolean reset) { - if (isLoading) return; - - if (reset) { - currentPage = 0; - isLastPage = false; - } - String query = binding.etSearchSale != null ? binding.etSearchSale.getText().toString().trim() : ""; if (query.isEmpty()) query = null; @@ -162,39 +144,12 @@ public class SaleFragment extends Fragment implements SaleAdapter.OnSaleClickLis } Long storeId = null; - if (binding.spinnerStore.getSelectedItemPosition() > 0 && !storeList.isEmpty()) { - storeId = storeList.get(binding.spinnerStore.getSelectedItemPosition() - 1).getStoreId(); + List stores = viewModel.getStores().getValue(); + if (binding.spinnerStore.getSelectedItemPosition() > 0 && stores != null && !stores.isEmpty()) { + storeId = stores.get(binding.spinnerStore.getSelectedItemPosition() - 1).getStoreId(); } - saleViewModel.getAllSales(currentPage, PAGE_SIZE, query, paymentMethod, storeId, "saleDate,desc").observe(getViewLifecycleOwner(), resource -> { - if (resource == null) return; - - switch (resource.status) { - case LOADING: - isLoading = true; - binding.swipeRefreshSale.setRefreshing(true); - break; - case SUCCESS: - isLoading = false; - binding.swipeRefreshSale.setRefreshing(false); - if (resource.data != null) { - if (reset) saleList.clear(); - saleList.addAll(resource.data.getContent()); - adapter.notifyDataSetChanged(); - isLastPage = resource.data.isLast(); - if (!isLastPage) currentPage++; - } - break; - case ERROR: - isLoading = false; - binding.swipeRefreshSale.setRefreshing(false); - Log.e(TAG, "Error loading sales: " + resource.message); - if (getContext() != null) { - Toast.makeText(getContext(), "Failed to load sales: " + resource.message, Toast.LENGTH_SHORT).show(); - } - break; - } - }); + viewModel.loadSales(reset, query, paymentMethod, storeId); } @Override @@ -210,4 +165,10 @@ public class SaleFragment extends Fragment implements SaleAdapter.OnSaleClickLis } NavHostFragment.findNavController(this).navigate(R.id.nav_sale_detail, args); } + + @Override + public void onDestroyView() { + super.onDestroyView(); + binding = null; + } } diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ServiceFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ServiceFragment.java index 77136d4e..3a1b45a1 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ServiceFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ServiceFragment.java @@ -1,7 +1,6 @@ package com.example.petstoremobile.fragments.listfragments; import android.os.Bundle; -import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -21,7 +20,7 @@ import com.example.petstoremobile.databinding.FragmentServiceBinding; import com.example.petstoremobile.dtos.ServiceDTO; import com.example.petstoremobile.utils.BulkDeleteHandler; import com.example.petstoremobile.utils.UIUtils; -import com.example.petstoremobile.viewmodels.ServiceViewModel; +import com.example.petstoremobile.viewmodels.ServiceListViewModel; import java.util.ArrayList; import java.util.List; @@ -34,32 +33,18 @@ import dagger.hilt.android.AndroidEntryPoint; @AndroidEntryPoint public class ServiceFragment extends Fragment implements ServiceAdapter.OnServiceClickListener { - private static final String TAG = "ServiceFragment"; - private static final int PAGE_SIZE = 20; - private FragmentServiceBinding binding; private final List serviceList = new ArrayList<>(); private ServiceAdapter adapter; - private ServiceViewModel viewModel; + private ServiceListViewModel viewModel; private BulkDeleteHandler bulkDeleteHandler; - // Pagination - private int currentPage = 0; - private boolean isLastPage = false; - private boolean isLoading = false; - - /** - * Initializes the fragment and its associated ViewModel. - */ @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); - viewModel = new ViewModelProvider(this).get(ServiceViewModel.class); + viewModel = new ViewModelProvider(this).get(ServiceListViewModel.class); } - /** - * Sets up the fragment's UI components, including RecyclerView, search, and swipe-to-refresh. - */ @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { @@ -70,15 +55,27 @@ public class ServiceFragment extends Fragment implements ServiceAdapter.OnServic setupSwipeRefresh(); setupFilterToggle(); setupBulkDelete(); + observeViewModel(); + loadServices(true); - binding.fabAddService.setOnClickListener(v -> openDetail(null)); - UIUtils.setupHamburgerMenu(binding.btnHamburger, this); return binding.getRoot(); } + private void observeViewModel() { + viewModel.getServices().observe(getViewLifecycleOwner(), list -> { + serviceList.clear(); + serviceList.addAll(list); + adapter.notifyDataSetChanged(); + }); + + viewModel.getIsLoading().observe(getViewLifecycleOwner(), loading -> { + binding.swipeRefreshService.setRefreshing(loading); + }); + } + private void setupBulkDelete() { bulkDeleteHandler = new BulkDeleteHandler( this, @@ -98,23 +95,14 @@ public class ServiceFragment extends Fragment implements ServiceAdapter.OnServic binding = null; } - /** - * Sets up the filter toggle button to show/hide the filter layout. - */ private void setupFilterToggle() { UIUtils.setupFilterToggle(binding.btnToggleFilter, binding.layoutFilter, binding.etSearchService); } - /** - * Sets up the search bar for filtering. - */ private void setupSearch() { UIUtils.attachSearch(binding.etSearchService, () -> loadServices(true)); } - /** - * Initializes the RecyclerView with a layout manager and adapter. - */ private void setupRecyclerView() { adapter = new ServiceAdapter(serviceList, this); binding.recyclerViewServices.setLayoutManager(new LinearLayoutManager(getContext())); @@ -129,66 +117,24 @@ public class ServiceFragment extends Fragment implements ServiceAdapter.OnServic int visible = lm.getChildCount(); int total = lm.getItemCount(); int firstVis = lm.findFirstVisibleItemPosition(); - if (!isLoading && !isLastPage && (visible + firstVis) >= total - 3) { + Boolean isLoading = viewModel.getIsLoading().getValue(); + if ((isLoading == null || !isLoading) && !viewModel.isLastPage() && (visible + firstVis) >= total - 3) { loadServices(false); } } }); } - /** - * Sets up the SwipeRefreshLayout. - */ private void setupSwipeRefresh() { binding.swipeRefreshService.setOnRefreshListener(() -> loadServices(true)); } - /** - * Fetches a page of services from the API. - */ private void loadServices(boolean reset) { - if (isLoading) return; - - if (reset) { - currentPage = 0; - isLastPage = false; - } - String query = binding.etSearchService.getText().toString().trim(); if (query.isEmpty()) query = null; - - viewModel.getAllServices(currentPage, PAGE_SIZE, query, "serviceName").observe(getViewLifecycleOwner(), resource -> { - if (resource == null) return; - - switch (resource.status) { - case LOADING: - isLoading = true; - binding.swipeRefreshService.setRefreshing(true); - break; - case SUCCESS: - isLoading = false; - binding.swipeRefreshService.setRefreshing(false); - if (resource.data != null) { - if (reset) serviceList.clear(); - serviceList.addAll(resource.data.getContent()); - adapter.notifyDataSetChanged(); - isLastPage = resource.data.isLast(); - if (!isLastPage) currentPage++; - } - break; - case ERROR: - isLoading = false; - binding.swipeRefreshService.setRefreshing(false); - Log.e(TAG, "Error: " + resource.message); - Toast.makeText(getContext(), "Failed to load services: " + resource.message, Toast.LENGTH_SHORT).show(); - break; - } - }); + viewModel.loadServices(reset, query); } - /** - * Navigates to the service detail screen. - */ private void openDetail(ServiceDTO service) { Bundle args = new Bundle(); if (service != null) { diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/StaffFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/StaffFragment.java index 383d702a..8407d3f6 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/StaffFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/StaffFragment.java @@ -1,7 +1,6 @@ package com.example.petstoremobile.fragments.listfragments; import android.os.Bundle; -import android.util.Log; import android.view.*; import android.widget.*; import androidx.annotation.NonNull; @@ -14,7 +13,7 @@ import com.example.petstoremobile.adapters.EmployeeAdapter; import com.example.petstoremobile.databinding.FragmentStaffBinding; import com.example.petstoremobile.dtos.EmployeeDTO; import com.example.petstoremobile.utils.UIUtils; -import com.example.petstoremobile.viewmodels.EmployeeViewModel; +import com.example.petstoremobile.viewmodels.StaffListViewModel; import dagger.hilt.android.AndroidEntryPoint; import java.util.*; @@ -22,21 +21,22 @@ import java.util.*; public class StaffFragment extends Fragment implements EmployeeAdapter.OnEmployeeClickListener { private FragmentStaffBinding binding; - private EmployeeViewModel employeeViewModel; - private List employeeList = new ArrayList<>(); - private List filteredList = new ArrayList<>(); + private StaffListViewModel viewModel; + private List staffList = new ArrayList<>(); private EmployeeAdapter adapter; @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { binding = FragmentStaffBinding.inflate(inflater, container, false); - employeeViewModel = new ViewModelProvider(this).get(EmployeeViewModel.class); + viewModel = new ViewModelProvider(this).get(StaffListViewModel.class); setupRecyclerView(); setupSearch(); setupSwipeRefresh(); - loadStaff(); + observeViewModel(); + + viewModel.loadStaff(); binding.fabAddStaff.setOnClickListener(v -> openDetail(-1)); @@ -46,70 +46,36 @@ public class StaffFragment extends Fragment implements EmployeeAdapter.OnEmploye return binding.getRoot(); } + private void observeViewModel() { + viewModel.getFilteredEmployees().observe(getViewLifecycleOwner(), list -> { + staffList.clear(); + staffList.addAll(list); + adapter.notifyDataSetChanged(); + }); + + viewModel.getIsLoading().observe(getViewLifecycleOwner(), loading -> { + binding.swipeRefreshStaff.setRefreshing(loading); + }); + } + private void setupRecyclerView() { - adapter = new EmployeeAdapter(filteredList, this); + adapter = new EmployeeAdapter(staffList, this); binding.recyclerViewStaff.setLayoutManager(new LinearLayoutManager(getContext())); binding.recyclerViewStaff.setAdapter(adapter); } private void setupSearch() { - UIUtils.attachSearch(binding.etSearchStaff, () -> filter(binding.etSearchStaff.getText().toString())); + UIUtils.attachSearch(binding.etSearchStaff, () -> viewModel.filter(binding.etSearchStaff.getText().toString())); } private void setupSwipeRefresh() { - binding.swipeRefreshStaff.setOnRefreshListener(this::loadStaff); - } - - private void filter(String query) { - filteredList.clear(); - if (query.isEmpty()) { - filteredList.addAll(employeeList); - } else { - String lower = query.toLowerCase(); - for (EmployeeDTO e : employeeList) { - if ((e.getFullName() != null && e.getFullName().toLowerCase().contains(lower)) - || (e.getUsername() != null && e.getUsername().toLowerCase().contains(lower)) - || (e.getEmail() != null && e.getEmail().toLowerCase().contains(lower)) - || (e.getPhone() != null && e.getPhone().toLowerCase().contains(lower))) { - filteredList.add(e); - } - } - } - adapter.notifyDataSetChanged(); - } - - private void loadStaff() { - binding.swipeRefreshStaff.setRefreshing(true); - employeeViewModel.getAllEmployees(0, 100).observe(getViewLifecycleOwner(), resource -> { - if (resource != null) { - switch (resource.status) { - case SUCCESS: - binding.swipeRefreshStaff.setRefreshing(false); - if (resource.data != null) { - employeeList.clear(); - employeeList.addAll(resource.data.getContent()); - filter(binding != null ? binding.etSearchStaff.getText().toString() : ""); - } - break; - case ERROR: - binding.swipeRefreshStaff.setRefreshing(false); - if (getContext() != null) { - Toast.makeText(getContext(), resource.message != null ? resource.message : "Failed to load staff", - Toast.LENGTH_SHORT).show(); - } - break; - case LOADING: - binding.swipeRefreshStaff.setRefreshing(true); - break; - } - } - }); + binding.swipeRefreshStaff.setOnRefreshListener(viewModel::loadStaff); } private void openDetail(int position) { Bundle args = new Bundle(); if (position != -1) { - EmployeeDTO e = filteredList.get(position); + EmployeeDTO e = staffList.get(position); args.putLong("employeeId", e.getEmployeeId()); args.putString("username", e.getUsername() != null ? e.getUsername() : ""); args.putString("firstName", e.getFirstName() != null ? e.getFirstName() : ""); diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/SupplierFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/SupplierFragment.java index eca755bb..78d43bd6 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/SupplierFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/SupplierFragment.java @@ -9,20 +9,17 @@ import androidx.lifecycle.ViewModelProvider; import androidx.navigation.fragment.NavHostFragment; import androidx.recyclerview.widget.LinearLayoutManager; -import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import android.widget.Toast; import com.example.petstoremobile.R; import com.example.petstoremobile.adapters.SupplierAdapter; import com.example.petstoremobile.databinding.FragmentSupplierBinding; import com.example.petstoremobile.dtos.SupplierDTO; import com.example.petstoremobile.utils.BulkDeleteHandler; -import com.example.petstoremobile.utils.Resource; import com.example.petstoremobile.utils.UIUtils; -import com.example.petstoremobile.viewmodels.SupplierViewModel; +import com.example.petstoremobile.viewmodels.SupplierListViewModel; import java.util.ArrayList; import java.util.List; @@ -35,21 +32,15 @@ public class SupplierFragment extends Fragment implements SupplierAdapter.OnSupp private FragmentSupplierBinding binding; private List supplierList = new ArrayList<>(); private SupplierAdapter adapter; - private SupplierViewModel viewModel; + private SupplierListViewModel viewModel; private BulkDeleteHandler bulkDeleteHandler; - /** - * Initializes the fragment and its associated SupplierViewModel. - */ @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); - viewModel = new ViewModelProvider(this).get(SupplierViewModel.class); + viewModel = new ViewModelProvider(this).get(SupplierListViewModel.class); } - /** - * Sets up the fragment's UI components, including RecyclerView, search, and swipe-to-refresh. - */ @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { @@ -60,9 +51,10 @@ public class SupplierFragment extends Fragment implements SupplierAdapter.OnSupp setupSwipeRefresh(); setupFilterToggle(); setupBulkDelete(); + observeViewModel(); + loadSupplierData(); - //Add button to opens the add dialog binding.fabAddSupplier.setOnClickListener(v -> openSupplierDetails(-1)); UIUtils.setupHamburgerMenu(binding.btnHamburger, this); @@ -70,6 +62,18 @@ public class SupplierFragment extends Fragment implements SupplierAdapter.OnSupp return binding.getRoot(); } + private void observeViewModel() { + viewModel.getSuppliers().observe(getViewLifecycleOwner(), list -> { + supplierList.clear(); + supplierList.addAll(list); + adapter.notifyDataSetChanged(); + }); + + viewModel.getIsLoading().observe(getViewLifecycleOwner(), loading -> { + binding.swipeRefreshSupplier.setRefreshing(loading); + }); + } + private void setupBulkDelete() { bulkDeleteHandler = new BulkDeleteHandler( this, @@ -89,47 +93,27 @@ public class SupplierFragment extends Fragment implements SupplierAdapter.OnSupp binding = null; } - /** - * Sets up the filter toggle button to show/hide the filter layout. - */ private void setupFilterToggle() { UIUtils.setupFilterToggle(binding.btnToggleFilter, binding.layoutFilter, binding.etSearchSupplier); } - /** - * Configures the search bar for filtering. - */ private void setupSearch() { UIUtils.attachSearch(binding.etSearchSupplier, this::loadSupplierData); } - /** - * Sets up the SwipeRefreshLayout to allow manual reloading of supplier data. - */ private void setupSwipeRefresh() { binding.swipeRefreshSupplier.setOnRefreshListener(this::loadSupplierData); } - /** - * Navigates to the supplier detail screen for editing an existing record or adding a new one. - */ private void openSupplierDetails(int position) { - //Make a bundle to pass data to the detail fragment Bundle args = new Bundle(); - - //if editing a supplier, add the supplier id to the bundle if (position != -1) { SupplierDTO supplier = supplierList.get(position); args.putLong("supId", supplier.getSupId()); } - NavHostFragment.findNavController(this).navigate(R.id.nav_supplier_detail, args); } - - /** - * Handles item click in the supplier list. - */ @Override public void onSupplierClick(int position) { openSupplierDetails(position); @@ -142,47 +126,12 @@ public class SupplierFragment extends Fragment implements SupplierAdapter.OnSupp } } - /** - * Fetches all supplier data from the server through the ViewModel and updates the UI. - */ private void loadSupplierData() { String query = binding.etSearchSupplier != null ? binding.etSearchSupplier.getText().toString().trim() : ""; if (query.isEmpty()) query = null; - - //Load suppliers from the backend with query and default sort - viewModel.getAllSuppliers(0, 100, query, "supCompany").observe(getViewLifecycleOwner(), resource -> { - if (resource == null) return; - - // Check the status to see if the resource is loaded and display the data - switch (resource.status) { - case LOADING: - // Show loading indicator - binding.swipeRefreshSupplier.setRefreshing(true); - break; - case SUCCESS: - // Hide loading indicator and display data - binding.swipeRefreshSupplier.setRefreshing(false); - if (resource.data != null) { - supplierList.clear(); - supplierList.addAll(resource.data.getContent()); - adapter.notifyDataSetChanged(); - } - break; - case ERROR: - // Hide loading indicator and toast error message - binding.swipeRefreshSupplier.setRefreshing(false); - if (getContext() != null) { - Toast.makeText(getContext(), "Failed to load suppliers: " + resource.message, Toast.LENGTH_SHORT).show(); - } - Log.e("SupplierFragment", "Error loading suppliers: " + resource.message); - break; - } - }); + viewModel.loadSuppliers(query); } - /** - * Initializes the RecyclerView with a layout manager and adapter for displaying suppliers. - */ private void setupRecyclerView() { adapter = new SupplierAdapter(supplierList, this); binding.recyclerViewSuppliers.setLayoutManager(new LinearLayoutManager(getContext())); diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/AdoptionDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/AdoptionDetailFragment.java index 7098b795..9b0f022b 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/AdoptionDetailFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/AdoptionDetailFragment.java @@ -1,6 +1,5 @@ package com.example.petstoremobile.fragments.listfragments.detailfragments; -import android.app.DatePickerDialog; import android.os.Bundle; import android.view.*; import android.widget.*; @@ -12,14 +11,13 @@ import androidx.navigation.fragment.NavHostFragment; import com.example.petstoremobile.databinding.FragmentAdoptionDetailBinding; import com.example.petstoremobile.dtos.*; +import com.example.petstoremobile.utils.DateTimeUtils; import com.example.petstoremobile.utils.DialogUtils; +import com.example.petstoremobile.utils.InputValidator; import com.example.petstoremobile.utils.Resource; import com.example.petstoremobile.utils.SpinnerUtils; -import com.example.petstoremobile.viewmodels.AdoptionViewModel; -import com.example.petstoremobile.viewmodels.CustomerViewModel; -import com.example.petstoremobile.viewmodels.PetViewModel; -import com.example.petstoremobile.viewmodels.StoreViewModel; -import com.example.petstoremobile.viewmodels.UserViewModel; +import com.example.petstoremobile.utils.UIUtils; +import com.example.petstoremobile.viewmodels.AdoptionDetailViewModel; import java.math.BigDecimal; import java.util.*; @@ -33,35 +31,19 @@ import dagger.hilt.android.AndroidEntryPoint; public class AdoptionDetailFragment extends Fragment { private FragmentAdoptionDetailBinding binding; + private AdoptionDetailViewModel viewModel; - private long adoptionId = -1; - private boolean isEditing = false; private long preselectedPetId = -1; private long preselectedCustomerId = -1; private long preselectedStoreId = -1; private long preselectedEmployeeId = -1; - private List petList = new ArrayList<>(); - private List customerList = new ArrayList<>(); - private List storeList = new ArrayList<>(); - private List employeeList = new ArrayList<>(); - private final String[] STATUSES = {"Pending", "Completed", "Cancelled"}; - private AdoptionViewModel adoptionViewModel; - private PetViewModel petViewModel; - private CustomerViewModel customerViewModel; - private StoreViewModel storeViewModel; - private UserViewModel userViewModel; - @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - adoptionViewModel = new ViewModelProvider(this).get(AdoptionViewModel.class); - petViewModel = new ViewModelProvider(this).get(PetViewModel.class); - customerViewModel = new ViewModelProvider(this).get(CustomerViewModel.class); - storeViewModel = new ViewModelProvider(this).get(StoreViewModel.class); - userViewModel = new ViewModelProvider(this).get(UserViewModel.class); + viewModel = new ViewModelProvider(this).get(AdoptionDetailViewModel.class); } @Override @@ -76,6 +58,7 @@ public class AdoptionDetailFragment extends Fragment { super.onViewCreated(view, savedInstanceState); setupSpinners(); setupDatePicker(); + observeViewModel(); loadSpinnersData(); handleArguments(); @@ -84,155 +67,146 @@ public class AdoptionDetailFragment extends Fragment { binding.btnDeleteAdoption.setOnClickListener(v -> confirmDelete()); } + private void observeViewModel() { + viewModel.getPetList().observe(getViewLifecycleOwner(), list -> refreshPetSpinner()); + viewModel.getCustomerList().observe(getViewLifecycleOwner(), list -> refreshCustomerSpinner()); + viewModel.getStoreList().observe(getViewLifecycleOwner(), list -> refreshStoreSpinner()); + viewModel.getEmployeeList().observe(getViewLifecycleOwner(), list -> refreshEmployeeSpinner()); + } + + private void setLoading(boolean loading) { + if (binding != null && binding.progressBar != null) { + binding.progressBar.setVisibility(loading ? View.VISIBLE : View.GONE); + } + } + @Override public void onDestroyView() { super.onDestroyView(); binding = null; } - /** - * Configures the spinner for adoption status. - */ private void setupSpinners() { SpinnerUtils.setupStringSpinner(requireContext(), binding.spinnerAdoptionStatus, STATUSES); + + UIUtils.setViewsEnabled(false, binding.spinnerAdoptionPet); + + binding.spinnerAdoptionCustomer.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { + @Override + public void onItemSelected(AdapterView parent, View view, int position, long id) { + if (position > 0) { + UIUtils.setViewsEnabled(true, binding.spinnerAdoptionPet); + } else { + if (!viewModel.isEditing()) { + binding.spinnerAdoptionPet.setSelection(0); + UIUtils.setViewsEnabled(false, binding.spinnerAdoptionPet); + } + } + } + @Override + public void onNothingSelected(AdapterView parent) {} + }); + + binding.spinnerAdoptionStore.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { + @Override + public void onItemSelected(AdapterView parent, View view, int position, long id) { + if (position > 0 && viewModel.getStoreList().getValue() != null && position <= viewModel.getStoreList().getValue().size()) { + DropdownDTO selectedStore = viewModel.getStoreList().getValue().get(position - 1); + loadEmployees(selectedStore.getId()); + } else { + viewModel.setEmployeeList(new ArrayList<>()); + } + } + @Override + public void onNothingSelected(AdapterView parent) {} + }); } - /** - * Configures the date picker dialog for the adoption date field. - */ private void setupDatePicker() { - binding.etAdoptionDate.setOnClickListener(v -> { - Calendar c = Calendar.getInstance(); - new DatePickerDialog(requireContext(), - (dp, y, m, d) -> binding.etAdoptionDate.setText( - String.format("%04d-%02d-%02d", y, m + 1, d)), - c.get(Calendar.YEAR), - c.get(Calendar.MONTH), - c.get(Calendar.DAY_OF_MONTH)).show(); - }); + binding.etAdoptionDate.setOnClickListener(v -> UIUtils.showDatePicker(requireContext(), binding.etAdoptionDate, null)); } - /** - * Fetches required data for spinners from the backend. - */ private void loadSpinnersData() { - loadPets(); - loadCustomers(); - loadStores(); - loadEmployees(); - } - - /** - * Loads the list of pets from the API. - */ - private void loadPets() { - petViewModel.getAllPets(0, 200, null, null, null, null, "petName").observe(getViewLifecycleOwner(), resource -> { + viewModel.loadPets().observe(getViewLifecycleOwner(), resource -> { + if (resource == null) return; + setLoading(resource.status == Resource.Status.LOADING); if (resource.status == Resource.Status.SUCCESS && resource.data != null) { - petList = resource.data.getContent(); - refreshPetSpinner(); + viewModel.setPetList(resource.data); + } + }); + viewModel.loadCustomers().observe(getViewLifecycleOwner(), resource -> { + if (resource == null) return; + setLoading(resource.status == Resource.Status.LOADING); + if (resource.status == Resource.Status.SUCCESS && resource.data != null) { + viewModel.setCustomerList(resource.data); + } + }); + viewModel.loadStores().observe(getViewLifecycleOwner(), resource -> { + if (resource == null) return; + setLoading(resource.status == Resource.Status.LOADING); + if (resource.status == Resource.Status.SUCCESS && resource.data != null) { + viewModel.setStoreList(resource.data); } }); } - /** - * Populates the pet selection spinner with data. - */ private void refreshPetSpinner() { - SpinnerUtils.populateSpinner(requireContext(), binding.spinnerAdoptionPet, petList, - PetDTO::getPetName, "-- Select Pet --", - preselectedPetId, PetDTO::getPetId); + SpinnerUtils.populateSpinner(requireContext(), binding.spinnerAdoptionPet, viewModel.getPetList().getValue(), + DropdownDTO::getLabel, "-- Select Pet --", + preselectedPetId, DropdownDTO::getId); } - /** - * Loads the list of customers from the API. - */ - private void loadCustomers() { - customerViewModel.getAllCustomers(0, 200).observe(getViewLifecycleOwner(), resource -> { - if (resource.status == Resource.Status.SUCCESS && resource.data != null) { - customerList = resource.data.getContent(); - refreshCustomerSpinner(); - } - }); - } - - /** - * Populates the customer selection spinner with data. - */ private void refreshCustomerSpinner() { - SpinnerUtils.populateSpinner(requireContext(), binding.spinnerAdoptionCustomer, customerList, - item -> item.getFirstName() + " " + item.getLastName(), - "-- Select Customer --", - preselectedCustomerId, CustomerDTO::getCustomerId); + SpinnerUtils.populateSpinner(requireContext(), binding.spinnerAdoptionCustomer, viewModel.getCustomerList().getValue(), + DropdownDTO::getLabel, "-- Select Customer --", + preselectedCustomerId, DropdownDTO::getId); } - /** - * Loads the list of stores from the API. - */ - private void loadStores() { - storeViewModel.getAllStores(0, 200).observe(getViewLifecycleOwner(), resource -> { - if (resource.status == Resource.Status.SUCCESS && resource.data != null) { - storeList = resource.data.getContent(); - refreshStoreSpinner(); - } - }); - } - - /** - * Populates the store selection spinner with data. - */ private void refreshStoreSpinner() { - SpinnerUtils.populateSpinner(requireContext(), binding.spinnerAdoptionStore, storeList, - StoreDTO::getStoreName, "-- Select Store --", - preselectedStoreId, StoreDTO::getStoreId); + SpinnerUtils.populateSpinner(requireContext(), binding.spinnerAdoptionStore, viewModel.getStoreList().getValue(), + DropdownDTO::getLabel, "-- Select Store --", + preselectedStoreId, DropdownDTO::getId); } - /** - * Loads the list of employees from the API. - */ - private void loadEmployees() { - userViewModel.getUsers("STAFF", 0, 100).observe(getViewLifecycleOwner(), resource -> { + private void loadEmployees(Long storeId) { + viewModel.loadEmployees(storeId).observe(getViewLifecycleOwner(), resource -> { + if (resource == null) return; + setLoading(resource.status == Resource.Status.LOADING); if (resource.status == Resource.Status.SUCCESS && resource.data != null) { - employeeList = resource.data.getContent(); - refreshEmployeeSpinner(); + viewModel.setEmployeeList(resource.data); } }); } - /** - * Populates the employee selection spinner with data. - */ private void refreshEmployeeSpinner() { - SpinnerUtils.populateSpinner(requireContext(), binding.spinnerAdoptionEmployee, employeeList, - UserDTO::getFullName, "-- Select Staff --", - preselectedEmployeeId, UserDTO::getId); + SpinnerUtils.populateSpinner(requireContext(), binding.spinnerAdoptionEmployee, viewModel.getEmployeeList().getValue(), + DropdownDTO::getLabel, "-- Select Staff --", + preselectedEmployeeId, DropdownDTO::getId); } - /** - * Handles arguments to determine if the fragment is in edit or add mode. - */ private void handleArguments() { Bundle a = getArguments(); if (a != null && a.containsKey("adoptionId")) { - isEditing = true; - adoptionId = a.getLong("adoptionId"); + long adoptionId = a.getLong("adoptionId"); + viewModel.setAdoptionId(adoptionId); binding.tvAdoptionMode.setText("Edit Adoption"); - binding.tvAdoptionId.setText("ID: " + adoptionId); + binding.tvAdoptionId.setText(DateTimeUtils.formatId(adoptionId)); binding.tvAdoptionId.setVisibility(View.VISIBLE); binding.btnDeleteAdoption.setVisibility(View.VISIBLE); loadAdoptionData(); } else { + viewModel.setAdoptionId(-1); binding.tvAdoptionMode.setText("Add Adoption"); binding.btnDeleteAdoption.setVisibility(View.GONE); binding.tvAdoptionId.setVisibility(View.GONE); + UIUtils.setViewsEnabled(false, binding.spinnerAdoptionPet); } } - /** - * Fetches specific adoption details from the backend using the ID. - */ private void loadAdoptionData() { - adoptionViewModel.getAdoptionById(adoptionId).observe(getViewLifecycleOwner(), resource -> { + viewModel.loadAdoption().observe(getViewLifecycleOwner(), resource -> { if (resource == null) return; + setLoading(resource.status == Resource.Status.LOADING); if (resource.status == Resource.Status.SUCCESS && resource.data != null) { AdoptionDTO a = resource.data; preselectedPetId = a.getPetId() != null ? a.getPetId() : -1; @@ -247,90 +221,68 @@ public class AdoptionDetailFragment extends Fragment { refreshPetSpinner(); refreshCustomerSpinner(); refreshStoreSpinner(); - refreshEmployeeSpinner(); + + if (preselectedCustomerId != -1) { + UIUtils.setViewsEnabled(true, binding.spinnerAdoptionPet); + } } else if (resource.status == Resource.Status.ERROR) { Toast.makeText(getContext(), "Failed to load adoption: " + resource.message, Toast.LENGTH_SHORT).show(); } }); } - /** - * Validates input and saves the adoption request to the backend. - */ private void saveAdoption() { - if (binding.spinnerAdoptionCustomer.getSelectedItemPosition() == 0) { - Toast.makeText(getContext(), "Select a customer", Toast.LENGTH_SHORT).show(); return; - } - if (binding.spinnerAdoptionPet.getSelectedItemPosition() == 0) { - Toast.makeText(getContext(), "Select a pet", Toast.LENGTH_SHORT).show(); return; - } - if (binding.spinnerAdoptionStore.getSelectedItemPosition() == 0) { - Toast.makeText(getContext(), "Select a store", Toast.LENGTH_SHORT).show(); return; - } - String date = binding.etAdoptionDate.getText().toString().trim(); - if (date.isEmpty()) { - Toast.makeText(getContext(), "Select a date", Toast.LENGTH_SHORT).show(); return; - } + if (!InputValidator.isSpinnerSelected(binding.spinnerAdoptionCustomer, "Customer")) return; + if (!InputValidator.isSpinnerSelected(binding.spinnerAdoptionPet, "Pet")) return; + if (!InputValidator.isSpinnerSelected(binding.spinnerAdoptionStore, "Store")) return; + if (!InputValidator.isNotEmpty(binding.etAdoptionDate, "Adoption Date")) return; BigDecimal fee = BigDecimal.ZERO; String feeStr = binding.etAdoptionFee.getText().toString().trim(); if (!feeStr.isEmpty()) { - try { - fee = new BigDecimal(feeStr); - } catch (NumberFormatException e) { - Toast.makeText(getContext(), "Invalid fee format", Toast.LENGTH_SHORT).show(); - return; - } + if (!InputValidator.isPositiveDecimal(binding.etAdoptionFee, "Adoption Fee")) return; + fee = new BigDecimal(feeStr); } - CustomerDTO customer = customerList.get(binding.spinnerAdoptionCustomer.getSelectedItemPosition() - 1); - PetDTO pet = petList.get(binding.spinnerAdoptionPet.getSelectedItemPosition() - 1); - StoreDTO store = storeList.get(binding.spinnerAdoptionStore.getSelectedItemPosition() - 1); + DropdownDTO customer = viewModel.getCustomerList().getValue().get(binding.spinnerAdoptionCustomer.getSelectedItemPosition() - 1); + DropdownDTO pet = viewModel.getPetList().getValue().get(binding.spinnerAdoptionPet.getSelectedItemPosition() - 1); + DropdownDTO store = viewModel.getStoreList().getValue().get(binding.spinnerAdoptionStore.getSelectedItemPosition() - 1); Long employeeId = null; if (binding.spinnerAdoptionEmployee.getSelectedItemPosition() > 0) { - employeeId = employeeList.get(binding.spinnerAdoptionEmployee.getSelectedItemPosition() - 1).getId(); + employeeId = viewModel.getEmployeeList().getValue().get(binding.spinnerAdoptionEmployee.getSelectedItemPosition() - 1).getId(); } + String adoptionDate = binding.etAdoptionDate.getText().toString().trim(); String status = STATUSES[binding.spinnerAdoptionStatus.getSelectedItemPosition()]; AdoptionDTO dto = new AdoptionDTO( - pet.getPetId(), - customer.getCustomerId(), + pet.getId(), + customer.getId(), employeeId, - store.getStoreId(), - date, + store.getId(), + adoptionDate, status, fee ); - if (isEditing) { - adoptionViewModel.updateAdoption(adoptionId, dto).observe(getViewLifecycleOwner(), resource -> { - if (resource.status == Resource.Status.SUCCESS) { - Toast.makeText(getContext(), "Updated", Toast.LENGTH_SHORT).show(); - navigateBack(); - } else if (resource.status == Resource.Status.ERROR) { - Toast.makeText(getContext(), "Error: " + resource.message, Toast.LENGTH_SHORT).show(); - } - }); - } else { - adoptionViewModel.createAdoption(dto).observe(getViewLifecycleOwner(), resource -> { - if (resource.status == Resource.Status.SUCCESS) { - Toast.makeText(getContext(), "Saved", Toast.LENGTH_SHORT).show(); - navigateBack(); - } else if (resource.status == Resource.Status.ERROR) { - Toast.makeText(getContext(), "Error: " + resource.message, Toast.LENGTH_SHORT).show(); - } - }); - } + viewModel.saveAdoption(dto).observe(getViewLifecycleOwner(), resource -> { + if (resource == null) return; + setLoading(resource.status == Resource.Status.LOADING); + if (resource.status == Resource.Status.SUCCESS) { + Toast.makeText(getContext(), viewModel.isEditing() ? "Updated" : "Saved", Toast.LENGTH_SHORT).show(); + navigateBack(); + } else if (resource.status == Resource.Status.ERROR) { + Toast.makeText(getContext(), "Error: " + resource.message, Toast.LENGTH_SHORT).show(); + } + }); } - /** - * Shows a confirmation dialog before deleting an adoption request. - */ private void confirmDelete() { - DialogUtils.showDeleteConfirmDialog(requireContext(), "Adoption", () -> - adoptionViewModel.deleteAdoption(adoptionId).observe(getViewLifecycleOwner(), resource -> { + DialogUtils.showDeleteConfirmDialog(requireContext(), "Adoption Record", () -> + viewModel.deleteAdoption().observe(getViewLifecycleOwner(), resource -> { + if (resource == null) return; + setLoading(resource.status == Resource.Status.LOADING); if (resource.status == Resource.Status.SUCCESS) { Toast.makeText(getContext(), "Deleted", Toast.LENGTH_SHORT).show(); navigateBack(); @@ -340,9 +292,6 @@ public class AdoptionDetailFragment extends Fragment { })); } - /** - * Navigates back to the previous fragment. - */ private void navigateBack() { NavHostFragment.findNavController(this).popBackStack(); } diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/AppointmentDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/AppointmentDetailFragment.java index 1081462a..bb2b9d58 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/AppointmentDetailFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/AppointmentDetailFragment.java @@ -1,10 +1,12 @@ package com.example.petstoremobile.fragments.listfragments.detailfragments; -import android.app.DatePickerDialog; import android.os.Bundle; -import android.util.Log; -import android.view.*; -import android.widget.*; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AdapterView; +import android.widget.Toast; + import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; @@ -12,18 +14,16 @@ import androidx.lifecycle.ViewModelProvider; import androidx.navigation.fragment.NavHostFragment; import com.example.petstoremobile.databinding.FragmentAppointmentDetailBinding; -import com.example.petstoremobile.dtos.*; +import com.example.petstoremobile.dtos.AppointmentDTO; +import com.example.petstoremobile.dtos.DropdownDTO; +import com.example.petstoremobile.dtos.ServiceDTO; +import com.example.petstoremobile.utils.DateTimeUtils; import com.example.petstoremobile.utils.DialogUtils; +import com.example.petstoremobile.utils.InputValidator; import com.example.petstoremobile.utils.Resource; import com.example.petstoremobile.utils.SpinnerUtils; -import com.example.petstoremobile.viewmodels.AppointmentViewModel; -import com.example.petstoremobile.viewmodels.CustomerViewModel; -import com.example.petstoremobile.viewmodels.PetViewModel; -import com.example.petstoremobile.viewmodels.ServiceViewModel; -import com.example.petstoremobile.viewmodels.StoreViewModel; -import com.example.petstoremobile.viewmodels.UserViewModel; - -import java.util.*; +import com.example.petstoremobile.utils.UIUtils; +import com.example.petstoremobile.viewmodels.AppointmentDetailViewModel; import dagger.hilt.android.AndroidEntryPoint; @@ -35,39 +35,22 @@ public class AppointmentDetailFragment extends Fragment { private FragmentAppointmentDetailBinding binding; - private long appointmentId = -1; - private boolean isEditing = false; private long preselectedPetId = -1; private long preselectedServiceId = -1; private long preselectedCustomerId = -1; private long preselectedStoreId = -1; private long preselectedStaffId = -1; - private List petList = new ArrayList<>(); - private List serviceList = new ArrayList<>(); - private List customerList = new ArrayList<>(); - private List storeList = new ArrayList<>(); - private List staffList = new ArrayList<>(); + private final Integer[] HOURS = {9, 10, 11, 12, 13, 14, 15, 16, 17}; + private final Integer[] MINUTES = {0, 15, 30, 45}; - private final Integer[] HOURS = {9,10,11,12,13,14,15,16,17}; - private final Integer[] MINUTES = {0,15,30,45}; - - private AppointmentViewModel appointmentViewModel; - private PetViewModel petViewModel; - private ServiceViewModel serviceViewModel; - private StoreViewModel storeViewModel; - private CustomerViewModel customerViewModel; - private UserViewModel userViewModel; + private AppointmentDetailViewModel viewModel; + private boolean isUpdatingUI = false; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - appointmentViewModel = new ViewModelProvider(this).get(AppointmentViewModel.class); - petViewModel = new ViewModelProvider(this).get(PetViewModel.class); - serviceViewModel = new ViewModelProvider(this).get(ServiceViewModel.class); - storeViewModel = new ViewModelProvider(this).get(StoreViewModel.class); - customerViewModel = new ViewModelProvider(this).get(CustomerViewModel.class); - userViewModel = new ViewModelProvider(this).get(UserViewModel.class); + viewModel = new ViewModelProvider(this).get(AppointmentDetailViewModel.class); } @Override @@ -81,7 +64,8 @@ public class AppointmentDetailFragment extends Fragment { super.onViewCreated(view, savedInstanceState); setupSpinners(); setupDatePicker(); - loadSpinnersData(); + observeViewModel(); + viewModel.loadInitialFormData(); handleArguments(); binding.btnApptBack.setOnClickListener(v -> navigateBack()); @@ -95,365 +79,214 @@ public class AppointmentDetailFragment extends Fragment { binding = null; } - /** - * Configures the adapters for spinners. - */ private void setupSpinners() { - SpinnerUtils.setupStringSpinner(requireContext(), binding.spinnerAppointmentStatus, - new String[]{"Booked", "Completed", "Cancelled", "Missed"}); + SpinnerUtils.setupStringSpinner(requireContext(), binding.spinnerAppointmentStatus, new String[]{}); String[] hours = new String[HOURS.length]; for (int i = 0; i < HOURS.length; i++) - hours[i] = String.format("%02d:00", HOURS[i]); + hours[i] = DateTimeUtils.formatTime(HOURS[i], 0); SpinnerUtils.setupStringSpinner(requireContext(), binding.spinnerHour, hours); - SpinnerUtils.setupStringSpinner(requireContext(), binding.spinnerMinute, new String[]{"00","15","30","45"}); + SpinnerUtils.setupStringSpinner(requireContext(), binding.spinnerMinute, new String[]{"00", "15", "30", "45"}); + + UIUtils.setViewsEnabled(false, binding.spinnerPet, binding.spinnerStaff); + + binding.spinnerCustomer.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { + @Override + public void onItemSelected(AdapterView parent, View view, int position, long id) { + viewModel.onCustomerSelected(position); + } + @Override + public void onNothingSelected(AdapterView parent) {} + }); + + binding.spinnerStore.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { + @Override + public void onItemSelected(AdapterView parent, View view, int position, long id) { + viewModel.onStoreSelected(position); + } + @Override + public void onNothingSelected(AdapterView parent) {} + }); + + SpinnerUtils.setOnIndexSelectedListener(binding.spinnerService, p -> viewModel.onServiceSelected(p)); + SpinnerUtils.setOnIndexSelectedListener(binding.spinnerPet, p -> viewModel.onPetSelected(p)); + SpinnerUtils.setOnIndexSelectedListener(binding.spinnerStaff, p -> viewModel.onStaffSelected(p)); + + SpinnerUtils.setOnIndexSelectedListener(binding.spinnerHour, p -> notifyDateTimeStatusChange()); + SpinnerUtils.setOnIndexSelectedListener(binding.spinnerMinute, p -> notifyDateTimeStatusChange()); + SpinnerUtils.setOnIndexSelectedListener(binding.spinnerAppointmentStatus, p -> notifyDateTimeStatusChange()); } - /** - * Configures the date picker dialog for the appointment date field. - */ private void setupDatePicker() { - binding.etAppointmentDate.setOnClickListener(v -> { - Calendar c = Calendar.getInstance(); - DatePickerDialog d = new DatePickerDialog(requireContext(), - (dp,y,m,d1) -> binding.etAppointmentDate.setText( - String.format("%04d-%02d-%02d", y, m+1, d1)), - c.get(Calendar.YEAR), c.get(Calendar.MONTH), - c.get(Calendar.DAY_OF_MONTH)); - d.getDatePicker().setMinDate(System.currentTimeMillis() - 1000); - d.show(); - }); + binding.etAppointmentDate.setOnClickListener(v -> + UIUtils.showDatePicker(requireContext(), binding.etAppointmentDate, this::notifyDateTimeStatusChange)); } - /** - * Fetches all required data for spinners from the backend. - */ - private void loadSpinnersData() { - loadPets(); - loadServices(); - loadCustomers(); - loadStores(); - loadStaff(); + private void observeViewModel() { + viewModel.getViewState().observe(getViewLifecycleOwner(), this::applyViewState); + + viewModel.getCustomers().observe(getViewLifecycleOwner(), list -> + SpinnerUtils.populateSpinner(requireContext(), binding.spinnerCustomer, list, DropdownDTO::getLabel, "-- Select Customer --", preselectedCustomerId, DropdownDTO::getId)); + + viewModel.getStores().observe(getViewLifecycleOwner(), list -> + SpinnerUtils.populateSpinner(requireContext(), binding.spinnerStore, list, DropdownDTO::getLabel, "-- Select Store --", preselectedStoreId, DropdownDTO::getId)); + + viewModel.getServices().observe(getViewLifecycleOwner(), list -> + SpinnerUtils.populateSpinner(requireContext(), binding.spinnerService, list, ServiceDTO::getServiceName, "-- Select Service --", preselectedServiceId, ServiceDTO::getServiceId)); + + viewModel.getCustomerPets().observe(getViewLifecycleOwner(), list -> + SpinnerUtils.populateSpinner(requireContext(), binding.spinnerPet, list, DropdownDTO::getLabel, "-- Select Pet --", preselectedPetId, DropdownDTO::getId)); + + viewModel.getStoreEmployees().observe(getViewLifecycleOwner(), list -> + SpinnerUtils.populateSpinner(requireContext(), binding.spinnerStaff, list, DropdownDTO::getLabel, "-- Select Staff --", preselectedStaffId, DropdownDTO::getId)); } - /** - * Loads the list of pets from the ViewModel. - */ - private void loadPets() { - petViewModel.getAllPets(0, 200, null, null, null, null, "petName").observe(getViewLifecycleOwner(), resource -> { - if (resource.status == Resource.Status.SUCCESS && resource.data != null) { - petList = resource.data.getContent(); - refreshPetSpinner(); - } - }); + private void setLoading(boolean loading) { + if (binding != null && binding.progressBar != null) { + binding.progressBar.setVisibility(loading ? View.VISIBLE : View.GONE); + } } - /** - * Populates the pet selection spinner. - */ - private void refreshPetSpinner() { - SpinnerUtils.populateSpinner(requireContext(), binding.spinnerPet, petList, - PetDTO::getPetName, "-- Select Pet --", - preselectedPetId, PetDTO::getPetId); + private void applyViewState(AppointmentDetailViewModel.ViewState state) { + isUpdatingUI = true; + + binding.tvApptMode.setText(state.isEditing ? "Edit Appointment" : "Add Appointment"); + binding.tvAppointmentId.setText(DateTimeUtils.formatId(viewModel.getAppointmentId())); + binding.tvAppointmentId.setVisibility(state.isEditing ? View.VISIBLE : View.GONE); + binding.btnDeleteAppointment.setVisibility(state.isDeleteVisible ? View.VISIBLE : View.GONE); + binding.btnSaveAppointment.setVisibility(state.isSaveVisible ? View.VISIBLE : View.GONE); + + UIUtils.setFieldEnabled(state.isCustomerEnabled, binding.spinnerCustomer, binding.tvLabelCustomer); + UIUtils.setFieldEnabled(state.isStoreEnabled, binding.spinnerStore, binding.tvLabelStore); + UIUtils.setFieldEnabled(state.isPetEnabled, binding.spinnerPet, binding.tvLabelPet); + UIUtils.setFieldEnabled(state.isServiceEnabled, binding.spinnerService, binding.tvLabelService); + UIUtils.setFieldEnabled(state.isStaffEnabled, binding.spinnerStaff, binding.tvLabelStaff); + UIUtils.setFieldEnabled(state.isDateEnabled, binding.etAppointmentDate, binding.tvLabelDate); + UIUtils.setFieldEnabled(state.isTimeEnabled, binding.spinnerHour, binding.tvLabelTime); + UIUtils.setViewsEnabled(state.isTimeEnabled, binding.spinnerMinute); + UIUtils.setViewsEnabled(state.isStatusEnabled, binding.spinnerAppointmentStatus); + + Object selected = binding.spinnerAppointmentStatus.getSelectedItem(); + String current = selected != null ? selected.toString() : ""; + SpinnerUtils.setupStringSpinner(requireContext(), binding.spinnerAppointmentStatus, state.availableStatuses); + SpinnerUtils.setSelectionByValue(binding.spinnerAppointmentStatus, current); + + isUpdatingUI = false; } - /** - * Loads the list of services from the API. - */ - private void loadServices() { - serviceViewModel.getAllServices(0, 200, null, "serviceName").observe(getViewLifecycleOwner(), resource -> { - if (resource.status == Resource.Status.SUCCESS && resource.data != null) { - serviceList = resource.data.getContent(); - refreshServiceSpinner(); - } - }); + private void notifyDateTimeStatusChange() { + if (isUpdatingUI) return; + + String date = binding.etAppointmentDate.getText().toString(); + String time = buildTimeString(); + Object selected = binding.spinnerAppointmentStatus.getSelectedItem(); + String status = selected != null ? selected.toString() : ""; + viewModel.onDateOrTimeChanged(date, time, status); } - /** - * Populates the service selection spinner. - */ - private void refreshServiceSpinner() { - SpinnerUtils.populateSpinner(requireContext(), binding.spinnerService, serviceList, - ServiceDTO::getServiceName, "-- Select Service --", - preselectedServiceId, ServiceDTO::getServiceId); - } - - /** - * Loads the list of customers from the API. - */ - private void loadCustomers() { - customerViewModel.getAllCustomers(0, 200).observe(getViewLifecycleOwner(), resource -> { - if (resource.status == Resource.Status.SUCCESS && resource.data != null) { - customerList = resource.data.getContent(); - refreshCustomerSpinner(); - } - }); - } - - /** - * Populates the customer selection spinner. - */ - private void refreshCustomerSpinner() { - SpinnerUtils.populateSpinner(requireContext(), binding.spinnerCustomer, customerList, - item -> item.getFirstName() + " " + item.getLastName(), - "-- Select Customer --", - preselectedCustomerId, CustomerDTO::getCustomerId); - } - - /** - * Loads the list of stores from the API. - */ - private void loadStores() { - storeViewModel.getAllStores(0, 50).observe(getViewLifecycleOwner(), resource -> { - if (resource.status == Resource.Status.SUCCESS && resource.data != null) { - storeList = resource.data.getContent(); - refreshStoreSpinner(); - } - }); - } - - /** - * Populates the store selection spinner. - */ - private void refreshStoreSpinner() { - SpinnerUtils.populateSpinner(requireContext(), binding.spinnerStore, storeList, - StoreDTO::getStoreName, "-- Select Store --", - preselectedStoreId, StoreDTO::getStoreId); - } - - /** - * Loads the list of staff from the API. - */ - private void loadStaff() { - userViewModel.getUsers("STAFF", 0, 100).observe(getViewLifecycleOwner(), resource -> { - if (resource.status == Resource.Status.SUCCESS && resource.data != null) { - staffList = resource.data.getContent(); - refreshStaffSpinner(); - } - }); - } - - /** - * Populates the staff selection spinner. - */ - private void refreshStaffSpinner() { - SpinnerUtils.populateSpinner(requireContext(), binding.spinnerStaff, staffList, - UserDTO::getFullName, "-- Select Staff --", - preselectedStaffId, UserDTO::getId); - } - - /** - * Handles arguments to determine if the fragment is in edit or add mode. - */ private void handleArguments() { Bundle a = getArguments(); if (a != null && a.containsKey("appointmentId")) { - isEditing = true; - appointmentId = a.getLong("appointmentId"); - binding.tvApptMode.setText("Edit Appointment"); - binding.tvAppointmentId.setText("ID: " + appointmentId); - binding.tvAppointmentId.setVisibility(View.VISIBLE); - binding.btnDeleteAppointment.setVisibility(View.VISIBLE); + viewModel.setAppointmentId(a.getLong("appointmentId")); loadAppointmentData(); } else { - binding.tvApptMode.setText("Add Appointment"); - binding.btnDeleteAppointment.setVisibility(View.GONE); - binding.tvAppointmentId.setVisibility(View.GONE); + viewModel.setAppointmentId(-1); } } - /** - * Fetches specific appointment details from the backend using the ID. - */ private void loadAppointmentData() { - appointmentViewModel.getAppointmentById(appointmentId).observe(getViewLifecycleOwner(), resource -> { + viewModel.loadAppointment().observe(getViewLifecycleOwner(), resource -> { if (resource == null) return; + setLoading(resource.status == Resource.Status.LOADING); if (resource.status == Resource.Status.SUCCESS && resource.data != null) { AppointmentDTO a = resource.data; - preselectedPetId = (a.getPetId() != null) ? a.getPetId() : -1; - preselectedServiceId = (a.getServiceId() != null) ? a.getServiceId() : -1; - preselectedCustomerId = (a.getCustomerId() != null) ? a.getCustomerId() : -1; - preselectedStoreId = (a.getStoreId() != null) ? a.getStoreId() : -1; - preselectedStaffId = (a.getEmployeeId() != null) ? a.getEmployeeId() : -1; + preselectedPetId = a.getPetId() != null ? a.getPetId() : -1; + preselectedServiceId = a.getServiceId() != null ? a.getServiceId() : -1; + preselectedCustomerId = a.getCustomerId() != null ? a.getCustomerId() : -1; + preselectedStoreId = a.getStoreId() != null ? a.getStoreId() : -1; + preselectedStaffId = a.getEmployeeId() != null ? a.getEmployeeId() : -1; binding.etAppointmentDate.setText(a.getAppointmentDate()); - - // Pre-fill time spinners - String time = a.getAppointmentTime() != null ? a.getAppointmentTime() : "09:00"; - if (time.length() > 5) time = time.substring(0, 5); - String[] parts = time.split(":"); - if (parts.length == 2) { - try { - int hour = Integer.parseInt(parts[0]); - int min = Integer.parseInt(parts[1]); - for (int i = 0; i < HOURS.length; i++) - if (HOURS[i] == hour) { binding.spinnerHour.setSelection(i); break; } - for (int i = 0; i < MINUTES.length; i++) - if (MINUTES[i] == min) { binding.spinnerMinute.setSelection(i); break; } - } catch (NumberFormatException ignored) {} - } - - // Match Title labels with backend values + parseAndSetTimeSpinners(a.getAppointmentTime() != null ? a.getAppointmentTime() : "09:00"); + String status = a.getAppointmentStatus(); if (status != null && !status.isEmpty()) { - String formattedStatus = status.substring(0, 1).toUpperCase() + status.substring(1).toLowerCase(); - SpinnerUtils.setSelectionByValue(binding.spinnerAppointmentStatus, formattedStatus); + SpinnerUtils.setSelectionByValue(binding.spinnerAppointmentStatus, DateTimeUtils.formatStatusFromBackend(status)); } - - refreshPetSpinner(); - refreshServiceSpinner(); - refreshCustomerSpinner(); - refreshStoreSpinner(); - refreshStaffSpinner(); - } else if (resource.status == Resource.Status.ERROR) { - Toast.makeText(getContext(), "Failed to load appointment: " + resource.message, Toast.LENGTH_SHORT).show(); + notifyDateTimeStatusChange(); } }); } - /** - * Validates input and saves the appointment to the backend. - */ private void saveAppointment() { - if (binding.spinnerCustomer.getSelectedItemPosition() == 0) { - Toast.makeText(getContext(), "Select a customer", Toast.LENGTH_SHORT).show(); return; - } - if (binding.spinnerStore.getSelectedItemPosition() == 0) { - Toast.makeText(getContext(), "Select a store", Toast.LENGTH_SHORT).show(); return; - } - if (binding.spinnerPet.getSelectedItemPosition() == 0) { - Toast.makeText(getContext(), "Select a pet", Toast.LENGTH_SHORT).show(); return; - } - if (binding.spinnerService.getSelectedItemPosition() == 0) { - Toast.makeText(getContext(), "Select a service", Toast.LENGTH_SHORT).show(); return; - } + if (!validateRequiredFields()) return; + String date = binding.etAppointmentDate.getText().toString().trim(); - if (date.isEmpty()) { - Toast.makeText(getContext(), "Select a date", Toast.LENGTH_SHORT).show(); return; - } - - CustomerDTO customer = customerList.get(binding.spinnerCustomer.getSelectedItemPosition() - 1); - StoreDTO store = storeList.get(binding.spinnerStore.getSelectedItemPosition() - 1); - PetDTO pet = petList.get(binding.spinnerPet.getSelectedItemPosition() - 1); - ServiceDTO service = serviceList.get(binding.spinnerService.getSelectedItemPosition() - 1); - - Long employeeId = null; - if (binding.spinnerStaff.getSelectedItemPosition() > 0) { - employeeId = staffList.get(binding.spinnerStaff.getSelectedItemPosition() - 1).getId(); - } - - String time = String.format("%02d:%02d", - HOURS[binding.spinnerHour.getSelectedItemPosition()], - MINUTES[binding.spinnerMinute.getSelectedItemPosition()]); - - // Get status and convert to uppercase for backend + String time = buildTimeString(); String status = binding.spinnerAppointmentStatus.getSelectedItem().toString().toUpperCase(); - - // Validate future date+time if status is BOOKED - if ("BOOKED".equalsIgnoreCase(status)) { - try { - String[] dateParts = date.split("-"); - String[] timeParts = time.split(":"); - Calendar selected = Calendar.getInstance(); - selected.set( - Integer.parseInt(dateParts[0]), - Integer.parseInt(dateParts[1]) - 1, - Integer.parseInt(dateParts[2]), - Integer.parseInt(timeParts[0]), - Integer.parseInt(timeParts[1]), - 0 - ); - if (selected.before(Calendar.getInstance())) { - DialogUtils.showInfoDialog(requireContext(), "Invalid Time", - "Booked appointments must be in the future. " + - "Please select a future date and time."); - return; - } - } catch (Exception e) { - Log.e("APPT_SAVE", "Date parse error: " + e.getMessage()); - } + if (!viewModel.isValidFutureBooking(status, date, time)) { + DialogUtils.showInfoDialog(requireContext(), "Invalid Time", "Booked appointments must be in the future."); + return; } - // Build DTO with all required IDs - AppointmentDTO dto = new AppointmentDTO( - customer.getCustomerId(), - store.getStoreId(), - service.getServiceId(), - employeeId, - date, - time, - status, - pet.getPetId() - ); - - androidx.lifecycle.Observer> observer = resource -> { + viewModel.saveAppointment(date, time, status).observe(getViewLifecycleOwner(), resource -> { + if (resource == null) return; + setLoading(resource.status == Resource.Status.LOADING); if (resource.status == Resource.Status.SUCCESS) { - Toast.makeText(getContext(), isEditing ? "Updated" : "Saved", Toast.LENGTH_SHORT).show(); + AppointmentDetailViewModel.ViewState state = viewModel.getViewState().getValue(); + String message = (state != null && state.isEditing) ? "Updated" : "Saved"; + Toast.makeText(getContext(), message, Toast.LENGTH_SHORT).show(); navigateBack(); } else if (resource.status == Resource.Status.ERROR) { handleSaveError(resource.message); } - }; - - if (isEditing) { - appointmentViewModel.updateAppointment(appointmentId, dto).observe(getViewLifecycleOwner(), observer); - } else { - appointmentViewModel.createAppointment(dto).observe(getViewLifecycleOwner(), observer); - } + }); + } + + private boolean validateRequiredFields() { + if (!InputValidator.isSpinnerSelected(binding.spinnerCustomer, "Customer")) return false; + if (!InputValidator.isSpinnerSelected(binding.spinnerStore, "Store")) return false; + if (!InputValidator.isSpinnerSelected(binding.spinnerPet, "Pet")) return false; + if (!InputValidator.isSpinnerSelected(binding.spinnerService, "Service")) return false; + if (!InputValidator.isNotEmpty(binding.etAppointmentDate, "Appointment Date")) return false; + return true; + } + + private String buildTimeString() { + return DateTimeUtils.formatTime(HOURS[binding.spinnerHour.getSelectedItemPosition()], MINUTES[binding.spinnerMinute.getSelectedItemPosition()]); } - /** - * Handles errors that occur during the saving process. - */ private void handleSaveError(String errorMessage) { - if (errorMessage != null) { - Log.e("APPT_SAVE", "Error: " + errorMessage); - if (errorMessage.toLowerCase().contains("future")) { - DialogUtils.showInfoDialog(requireContext(), "Invalid Date/Time", - "Booked appointments must be scheduled in the future."); - } else if (errorMessage.toLowerCase().contains("not available")) { - showNoAvailabilityDialog(); - } else { - Toast.makeText(getContext(), errorMessage, Toast.LENGTH_SHORT).show(); - } - } else { - Toast.makeText(getContext(), "Something went wrong", Toast.LENGTH_SHORT).show(); - } + if (errorMessage != null && errorMessage.toLowerCase().contains("not available")) showNoAvailabilityDialog(); + else Toast.makeText(getContext(), errorMessage != null ? errorMessage : "Error saving", Toast.LENGTH_SHORT).show(); } - /** - * Shows a specialized dialog when a time slot is not available. - */ private void showNoAvailabilityDialog() { new androidx.appcompat.app.AlertDialog.Builder(requireContext()) .setTitle("No Availability") - .setMessage("This time slot is already booked. Please choose a different time or date.") + .setMessage("This time slot is already booked.") .setPositiveButton("Change Time", (d, w) -> d.dismiss()) - .setNegativeButton("Cancel Booking", (d, w) -> navigateBack()) - .setCancelable(false) - .show(); + .setNegativeButton("Cancel Booking", (d, w) -> navigateBack()).show(); } - /** - * Shows a confirmation dialog and handles the deletion of an appointment. - */ private void confirmDelete() { DialogUtils.showDeleteConfirmDialog(requireContext(), "Appointment", () -> - appointmentViewModel.deleteAppointment(appointmentId).observe(getViewLifecycleOwner(), resource -> { - if (resource.status == Resource.Status.SUCCESS) { - Toast.makeText(getContext(), "Deleted", Toast.LENGTH_SHORT).show(); - navigateBack(); - } else if (resource.status == Resource.Status.ERROR) { - Toast.makeText(getContext(), "Delete failed", Toast.LENGTH_SHORT).show(); - } + viewModel.deleteAppointment().observe(getViewLifecycleOwner(), resource -> { + if (resource == null) return; + setLoading(resource.status == Resource.Status.LOADING); + if (resource.status == Resource.Status.SUCCESS) navigateBack(); })); } - /** - * Navigates back to the previous screen. - */ private void navigateBack() { NavHostFragment.findNavController(this).popBackStack(); } + + private void parseAndSetTimeSpinners(String time) { + int[] parsedTime = DateTimeUtils.parseTimeString(time); + if (parsedTime == null) return; + SpinnerUtils.setSelectionByValueArray(binding.spinnerHour, HOURS, parsedTime[0]); + SpinnerUtils.setSelectionByValueArray(binding.spinnerMinute, MINUTES, parsedTime[1]); + } } diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/InventoryDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/InventoryDetailFragment.java index 7a729b94..ee52c960 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/InventoryDetailFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/InventoryDetailFragment.java @@ -8,24 +8,20 @@ import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import androidx.appcompat.app.AlertDialog; import androidx.fragment.app.Fragment; import androidx.lifecycle.ViewModelProvider; import androidx.navigation.fragment.NavHostFragment; import com.example.petstoremobile.databinding.FragmentInventoryDetailBinding; +import com.example.petstoremobile.dtos.DropdownDTO; import com.example.petstoremobile.dtos.InventoryDTO; import com.example.petstoremobile.dtos.ProductDTO; -import com.example.petstoremobile.dtos.StoreDTO; +import com.example.petstoremobile.utils.DialogUtils; import com.example.petstoremobile.utils.InputValidator; import com.example.petstoremobile.utils.Resource; import com.example.petstoremobile.utils.SpinnerUtils; -import com.example.petstoremobile.viewmodels.InventoryViewModel; -import com.example.petstoremobile.viewmodels.ProductViewModel; -import com.example.petstoremobile.viewmodels.StoreViewModel; - -import java.util.ArrayList; -import java.util.List; +import com.example.petstoremobile.utils.UIUtils; +import com.example.petstoremobile.viewmodels.InventoryDetailViewModel; import dagger.hilt.android.AndroidEntryPoint; @@ -36,33 +32,17 @@ import dagger.hilt.android.AndroidEntryPoint; public class InventoryDetailFragment extends Fragment { private FragmentInventoryDetailBinding binding; + private InventoryDetailViewModel viewModel; - private InventoryViewModel inventoryViewModel; - private ProductViewModel productViewModel; - private StoreViewModel storeViewModel; - - private boolean isEditing = false; - private long inventoryId = -1; private long preselectedStoreId = -1; private long preselectedProductId = -1; - private List storeList = new ArrayList<>(); - private List productList = new ArrayList<>(); - - /** - * Initializes the view models. - */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - inventoryViewModel = new ViewModelProvider(this).get(InventoryViewModel.class); - productViewModel = new ViewModelProvider(this).get(ProductViewModel.class); - storeViewModel = new ViewModelProvider(this).get(StoreViewModel.class); + viewModel = new ViewModelProvider(this).get(InventoryDetailViewModel.class); } - /** - * Inflates the layout. - */ @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { @@ -70,13 +50,11 @@ public class InventoryDetailFragment extends Fragment { return binding.getRoot(); } - /** - * Sets up UI components after the view is created. - */ @Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); + observeViewModel(); loadSpinnersData(); handleArguments(); @@ -85,64 +63,57 @@ public class InventoryDetailFragment extends Fragment { binding.btnDeleteInventory.setOnClickListener(v -> confirmDelete()); } + private void observeViewModel() { + viewModel.getStoreList().observe(getViewLifecycleOwner(), list -> refreshStoreSpinner()); + viewModel.getProductList().observe(getViewLifecycleOwner(), list -> refreshProductSpinner()); + } + + private void setLoading(boolean loading) { + if (binding != null && binding.progressBar != null) { + binding.progressBar.setVisibility(loading ? View.VISIBLE : View.GONE); + } + } + @Override public void onDestroyView() { super.onDestroyView(); binding = null; } - /** - * Fetches required data for spinners from the backend. - */ private void loadSpinnersData() { - loadStores(); - loadProducts(); - } - - /** - * Loads the list of stores for the spinner. - */ - private void loadStores() { - storeViewModel.getAllStores(0, 100).observe(getViewLifecycleOwner(), resource -> { + viewModel.loadStores().observe(getViewLifecycleOwner(), resource -> { + if (resource == null) return; + setLoading(resource.status == Resource.Status.LOADING); if (resource.status == Resource.Status.SUCCESS && resource.data != null) { - storeList = resource.data.getContent(); - refreshStoreSpinner(); + viewModel.setStoreList(resource.data); + } + }); + viewModel.loadProducts().observe(getViewLifecycleOwner(), resource -> { + if (resource == null) return; + setLoading(resource.status == Resource.Status.LOADING); + if (resource.status == Resource.Status.SUCCESS && resource.data != null) { + viewModel.setProductList(resource.data.getContent()); } }); } private void refreshStoreSpinner() { - SpinnerUtils.populateSpinner(requireContext(), binding.spinnerInventoryStore, storeList, - StoreDTO::getStoreName, "-- Select Store --", - preselectedStoreId, StoreDTO::getStoreId); - } - - /** - * Loads the list of products for the spinner. - */ - private void loadProducts() { - productViewModel.getAllProducts(null, null, 0, 500, "prodName").observe(getViewLifecycleOwner(), resource -> { - if (resource.status == Resource.Status.SUCCESS && resource.data != null) { - productList = resource.data.getContent(); - refreshProductSpinner(); - } - }); + SpinnerUtils.populateSpinner(requireContext(), binding.spinnerInventoryStore, viewModel.getStoreList().getValue(), + DropdownDTO::getLabel, "-- Select Store --", + preselectedStoreId, DropdownDTO::getId); } private void refreshProductSpinner() { - SpinnerUtils.populateSpinner(requireContext(), binding.spinnerInventoryProduct, productList, + SpinnerUtils.populateSpinner(requireContext(), binding.spinnerInventoryProduct, viewModel.getProductList().getValue(), ProductDTO::getProdName, "-- Select Product --", preselectedProductId, ProductDTO::getProdId); } - /** - * Handles fragment arguments to determine if we are in edit or add mode. - */ private void handleArguments() { Bundle args = getArguments(); if (args != null && args.containsKey("inventoryId")) { - isEditing = true; - inventoryId = args.getLong("inventoryId"); + long inventoryId = args.getLong("inventoryId"); + viewModel.setInventoryId(inventoryId); binding.tvInventoryMode.setText("Edit Inventory"); binding.tvInventoryId.setText("Inventory ID: " + inventoryId); @@ -152,7 +123,7 @@ public class InventoryDetailFragment extends Fragment { loadInventoryData(); } else { - isEditing = false; + viewModel.setInventoryId(-1); binding.tvInventoryMode.setText("Add Inventory"); binding.tvInventoryId.setVisibility(View.GONE); binding.btnDeleteInventory.setVisibility(View.GONE); @@ -160,12 +131,10 @@ public class InventoryDetailFragment extends Fragment { } } - /** - * Loads existing inventory data from the backend. - */ private void loadInventoryData() { - inventoryViewModel.getInventoryById(inventoryId).observe(getViewLifecycleOwner(), resource -> { + viewModel.loadInventory().observe(getViewLifecycleOwner(), resource -> { if (resource == null) return; + setLoading(resource.status == Resource.Status.LOADING); if (resource.status == Resource.Status.SUCCESS && resource.data != null) { InventoryDTO inv = resource.data; binding.etQuantity.setText(String.valueOf(inv.getQuantity())); @@ -180,95 +149,59 @@ public class InventoryDetailFragment extends Fragment { }); } - /** - * Validates input and saves the current inventory item details to the backend. - */ private void saveInventory() { - if (binding.spinnerInventoryStore.getSelectedItemPosition() == 0) { - Toast.makeText(getContext(), "Please select a store", Toast.LENGTH_SHORT).show(); - return; - } - if (binding.spinnerInventoryProduct.getSelectedItemPosition() == 0) { - Toast.makeText(getContext(), "Please select a product", Toast.LENGTH_SHORT).show(); - return; - } - - if (!InputValidator.isNotEmpty(binding.etQuantity, "Quantity") || - !InputValidator.isPositiveInteger(binding.etQuantity, "Quantity")) { - return; - } + if (!InputValidator.isSpinnerSelected(binding.spinnerInventoryStore, "Store")) return; + if (!InputValidator.isSpinnerSelected(binding.spinnerInventoryProduct, "Product")) return; + if (!InputValidator.isPositiveInteger(binding.etQuantity, "Quantity")) return; int quantity = Integer.parseInt(binding.etQuantity.getText().toString().trim()); - StoreDTO store = storeList.get(binding.spinnerInventoryStore.getSelectedItemPosition() - 1); - ProductDTO product = productList.get(binding.spinnerInventoryProduct.getSelectedItemPosition() - 1); + DropdownDTO store = viewModel.getStoreList().getValue().get(binding.spinnerInventoryStore.getSelectedItemPosition() - 1); + ProductDTO product = viewModel.getProductList().getValue().get(binding.spinnerInventoryProduct.getSelectedItemPosition() - 1); - InventoryDTO request = new InventoryDTO(product.getProdId(), store.getStoreId(), quantity); + InventoryDTO request = new InventoryDTO(product.getProdId(), store.getId(), quantity); setButtonsEnabled(false); - if (isEditing) { - inventoryViewModel.updateInventory(inventoryId, request).observe(getViewLifecycleOwner(), resource -> { + viewModel.saveInventory(request).observe(getViewLifecycleOwner(), resource -> { + if (resource == null) return; + setLoading(resource.status == Resource.Status.LOADING); + if (resource.status != Resource.Status.LOADING) { setButtonsEnabled(true); if (resource.status == Resource.Status.SUCCESS) { - Toast.makeText(getContext(), "Inventory updated", Toast.LENGTH_SHORT).show(); + Toast.makeText(getContext(), viewModel.isEditing() ? "Inventory updated" : "Inventory created", Toast.LENGTH_SHORT).show(); navigateBack(); } else if (resource.status == Resource.Status.ERROR) { Toast.makeText(getContext(), "Error: " + resource.message, Toast.LENGTH_SHORT).show(); } - }); - } else { - inventoryViewModel.createInventory(request).observe(getViewLifecycleOwner(), resource -> { - setButtonsEnabled(true); - if (resource.status == Resource.Status.SUCCESS) { - Toast.makeText(getContext(), "Inventory created", Toast.LENGTH_SHORT).show(); - navigateBack(); - } else if (resource.status == Resource.Status.ERROR) { - Toast.makeText(getContext(), "Error: " + resource.message, Toast.LENGTH_SHORT).show(); - } - }); - } - } - - /** - * Shows a confirmation dialog before deleting an inventory item. - */ - private void confirmDelete() { - new AlertDialog.Builder(requireContext()) - .setTitle("Delete inventory item?") - .setMessage("This cannot be undone.") - .setPositiveButton("Delete", (d, w) -> deleteInventory()) - .setNegativeButton("Cancel", null) - .show(); - } - - /** - * Sends a request to the API to delete the inventory item. - */ - private void deleteInventory() { - setButtonsEnabled(false); - inventoryViewModel.deleteInventory(inventoryId).observe(getViewLifecycleOwner(), resource -> { - setButtonsEnabled(true); - if (resource.status == Resource.Status.SUCCESS) { - Toast.makeText(getContext(), "Inventory deleted", Toast.LENGTH_SHORT).show(); - navigateBack(); - } else if (resource.status == Resource.Status.ERROR) { - Toast.makeText(getContext(), "Delete failed: " + resource.message, Toast.LENGTH_SHORT).show(); } }); } - /** - * Navigates back to the previous fragment. - */ + private void confirmDelete() { + DialogUtils.showDeleteConfirmDialog(requireContext(), "Inventory Item", this::deleteInventory); + } + + private void deleteInventory() { + setButtonsEnabled(false); + viewModel.deleteInventory().observe(getViewLifecycleOwner(), resource -> { + if (resource == null) return; + setLoading(resource.status == Resource.Status.LOADING); + if (resource.status != Resource.Status.LOADING) { + setButtonsEnabled(true); + if (resource.status == Resource.Status.SUCCESS) { + Toast.makeText(getContext(), "Inventory deleted", Toast.LENGTH_SHORT).show(); + navigateBack(); + } else if (resource.status == Resource.Status.ERROR) { + Toast.makeText(getContext(), "Delete failed: " + resource.message, Toast.LENGTH_SHORT).show(); + } + } + }); + } + private void navigateBack() { NavHostFragment.findNavController(this).popBackStack(); } - /** - * Enables or disables action buttons. - */ private void setButtonsEnabled(boolean enabled) { - binding.btnSaveInventory.setEnabled(enabled); - binding.btnDeleteInventory.setEnabled(enabled); - binding.btnInventoryBack.setEnabled(enabled); + UIUtils.setViewsEnabled(enabled, binding.btnSaveInventory, binding.btnDeleteInventory, binding.btnInventoryBack); } } diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/PetDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/PetDetailFragment.java index ea08d16d..ee1b34fc 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/PetDetailFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/PetDetailFragment.java @@ -18,20 +18,17 @@ import android.widget.Toast; import com.example.petstoremobile.R; import com.example.petstoremobile.databinding.FragmentPetDetailBinding; -import com.example.petstoremobile.dtos.CustomerDTO; +import com.example.petstoremobile.dtos.DropdownDTO; import com.example.petstoremobile.dtos.PetDTO; -import com.example.petstoremobile.dtos.StoreDTO; import com.example.petstoremobile.utils.ActivityLogger; +import com.example.petstoremobile.utils.DateTimeUtils; import com.example.petstoremobile.utils.DialogUtils; import com.example.petstoremobile.utils.InputValidator; import com.example.petstoremobile.utils.Resource; import com.example.petstoremobile.utils.SpinnerUtils; -import com.example.petstoremobile.viewmodels.CustomerViewModel; -import com.example.petstoremobile.viewmodels.PetViewModel; -import com.example.petstoremobile.viewmodels.StoreViewModel; +import com.example.petstoremobile.utils.UIUtils; +import com.example.petstoremobile.viewmodels.PetDetailViewModel; -import java.util.ArrayList; -import java.util.List; import java.util.Locale; import dagger.hilt.android.AndroidEntryPoint; @@ -43,23 +40,15 @@ import dagger.hilt.android.AndroidEntryPoint; public class PetDetailFragment extends Fragment { private FragmentPetDetailBinding binding; - private long petId; - private boolean isEditing = false; - - private PetViewModel viewModel; - private CustomerViewModel customerViewModel; - private StoreViewModel storeViewModel; - private List customerList = new ArrayList<>(); - private List storeList = new ArrayList<>(); + private PetDetailViewModel viewModel; + private Long selectedCustomerId = null; private Long selectedStoreId = null; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - viewModel = new ViewModelProvider(this).get(PetViewModel.class); - customerViewModel = new ViewModelProvider(this).get(CustomerViewModel.class); - storeViewModel = new ViewModelProvider(this).get(StoreViewModel.class); + viewModel = new ViewModelProvider(this).get(PetDetailViewModel.class); } @Override @@ -74,34 +63,54 @@ public class PetDetailFragment extends Fragment { super.onViewCreated(view, savedInstanceState); setupSpinner(); - loadCustomers(); - loadStores(); + observeViewModel(); handleArguments(); - //set button click listeners binding.btnBack.setOnClickListener(v -> navigateBack()); binding.btnSavePet.setOnClickListener(v -> savePet()); binding.btnDeletePet.setOnClickListener(v -> deletePet()); } + private void observeViewModel() { + viewModel.getCustomerList().observe(getViewLifecycleOwner(), list -> updateCustomerSpinnerSelection()); + viewModel.getStoreList().observe(getViewLifecycleOwner(), list -> updateStoreSpinnerSelection()); + + viewModel.loadCustomers().observe(getViewLifecycleOwner(), resource -> { + if (resource == null) return; + setLoading(resource.status == Resource.Status.LOADING); + if (resource.status == Resource.Status.SUCCESS && resource.data != null) { + viewModel.setCustomerList(resource.data); + } + }); + + viewModel.loadStores().observe(getViewLifecycleOwner(), resource -> { + if (resource == null) return; + setLoading(resource.status == Resource.Status.LOADING); + if (resource.status == Resource.Status.SUCCESS && resource.data != null) { + viewModel.setStoreList(resource.data); + } + }); + } + + private void setLoading(boolean loading) { + if (binding != null && binding.progressBar != null) { + binding.progressBar.setVisibility(loading ? View.VISIBLE : View.GONE); + } + } + @Override public void onDestroyView() { super.onDestroyView(); binding = null; } - /** - * Handles the saving of pet data (adding/updating). - */ private void savePet() { - // Validates all fields using InputValidator if (!InputValidator.isNotEmpty(binding.etPetName, "Pet Name")) return; if (!InputValidator.isNotEmpty(binding.etPetSpecies, "Species")) return; if (!InputValidator.isNotEmpty(binding.etPetBreed, "Breed")) return; if (!InputValidator.isPositiveInteger(binding.etPetAge, "Age")) return; if (!InputValidator.isPositiveDecimal(binding.etPetPrice, "Price")) return; - //get all the values from the fields String name = binding.etPetName.getText().toString().trim(); String species = binding.etPetSpecies.getText().toString().trim(); String breed = binding.etPetBreed.getText().toString().trim(); @@ -109,37 +118,27 @@ public class PetDetailFragment extends Fragment { double price = Double.parseDouble(binding.etPetPrice.getText().toString().trim()); String status = binding.spinnerPetStatus.getSelectedItem().toString(); - // Get selected customer Long customerId = null; - int customerPos = binding.spinnerCustomer.getSelectedItemPosition(); - if (customerPos > 0) { // 0 means no customer for pet - customerId = customerList.get(customerPos - 1).getCustomerId(); + if (binding.spinnerCustomer.getSelectedItemPosition() > 0) { + customerId = viewModel.getCustomerList().getValue().get(binding.spinnerCustomer.getSelectedItemPosition() - 1).getId(); } - // Get selected store Long storeId = null; - int storePos = binding.spinnerStore.getSelectedItemPosition(); - if (storePos > 0) { - storeId = storeList.get(storePos - 1).getStoreId(); + if (binding.spinnerStore.getSelectedItemPosition() > 0) { + storeId = viewModel.getStoreList().getValue().get(binding.spinnerStore.getSelectedItemPosition() - 1).getId(); } - // Validation: If status is Available, a store must be selected if ("Available".equalsIgnoreCase(status)) { if (!InputValidator.isSpinnerSelected(binding.spinnerStore, "Store")) return; } - - // Validation: If status is Owned, an owner must be selected if ("Owned".equalsIgnoreCase(status)) { if (!InputValidator.isSpinnerSelected(binding.spinnerCustomer, "Owner")) return; } - - // Validation: If status is Adopted, an owner and store must be selected if ("Adopted".equalsIgnoreCase(status)) { if (!InputValidator.isSpinnerSelected(binding.spinnerCustomer, "Owner")) return; if (!InputValidator.isSpinnerSelected(binding.spinnerStore, "Store")) return; } - //create a pet object to send to the API PetDTO petDTO = new PetDTO(); petDTO.setPetName(name); petDTO.setPetSpecies(species); @@ -150,107 +149,74 @@ public class PetDetailFragment extends Fragment { petDTO.setCustomerId(customerId); petDTO.setStoreId(storeId); - //check if the pet is being edited or added - if (isEditing) { - // Update existing pet - petDTO.setPetId(petId); - viewModel.updatePet(petId, petDTO).observe(getViewLifecycleOwner(), resource -> { - if (resource.status == Resource.Status.SUCCESS) { - ActivityLogger.logChange(requireContext(), "Pet", "UPDATED", (int) petId); + viewModel.savePet(petDTO).observe(getViewLifecycleOwner(), resource -> { + if (resource == null) return; + setLoading(resource.status == Resource.Status.LOADING); + if (resource.status == Resource.Status.SUCCESS) { + if (viewModel.isEditing()) { + ActivityLogger.logChange(requireContext(), "Pet", "UPDATED", (int) viewModel.getPetId()); Toast.makeText(getContext(), "Pet updated successfully!", Toast.LENGTH_SHORT).show(); - navigateToPetList(); - } else if (resource.status == Resource.Status.ERROR) { - Toast.makeText(getContext(), "Error: " + resource.message, Toast.LENGTH_SHORT).show(); - } - }); - } else { - // Add new pet - viewModel.createPet(petDTO).observe(getViewLifecycleOwner(), resource -> { - if (resource.status == Resource.Status.SUCCESS) { + } else { ActivityLogger.log(requireContext(), "Added new Pet: " + name); Toast.makeText(getContext(), "Pet added successfully!", Toast.LENGTH_SHORT).show(); - navigateToPetList(); - } else if (resource.status == Resource.Status.ERROR) { - Toast.makeText(getContext(), "Error: " + resource.message, Toast.LENGTH_SHORT).show(); } - }); - } + navigateToPetList(); + } else if (resource.status == Resource.Status.ERROR) { + Toast.makeText(getContext(), "Error: " + resource.message, Toast.LENGTH_SHORT).show(); + } + }); } - /** - * Displays a confirmation dialog and handles the deletion of a pet. - */ private void deletePet() { - DialogUtils.showDeleteConfirmDialog(requireContext(), "Pet", () -> - viewModel.deletePet(petId).observe(getViewLifecycleOwner(), resource -> { + DialogUtils.showDeleteConfirmDialog(requireContext(), "Pet", () -> { + viewModel.deletePet().observe(getViewLifecycleOwner(), resource -> { + if (resource == null) return; + setLoading(resource.status == Resource.Status.LOADING); if (resource.status == Resource.Status.SUCCESS) { - ActivityLogger.logChange(requireContext(), "Pet", "DELETED", (int) petId); + ActivityLogger.logChange(requireContext(), "Pet", "DELETED", (int) viewModel.getPetId()); Toast.makeText(getContext(), "Pet deleted successfully!", Toast.LENGTH_SHORT).show(); navigateToPetList(); } else if (resource.status == Resource.Status.ERROR) { Toast.makeText(getContext(), "Delete failed: " + resource.message, Toast.LENGTH_SHORT).show(); } - })); + }); + }); } - /** - * Navigates back to the pet list screen. - */ private void navigateToPetList() { NavHostFragment.findNavController(this).popBackStack(R.id.nav_pet, false); } - /** - * Navigates back to the previous screen. - */ private void navigateBack() { NavHostFragment.findNavController(this).popBackStack(); } - /** - * Handles arguments passed to the fragment to determine if it's in edit or add mode. - */ private void handleArguments() { - // Pet is being edited if the bundle contains a petId if (getArguments() != null && getArguments().containsKey("petId")) { - // Get pet data from arguments and populate fields - isEditing = true; - petId = getArguments().getLong("petId"); + long petId = getArguments().getLong("petId"); + viewModel.setPetId(petId); binding.tvMode.setText("Edit Pet"); - binding.tvPetId.setText("ID: " + petId); + binding.tvPetId.setText(DateTimeUtils.formatId(petId)); binding.tvPetId.setVisibility(View.VISIBLE); binding.btnDeletePet.setVisibility(View.VISIBLE); - // Disable species and breed fields in edit mode - binding.etPetSpecies.setEnabled(false); - binding.etPetBreed.setEnabled(false); - binding.etPetSpecies.setAlpha(0.5f); - binding.etPetBreed.setAlpha(0.5f); - + UIUtils.setViewsEnabled(false, binding.etPetSpecies, binding.etPetBreed); loadPetData(); } else { - // Pet is being added - // Set default values for add a new pet - isEditing = false; + viewModel.setPetId(-1); binding.tvMode.setText("Add Pet"); binding.tvPetId.setVisibility(View.GONE); binding.btnDeletePet.setVisibility(View.GONE); binding.btnSavePet.setText("Add"); - // Enable species and breed fields in edit mode - binding.etPetSpecies.setEnabled(true); - binding.etPetBreed.setEnabled(true); - binding.etPetSpecies.setAlpha(1.0f); - binding.etPetBreed.setAlpha(1.0f); + UIUtils.setViewsEnabled(true, binding.etPetSpecies, binding.etPetBreed); } } - /** - * Fetches specific pet details from the backend using the ID. - */ private void loadPetData() { - viewModel.getPetById(petId).observe(getViewLifecycleOwner(), resource -> { + viewModel.loadPet().observe(getViewLifecycleOwner(), resource -> { if (resource == null) return; + setLoading(resource.status == Resource.Status.LOADING); if (resource.status == Resource.Status.SUCCESS && resource.data != null) { PetDTO p = resource.data; binding.etPetName.setText(p.getPetName()); @@ -273,63 +239,30 @@ public class PetDetailFragment extends Fragment { }); } - /** - * Fetches the list of customers and populates the spinner. - */ - private void loadCustomers() { - customerViewModel.getAllCustomers(0, 1000).observe(getViewLifecycleOwner(), resource -> { - if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) { - customerList = resource.data.getContent(); - updateCustomerSpinnerSelection(); - } - }); - } - - /** - * Fetches the list of stores and populates the spinner. - */ - private void loadStores() { - storeViewModel.getAllStores(0, 1000).observe(getViewLifecycleOwner(), resource -> { - if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) { - storeList = resource.data.getContent(); - updateStoreSpinnerSelection(); - } - }); - } - - /** - * Updates the customer spinner with the current list and sets the selection if needed. - */ private void updateCustomerSpinnerSelection() { SpinnerUtils.populateSpinner( requireContext(), binding.spinnerCustomer, - customerList, - CustomerDTO::getFullName, + viewModel.getCustomerList().getValue(), + DropdownDTO::getLabel, "No Owner", selectedCustomerId, - CustomerDTO::getCustomerId + DropdownDTO::getId ); } - /** - * Updates the store spinner with the current list and sets the selection if needed. - */ private void updateStoreSpinnerSelection() { SpinnerUtils.populateSpinner( requireContext(), binding.spinnerStore, - storeList, - StoreDTO::getStoreName, + viewModel.getStoreList().getValue(), + DropdownDTO::getLabel, "None", selectedStoreId, - StoreDTO::getStoreId + DropdownDTO::getId ); } - /** - * Initializes the spinner for pet status selection. - */ private void setupSpinner() { SpinnerUtils.setupStringSpinner(requireContext(), binding.spinnerPetStatus, new String[]{"Available", "Adopted", "Owned"}); @@ -339,28 +272,21 @@ public class PetDetailFragment extends Fragment { public void onItemSelected(AdapterView parent, View view, int position, long id) { String status = parent.getItemAtPosition(position).toString(); - // Clear any existing error icons when status changes clearSpinnerError(binding.spinnerCustomer); clearSpinnerError(binding.spinnerStore); - //Disable the customer spinner if the status is "Available" if ("Available".equalsIgnoreCase(status)) { binding.spinnerCustomer.setSelection(0); - binding.spinnerCustomer.setEnabled(false); - binding.spinnerCustomer.setAlpha(0.5f); + UIUtils.setViewsEnabled(false, binding.spinnerCustomer); } else { - binding.spinnerCustomer.setEnabled(true); - binding.spinnerCustomer.setAlpha(1.0f); + UIUtils.setViewsEnabled(true, binding.spinnerCustomer); } - //Disable the store spinner if the status is "Owned" if ("Owned".equalsIgnoreCase(status)) { binding.spinnerStore.setSelection(0); - binding.spinnerStore.setEnabled(false); - binding.spinnerStore.setAlpha(0.5f); + UIUtils.setViewsEnabled(false, binding.spinnerStore); } else { - binding.spinnerStore.setEnabled(true); - binding.spinnerStore.setAlpha(1.0f); + UIUtils.setViewsEnabled(true, binding.spinnerStore); } } @@ -370,9 +296,6 @@ public class PetDetailFragment extends Fragment { }); } - /** - * Clears error messages from a Spinner's selected view. - */ private void clearSpinnerError(Spinner spinner) { View selectedView = spinner.getSelectedView(); if (selectedView instanceof TextView) { diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ProductDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ProductDetailFragment.java index d2527d71..9f072b51 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ProductDetailFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ProductDetailFragment.java @@ -17,7 +17,8 @@ import com.example.petstoremobile.api.*; import com.example.petstoremobile.api.auth.TokenManager; import com.example.petstoremobile.databinding.FragmentProductDetailBinding; import com.example.petstoremobile.dtos.*; -import com.example.petstoremobile.viewmodels.ProductViewModel; +import com.example.petstoremobile.viewmodels.ProductDetailViewModel; +import com.example.petstoremobile.utils.DateTimeUtils; import com.example.petstoremobile.utils.DialogUtils; import com.example.petstoremobile.utils.FileUtils; import com.example.petstoremobile.utils.GlideUtils; @@ -31,7 +32,6 @@ import java.math.BigDecimal; import java.util.*; import javax.inject.Inject; - import javax.inject.Named; import dagger.hilt.android.AndroidEntryPoint; @@ -46,29 +46,22 @@ import okhttp3.RequestBody; public class ProductDetailFragment extends Fragment { private FragmentProductDetailBinding binding; + private ProductDetailViewModel viewModel; + private ImagePickerHelper imagePickerHelper; - private long prodId = -1; - private boolean isEditing = false; private long preselectedCategoryId = -1; private boolean hasImage = false; private boolean isImageChanged = false; private boolean isImageRemoved = false; - - private List categoryList = new ArrayList<>(); private Uri photoUri; - private ProductViewModel viewModel; - private ImagePickerHelper imagePickerHelper; @Inject @Named("baseUrl") String baseUrl; @Inject TokenManager tokenManager; - /** - * Initializes activity launchers and the ImagePickerHelper. - */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - viewModel = new ViewModelProvider(this).get(ProductViewModel.class); + viewModel = new ViewModelProvider(this).get(ProductDetailViewModel.class); imagePickerHelper = new ImagePickerHelper(this, "product_photo.jpg", new ImagePickerHelper.ImagePickerListener() { @Override @@ -95,9 +88,6 @@ public class ProductDetailFragment extends Fragment { }); } - /** - * Inflates the layout. - */ @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { @@ -105,14 +95,11 @@ public class ProductDetailFragment extends Fragment { return binding.getRoot(); } - /** - * Sets up UI components and listeners after the view is created. - */ @Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); - loadCategories(); + observeViewModel(); handleArguments(); binding.btnProductBack.setOnClickListener(v -> navigateBack()); @@ -121,41 +108,49 @@ public class ProductDetailFragment extends Fragment { binding.ivProductImage.setOnClickListener(v -> imagePickerHelper.showImagePickerDialog("Select Product Image", hasImage)); } + private void observeViewModel() { + viewModel.getCategoryList().observe(getViewLifecycleOwner(), list -> updateCategorySpinner()); + + viewModel.loadCategories().observe(getViewLifecycleOwner(), resource -> { + if (resource == null) return; + setLoading(resource.status == Resource.Status.LOADING); + if (resource.status == Resource.Status.SUCCESS && resource.data != null) { + viewModel.setCategoryList(resource.data.getContent()); + } + }); + } + + private void setLoading(boolean loading) { + if (binding != null && binding.progressBar != null) { + binding.progressBar.setVisibility(loading ? View.VISIBLE : View.GONE); + } + } + + private void updateCategorySpinner() { + SpinnerUtils.populateSpinner(requireContext(), binding.spinnerProductCategory, viewModel.getCategoryList().getValue(), + CategoryDTO::getCategoryName, "-- Select Category --", + preselectedCategoryId, CategoryDTO::getCategoryId); + } + @Override public void onDestroyView() { super.onDestroyView(); binding = null; } - /** - * Fetches all product categories for the selection spinner. - */ - private void loadCategories() { - viewModel.getAllCategories(0, 100).observe(getViewLifecycleOwner(), resource -> { - if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) { - categoryList = resource.data.getContent(); - SpinnerUtils.populateSpinner(requireContext(), binding.spinnerProductCategory, categoryList, - CategoryDTO::getCategoryName, "-- Select Category --", - preselectedCategoryId, CategoryDTO::getCategoryId); - } - }); - } - - /** - * Checks if the fragment was opened with existing product data for editing. - */ private void handleArguments() { Bundle a = getArguments(); if (a != null && a.containsKey("prodId")) { - isEditing = true; - prodId = a.getLong("prodId"); + long prodId = a.getLong("prodId"); + viewModel.setProdId(prodId); binding.tvProductMode.setText("Edit Product"); - binding.tvProductId.setText("ID: " + prodId); + binding.tvProductId.setText(DateTimeUtils.formatId(prodId)); binding.tvProductId.setVisibility(View.VISIBLE); binding.btnDeleteProduct.setVisibility(View.VISIBLE); loadProductData(); loadProductImage(); } else { + viewModel.setProdId(-1); binding.tvProductMode.setText("Add Product"); binding.btnDeleteProduct.setVisibility(View.GONE); binding.tvProductId.setVisibility(View.GONE); @@ -163,36 +158,25 @@ public class ProductDetailFragment extends Fragment { } } - /** - * Loads the product data from the backend. - */ private void loadProductData() { - viewModel.getProductById(prodId).observe(getViewLifecycleOwner(), resource -> { + viewModel.loadProduct().observe(getViewLifecycleOwner(), resource -> { if (resource == null) return; + setLoading(resource.status == Resource.Status.LOADING); if (resource.status == Resource.Status.SUCCESS && resource.data != null) { ProductDTO p = resource.data; binding.etProductName.setText(p.getProdName()); binding.etProductDesc.setText(p.getProdDesc()); binding.etProductPrice.setText(p.getProdPrice() != null ? p.getProdPrice().toString() : ""); preselectedCategoryId = p.getCategoryId() != null ? p.getCategoryId() : -1; - - // Refresh spinner selection once data is loaded - if (!categoryList.isEmpty()) { - SpinnerUtils.populateSpinner(requireContext(), binding.spinnerProductCategory, categoryList, - CategoryDTO::getCategoryName, "-- Select Category --", - preselectedCategoryId, CategoryDTO::getCategoryId); - } + updateCategorySpinner(); } else if (resource.status == Resource.Status.ERROR) { Toast.makeText(getContext(), "Failed to load product: " + resource.message, Toast.LENGTH_SHORT).show(); } }); } - /** - * Loads the product image from the backend. - */ private void loadProductImage() { - String imageUrl = baseUrl + String.format(Locale.US, ProductApi.PRODUCT_IMAGE_PATH, prodId); + String imageUrl = baseUrl + String.format(Locale.US, ProductApi.PRODUCT_IMAGE_PATH, viewModel.getProdId()); String token = tokenManager.getToken(); GlideUtils.loadImageWithToken(requireContext(), binding.ivProductImage, imageUrl, token, R.drawable.placeholder2, new GlideUtils.ImageLoadListener() { @@ -208,13 +192,12 @@ public class ProductDetailFragment extends Fragment { }); } - /** - * Performs image related actions (upload/delete) after product details are saved. - */ private void performPendingImageActions(String successMsg) { if (isImageRemoved) { - viewModel.deleteProductImage(prodId).observe(getViewLifecycleOwner(), resource -> { - if (resource != null && resource.status != Resource.Status.LOADING) { + viewModel.deleteProductImage().observe(getViewLifecycleOwner(), resource -> { + if (resource == null) return; + setLoading(resource.status == Resource.Status.LOADING); + if (resource.status != Resource.Status.LOADING) { if (resource.status == Resource.Status.SUCCESS) { Toast.makeText(getContext(), successMsg, Toast.LENGTH_SHORT).show(); } else { @@ -231,9 +214,6 @@ public class ProductDetailFragment extends Fragment { } } - /** - * Uploads the selected image file to the server. - */ private void uploadProductImageAndNavigate(Uri uri, String successMsg) { File file = FileUtils.getFileFromUri(requireContext(), uri); if (file == null) { @@ -245,8 +225,10 @@ public class ProductDetailFragment extends Fragment { RequestBody requestFile = RequestBody.create(file, MediaType.parse(requireContext().getContentResolver().getType(uri))); MultipartBody.Part body = MultipartBody.Part.createFormData("image", file.getName(), requestFile); - viewModel.uploadProductImage(prodId, body).observe(getViewLifecycleOwner(), resource -> { - if (resource != null && resource.status != Resource.Status.LOADING) { + viewModel.uploadProductImage(body).observe(getViewLifecycleOwner(), resource -> { + if (resource == null) return; + setLoading(resource.status == Resource.Status.LOADING); + if (resource.status != Resource.Status.LOADING) { if (resource.status == Resource.Status.SUCCESS) { Toast.makeText(getContext(), successMsg, Toast.LENGTH_SHORT).show(); } else { @@ -257,69 +239,47 @@ public class ProductDetailFragment extends Fragment { }); } - /** - * Validates input fields and saves product information to the backend. - */ private void saveProduct() { if (!InputValidator.isNotEmpty(binding.etProductName, "Product Name")) return; - - if (binding.spinnerProductCategory.getSelectedItemPosition() == 0) { - Toast.makeText(getContext(), "Select a category", Toast.LENGTH_SHORT).show(); return; - } - - if (!InputValidator.isNotEmpty(binding.etProductPrice, "Price") || - !InputValidator.isPositiveDecimal(binding.etProductPrice, "Price")) { - return; - } + if (!InputValidator.isSpinnerSelected(binding.spinnerProductCategory, "Category")) return; + if (!InputValidator.isPositiveDecimal(binding.etProductPrice, "Price")) return; String name = binding.etProductName.getText().toString().trim(); String desc = binding.etProductDesc.getText().toString().trim(); BigDecimal price = new BigDecimal(binding.etProductPrice.getText().toString().trim()); - CategoryDTO category = categoryList.get(binding.spinnerProductCategory.getSelectedItemPosition() - 1); + CategoryDTO category = viewModel.getCategoryList().getValue().get(binding.spinnerProductCategory.getSelectedItemPosition() - 1); ProductDTO dto = new ProductDTO(name, category.getCategoryId(), desc, price); - if (isEditing) { - viewModel.updateProduct(prodId, dto).observe(getViewLifecycleOwner(), resource -> { - if (resource != null && resource.status != Resource.Status.LOADING) { - if (resource.status == Resource.Status.SUCCESS) { - performPendingImageActions("Updated"); - } else { - Toast.makeText(getContext(), "Error: " + resource.message, Toast.LENGTH_SHORT).show(); + viewModel.saveProduct(dto).observe(getViewLifecycleOwner(), resource -> { + if (resource == null) return; + setLoading(resource.status == Resource.Status.LOADING); + if (resource.status != Resource.Status.LOADING) { + if (resource.status == Resource.Status.SUCCESS) { + if (resource.data != null) { + viewModel.setProdId(resource.data.getProdId()); } + performPendingImageActions(viewModel.isEditing() ? "Updated" : "Saved"); + } else { + Toast.makeText(getContext(), "Error: " + resource.message, Toast.LENGTH_SHORT).show(); } - }); - } else { - viewModel.createProduct(dto).observe(getViewLifecycleOwner(), resource -> { - if (resource != null && resource.status != Resource.Status.LOADING) { - if (resource.status == Resource.Status.SUCCESS && resource.data != null) { - prodId = resource.data.getProdId(); - performPendingImageActions("Saved"); - } else { - Toast.makeText(getContext(), "Error saving: " + resource.message, Toast.LENGTH_SHORT).show(); - } - } - }); - } + } + }); } - /** - * Displays a confirmation dialog before deleting the product. - */ private void confirmDelete() { DialogUtils.showDeleteConfirmDialog(requireContext(), "Product", () -> - viewModel.deleteProduct(prodId).observe(getViewLifecycleOwner(), resource -> { - if (resource != null && resource.status == Resource.Status.SUCCESS) { + viewModel.deleteProduct().observe(getViewLifecycleOwner(), resource -> { + if (resource == null) return; + setLoading(resource.status == Resource.Status.LOADING); + if (resource.status == Resource.Status.SUCCESS) { navigateBack(); - } else if (resource != null && resource.status == Resource.Status.ERROR) { + } else if (resource.status == Resource.Status.ERROR) { Toast.makeText(getContext(), "Delete failed: " + resource.message, Toast.LENGTH_SHORT).show(); } })); } - /** - * Navigates back to the previous fragment. - */ private void navigateBack() { NavHostFragment.findNavController(this).popBackStack(); } diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ProductSupplierDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ProductSupplierDetailFragment.java index aa570d13..770c6b82 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ProductSupplierDetailFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ProductSupplierDetailFragment.java @@ -15,9 +15,8 @@ import com.example.petstoremobile.utils.DialogUtils; import com.example.petstoremobile.utils.InputValidator; import com.example.petstoremobile.utils.Resource; import com.example.petstoremobile.utils.SpinnerUtils; -import com.example.petstoremobile.viewmodels.ProductSupplierViewModel; -import com.example.petstoremobile.viewmodels.ProductViewModel; -import com.example.petstoremobile.viewmodels.SupplierViewModel; +import com.example.petstoremobile.utils.UIUtils; +import com.example.petstoremobile.viewmodels.ProductSupplierDetailViewModel; import java.math.BigDecimal; import java.util.*; @@ -31,26 +30,15 @@ import dagger.hilt.android.AndroidEntryPoint; public class ProductSupplierDetailFragment extends Fragment { private FragmentProductSupplierDetailBinding binding; + private ProductSupplierDetailViewModel viewModel; - private boolean isEditing = false; - private long editProductId = -1; - private long editSupplierId = -1; private long preselectedProductId = -1; private long preselectedSupplierId = -1; - private List productList = new ArrayList<>(); - private List supplierList = new ArrayList<>(); - - private ProductSupplierViewModel psViewModel; - private ProductViewModel productViewModel; - private SupplierViewModel supplierViewModel; - @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - psViewModel = 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(ProductSupplierDetailViewModel.class); } @Override @@ -63,6 +51,7 @@ public class ProductSupplierDetailFragment extends Fragment { @Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); + observeViewModel(); loadSpinnersData(); handleArguments(); @@ -71,128 +60,97 @@ public class ProductSupplierDetailFragment extends Fragment { binding.btnDeletePS.setOnClickListener(v -> confirmDelete()); } + private void observeViewModel() { + viewModel.getProductList().observe(getViewLifecycleOwner(), list -> refreshProductSpinner()); + viewModel.getSupplierList().observe(getViewLifecycleOwner(), list -> refreshSupplierSpinner()); + } + + private void setLoading(boolean loading) { + if (binding != null && binding.progressBar != null) { + binding.progressBar.setVisibility(loading ? View.VISIBLE : View.GONE); + } + } + @Override public void onDestroyView() { super.onDestroyView(); binding = null; } - /** - * Fetches products and suppliers to populate the spinners. - */ private void loadSpinnersData() { - loadProducts(); - loadSuppliers(); - } - - /** - * Loads the list of products from the API. - */ - private void loadProducts() { - productViewModel.getAllProducts(null, null, 0, 200, "prodName").observe(getViewLifecycleOwner(), resource -> { + viewModel.loadProducts().observe(getViewLifecycleOwner(), resource -> { + if (resource == null) return; + setLoading(resource.status == Resource.Status.LOADING); if (resource.status == Resource.Status.SUCCESS && resource.data != null) { - productList = resource.data.getContent(); - refreshProductSpinner(); + viewModel.setProductList(resource.data.getContent()); + } + }); + viewModel.loadSuppliers().observe(getViewLifecycleOwner(), resource -> { + if (resource == null) return; + setLoading(resource.status == Resource.Status.LOADING); + if (resource.status == Resource.Status.SUCCESS && resource.data != null) { + viewModel.setSupplierList(resource.data.getContent()); } }); } private void refreshProductSpinner() { - SpinnerUtils.populateSpinner(requireContext(), binding.spinnerPSProduct, productList, + SpinnerUtils.populateSpinner(requireContext(), binding.spinnerPSProduct, viewModel.getProductList().getValue(), ProductDTO::getProdName, "-- Select Product --", preselectedProductId, ProductDTO::getProdId); } - /** - * Loads the list of suppliers from the API. - */ - private void loadSuppliers() { - supplierViewModel.getAllSuppliers(0, 200, null, "supCompany").observe(getViewLifecycleOwner(), resource -> { - if (resource.status == Resource.Status.SUCCESS && resource.data != null) { - supplierList = resource.data.getContent(); - refreshSupplierSpinner(); - } - }); - } - private void refreshSupplierSpinner() { - SpinnerUtils.populateSpinner(requireContext(), binding.spinnerPSSupplier, supplierList, + SpinnerUtils.populateSpinner(requireContext(), binding.spinnerPSSupplier, viewModel.getSupplierList().getValue(), SupplierDTO::getSupCompany, "-- Select Supplier --", preselectedSupplierId, SupplierDTO::getSupId); } - /** - * Handles arguments to determine if the fragment is in edit or add mode. - */ private void handleArguments() { Bundle a = getArguments(); if (a != null && a.containsKey("productId") && a.containsKey("supplierId")) { - isEditing = true; - editProductId = a.getLong("productId"); - editSupplierId = a.getLong("supplierId"); - preselectedProductId = editProductId; - preselectedSupplierId = editSupplierId; + long productId = a.getLong("productId"); + long supplierId = a.getLong("supplierId"); + viewModel.setEditMode(productId, supplierId); + preselectedProductId = productId; + preselectedSupplierId = supplierId; binding.tvPSMode.setText("Edit Product Supplier"); binding.btnDeletePS.setVisibility(View.VISIBLE); - } else { binding.tvPSMode.setText("Add Product Supplier"); binding.btnDeletePS.setVisibility(View.GONE); } } - - /** - * Validates input and saves the product-supplier to the backend. - */ private void save() { - if (binding.spinnerPSProduct.getSelectedItemPosition() == 0) { - Toast.makeText(getContext(), "Select a product", Toast.LENGTH_SHORT).show(); return; - } - if (binding.spinnerPSSupplier.getSelectedItemPosition() == 0) { - Toast.makeText(getContext(), "Select a supplier", Toast.LENGTH_SHORT).show(); return; - } + if (!InputValidator.isSpinnerSelected(binding.spinnerPSProduct, "Product")) return; + if (!InputValidator.isSpinnerSelected(binding.spinnerPSSupplier, "Supplier")) return; + if (!InputValidator.isPositiveDecimal(binding.etPSCost, "Cost")) return; - if (!InputValidator.isNotEmpty(binding.etPSCost, "Cost") || - !InputValidator.isPositiveDecimal(binding.etPSCost, "Cost")) { - return; - } - - ProductDTO product = productList.get(binding.spinnerPSProduct.getSelectedItemPosition() - 1); - SupplierDTO supplier = supplierList.get(binding.spinnerPSSupplier.getSelectedItemPosition() - 1); + ProductDTO product = viewModel.getProductList().getValue().get(binding.spinnerPSProduct.getSelectedItemPosition() - 1); + SupplierDTO supplier = viewModel.getSupplierList().getValue().get(binding.spinnerPSSupplier.getSelectedItemPosition() - 1); BigDecimal cost = new BigDecimal(binding.etPSCost.getText().toString().trim()); - ProductSupplierDTO dto = new ProductSupplierDTO( - product.getProdId(), supplier.getSupId(), cost); + ProductSupplierDTO dto = new ProductSupplierDTO(product.getProdId(), supplier.getSupId(), cost); - if (isEditing) { - psViewModel.updateProductSupplier(editProductId, editSupplierId, dto).observe(getViewLifecycleOwner(), resource -> { - if (resource.status == Resource.Status.SUCCESS) { - Toast.makeText(getContext(), "Updated", Toast.LENGTH_SHORT).show(); - navigateBack(); - } else if (resource.status == Resource.Status.ERROR) { - Toast.makeText(getContext(), "Error: " + resource.message, Toast.LENGTH_SHORT).show(); - } - }); - } else { - psViewModel.createProductSupplier(dto).observe(getViewLifecycleOwner(), resource -> { - if (resource.status == Resource.Status.SUCCESS) { - Toast.makeText(getContext(), "Saved", Toast.LENGTH_SHORT).show(); - navigateBack(); - } else if (resource.status == Resource.Status.ERROR) { - Toast.makeText(getContext(), "Error: " + resource.message, Toast.LENGTH_SHORT).show(); - } - }); - } + viewModel.saveProductSupplier(dto).observe(getViewLifecycleOwner(), resource -> { + if (resource == null) return; + setLoading(resource.status == Resource.Status.LOADING); + if (resource.status == Resource.Status.SUCCESS) { + Toast.makeText(getContext(), viewModel.isEditing() ? "Updated" : "Saved", Toast.LENGTH_SHORT).show(); + navigateBack(); + } else if (resource.status == Resource.Status.ERROR) { + Toast.makeText(getContext(), "Error: " + resource.message, Toast.LENGTH_SHORT).show(); + } + }); } - /** - * Shows a confirmation dialog before deleting a product-supplier relationship. - */ private void confirmDelete() { - DialogUtils.showDeleteConfirmDialog(requireContext(), "Product Supplier", () -> - psViewModel.deleteProductSupplier(editProductId, editSupplierId).observe(getViewLifecycleOwner(), resource -> { + DialogUtils.showDeleteConfirmDialog(requireContext(), "Product Supplier Relationship", () -> + viewModel.deleteProductSupplier().observe(getViewLifecycleOwner(), resource -> { + if (resource == null) return; + setLoading(resource.status == Resource.Status.LOADING); if (resource.status == Resource.Status.SUCCESS) { Toast.makeText(getContext(), "Deleted", Toast.LENGTH_SHORT).show(); navigateBack(); @@ -202,9 +160,6 @@ public class ProductSupplierDetailFragment extends Fragment { })); } - /** - * Navigates back to the previous screen. - */ private void navigateBack() { NavHostFragment.findNavController(this).popBackStack(); } diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/PurchaseOrderDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/PurchaseOrderDetailFragment.java index eb69bd16..4b28ed92 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/PurchaseOrderDetailFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/PurchaseOrderDetailFragment.java @@ -14,7 +14,7 @@ import androidx.navigation.fragment.NavHostFragment; import com.example.petstoremobile.databinding.FragmentPurchaseOrderDetailBinding; import com.example.petstoremobile.dtos.PurchaseOrderDTO; import com.example.petstoremobile.utils.Resource; -import com.example.petstoremobile.viewmodels.PurchaseOrderViewModel; +import com.example.petstoremobile.viewmodels.PurchaseOrderDetailViewModel; import dagger.hilt.android.AndroidEntryPoint; @@ -25,13 +25,13 @@ import dagger.hilt.android.AndroidEntryPoint; public class PurchaseOrderDetailFragment extends Fragment { private FragmentPurchaseOrderDetailBinding binding; - private PurchaseOrderViewModel viewModel; + private PurchaseOrderDetailViewModel viewModel; private long purchaseOrderId; @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); - viewModel = new ViewModelProvider(this).get(PurchaseOrderViewModel.class); + viewModel = new ViewModelProvider(this).get(PurchaseOrderDetailViewModel.class); } /** @@ -66,9 +66,16 @@ public class PurchaseOrderDetailFragment extends Fragment { } } + private void setLoading(boolean loading) { + if (binding != null && binding.progressBar != null) { + binding.progressBar.setVisibility(loading ? View.VISIBLE : View.GONE); + } + } + private void loadPurchaseOrderData() { - viewModel.getPurchaseOrderById(purchaseOrderId).observe(getViewLifecycleOwner(), resource -> { + viewModel.loadPurchaseOrder(purchaseOrderId).observe(getViewLifecycleOwner(), resource -> { if (resource == null) return; + setLoading(resource.status == Resource.Status.LOADING); if (resource.status == Resource.Status.SUCCESS && resource.data != null) { PurchaseOrderDTO po = resource.data; binding.tvPODetailId.setText("PO #" + po.getPurchaseOrderId()); diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/RefundFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/RefundFragment.java index 8d05252c..7d4841f3 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/RefundFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/RefundFragment.java @@ -1,6 +1,5 @@ package com.example.petstoremobile.fragments.listfragments.detailfragments; -import android.app.AlertDialog; import android.os.Bundle; import android.util.Log; import android.view.*; @@ -12,7 +11,10 @@ import androidx.navigation.fragment.NavHostFragment; import com.example.petstoremobile.R; import com.example.petstoremobile.databinding.FragmentRefundBinding; import com.example.petstoremobile.dtos.SaleDTO; -import com.example.petstoremobile.viewmodels.SaleViewModel; +import com.example.petstoremobile.viewmodels.RefundViewModel; +import com.example.petstoremobile.utils.DialogUtils; +import com.example.petstoremobile.utils.Resource; +import com.example.petstoremobile.utils.SpinnerUtils; import dagger.hilt.android.AndroidEntryPoint; import java.math.BigDecimal; import java.math.RoundingMode; @@ -22,53 +24,23 @@ import java.util.*; public class RefundFragment extends Fragment { private FragmentRefundBinding binding; - private SaleViewModel saleViewModel; - private SaleDTO currentSale; - private List allSales = new ArrayList<>(); - - // Items available to refund (after accounting for previous refunds) - private List availableItems = new ArrayList<>(); - // Items user has added to refund cart - private List refundCart = new ArrayList<>(); + private RefundViewModel viewModel; private final String[] PAYMENT_METHODS = {"Cash", "Card"}; - // Inner class to track refund items - static class RefundItem { - long prodId; - String productName; - int quantity; - BigDecimal unitPrice; - - RefundItem(long prodId, String productName, int quantity, BigDecimal unitPrice) { - this.prodId = prodId; - this.productName = productName; - this.quantity = quantity; - this.unitPrice = unitPrice; - } - - BigDecimal getTotal() { - return unitPrice != null - ? unitPrice.multiply(BigDecimal.valueOf(quantity)) - : BigDecimal.ZERO; - } - } - @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { binding = FragmentRefundBinding.inflate(inflater, container, false); - saleViewModel = new ViewModelProvider(this).get(SaleViewModel.class); + viewModel = new ViewModelProvider(this).get(RefundViewModel.class); setupSpinner(); + observeViewModel(); loadAllSales(); - // Pre-fill sale ID if passed from SaleFragment Bundle args = getArguments(); if (args != null && args.containsKey("saleId")) { - long saleId = args.getLong("saleId"); - binding.etRefundSaleId.setText(String.valueOf(saleId)); - // Auto-load after sales are fetched + binding.etRefundSaleId.setText(String.valueOf(args.getLong("saleId"))); } binding.btnLoadSale.setOnClickListener(v -> loadSale()); @@ -79,31 +51,36 @@ public class RefundFragment extends Fragment { } private void setupSpinner() { - binding.spinnerRefundPayment.setAdapter(new ArrayAdapter<>(requireContext(), - android.R.layout.simple_spinner_item, PAYMENT_METHODS)); + SpinnerUtils.setupStringSpinner(requireContext(), binding.spinnerRefundPayment, PAYMENT_METHODS); + } + + private void observeViewModel() { + viewModel.getAvailableItems().observe(getViewLifecycleOwner(), items -> renderOriginalItems()); + viewModel.getRefundCart().observe(getViewLifecycleOwner(), cart -> { + renderRefundCart(); + updateRefundTotal(); + renderOriginalItems(); // Re-render to reflect quantities in cart + }); + } + + private void setLoading(boolean loading) { + if (binding != null && binding.progressBar != null) { + binding.progressBar.setVisibility(loading ? View.VISIBLE : View.GONE); + } } private void loadAllSales() { - saleViewModel.getAllSales(0, 1000, null, null, null, "saleDate,desc") - .observe(getViewLifecycleOwner(), resource -> { - if (resource != null) { - switch (resource.status) { - case SUCCESS: - if (resource.data != null) { - allSales = resource.data.getContent(); - // Auto-load if saleId was pre-filled - Bundle args = getArguments(); - if (args != null && args.containsKey("saleId")) { - loadSale(); - } - } - break; - case ERROR: - Log.e("Refund", "Failed to load sales: " + resource.message); - break; - } - } - }); + viewModel.loadAllSales().observe(getViewLifecycleOwner(), resource -> { + if (resource == null) return; + setLoading(resource.status == Resource.Status.LOADING); + if (resource.status == Resource.Status.SUCCESS && resource.data != null) { + viewModel.setAllSales(resource.data.getContent()); + Bundle args = getArguments(); + if (args != null && args.containsKey("saleId")) { + loadSale(); + } + } + }); } private void loadSale() { @@ -120,11 +97,12 @@ public class RefundFragment extends Fragment { return; } - // Find sale in loaded list SaleDTO found = null; - for (SaleDTO s : allSales) { - if (s.getSaleId() != null && s.getSaleId() == saleId) { - found = s; break; + if (viewModel.getAllSalesList() != null) { + for (SaleDTO s : viewModel.getAllSalesList()) { + if (s.getSaleId() != null && s.getSaleId() == saleId) { + found = s; break; + } } } @@ -139,9 +117,9 @@ public class RefundFragment extends Fragment { return; } - currentSale = found; + viewModel.setCurrentSale(found); + SaleDTO currentSale = viewModel.getCurrentSale(); - // Show sale info binding.tvSaleInfo.setVisibility(View.VISIBLE); binding.tvSaleInfo.setText("Sale #" + currentSale.getSaleId() + " | " + (currentSale.getSaleDate() != null @@ -151,94 +129,44 @@ public class RefundFragment extends Fragment { + " | Total: $" + currentSale.getTotalAmount() + " | Payment: " + currentSale.getPaymentMethod()); - // Pre-select payment method if (currentSale.getPaymentMethod() != null) { - for (int i = 0; i < PAYMENT_METHODS.length; i++) { - if (PAYMENT_METHODS[i].equalsIgnoreCase(currentSale.getPaymentMethod())) { - binding.spinnerRefundPayment.setSelection(i); break; - } - } + SpinnerUtils.setSelectionByValue(binding.spinnerRefundPayment, currentSale.getPaymentMethod()); } - // Build refundable items accounting for previous refunds - buildRefundableItems(); - - if (availableItems.isEmpty()) { - Toast.makeText(getContext(), - "This sale has no remaining refundable items", Toast.LENGTH_LONG).show(); + if (viewModel.getAvailableItems().getValue() == null || viewModel.getAvailableItems().getValue().isEmpty()) { + Toast.makeText(getContext(), "This sale has no remaining refundable items", Toast.LENGTH_LONG).show(); return; } - // Reset refund cart - refundCart.clear(); - - // Show cards binding.cardOriginalItems.setVisibility(View.VISIBLE); binding.cardRefundItems.setVisibility(View.VISIBLE); binding.cardPayment.setVisibility(View.VISIBLE); binding.btnProcessRefund.setVisibility(View.VISIBLE); - - renderOriginalItems(); - renderRefundCart(); - updateRefundTotal(); - } - - private void buildRefundableItems() { - availableItems.clear(); - if (currentSale.getItems() == null) return; - - // Find all previous refunds for this sale - Map alreadyRefunded = new HashMap<>(); - for (SaleDTO s : allSales) { - if (Boolean.TRUE.equals(s.getIsRefund()) - && currentSale.getSaleId().equals(s.getOriginalSaleId()) - && s.getItems() != null) { - for (SaleDTO.SaleItemDTO item : s.getItems()) { - if (item.getProdId() != null && item.getQuantity() != null) { - alreadyRefunded.merge(item.getProdId(), - Math.abs(item.getQuantity()), Integer::sum); - } - } - } - } - - // Build available items - for (SaleDTO.SaleItemDTO item : currentSale.getItems()) { - if (item.getProdId() == null || item.getQuantity() == null) continue; - int refunded = alreadyRefunded.getOrDefault(item.getProdId(), 0); - int remaining = item.getQuantity() - refunded; - if (remaining > 0) { - availableItems.add(new RefundItem( - item.getProdId(), - item.getProductName() != null ? item.getProductName() : "Unknown", - remaining, - item.getUnitPrice() - )); - } - } } private void renderOriginalItems() { binding.llOriginalItems.removeAllViews(); + List available = viewModel.getAvailableItems().getValue(); + if (available == null) return; - // Header addTableHeader(binding.llOriginalItems); - for (RefundItem item : availableItems) { - // Calculate pending in cart - int pendingQty = 0; - for (RefundItem r : refundCart) { - if (r.prodId == item.prodId) { pendingQty = r.quantity; break; } + for (RefundViewModel.RefundItem item : available) { + int inCart = 0; + if (viewModel.getRefundCart().getValue() != null) { + for (RefundViewModel.RefundItem r : viewModel.getRefundCart().getValue()) { + if (r.prodId == item.prodId) { inCart = r.quantity; break; } + } } - int displayQty = item.quantity - pendingQty; + int displayQty = item.quantity - inCart; if (displayQty <= 0) continue; LinearLayout row = buildItemRow( item.productName, displayQty, item.unitPrice, - true, // show add button - () -> showQuantityDialog(item) + true, + () -> showQuantityDialog(item, displayQty) ); binding.llOriginalItems.addView(row); } @@ -246,8 +174,9 @@ public class RefundFragment extends Fragment { private void renderRefundCart() { binding.llRefundItems.removeAllViews(); + List cart = viewModel.getRefundCart().getValue(); - if (refundCart.isEmpty()) { + if (cart == null || cart.isEmpty()) { TextView empty = new TextView(getContext()); empty.setText("No items added to refund yet"); empty.setTextColor(0xFF888780); @@ -258,18 +187,13 @@ public class RefundFragment extends Fragment { addTableHeader(binding.llRefundItems); - for (RefundItem item : refundCart) { + for (RefundViewModel.RefundItem item : cart) { LinearLayout row = buildItemRow( item.productName, item.quantity, item.unitPrice, - false, // show remove button - () -> { - refundCart.remove(item); - renderOriginalItems(); - renderRefundCart(); - updateRefundTotal(); - } + false, + () -> viewModel.removeFromCart(item) ); binding.llRefundItems.addView(row); } @@ -342,146 +266,79 @@ public class RefundFragment extends Fragment { return row; } - private void showQuantityDialog(RefundItem item) { - // Calculate how many are already in cart - int inCart = 0; - for (RefundItem r : refundCart) { - if (r.prodId == item.prodId) { inCart = r.quantity; break; } - } - int available = item.quantity - inCart; - if (available <= 0) { - Toast.makeText(getContext(), "All units already added to refund", - Toast.LENGTH_SHORT).show(); - return; - } - - // Build dialog - AlertDialog.Builder builder = new AlertDialog.Builder(requireContext()); - builder.setTitle("Refund Quantity"); - builder.setMessage("Product: " + item.productName - + "\nAvailable: " + available); - + private void showQuantityDialog(RefundViewModel.RefundItem item, int available) { EditText input = new EditText(getContext()); input.setInputType(android.text.InputType.TYPE_CLASS_NUMBER); input.setText(String.valueOf(available)); input.setSelectAllOnFocus(true); - builder.setView(input); + input.setPadding(40, 40, 40, 40); - builder.setPositiveButton("Add to Refund", (d, w) -> { - String val = input.getText().toString().trim(); - if (val.isEmpty()) return; - int qty; - try { qty = Integer.parseInt(val); } - catch (Exception e) { - Toast.makeText(getContext(), "Invalid quantity", Toast.LENGTH_SHORT).show(); - return; - } - if (qty <= 0) { - Toast.makeText(getContext(), "Quantity must be at least 1", - Toast.LENGTH_SHORT).show(); - return; - } - if (qty > available) { - Toast.makeText(getContext(), "Cannot exceed " + available, - Toast.LENGTH_SHORT).show(); - return; - } - - // Add or merge into cart - boolean merged = false; - for (int i = 0; i < refundCart.size(); i++) { - if (refundCart.get(i).prodId == item.prodId) { - RefundItem existing = refundCart.get(i); - refundCart.set(i, new RefundItem(existing.prodId, - existing.productName, - existing.quantity + qty, - existing.unitPrice)); - merged = true; break; - } - } - if (!merged) { - refundCart.add(new RefundItem(item.prodId, item.productName, - qty, item.unitPrice)); - } - - renderOriginalItems(); - renderRefundCart(); - updateRefundTotal(); - }); - - builder.setNegativeButton("Cancel", null); - builder.show(); + new androidx.appcompat.app.AlertDialog.Builder(requireContext()) + .setTitle("Refund Quantity") + .setMessage("Product: " + item.productName + "\nAvailable: " + available) + .setView(input) + .setPositiveButton("Add to Refund", (d, w) -> { + String val = input.getText().toString().trim(); + if (val.isEmpty()) return; + int qty; + try { qty = Integer.parseInt(val); } + catch (Exception e) { + Toast.makeText(getContext(), "Invalid quantity", Toast.LENGTH_SHORT).show(); + return; + } + if (qty <= 0) { + Toast.makeText(getContext(), "Quantity must be at least 1", Toast.LENGTH_SHORT).show(); + return; + } + if (qty > available) { + Toast.makeText(getContext(), "Cannot exceed " + available, Toast.LENGTH_SHORT).show(); + return; + } + viewModel.addToCart(item, qty); + }) + .setNegativeButton("Cancel", null) + .show(); } private void updateRefundTotal() { BigDecimal total = BigDecimal.ZERO; - for (RefundItem item : refundCart) total = total.add(item.getTotal()); + List cart = viewModel.getRefundCart().getValue(); + if (cart != null) { + for (RefundViewModel.RefundItem item : cart) total = total.add(item.getTotal()); + } binding.tvRefundTotal.setText("Refund Total: $" + total.setScale(2, RoundingMode.HALF_UP)); } private void processRefund() { - if (currentSale == null) { + if (viewModel.getCurrentSale() == null) { Toast.makeText(getContext(), "Load a sale first", Toast.LENGTH_SHORT).show(); return; } - if (refundCart.isEmpty()) { - Toast.makeText(getContext(), "Add at least one item to refund", - Toast.LENGTH_SHORT).show(); + if (viewModel.getRefundCart().getValue() == null || viewModel.getRefundCart().getValue().isEmpty()) { + Toast.makeText(getContext(), "Add at least one item to refund", Toast.LENGTH_SHORT).show(); return; } String payment = PAYMENT_METHODS[binding.spinnerRefundPayment.getSelectedItemPosition()]; - - // Confirm dialog BigDecimal total = BigDecimal.ZERO; - for (RefundItem item : refundCart) total = total.add(item.getTotal()); + for (RefundViewModel.RefundItem item : viewModel.getRefundCart().getValue()) total = total.add(item.getTotal()); final BigDecimal finalTotal = total; - new AlertDialog.Builder(requireContext()) - .setTitle("Confirm Refund") - .setMessage("Process refund for Sale #" + currentSale.getSaleId() - + "?\nRefund amount: $" + finalTotal.setScale(2, RoundingMode.HALF_UP)) - .setPositiveButton("Yes", (d, w) -> submitRefund(payment)) - .setNegativeButton("No", null) - .show(); + DialogUtils.showConfirmDialog(requireContext(), "Confirm Refund", + "Process refund for Sale #" + viewModel.getCurrentSale().getSaleId() + + "?\nRefund amount: $" + finalTotal.setScale(2, RoundingMode.HALF_UP), + () -> submitRefund(payment)); } private void submitRefund(String payment) { - // Build sale items list - List items = new ArrayList<>(); - for (RefundItem item : refundCart) { - // Backend expects negative quantity for refunds - items.add(new SaleDTO.SaleItemDTO(item.prodId, -item.quantity)); - } - - SaleDTO dto = new SaleDTO( - currentSale.getStoreId(), - payment, - items, - true, // isRefund = true - currentSale.getSaleId(), // originalSaleId - null // no customer needed - ); - - Log.d("REFUND", "Submitting refund for saleId=" + currentSale.getSaleId() - + " items=" + items.size()); - - saleViewModel.createSale(dto).observe(getViewLifecycleOwner(), resource -> { + viewModel.submitRefund(payment).observe(getViewLifecycleOwner(), resource -> { if (resource != null) { - switch (resource.status) { - case SUCCESS: - if (resource.data != null) { - Toast.makeText(getContext(), - "Refund #" + resource.data.getSaleId() + " processed successfully!", - Toast.LENGTH_LONG).show(); - navigateBack(); - } - break; - case ERROR: - Log.e("REFUND", "Error: " + resource.message); - Toast.makeText(getContext(), "Error: " + resource.message, - Toast.LENGTH_LONG).show(); - break; + setLoading(resource.status == Resource.Status.LOADING); + if (resource.status == Resource.Status.SUCCESS) { + Toast.makeText(getContext(), "Refund processed successfully!", Toast.LENGTH_LONG).show(); + navigateBack(); + } else if (resource.status == Resource.Status.ERROR) { + Toast.makeText(getContext(), "Error: " + resource.message, Toast.LENGTH_LONG).show(); } } }); diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/SaleDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/SaleDetailFragment.java index 40c7896b..93a5e244 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/SaleDetailFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/SaleDetailFragment.java @@ -11,10 +11,12 @@ import androidx.navigation.fragment.NavHostFragment; import com.example.petstoremobile.R; import com.example.petstoremobile.databinding.FragmentSaleDetailBinding; import com.example.petstoremobile.dtos.*; -import com.example.petstoremobile.viewmodels.*; +import com.example.petstoremobile.viewmodels.SaleDetailViewModel; import com.example.petstoremobile.utils.SpinnerUtils; import com.example.petstoremobile.utils.DialogUtils; +import com.example.petstoremobile.utils.InputValidator; import com.example.petstoremobile.utils.Resource; +import com.example.petstoremobile.utils.UIUtils; import dagger.hilt.android.AndroidEntryPoint; import java.math.BigDecimal; import java.util.*; @@ -23,18 +25,7 @@ import java.util.*; public class SaleDetailFragment extends Fragment { private FragmentSaleDetailBinding binding; - private SaleViewModel saleViewModel; - private StoreViewModel storeViewModel; - private CustomerViewModel customerViewModel; - private ProductViewModel productViewModel; - - private boolean viewOnly = false; - private long saleId = -1; - - private List storeList = new ArrayList<>(); - private List customerList = new ArrayList<>(); - private List productList = new ArrayList<>(); - private List cartItems = new ArrayList<>(); + private SaleDetailViewModel viewModel; private final String[] PAYMENT_METHODS = { "Cash", "Card"}; @@ -42,17 +33,14 @@ public class SaleDetailFragment extends Fragment { public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { binding = FragmentSaleDetailBinding.inflate(inflater, container, false); - - saleViewModel = new ViewModelProvider(this).get(SaleViewModel.class); - storeViewModel = new ViewModelProvider(this).get(StoreViewModel.class); - customerViewModel = new ViewModelProvider(this).get(CustomerViewModel.class); - productViewModel = new ViewModelProvider(this).get(ProductViewModel.class); + viewModel = new ViewModelProvider(this).get(SaleDetailViewModel.class); SpinnerUtils.setupStringSpinner(requireContext(), binding.spinnerPaymentMethod, PAYMENT_METHODS); + observeViewModel(); handleArguments(); - if (!viewOnly) { + if (!viewModel.isViewOnly()) { loadData(); setupAddItem(); } @@ -64,32 +52,55 @@ public class SaleDetailFragment extends Fragment { return binding.getRoot(); } + private void observeViewModel() { + viewModel.getStoreList().observe(getViewLifecycleOwner(), list -> { + SpinnerUtils.populateSpinner(requireContext(), binding.spinnerSaleStore, list, + DropdownDTO::getLabel, "-- Select Store --", -1L, DropdownDTO::getId); + }); + + viewModel.getCustomerList().observe(getViewLifecycleOwner(), list -> { + SpinnerUtils.populateSpinner(requireContext(), binding.spinnerSaleCustomer, list, + DropdownDTO::getLabel, "-- No Customer --", -1L, DropdownDTO::getId); + }); + + viewModel.getProductList().observe(getViewLifecycleOwner(), list -> { + SpinnerUtils.populateSpinner(requireContext(), binding.spinnerSaleProduct, list, + ProductDTO::getProdName, "Select Product", -1L, ProductDTO::getProdId); + }); + + viewModel.getCartItems().observe(getViewLifecycleOwner(), items -> { + renderCartItems(); + updateTotal(); + }); + } + private void handleArguments() { Bundle a = getArguments(); if (a != null && a.containsKey("saleId")) { - saleId = a.getLong("saleId"); - viewOnly = a.getBoolean("viewOnly", false); + long saleId = a.getLong("saleId"); + boolean viewOnly = a.getBoolean("viewOnly", false); + viewModel.setSaleId(saleId, viewOnly); + binding.tvSaleMode.setText("Sale #" + saleId); binding.tvSaleDetailId.setText("ID: " + saleId); - // Show refund button for existing non-refund sales if (!a.getBoolean("isRefund", false)) { binding.btnRefundSale.setVisibility(View.VISIBLE); } - // Hide save and input controls for view only if (viewOnly) { binding.btnSaveSale.setVisibility(View.GONE); - binding.spinnerSaleStore.setEnabled(false); - binding.spinnerSaleCustomer.setEnabled(false); - binding.spinnerPaymentMethod.setEnabled(false); + UIUtils.setViewsEnabled(false, + binding.spinnerSaleStore, + binding.spinnerSaleCustomer, + binding.spinnerPaymentMethod); binding.llAddItemRow.setVisibility(View.GONE); binding.llExtraInfo.setVisibility(View.VISIBLE); } - // Load sale details loadSaleDetails(); } else { + viewModel.setSaleId(-1, false); binding.tvSaleMode.setText("New Sale"); binding.tvSaleDetailId.setVisibility(View.GONE); binding.btnRefundSale.setVisibility(View.GONE); @@ -97,89 +108,62 @@ public class SaleDetailFragment extends Fragment { } } + private void setLoading(boolean loading) { + if (binding != null && binding.progressBar != null) { + binding.progressBar.setVisibility(loading ? View.VISIBLE : View.GONE); + } + } + private void loadData() { - loadStores(); - loadCustomers(); - loadProducts(); - } - - private void loadStores() { - storeViewModel.getAllStores(0, 50).observe(getViewLifecycleOwner(), resource -> { - if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) { - storeList = resource.data.getContent(); - if (binding != null) { - SpinnerUtils.populateSpinner(requireContext(), binding.spinnerSaleStore, storeList, - StoreDTO::getStoreName, "-- Select Store --", -1L, StoreDTO::getStoreId); - } - } else if (storeList.isEmpty()) { - storeList = Collections.singletonList(new StoreDTO(1L, "Downtown Branch")); - if (binding != null) { - SpinnerUtils.populateSpinner(requireContext(), binding.spinnerSaleStore, storeList, - StoreDTO::getStoreName, "-- Select Store --", -1L, StoreDTO::getStoreId); - } - } + viewModel.loadStores().observe(getViewLifecycleOwner(), resource -> { + if (resource == null) return; + setLoading(resource.status == Resource.Status.LOADING); + if (resource.status == Resource.Status.SUCCESS && resource.data != null) viewModel.setStoreList(resource.data); }); - } - - private void loadCustomers() { - customerViewModel.getAllCustomers(0, 200).observe(getViewLifecycleOwner(), resource -> { - if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) { - customerList = resource.data.getContent(); - if (binding != null) { - SpinnerUtils.populateSpinner(requireContext(), binding.spinnerSaleCustomer, customerList, - CustomerDTO::getFullName, "-- No Customer --", -1L, CustomerDTO::getCustomerId); - } - } + viewModel.loadCustomers().observe(getViewLifecycleOwner(), resource -> { + if (resource == null) return; + setLoading(resource.status == Resource.Status.LOADING); + if (resource.status == Resource.Status.SUCCESS && resource.data != null) viewModel.setCustomerList(resource.data); }); - } - - private void loadProducts() { - productViewModel.getAllProducts(null, null, 0, 200, null).observe(getViewLifecycleOwner(), resource -> { - if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) { - productList = resource.data.getContent(); - if (binding != null) { - SpinnerUtils.populateSpinner(requireContext(), binding.spinnerSaleProduct, productList, - ProductDTO::getProdName, "Select Product", -1L, ProductDTO::getProdId); - } - } + viewModel.loadProducts().observe(getViewLifecycleOwner(), resource -> { + if (resource == null) return; + setLoading(resource.status == Resource.Status.LOADING); + if (resource.status == Resource.Status.SUCCESS && resource.data != null) viewModel.setProductList(resource.data.getContent()); }); } private void loadSaleDetails() { - saleViewModel.getSaleById(saleId).observe(getViewLifecycleOwner(), resource -> { - if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) { + viewModel.loadSaleDetails().observe(getViewLifecycleOwner(), resource -> { + if (resource == null) return; + setLoading(resource.status == Resource.Status.LOADING); + if (resource.status == Resource.Status.SUCCESS && resource.data != null) { SaleDTO sale = resource.data; - if (binding != null) { - binding.tvSaleDetailTotal.setText("Total: $" + sale.getTotalAmount()); - binding.tvSaleSubtotal.setText("$" + (sale.getSubtotalAmount() != null ? sale.getSubtotalAmount() : sale.getTotalAmount())); - - if (sale.getCouponDiscountAmount() != null && sale.getCouponDiscountAmount().compareTo(BigDecimal.ZERO) > 0) { - binding.llCouponDiscount.setVisibility(View.VISIBLE); - binding.tvSaleCouponDiscount.setText("-$" + sale.getCouponDiscountAmount()); - } else { - binding.llCouponDiscount.setVisibility(View.GONE); - } + binding.tvSaleDetailTotal.setText("Total: $" + sale.getTotalAmount()); + binding.tvSaleSubtotal.setText("$" + (sale.getSubtotalAmount() != null ? sale.getSubtotalAmount() : sale.getTotalAmount())); + + if (sale.getCouponDiscountAmount() != null && sale.getCouponDiscountAmount().compareTo(BigDecimal.ZERO) > 0) { + binding.llCouponDiscount.setVisibility(View.VISIBLE); + binding.tvSaleCouponDiscount.setText("-$" + sale.getCouponDiscountAmount()); + } else { + binding.llCouponDiscount.setVisibility(View.GONE); + } - if (sale.getEmployeeDiscountAmount() != null && sale.getEmployeeDiscountAmount().compareTo(BigDecimal.ZERO) > 0) { - binding.llEmployeeDiscount.setVisibility(View.VISIBLE); - binding.tvSaleEmployeeDiscount.setText("-$" + sale.getEmployeeDiscountAmount()); - } else { - binding.llEmployeeDiscount.setVisibility(View.GONE); - } + if (sale.getEmployeeDiscountAmount() != null && sale.getEmployeeDiscountAmount().compareTo(BigDecimal.ZERO) > 0) { + binding.llEmployeeDiscount.setVisibility(View.VISIBLE); + binding.tvSaleEmployeeDiscount.setText("-$" + sale.getEmployeeDiscountAmount()); + } else { + binding.llEmployeeDiscount.setVisibility(View.GONE); + } - binding.tvSaleChannel.setText(sale.getChannel() != null ? sale.getChannel() : "—"); - binding.tvSalePoints.setText(String.valueOf(sale.getPointsEarned() != null ? sale.getPointsEarned() : 0)); + binding.tvSaleChannel.setText(sale.getChannel() != null ? sale.getChannel() : "—"); + binding.tvSalePoints.setText(String.valueOf(sale.getPointsEarned() != null ? sale.getPointsEarned() : 0)); - SpinnerUtils.setSelectionByValue(binding.spinnerPaymentMethod, sale.getPaymentMethod()); + SpinnerUtils.setSelectionByValue(binding.spinnerPaymentMethod, sale.getPaymentMethod()); - // Display items - if (sale.getItems() != null) { - binding.llSaleItems.removeAllViews(); - for (SaleDTO.SaleItemDTO item : sale.getItems()) { - addItemRow(item.getProductName(), - Math.abs(item.getQuantity()), - item.getUnitPrice()); - } + if (sale.getItems() != null) { + binding.llSaleItems.removeAllViews(); + for (SaleDTO.SaleItemDTO item : sale.getItems()) { + addItemRow(item.getProductName(), Math.abs(item.getQuantity()), item.getUnitPrice()); } } } @@ -188,41 +172,44 @@ public class SaleDetailFragment extends Fragment { private void setupAddItem() { binding.btnAddItem.setOnClickListener(v -> { - if (binding.spinnerSaleProduct.getSelectedItemPosition() == 0) { - Toast.makeText(getContext(), "Select a product", Toast.LENGTH_SHORT).show(); - return; - } - String qtyStr = binding.etSaleQuantity.getText().toString().trim(); - if (qtyStr.isEmpty()) { - binding.etSaleQuantity.setError("Enter quantity"); - return; - } - int qty; - try { - qty = Integer.parseInt(qtyStr); - } catch (Exception e) { - binding.etSaleQuantity.setError("Invalid quantity"); - return; - } + if (!InputValidator.isSpinnerSelected(binding.spinnerSaleProduct, "Product")) return; + if (!InputValidator.isPositiveInteger(binding.etSaleQuantity, "Quantity")) return; - ProductDTO product = productList.get(binding.spinnerSaleProduct.getSelectedItemPosition() - 1); + int qty = Integer.parseInt(binding.etSaleQuantity.getText().toString().trim()); + ProductDTO product = viewModel.getProductList().getValue().get(binding.spinnerSaleProduct.getSelectedItemPosition() - 1); - // Check if product already in cart - for (SaleDTO.SaleItemDTO existing : cartItems) { + for (SaleDTO.SaleItemDTO existing : viewModel.getCartItems().getValue()) { if (existing.getProdId().equals(product.getProdId())) { Toast.makeText(getContext(), "Product already added", Toast.LENGTH_SHORT).show(); return; } } - SaleDTO.SaleItemDTO item = new SaleDTO.SaleItemDTO(product.getProdId(), qty); - cartItems.add(item); - addItemRow(product.getProdName(), qty, product.getProdPrice()); - updateTotal(); + viewModel.addToCart(new SaleDTO.SaleItemDTO(product.getProdId(), qty)); binding.etSaleQuantity.setText(""); }); } + private void renderCartItems() { + binding.llSaleItems.removeAllViews(); + List items = viewModel.getCartItems().getValue(); + List products = viewModel.getProductList().getValue(); + if (items == null || products == null) return; + + for (SaleDTO.SaleItemDTO item : items) { + String name = "Unknown"; + BigDecimal price = BigDecimal.ZERO; + for (ProductDTO p : products) { + if (p.getProdId().equals(item.getProdId())) { + name = p.getProdName(); + price = p.getProdPrice(); + break; + } + } + addItemRow(name, item.getQuantity(), price); + } + } + private void addItemRow(String name, int qty, BigDecimal price) { if (getContext() == null) return; LinearLayout row = new LinearLayout(getContext()); @@ -230,18 +217,15 @@ public class SaleDetailFragment extends Fragment { row.setPadding(0, 8, 0, 8); TextView tvName = new TextView(getContext()); - tvName.setLayoutParams(new LinearLayout.LayoutParams( - 0, LinearLayout.LayoutParams.WRAP_CONTENT, 2f)); + tvName.setLayoutParams(new LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 2f)); tvName.setText(name); TextView tvQty = new TextView(getContext()); - tvQty.setLayoutParams(new LinearLayout.LayoutParams( - 0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f)); + tvQty.setLayoutParams(new LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f)); tvQty.setText("x" + qty); TextView tvPrice = new TextView(getContext()); - tvPrice.setLayoutParams(new LinearLayout.LayoutParams( - 0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f)); + tvPrice.setLayoutParams(new LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f)); tvPrice.setText(price != null ? "$" + price : ""); row.addView(tvName); @@ -251,59 +235,37 @@ public class SaleDetailFragment extends Fragment { } private void updateTotal() { - BigDecimal total = BigDecimal.ZERO; - for (SaleDTO.SaleItemDTO item : cartItems) { - for (ProductDTO p : productList) { - if (p.getProdId().equals(item.getProdId()) && p.getProdPrice() != null) { - total = total.add(p.getProdPrice() - .multiply(BigDecimal.valueOf(item.getQuantity()))); - break; - } - } - } + BigDecimal total = viewModel.calculateSubtotal(); binding.tvSaleSubtotal.setText("$" + total); binding.tvSaleDetailTotal.setText("Total: $" + total); } private void saveSale() { - if (binding.spinnerSaleStore.getSelectedItemPosition() == 0) { - Toast.makeText(getContext(), "Select a store", Toast.LENGTH_SHORT).show(); - return; - } - if (cartItems.isEmpty()) { + if (!InputValidator.isSpinnerSelected(binding.spinnerSaleStore, "Store")) return; + + if (viewModel.getCartItems().getValue() == null || viewModel.getCartItems().getValue().isEmpty()) { Toast.makeText(getContext(), "Add at least one item", Toast.LENGTH_SHORT).show(); return; } - StoreDTO store = storeList.get(binding.spinnerSaleStore.getSelectedItemPosition() - 1); + DropdownDTO store = viewModel.getStoreList().getValue().get(binding.spinnerSaleStore.getSelectedItemPosition() - 1); String payment = PAYMENT_METHODS[binding.spinnerPaymentMethod.getSelectedItemPosition()]; - // Optional customer Long customerId = null; if (binding.spinnerSaleCustomer.getSelectedItemPosition() > 0) { - customerId = customerList.get(binding.spinnerSaleCustomer.getSelectedItemPosition() - 1) - .getCustomerId(); + customerId = viewModel.getCustomerList().getValue().get(binding.spinnerSaleCustomer.getSelectedItemPosition() - 1).getId(); } - SaleDTO dto = new SaleDTO( - store.getStoreId(), - payment, - cartItems, - false, - null, - customerId); + SaleDTO dto = new SaleDTO(store.getId(), payment, viewModel.getCartItems().getValue(), false, null, customerId); - saleViewModel.createSale(dto).observe(getViewLifecycleOwner(), resource -> { + viewModel.createSale(dto).observe(getViewLifecycleOwner(), resource -> { if (resource != null) { - switch (resource.status) { - case SUCCESS: - Toast.makeText(getContext(), "Sale saved!", Toast.LENGTH_SHORT).show(); - navigateBack(); - break; - case ERROR: - Log.e("SALE_SAVE", "Error: " + resource.message); - DialogUtils.showInfoDialog(requireContext(), "Save Error", resource.message); - break; + setLoading(resource.status == Resource.Status.LOADING); + if (resource.status == Resource.Status.SUCCESS) { + Toast.makeText(getContext(), "Sale saved!", Toast.LENGTH_SHORT).show(); + navigateBack(); + } else if (resource.status == Resource.Status.ERROR) { + DialogUtils.showInfoDialog(requireContext(), "Save Error", resource.message); } } }); @@ -313,7 +275,7 @@ public class SaleDetailFragment extends Fragment { DialogUtils.showConfirmDialog(requireContext(), "Process Refund", "Are you sure you want to process a refund for this sale?", () -> { Bundle args = new Bundle(); - args.putLong("saleId", saleId); + args.putLong("saleId", viewModel.getSaleId()); NavHostFragment.findNavController(this).navigate(R.id.nav_refund, args); }); } diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ServiceDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ServiceDetailFragment.java index 49c51141..2374fc4c 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ServiceDetailFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ServiceDetailFragment.java @@ -17,10 +17,11 @@ import com.example.petstoremobile.R; import com.example.petstoremobile.databinding.FragmentServiceDetailBinding; import com.example.petstoremobile.dtos.ServiceDTO; import com.example.petstoremobile.utils.ActivityLogger; +import com.example.petstoremobile.utils.DateTimeUtils; import com.example.petstoremobile.utils.DialogUtils; import com.example.petstoremobile.utils.InputValidator; import com.example.petstoremobile.utils.Resource; -import com.example.petstoremobile.viewmodels.ServiceViewModel; +import com.example.petstoremobile.viewmodels.ServiceDetailViewModel; import dagger.hilt.android.AndroidEntryPoint; @@ -31,15 +32,12 @@ import dagger.hilt.android.AndroidEntryPoint; public class ServiceDetailFragment extends Fragment { private FragmentServiceDetailBinding binding; - private long serviceId; - private boolean isEditing = false; - - private ServiceViewModel viewModel; + private ServiceDetailViewModel viewModel; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - viewModel = new ViewModelProvider(this).get(ServiceViewModel.class); + viewModel = new ViewModelProvider(this).get(ServiceDetailViewModel.class); } @Override @@ -53,78 +51,67 @@ public class ServiceDetailFragment extends Fragment { public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); - //get controls from layout and display the view depending on the mode handleArguments(); - //set button click listeners binding.btnBack.setOnClickListener(v -> navigateBack()); binding.btnSaveService.setOnClickListener(v -> saveService()); binding.btnDeleteService.setOnClickListener(v -> deleteService()); } + private void setLoading(boolean loading) { + if (binding != null && binding.progressBar != null) { + binding.progressBar.setVisibility(loading ? View.VISIBLE : View.GONE); + } + } + @Override public void onDestroyView() { super.onDestroyView(); binding = null; } - /** - * Handles the saving of service data (adding or updating). - */ private void saveService() { - // Validates all fields using InputValidator if (!InputValidator.isNotEmpty(binding.etServiceName, "Service Name")) return; if (!InputValidator.isNotEmpty(binding.etServiceDesc, "Description")) return; if (!InputValidator.isPositiveInteger(binding.etServiceDuration, "Duration")) return; if (!InputValidator.isPositiveDecimal(binding.etServicePrice, "Price")) return; - //get all the values from the fields String name = binding.etServiceName.getText().toString().trim(); String desc = binding.etServiceDesc.getText().toString().trim(); int duration = Integer.parseInt(binding.etServiceDuration.getText().toString().trim()); double price = Double.parseDouble(binding.etServicePrice.getText().toString().trim()); - //create a service object to send to the API ServiceDTO serviceDTO = new ServiceDTO(); serviceDTO.setServiceName(name); serviceDTO.setServiceDesc(desc); serviceDTO.setServiceDuration(duration); serviceDTO.setServicePrice(price); - //check if the service is being edited or added - if (isEditing) { - // Update existing service - serviceDTO.setServiceId(serviceId); - viewModel.updateService(serviceId, serviceDTO).observe(getViewLifecycleOwner(), resource -> { - if (resource.status == Resource.Status.SUCCESS) { - ActivityLogger.logChange(requireContext(), "Service", "UPDATED", (int) serviceId); + viewModel.saveService(serviceDTO).observe(getViewLifecycleOwner(), resource -> { + if (resource == null) return; + setLoading(resource.status == Resource.Status.LOADING); + if (resource.status == Resource.Status.SUCCESS) { + if (viewModel.isEditing()) { + ActivityLogger.logChange(requireContext(), "Service", "UPDATED", (int) viewModel.getServiceId()); Toast.makeText(getContext(), "Service updated successfully!", Toast.LENGTH_SHORT).show(); - navigateBack(); - } else if (resource.status == Resource.Status.ERROR) { - Toast.makeText(getContext(), "Error: " + resource.message, Toast.LENGTH_SHORT).show(); - } - }); - } else { - viewModel.createService(serviceDTO).observe(getViewLifecycleOwner(), resource -> { - if (resource.status == Resource.Status.SUCCESS) { + } else { ActivityLogger.log(requireContext(), "Added new Service: " + name); Toast.makeText(getContext(), "Service added successfully!", Toast.LENGTH_SHORT).show(); - navigateBack(); - } else if (resource.status == Resource.Status.ERROR) { - Toast.makeText(getContext(), "Error: " + resource.message, Toast.LENGTH_SHORT).show(); } - }); - } + navigateBack(); + } else if (resource.status == Resource.Status.ERROR) { + Toast.makeText(getContext(), "Error: " + resource.message, Toast.LENGTH_SHORT).show(); + } + }); } - /** - * Displays a confirmation dialog and handles the deletion of a service. - */ private void deleteService() { DialogUtils.showDeleteConfirmDialog(requireContext(), "Service", () -> - viewModel.deleteService(serviceId).observe(getViewLifecycleOwner(), resource -> { + viewModel.deleteService().observe(getViewLifecycleOwner(), resource -> { + if (resource == null) return; + setLoading(resource.status == Resource.Status.LOADING); if (resource.status == Resource.Status.SUCCESS) { - ActivityLogger.logChange(requireContext(), "Service", "DELETED", (int) serviceId); + ActivityLogger.logChange(requireContext(), "Service", "DELETED", (int) viewModel.getServiceId()); Toast.makeText(getContext(), "Service deleted successfully!", Toast.LENGTH_SHORT).show(); navigateBack(); } else if (resource.status == Resource.Status.ERROR) { @@ -133,30 +120,20 @@ public class ServiceDetailFragment extends Fragment { })); } - /** - * Navigates back to the previous screen. - */ private void navigateBack() { NavHostFragment.findNavController(this).popBackStack(); } - /** - * Handles arguments passed to the fragment to determine if it's in edit or add mode. - */ private void handleArguments() { - // Service is being edited if the bundle contains a serviceId if (getArguments() != null && getArguments().containsKey("serviceId")) { - // Get service data from arguments and populate fields - isEditing = true; - serviceId = getArguments().getLong("serviceId"); + long serviceId = getArguments().getLong("serviceId"); + viewModel.setServiceId(serviceId); binding.tvMode.setText("Edit Service"); - binding.tvServiceId.setText("ID: " + serviceId); + binding.tvServiceId.setText(DateTimeUtils.formatId(serviceId)); binding.btnDeleteService.setVisibility(View.VISIBLE); loadServiceData(); } else { - // Service is being added - // Set default values for add a new service - isEditing = false; + viewModel.setServiceId(-1); binding.tvMode.setText("Add Service"); binding.tvServiceId.setVisibility(View.GONE); binding.btnDeleteService.setVisibility(View.GONE); @@ -164,12 +141,10 @@ public class ServiceDetailFragment extends Fragment { } } - /** - * Fetches specific service details from the backend using the ID. - */ private void loadServiceData() { - viewModel.getServiceById(serviceId).observe(getViewLifecycleOwner(), resource -> { + viewModel.loadService().observe(getViewLifecycleOwner(), resource -> { if (resource == null) return; + setLoading(resource.status == Resource.Status.LOADING); if (resource.status == Resource.Status.SUCCESS && resource.data != null) { ServiceDTO s = resource.data; binding.etServiceName.setText(s.getServiceName()); diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/StaffDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/StaffDetailFragment.java index 508282bc..1c1bfc4e 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/StaffDetailFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/StaffDetailFragment.java @@ -1,27 +1,28 @@ package com.example.petstoremobile.fragments.listfragments.detailfragments; import android.os.Bundle; -import android.util.Log; import android.view.*; import android.widget.*; import androidx.annotation.NonNull; -import androidx.appcompat.app.AlertDialog; import androidx.fragment.app.Fragment; import androidx.lifecycle.ViewModelProvider; import androidx.navigation.fragment.NavHostFragment; import com.example.petstoremobile.R; import com.example.petstoremobile.databinding.FragmentStaffDetailBinding; import com.example.petstoremobile.dtos.EmployeeDTO; -import com.example.petstoremobile.viewmodels.EmployeeViewModel; +import com.example.petstoremobile.utils.DialogUtils; +import com.example.petstoremobile.utils.InputValidator; +import com.example.petstoremobile.utils.SpinnerUtils; +import com.example.petstoremobile.utils.UIUtils; +import com.example.petstoremobile.viewmodels.StaffDetailViewModel; +import com.example.petstoremobile.utils.Resource; import dagger.hilt.android.AndroidEntryPoint; @AndroidEntryPoint public class StaffDetailFragment extends Fragment { private FragmentStaffDetailBinding binding; - private EmployeeViewModel employeeViewModel; - private long employeeId = -1; - private boolean isEditing = false; + private StaffDetailViewModel viewModel; private final String[] ROLES = {"STAFF", "ADMIN"}; private final String[] STATUSES = {"Active", "Inactive"}; @@ -30,7 +31,7 @@ public class StaffDetailFragment extends Fragment { public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { binding = FragmentStaffDetailBinding.inflate(inflater, container, false); - employeeViewModel = new ViewModelProvider(this).get(EmployeeViewModel.class); + viewModel = new ViewModelProvider(this).get(StaffDetailViewModel.class); setupSpinners(); handleArguments(); @@ -38,21 +39,22 @@ public class StaffDetailFragment extends Fragment { binding.btnStaffBack.setOnClickListener(v -> navigateBack()); binding.btnSaveStaff.setOnClickListener(v -> save()); binding.btnDeleteStaff.setOnClickListener(v -> confirmDelete()); + + UIUtils.formatPhoneInput(binding.etStaffPhone); + return binding.getRoot(); } private void setupSpinners() { - binding.spinnerStaffRole.setAdapter(new ArrayAdapter<>(requireContext(), - android.R.layout.simple_spinner_item, ROLES)); - binding.spinnerStaffStatus.setAdapter(new ArrayAdapter<>(requireContext(), - android.R.layout.simple_spinner_item, STATUSES)); + SpinnerUtils.setupStringSpinner(requireContext(), binding.spinnerStaffRole, ROLES); + SpinnerUtils.setupStringSpinner(requireContext(), binding.spinnerStaffStatus, STATUSES); } private void handleArguments() { Bundle a = getArguments(); if (a != null && a.getBoolean("isEditing", false)) { - isEditing = true; - employeeId = a.getLong("employeeId", -1); + long employeeId = a.getLong("employeeId", -1); + viewModel.setEmployeeId(employeeId, true); binding.tvStaffMode.setText("Edit Staff Account"); binding.tvStaffId.setText("ID: " + employeeId); @@ -64,52 +66,50 @@ public class StaffDetailFragment extends Fragment { binding.etStaffPhone.setText(a.getString("phone", "")); binding.btnDeleteStaff.setVisibility(View.VISIBLE); - // Pre-fill role - String role = a.getString("role", "STAFF"); - for (int i = 0; i < ROLES.length; i++) { - if (ROLES[i].equals(role)) { - binding.spinnerStaffRole.setSelection(i); - break; - } - } - - // Pre-fill status - boolean active = a.getBoolean("active", true); - binding.spinnerStaffStatus.setSelection(active ? 0 : 1); + SpinnerUtils.setSelectionByValue(binding.spinnerStaffRole, a.getString("role", "STAFF")); + binding.spinnerStaffStatus.setSelection(a.getBoolean("active", true) ? 0 : 1); } else { - isEditing = false; - employeeId = -1; + viewModel.setEmployeeId(-1, false); binding.tvStaffMode.setText("Add Staff Account"); binding.btnDeleteStaff.setVisibility(View.GONE); binding.tvStaffId.setVisibility(View.GONE); } } + private void setLoading(boolean loading) { + if (binding != null && binding.progressBar != null) { + binding.progressBar.setVisibility(loading ? View.VISIBLE : View.GONE); + } + } private void save() { - String username = binding.etStaffUsername.getText() != null ? binding.etStaffUsername.getText().toString().trim() : ""; - String password = binding.etStaffPassword.getText() != null ? binding.etStaffPassword.getText().toString().trim() : ""; - String firstName = binding.etStaffFirstName.getText() != null ? binding.etStaffFirstName.getText().toString().trim() : ""; - String lastName = binding.etStaffLastName.getText() != null ? binding.etStaffLastName.getText().toString().trim() : ""; - String email = binding.etStaffEmail.getText() != null ? binding.etStaffEmail.getText().toString().trim() : ""; - String phone = binding.etStaffPhone.getText() != null ? binding.etStaffPhone.getText().toString().trim() : ""; + if (!InputValidator.isNotEmpty(binding.etStaffUsername, "Username")) return; + + if (!viewModel.isEditing()) { + if (!InputValidator.isNotEmpty(binding.etStaffPassword, "Password")) return; + String pass = binding.etStaffPassword.getText().toString(); + if (pass.length() < 6) { + binding.etStaffPassword.setError("At least 6 characters"); + binding.etStaffPassword.requestFocus(); + return; + } + } + + if (!InputValidator.isNotEmpty(binding.etStaffFirstName, "First Name")) return; + if (!InputValidator.isNotEmpty(binding.etStaffLastName, "Last Name")) return; + if (!InputValidator.isValidEmail(binding.etStaffEmail)) return; + if (!InputValidator.isValidPhone(binding.etStaffPhone)) return; + + String username = binding.etStaffUsername.getText().toString().trim(); + String password = binding.etStaffPassword.getText().toString().trim(); + String firstName = binding.etStaffFirstName.getText().toString().trim(); + String lastName = binding.etStaffLastName.getText().toString().trim(); + String email = binding.etStaffEmail.getText().toString().trim(); + String phone = binding.etStaffPhone.getText().toString().trim(); String role = ROLES[binding.spinnerStaffRole.getSelectedItemPosition()]; boolean active = binding.spinnerStaffStatus.getSelectedItemPosition() == 0; - // Validation - if (username.isEmpty()) { binding.etStaffUsername.setError("Required"); return; } - if (!isEditing && password.isEmpty()) { - binding.etStaffPassword.setError("Required for new account"); return; - } - if (!isEditing && password.length() < 6) { - binding.etStaffPassword.setError("At least 6 characters"); return; - } - if (firstName.isEmpty()) { binding.etStaffFirstName.setError("Required"); return; } - if (lastName.isEmpty()) { binding.etStaffLastName.setError("Required"); return; } - if (email.isEmpty()) { binding.etStaffEmail.setError("Required"); return; } - if (phone.isEmpty()) { binding.etStaffPhone.setError("Required"); return; } - EmployeeDTO dto = new EmployeeDTO( username, password.isEmpty() ? null : password, @@ -121,56 +121,32 @@ public class StaffDetailFragment extends Fragment { active ); - if (isEditing && employeeId > 0) { - employeeViewModel.updateEmployee(employeeId, dto).observe(getViewLifecycleOwner(), resource -> { - if (resource != null) { - switch (resource.status) { - case SUCCESS: - Toast.makeText(getContext(), "Updated successfully", Toast.LENGTH_SHORT).show(); - navigateBack(); - break; - case ERROR: - Toast.makeText(getContext(), "Error: " + resource.message, Toast.LENGTH_LONG).show(); - break; - } + viewModel.saveEmployee(dto).observe(getViewLifecycleOwner(), resource -> { + if (resource != null) { + setLoading(resource.status == Resource.Status.LOADING); + if (resource.status == Resource.Status.SUCCESS) { + Toast.makeText(getContext(), viewModel.isEditing() ? "Updated successfully" : "Staff account created", Toast.LENGTH_SHORT).show(); + navigateBack(); + } else if (resource.status == Resource.Status.ERROR) { + Toast.makeText(getContext(), "Error: " + resource.message, Toast.LENGTH_LONG).show(); } - }); - } else { - employeeViewModel.createEmployee(dto).observe(getViewLifecycleOwner(), resource -> { - if (resource != null) { - switch (resource.status) { - case SUCCESS: - Toast.makeText(getContext(), "Staff account created", Toast.LENGTH_SHORT).show(); - navigateBack(); - break; - case ERROR: - Toast.makeText(getContext(), "Error: " + resource.message, Toast.LENGTH_LONG).show(); - break; - } - } - }); - } + } + }); } private void confirmDelete() { - new AlertDialog.Builder(requireContext()) - .setTitle("Delete Staff Account?") - .setMessage("This will permanently delete this staff account.") - .setPositiveButton("Yes", (d, w) -> - employeeViewModel.deleteEmployee(employeeId).observe(getViewLifecycleOwner(), resource -> { - if (resource != null) { - switch (resource.status) { - case SUCCESS: - navigateBack(); - break; - case ERROR: - Toast.makeText(getContext(), "Delete failed: " + resource.message, - Toast.LENGTH_SHORT).show(); - break; - } - } - })) - .setNegativeButton("No", null).show(); + DialogUtils.showDeleteConfirmDialog(requireContext(), "Staff Account", () -> + viewModel.deleteEmployee().observe(getViewLifecycleOwner(), resource -> { + if (resource != null) { + setLoading(resource.status == Resource.Status.LOADING); + if (resource.status == Resource.Status.SUCCESS) { + navigateBack(); + } else if (resource.status == Resource.Status.ERROR) { + Toast.makeText(getContext(), "Delete failed: " + resource.message, + Toast.LENGTH_SHORT).show(); + } + } + })); } private void navigateBack() { diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/SupplierDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/SupplierDetailFragment.java index 4935cb8b..a7c64079 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/SupplierDetailFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/SupplierDetailFragment.java @@ -20,7 +20,7 @@ import com.example.petstoremobile.utils.DialogUtils; import com.example.petstoremobile.utils.InputValidator; import com.example.petstoremobile.utils.Resource; import com.example.petstoremobile.utils.UIUtils; -import com.example.petstoremobile.viewmodels.SupplierViewModel; +import com.example.petstoremobile.viewmodels.SupplierDetailViewModel; import dagger.hilt.android.AndroidEntryPoint; @@ -31,15 +31,12 @@ import dagger.hilt.android.AndroidEntryPoint; public class SupplierDetailFragment extends Fragment { private FragmentSupplierDetailBinding binding; - private long supId; - private boolean isEditing = false; - - private SupplierViewModel viewModel; + private SupplierDetailViewModel viewModel; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - viewModel = new ViewModelProvider(this).get(SupplierViewModel.class); + viewModel = new ViewModelProvider(this).get(SupplierDetailViewModel.class); } @Override @@ -53,42 +50,39 @@ public class SupplierDetailFragment extends Fragment { public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); - // Add phone number formatting (CA) and limit length to 14 characters UIUtils.formatPhoneInput(binding.etSupPhone); - handleArguments(); - //set button click listeners binding.btnBack.setOnClickListener(v -> navigateBack()); binding.btnSaveSupplier.setOnClickListener(v -> saveSupplier()); binding.btnDeleteSupplier.setOnClickListener(v -> deleteSupplier()); } + private void setLoading(boolean loading) { + if (binding != null && binding.progressBar != null) { + binding.progressBar.setVisibility(loading ? View.VISIBLE : View.GONE); + } + } + @Override public void onDestroyView() { super.onDestroyView(); binding = null; } - /** - * Handles the saving of supplier data (adding or updating). - */ private void saveSupplier() { - // Validates all fields using InputValidator if (!InputValidator.isNotEmpty(binding.etSupCompany, "Company Name")) return; if (!InputValidator.isNotEmpty(binding.etSupContactFirstName, "First Name")) return; if (!InputValidator.isNotEmpty(binding.etSupContactLastName, "Last Name")) return; if (!InputValidator.isValidEmail(binding.etSupEmail)) return; if (!InputValidator.isValidPhone(binding.etSupPhone)) return; - //get all the values from the fields String company = binding.etSupCompany.getText().toString().trim(); String firstName = binding.etSupContactFirstName.getText().toString().trim(); String lastName = binding.etSupContactLastName.getText().toString().trim(); String email = binding.etSupEmail.getText().toString().trim(); String phone = binding.etSupPhone.getText().toString().trim(); - //create a supplier object to send to the API SupplierDTO supplierDTO = new SupplierDTO(); supplierDTO.setSupCompany(company); supplierDTO.setSupContactFirstName(firstName); @@ -96,41 +90,31 @@ public class SupplierDetailFragment extends Fragment { supplierDTO.setSupEmail(email); supplierDTO.setSupPhone(phone); - //check if the supplier is being edited or added - if (isEditing) { - // Update existing supplier - supplierDTO.setSupId(supId); - viewModel.updateSupplier(supId, supplierDTO).observe(getViewLifecycleOwner(), resource -> { - if (resource.status == Resource.Status.SUCCESS) { - ActivityLogger.logChange(requireContext(), "Supplier", "UPDATED", (int) supId); + viewModel.saveSupplier(supplierDTO).observe(getViewLifecycleOwner(), resource -> { + if (resource == null) return; + setLoading(resource.status == Resource.Status.LOADING); + if (resource.status == Resource.Status.SUCCESS) { + if (viewModel.isEditing()) { + ActivityLogger.logChange(requireContext(), "Supplier", "UPDATED", (int) viewModel.getSupId()); Toast.makeText(getContext(), "Supplier updated successfully!", Toast.LENGTH_SHORT).show(); - navigateBack(); - } else if (resource.status == Resource.Status.ERROR) { - Toast.makeText(getContext(), "Error: " + resource.message, Toast.LENGTH_SHORT).show(); - } - }); - } else { - // Add new supplier - viewModel.createSupplier(supplierDTO).observe(getViewLifecycleOwner(), resource -> { - if (resource.status == Resource.Status.SUCCESS) { + } else { ActivityLogger.log(requireContext(), "Added new Supplier: " + company); Toast.makeText(getContext(), "Supplier added successfully!", Toast.LENGTH_SHORT).show(); - navigateBack(); - } else if (resource.status == Resource.Status.ERROR) { - Toast.makeText(getContext(), "Error: " + resource.message, Toast.LENGTH_SHORT).show(); } - }); - } + navigateBack(); + } else if (resource.status == Resource.Status.ERROR) { + Toast.makeText(getContext(), "Error: " + resource.message, Toast.LENGTH_SHORT).show(); + } + }); } - /** - * Displays a confirmation dialog and handles the deletion of a supplier. - */ private void deleteSupplier() { DialogUtils.showDeleteConfirmDialog(requireContext(), "Supplier", () -> - viewModel.deleteSupplier(supId).observe(getViewLifecycleOwner(), resource -> { + viewModel.deleteSupplier().observe(getViewLifecycleOwner(), resource -> { + if (resource == null) return; + setLoading(resource.status == Resource.Status.LOADING); if (resource.status == Resource.Status.SUCCESS) { - ActivityLogger.logChange(requireContext(), "Supplier", "DELETED", (int) supId); + ActivityLogger.logChange(requireContext(), "Supplier", "DELETED", (int) viewModel.getSupId()); Toast.makeText(getContext(), "Supplier deleted successfully!", Toast.LENGTH_SHORT).show(); navigateBack(); } else if (resource.status == Resource.Status.ERROR) { @@ -139,31 +123,21 @@ public class SupplierDetailFragment extends Fragment { })); } - /** - * Navigates back to the previous screen. - */ private void navigateBack() { NavHostFragment.findNavController(this).popBackStack(); } - /** - * Handles arguments passed to the fragment to determine if it's in edit or add mode. - */ private void handleArguments() { - // Supplier is being edited if the bundle contains a supId if (getArguments() != null && getArguments().containsKey("supId")) { - // Get supplier data from arguments and populate fields - isEditing = true; - supId = getArguments().getLong("supId"); + long supId = getArguments().getLong("supId"); + viewModel.setSupId(supId); binding.tvMode.setText("Edit Supplier"); binding.tvSupId.setText("ID: " + supId); binding.tvSupId.setVisibility(View.VISIBLE); binding.btnDeleteSupplier.setVisibility(View.VISIBLE); loadSupplierData(); } else { - // Supplier is being added - // Set default values for add a new supplier - isEditing = false; + viewModel.setSupId(-1); binding.tvMode.setText("Add Supplier"); binding.tvSupId.setVisibility(View.GONE); binding.btnDeleteSupplier.setVisibility(View.GONE); @@ -171,12 +145,10 @@ public class SupplierDetailFragment extends Fragment { } } - /** - * Fetches specific supplier details from the backend using the ID. - */ private void loadSupplierData() { - viewModel.getSupplierById(supId).observe(getViewLifecycleOwner(), resource -> { + viewModel.loadSupplier().observe(getViewLifecycleOwner(), resource -> { if (resource == null) return; + setLoading(resource.status == Resource.Status.LOADING); if (resource.status == Resource.Status.SUCCESS && resource.data != null) { SupplierDTO s = resource.data; binding.etSupCompany.setText(s.getSupCompany()); diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/listprofilefragments/PetProfileFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/listprofilefragments/PetProfileFragment.java index 371e5c20..73da4613 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/listprofilefragments/PetProfileFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/listprofilefragments/PetProfileFragment.java @@ -23,7 +23,7 @@ import com.example.petstoremobile.utils.FileUtils; import com.example.petstoremobile.utils.GlideUtils; import com.example.petstoremobile.utils.ImagePickerHelper; import com.example.petstoremobile.utils.Resource; -import com.example.petstoremobile.viewmodels.PetViewModel; +import com.example.petstoremobile.viewmodels.PetProfileViewModel; import java.io.File; import java.util.Locale; @@ -46,17 +46,13 @@ public class PetProfileFragment extends Fragment { @Inject @Named("baseUrl") String baseUrl; @Inject TokenManager tokenManager; - private PetViewModel viewModel; + private PetProfileViewModel viewModel; private ImagePickerHelper imagePickerHelper; - - /** - * Initializes activity launchers for gallery, camera, and permissions. - */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - viewModel = new ViewModelProvider(this).get(PetViewModel.class); + viewModel = new ViewModelProvider(this).get(PetProfileViewModel.class); imagePickerHelper = new ImagePickerHelper(this, "pet_photo.jpg", new ImagePickerHelper.ImagePickerListener() { @Override @@ -71,34 +67,27 @@ public class PetProfileFragment extends Fragment { }); } - /** - * Inflates the layout using view binding, initializes views, and sets up click listeners. - */ @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { binding = FragmentPetProfileBinding.inflate(inflater, container, false); - // Set pet details to display if (getArguments() != null) { petId = getArguments().getLong("petId"); loadPetData(); loadPetImage((int) petId); } - //set button click listeners binding.btnBack.setOnClickListener(v -> { NavHostFragment.findNavController(this).popBackStack(); }); - //Make the edit button go to the pet detail view binding.btnEditPet.setOnClickListener(v -> { Bundle args = new Bundle(); args.putLong("petId", petId); NavHostFragment.findNavController(this).navigate(R.id.nav_pet_detail, args); }); - //Make change photo button ask user to select a new photo binding.btnChangePhoto.setOnClickListener(v -> { imagePickerHelper.showImagePickerDialog("Change Pet Photo", hasImage); }); @@ -106,18 +95,22 @@ public class PetProfileFragment extends Fragment { return binding.getRoot(); } + private void setLoading(boolean loading) { + if (binding != null && binding.progressBar != null) { + binding.progressBar.setVisibility(loading ? View.VISIBLE : View.GONE); + } + } + @Override public void onDestroyView() { super.onDestroyView(); 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; + setLoading(resource.status == Resource.Status.LOADING); if (resource.status == Resource.Status.SUCCESS && resource.data != null) { PetDTO pet = resource.data; binding.tvPetName.setText(pet.getPetName()); @@ -133,7 +126,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 +137,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 +153,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,27 +170,22 @@ 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) { - Toast.makeText(getContext(), "Pet photo updated successfully", Toast.LENGTH_SHORT).show(); - loadPetImage((int) petId); - } else { - Toast.makeText(getContext(), "Upload failed: " + resource.message, Toast.LENGTH_SHORT).show(); - } + if (resource == null) return; + setLoading(resource.status == Resource.Status.LOADING); + if (resource.status == Resource.Status.SUCCESS) { + Toast.makeText(getContext(), "Pet photo updated successfully", Toast.LENGTH_SHORT).show(); + loadPetImage((int) petId); + } else if (resource.status == Resource.Status.ERROR) { + Toast.makeText(getContext(), "Upload failed: " + resource.message, Toast.LENGTH_SHORT).show(); } }); } catch (Exception e) { @@ -210,19 +193,16 @@ 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) { - if (resource.status == Resource.Status.SUCCESS) { - Toast.makeText(getContext(), "Pet photo removed", Toast.LENGTH_SHORT).show(); - hasImage = false; - binding.imgPet.setImageResource(R.drawable.placeholder); - } else { - Toast.makeText(getContext(), "Delete failed: " + resource.message, Toast.LENGTH_SHORT).show(); - } + if (resource == null) return; + setLoading(resource.status == Resource.Status.LOADING); + if (resource.status == Resource.Status.SUCCESS) { + Toast.makeText(getContext(), "Pet photo removed", Toast.LENGTH_SHORT).show(); + hasImage = false; + binding.imgPet.setImageResource(R.drawable.placeholder); + } else if (resource.status == Resource.Status.ERROR) { + Toast.makeText(getContext(), "Delete failed: " + resource.message, Toast.LENGTH_SHORT).show(); } }); } diff --git a/android/app/src/main/java/com/example/petstoremobile/models/Chat.java b/android/app/src/main/java/com/example/petstoremobile/models/Chat.java index f3a9a4eb..a519093c 100644 --- a/android/app/src/main/java/com/example/petstoremobile/models/Chat.java +++ b/android/app/src/main/java/com/example/petstoremobile/models/Chat.java @@ -6,13 +6,15 @@ public class Chat { private String lastMessage; private Long customerId; private Long staffId; + private String status; - public Chat(String chatId, String customerName, String lastMessage, Long customerId, Long staffId) { + public Chat(String chatId, String customerName, String lastMessage, Long customerId, Long staffId, String status) { this.chatId = chatId; this.customerName = customerName; this.lastMessage = lastMessage; this.customerId = customerId; this.staffId = staffId; + this.status = status; } public String getChatId() { @@ -34,4 +36,8 @@ public class Chat { public Long getStaffId() { return staffId; } + + public String getStatus() { + return status; + } } \ No newline at end of file diff --git a/android/app/src/main/java/com/example/petstoremobile/models/Message.java b/android/app/src/main/java/com/example/petstoremobile/models/Message.java index bf76b4c4..082baa58 100644 --- a/android/app/src/main/java/com/example/petstoremobile/models/Message.java +++ b/android/app/src/main/java/com/example/petstoremobile/models/Message.java @@ -9,7 +9,8 @@ public class Message { private Boolean isRead; private String attachmentUrl; private String attachmentName; - private String attachmentType; + private String attachmentMimeType; + private Long attachmentSizeBytes; public Message() {} @@ -43,6 +44,9 @@ public class Message { public String getAttachmentName() { return attachmentName; } public void setAttachmentName(String attachmentName) { this.attachmentName = attachmentName; } - public String getAttachmentType() { return attachmentType; } - public void setAttachmentType(String attachmentType) { this.attachmentType = attachmentType; } + public String getAttachmentMimeType() { return attachmentMimeType; } + public void setAttachmentMimeType(String attachmentMimeType) { this.attachmentMimeType = attachmentMimeType; } + + public Long getAttachmentSizeBytes() { return attachmentSizeBytes; } + public void setAttachmentSizeBytes(Long attachmentSizeBytes) { this.attachmentSizeBytes = attachmentSizeBytes; } } \ No newline at end of file diff --git a/android/app/src/main/java/com/example/petstoremobile/repositories/ChatRepository.java b/android/app/src/main/java/com/example/petstoremobile/repositories/ChatRepository.java index c3a31ad4..bdc250c4 100644 --- a/android/app/src/main/java/com/example/petstoremobile/repositories/ChatRepository.java +++ b/android/app/src/main/java/com/example/petstoremobile/repositories/ChatRepository.java @@ -17,6 +17,10 @@ import java.util.List; import javax.inject.Inject; import javax.inject.Singleton; +import okhttp3.MultipartBody; +import okhttp3.RequestBody; +import okhttp3.ResponseBody; + /** * Repository for handling chat-related data operations. */ @@ -55,6 +59,20 @@ public class ChatRepository extends BaseRepository { return executeCall(messageApi.sendMessage(conversationId, request)); } + /** + * Sends a message with an attachment. + */ + public LiveData> sendMessageWithAttachment(Long conversationId, MultipartBody.Part content, MultipartBody.Part attachment) { + return executeCall(messageApi.sendMessageWithAttachment(conversationId, content, attachment)); + } + + /** + * Downloads an attachment for a specific message. + */ + public LiveData> downloadAttachment(Long messageId) { + return executeCall(messageApi.downloadAttachment(messageId)); + } + /** * Fetches a paginated list of customers. */ diff --git a/android/app/src/main/java/com/example/petstoremobile/repositories/CustomerRepository.java b/android/app/src/main/java/com/example/petstoremobile/repositories/CustomerRepository.java index 4006ae69..9fa9d2e6 100644 --- a/android/app/src/main/java/com/example/petstoremobile/repositories/CustomerRepository.java +++ b/android/app/src/main/java/com/example/petstoremobile/repositories/CustomerRepository.java @@ -4,9 +4,12 @@ import androidx.lifecycle.LiveData; import com.example.petstoremobile.api.CustomerApi; import com.example.petstoremobile.dtos.CustomerDTO; +import com.example.petstoremobile.dtos.DropdownDTO; import com.example.petstoremobile.dtos.PageResponse; import com.example.petstoremobile.utils.Resource; +import java.util.List; + import javax.inject.Inject; import javax.inject.Singleton; @@ -33,4 +36,11 @@ public class CustomerRepository extends BaseRepository { public LiveData> getCustomerById(Long id) { return executeCall(customerApi.getCustomerById(id)); } -} + + /** + * Retrieves a list of customer dropdowns from the API. + */ + public LiveData>> getCustomerDropdowns() { + return executeCall(customerApi.getCustomerDropdowns()); + } +} \ No newline at end of file diff --git a/android/app/src/main/java/com/example/petstoremobile/repositories/PetRepository.java b/android/app/src/main/java/com/example/petstoremobile/repositories/PetRepository.java index 019b5884..623a4daa 100644 --- a/android/app/src/main/java/com/example/petstoremobile/repositories/PetRepository.java +++ b/android/app/src/main/java/com/example/petstoremobile/repositories/PetRepository.java @@ -4,10 +4,13 @@ import androidx.lifecycle.LiveData; import com.example.petstoremobile.api.PetApi; import com.example.petstoremobile.dtos.BulkDeleteRequest; +import com.example.petstoremobile.dtos.DropdownDTO; import com.example.petstoremobile.dtos.PageResponse; import com.example.petstoremobile.dtos.PetDTO; import com.example.petstoremobile.utils.Resource; +import java.util.List; + import javax.inject.Inject; import javax.inject.Singleton; @@ -26,8 +29,22 @@ public class PetRepository extends BaseRepository { /** * Retrieves a paginated list of pets from the API with optional filters. */ - public LiveData>> getAllPets(int page, int size, String query, String status, String species, Long storeId, String sort) { - return executeCall(petApi.getAllPets(page, size, query, status, species, storeId, sort)); + public LiveData>> getAllPets(int page, int size, String query, String status, String species, Long storeId, Long customerId, String sort) { + return executeCall(petApi.getAllPets(page, size, query, status, species, storeId, customerId, sort)); + } + + /** + * Retrieves a list of pets for a specific customer from the dropdowns API. + */ + public LiveData>> getCustomerPets(Long customerId) { + return executeCall(petApi.getCustomerPets(customerId)); + } + + /** + * Retrieves a list of pets available for adoption from the dropdowns API. + */ + public LiveData>> getAdoptionPets() { + return executeCall(petApi.getAdoptionPets()); } /** diff --git a/android/app/src/main/java/com/example/petstoremobile/repositories/StoreRepository.java b/android/app/src/main/java/com/example/petstoremobile/repositories/StoreRepository.java index 44781a32..0df93ab1 100644 --- a/android/app/src/main/java/com/example/petstoremobile/repositories/StoreRepository.java +++ b/android/app/src/main/java/com/example/petstoremobile/repositories/StoreRepository.java @@ -3,10 +3,13 @@ package com.example.petstoremobile.repositories; import androidx.lifecycle.LiveData; import com.example.petstoremobile.api.StoreApi; +import com.example.petstoremobile.dtos.DropdownDTO; import com.example.petstoremobile.dtos.PageResponse; import com.example.petstoremobile.dtos.StoreDTO; import com.example.petstoremobile.utils.Resource; +import java.util.List; + import javax.inject.Inject; import javax.inject.Singleton; @@ -26,4 +29,18 @@ public class StoreRepository extends BaseRepository { public LiveData>> getAllStores(int page, int size) { return executeCall(storeApi.getAllStores(page, size)); } + + /** + * Retrieves a list of store dropdowns from the API. + */ + public LiveData>> getStoreDropdowns() { + return executeCall(storeApi.getStoreDropdowns()); + } + + /** + * Retrieves a list of employees for a specific store from the dropdowns API. + */ + public LiveData>> getStoreEmployees(Long storeId) { + return executeCall(storeApi.getStoreEmployees(storeId)); + } } diff --git a/android/app/src/main/java/com/example/petstoremobile/utils/DateTimeUtils.java b/android/app/src/main/java/com/example/petstoremobile/utils/DateTimeUtils.java new file mode 100644 index 00000000..11e8f4fd --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/utils/DateTimeUtils.java @@ -0,0 +1,113 @@ +package com.example.petstoremobile.utils; + +import android.util.Log; + +import java.util.Calendar; +import java.util.Locale; + +/** + * Utility class for date and time operations. + */ +public class DateTimeUtils { + + private static final String TAG = "DateTimeUtils"; + + /** + * Formats status from backend format to UI format + * (backend is using all caps so we lower case them with this function) + */ + public static String formatStatusFromBackend(String status) { + if (status == null || status.isEmpty()) return status; + return status.substring(0, 1).toUpperCase() + status.substring(1).toLowerCase(); + } + + /** + * Converts a date and time string to a Calendar object. + * format: date = "YYYY-MM-DD", time = "HH:MM" + */ + public static Calendar parseDateTimeToCalendar(String date, String time) throws Exception { + Calendar calendar = Calendar.getInstance(); + String[] dateParts = date.split("-"); + String[] timeParts = time.split(":"); + + calendar.set( + Integer.parseInt(dateParts[0]), + Integer.parseInt(dateParts[1]) - 1, + Integer.parseInt(dateParts[2]), + Integer.parseInt(timeParts[0]), + Integer.parseInt(timeParts[1]), + 0 + ); + return calendar; + } + + /** + * Checks if a given date is in the past. + * format: date = "YYYY-MM-DD" + */ + public static boolean isDateInPast(String date) { + if (date == null || date.isEmpty()) return false; + try { + Calendar selected = Calendar.getInstance(); + String[] dateParts = date.split("-"); + selected.set( + Integer.parseInt(dateParts[0]), + Integer.parseInt(dateParts[1]) - 1, + Integer.parseInt(dateParts[2]), + 0, 0, 0 + ); + return selected.before(Calendar.getInstance()); + } catch (Exception e) { + Log.e(TAG, "Error parsing date: " + e.getMessage()); + return false; + } + } + + /** + * Checks if a given date and time are in the past. + * format: date = "YYYY-MM-DD", time = "HH:MM" + */ + public static boolean isDateTimeInPast(String date, String time) { + if (date == null || date.isEmpty() || time == null || time.isEmpty()) return false; + try { + Calendar selected = parseDateTimeToCalendar(date, time); + return selected.before(Calendar.getInstance()); + } catch (Exception e) { + Log.e(TAG, "Error parsing date/time: " + e.getMessage()); + return false; + } + } + + /** + * Parses a time string and returns hour and minute values. + */ + public static int[] parseTimeString(String time) { + if (time == null || time.isEmpty()) return null; + if (time.length() > 5) time = time.substring(0, 5); + + String[] parts = time.split(":"); + if (parts.length != 2) return null; + + try { + int hour = Integer.parseInt(parts[0]); + int min = Integer.parseInt(parts[1]); + return new int[]{hour, min}; + } catch (NumberFormatException e) { + return null; + } + } + + /** + * Formats an hour and minute into an HH:mm string. + */ + public static String formatTime(int hour, int minute) { + return String.format(Locale.getDefault(), "%02d:%02d", hour, minute); + } + + /** + * Formats an ID for display (e.g., "ID: 123"). + */ + public static String formatId(long id) { + return String.format(Locale.getDefault(), "ID: %d", id); + } +} diff --git a/android/app/src/main/java/com/example/petstoremobile/utils/FileUtils.java b/android/app/src/main/java/com/example/petstoremobile/utils/FileUtils.java index dcdc1bd7..bf8c8770 100644 --- a/android/app/src/main/java/com/example/petstoremobile/utils/FileUtils.java +++ b/android/app/src/main/java/com/example/petstoremobile/utils/FileUtils.java @@ -1,7 +1,9 @@ package com.example.petstoremobile.utils; import android.content.Context; +import android.database.Cursor; import android.net.Uri; +import android.provider.OpenableColumns; import java.io.File; import java.io.FileOutputStream; import java.io.InputStream; @@ -9,8 +11,11 @@ import java.io.InputStream; public class FileUtils { public static File getFileFromUri(Context context, Uri uri) { try { + String fileName = getFileName(context, uri); + if (fileName == null) fileName = "upload_" + System.currentTimeMillis(); + InputStream inputStream = context.getContentResolver().openInputStream(uri); - File tempFile = new File(context.getCacheDir(), "upload_image_" + System.currentTimeMillis() + ".jpg"); + File tempFile = new File(context.getCacheDir(), fileName); FileOutputStream outputStream = new FileOutputStream(tempFile); byte[] buffer = new byte[1024]; int length; @@ -24,4 +29,22 @@ public class FileUtils { return null; } } + + public static String getFileName(Context context, Uri uri) { + String result = null; + if (uri.getScheme().equals("content")) { + try (Cursor cursor = context.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 (result == null) { + result = uri.getPath(); + int cut = result.lastIndexOf('/'); + if (cut != -1) result = result.substring(cut + 1); + } + return result; + } } diff --git a/android/app/src/main/java/com/example/petstoremobile/utils/SpinnerUtils.java b/android/app/src/main/java/com/example/petstoremobile/utils/SpinnerUtils.java index b0aae8b8..de1e6304 100644 --- a/android/app/src/main/java/com/example/petstoremobile/utils/SpinnerUtils.java +++ b/android/app/src/main/java/com/example/petstoremobile/utils/SpinnerUtils.java @@ -10,8 +10,10 @@ import com.example.petstoremobile.adapters.BlackTextArrayAdapter; import com.example.petstoremobile.adapters.WhiteTextArrayAdapter; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.Objects; +import java.util.function.Consumer; import java.util.function.Function; /** @@ -50,6 +52,12 @@ public class SpinnerUtils { names.add(nameExtractor.apply(item)); } + // Only update adapter if contents changed to remove infinite loop when spinner is opened + if (isAdapterDataSame(spinner, names)) { + setSelectedId(spinner, data, defaultText, preselectedId, idExtractor); + return; + } + ArrayAdapter adapter; if (useWhiteText) { adapter = new WhiteTextArrayAdapter<>(context, android.R.layout.simple_spinner_item, names); @@ -60,26 +68,41 @@ public class SpinnerUtils { adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); spinner.setAdapter(adapter); + setSelectedId(spinner, data, defaultText, preselectedId, idExtractor); + } + + private static void setSelectedId(Spinner spinner, List data, String defaultText, Long preselectedId, Function idExtractor) { if (preselectedId != null && preselectedId != -1) { int offset = (defaultText != null) ? 1 : 0; for (int i = 0; i < data.size(); i++) { Long currentId = idExtractor.apply(data.get(i)); if (Objects.equals(currentId, preselectedId)) { - spinner.setSelection(i + offset); + if (spinner.getSelectedItemPosition() != i + offset) { + spinner.setSelection(i + offset); + } break; } } } } + /** + * Checks if the adapter data is the same as the new data. + */ + private static boolean isAdapterDataSame(Spinner spinner, List newNames) { + if (spinner.getAdapter() == null) return false; + if (spinner.getAdapter().getCount() != newNames.size()) return false; + for (int i = 0; i < newNames.size(); i++) { + if (!Objects.equals(spinner.getAdapter().getItem(i), newNames.get(i))) return false; + } + return true; + } + /** * Sets up a simple string spinner for filtering with a callback. */ public static void setupStringFilterSpinner(Context context, Spinner spinner, String[] items, Runnable onSelectionChanged) { - WhiteTextArrayAdapter adapter = new WhiteTextArrayAdapter<>(context, - android.R.layout.simple_spinner_item, items); - adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); - spinner.setAdapter(adapter); + updateStringSpinnerIfChanged(context, spinner, items, true); setupFilterSpinner(spinner, onSelectionChanged); } @@ -96,6 +119,13 @@ public class SpinnerUtils { }); } + /** + * Sets a listener that provides the selected index to a consumer. + */ + public static void setOnIndexSelectedListener(Spinner spinner, Consumer callback) { + spinner.setOnItemSelectedListener(new OnIndexSelected(callback)); + } + /** * Sets the selection of a spinner based on a string value. */ @@ -103,18 +133,65 @@ public class SpinnerUtils { if (value == null || spinner.getAdapter() == null) return; ArrayAdapter adapter = (ArrayAdapter) spinner.getAdapter(); int pos = adapter.getPosition(value); - if (pos >= 0) { + if (pos >= 0 && spinner.getSelectedItemPosition() != pos) { spinner.setSelection(pos); } } + /** + * Sets the selection of a spinner based on a value within an array. + */ + public static void setSelectionByValueArray(Spinner spinner, T[] array, T value) { + if (spinner == null || array == null || value == null) return; + for (int i = 0; i < array.length; i++) { + if (Objects.equals(array[i], value)) { + if (spinner.getSelectedItemPosition() != i) { + spinner.setSelection(i); + } + return; + } + } + } + /** * Configures a simple string array spinner. */ public static void setupStringSpinner(Context context, Spinner spinner, String[] items) { - BlackTextArrayAdapter adapter = new BlackTextArrayAdapter<>(context, - android.R.layout.simple_spinner_item, items); + updateStringSpinnerIfChanged(context, spinner, items, false); + } + + /** + * Updates a string spinner only if the items have changed. + */ + public static void updateStringSpinnerIfChanged(Context context, Spinner spinner, String[] items, boolean useWhiteText) { + if (isAdapterDataSame(spinner, Arrays.asList(items))) return; + + ArrayAdapter adapter; + if (useWhiteText) { + adapter = new WhiteTextArrayAdapter<>(context, android.R.layout.simple_spinner_item, items); + } else { + adapter = new BlackTextArrayAdapter<>(context, android.R.layout.simple_spinner_item, items); + } adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); spinner.setAdapter(adapter); } + + /** + * Helper listener to get selected index from a spinner. + */ + public static class OnIndexSelected implements AdapterView.OnItemSelectedListener { + private final Consumer callback; + + public OnIndexSelected(Consumer callback) { + this.callback = callback; + } + + @Override + public void onItemSelected(AdapterView parent, View view, int position, long id) { + callback.accept(position); + } + + @Override + public void onNothingSelected(AdapterView parent) {} + } } diff --git a/android/app/src/main/java/com/example/petstoremobile/utils/UIUtils.java b/android/app/src/main/java/com/example/petstoremobile/utils/UIUtils.java index 399cabc4..09b98cb6 100644 --- a/android/app/src/main/java/com/example/petstoremobile/utils/UIUtils.java +++ b/android/app/src/main/java/com/example/petstoremobile/utils/UIUtils.java @@ -1,5 +1,7 @@ package com.example.petstoremobile.utils; +import android.app.DatePickerDialog; +import android.content.Context; import android.telephony.PhoneNumberFormattingTextWatcher; import android.text.Editable; import android.text.InputFilter; @@ -8,10 +10,14 @@ import android.view.View; import android.widget.EditText; import android.widget.ImageButton; import android.widget.Spinner; +import android.widget.Toast; import androidx.fragment.app.Fragment; import com.example.petstoremobile.fragments.ListFragment; +import java.util.Calendar; +import java.util.Locale; + /** * Utility class for shared UI component logic and formatting. */ @@ -25,24 +31,16 @@ public class UIUtils { } /** - * Sets up a toggle for a filter layout, including icon changes and field resets. + * Sets up a toggle for a filter layout, including icon changes. */ public static void setupFilterToggle(ImageButton btnToggle, View layoutFilter, EditText etSearch, Spinner... spinners) { btnToggle.setOnClickListener(v -> { boolean isVisible = layoutFilter.getVisibility() == View.VISIBLE; layoutFilter.setVisibility(isVisible ? View.GONE : View.VISIBLE); - // Use Android default icons or app-specific ones if available btnToggle.setImageResource(isVisible ? android.R.drawable.ic_menu_search : android.R.drawable.ic_menu_close_clear_cancel); - - if (isVisible) { - if (etSearch != null) etSearch.setText(""); - for (Spinner spinner : spinners) { - if (spinner != null) spinner.setSelection(0); - } - } }); } @@ -73,4 +71,80 @@ public class UIUtils { @Override public void afterTextChanged(Editable s) {} }); } + + /** + * Sets the enabled state and alpha for multiple views, only if changed. + */ + public static void setViewsEnabled(boolean enabled, View... views) { + for (View v : views) { + if (v != null) { + if (v.isEnabled() != enabled) { + v.setEnabled(enabled); + } + float targetAlpha = enabled ? 1.0f : 0.5f; + if (Math.abs(v.getAlpha() - targetAlpha) > 0.01f) { + v.setAlpha(targetAlpha); + } + } + } + } + + /** + * Sets enabled state for a field and updates alpha for both the field and its label, only if changed. + */ + public static void setFieldEnabled(boolean enabled, View field, View label) { + if (field != null) { + if (field.isEnabled() != enabled) { + field.setEnabled(enabled); + } + float targetAlpha = enabled ? 1.0f : 0.5f; + if (Math.abs(field.getAlpha() - targetAlpha) > 0.01f) { + field.setAlpha(targetAlpha); + } + } + if (label != null) { + float targetAlpha = enabled ? 1.0f : 0.5f; + if (Math.abs(label.getAlpha() - targetAlpha) > 0.01f) { + label.setAlpha(targetAlpha); + } + } + } + + /** + * Sets the alpha for multiple views, only if changed. + */ + public static void setViewsAlpha(float alpha, View... views) { + for (View v : views) { + if (v != null && Math.abs(v.getAlpha() - alpha) > 0.01f) { + v.setAlpha(alpha); + } + } + } + + /** + * Displays a DatePickerDialog and sets the result to an EditText. + */ + public static void showDatePicker(Context context, EditText editText, Runnable onDateSet) { + Calendar c = Calendar.getInstance(); + DatePickerDialog d = new DatePickerDialog(context, + (dp, y, m, d1) -> { + String selectedDate = String.format(Locale.getDefault(), "%04d-%02d-%02d", y, m + 1, d1); + editText.setText(selectedDate); + if (onDateSet != null) onDateSet.run(); + }, + c.get(Calendar.YEAR), c.get(Calendar.MONTH), + c.get(Calendar.DAY_OF_MONTH)); + d.getDatePicker().setMinDate(System.currentTimeMillis() - 1000); + d.show(); + } + + /** + * Displays a toast and returns false. Useful for validation chains. + */ + public static boolean showToast(Context context, String msg) { + if (context != null) { + Toast.makeText(context, msg, Toast.LENGTH_SHORT).show(); + } + return false; + } } diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/AdoptionDetailViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/AdoptionDetailViewModel.java new file mode 100644 index 00000000..f6e24cd6 --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/AdoptionDetailViewModel.java @@ -0,0 +1,102 @@ +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.DropdownDTO; +import com.example.petstoremobile.repositories.AdoptionRepository; +import com.example.petstoremobile.repositories.CustomerRepository; +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 AdoptionDetailViewModel extends ViewModel { + private final AdoptionRepository adoptionRepository; + private final PetRepository petRepository; + private final CustomerRepository customerRepository; + private final StoreRepository storeRepository; + + private long adoptionId = -1; + private boolean isEditing = false; + + private final MutableLiveData> petList = new MutableLiveData<>(new ArrayList<>()); + private final MutableLiveData> customerList = new MutableLiveData<>(new ArrayList<>()); + private final MutableLiveData> storeList = new MutableLiveData<>(new ArrayList<>()); + private final MutableLiveData> employeeList = new MutableLiveData<>(new ArrayList<>()); + + @Inject + public AdoptionDetailViewModel(AdoptionRepository adoptionRepository, PetRepository petRepository, + CustomerRepository customerRepository, StoreRepository storeRepository) { + this.adoptionRepository = adoptionRepository; + this.petRepository = petRepository; + this.customerRepository = customerRepository; + this.storeRepository = storeRepository; + } + + public void setAdoptionId(long id) { + this.adoptionId = id; + this.isEditing = id != -1; + } + + public long getAdoptionId() { + return adoptionId; + } + + public boolean isEditing() { + return isEditing; + } + + public LiveData> loadAdoption() { + return adoptionRepository.getAdoptionById(adoptionId); + } + + public LiveData>> loadPets() { + return petRepository.getAdoptionPets(); + } + + public LiveData>> loadCustomers() { + return customerRepository.getCustomerDropdowns(); + } + + public LiveData>> loadStores() { + return storeRepository.getStoreDropdowns(); + } + + public LiveData>> loadEmployees(Long storeId) { + return storeRepository.getStoreEmployees(storeId); + } + + public LiveData> saveAdoption(AdoptionDTO dto) { + if (isEditing) { + return adoptionRepository.updateAdoption(adoptionId, dto); + } else { + return adoptionRepository.createAdoption(dto); + } + } + + public LiveData> deleteAdoption() { + return adoptionRepository.deleteAdoption(adoptionId); + } + + public void setPetList(List list) { petList.setValue(list); } + public LiveData> getPetList() { return petList; } + + public void setCustomerList(List list) { customerList.setValue(list); } + public LiveData> getCustomerList() { return customerList; } + + public void setStoreList(List list) { storeList.setValue(list); } + public LiveData> getStoreList() { return storeList; } + + public void setEmployeeList(List list) { employeeList.setValue(list); } + public LiveData> getEmployeeList() { return employeeList; } +} diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/AdoptionListViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/AdoptionListViewModel.java new file mode 100644 index 00000000..7c0b72b8 --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/AdoptionListViewModel.java @@ -0,0 +1,83 @@ +package com.example.petstoremobile.viewmodels; + +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; +import androidx.lifecycle.ViewModel; + +import com.example.petstoremobile.dtos.AdoptionDTO; +import com.example.petstoremobile.dtos.BulkDeleteRequest; +import com.example.petstoremobile.dtos.StoreDTO; +import com.example.petstoremobile.repositories.AdoptionRepository; +import com.example.petstoremobile.repositories.StoreRepository; +import com.example.petstoremobile.utils.Resource; + +import java.util.ArrayList; +import java.util.List; + +import javax.inject.Inject; + +import dagger.hilt.android.lifecycle.HiltViewModel; + +@HiltViewModel +public class AdoptionListViewModel extends ViewModel { + private final AdoptionRepository adoptionRepository; + private final StoreRepository storeRepository; + + private final MutableLiveData> adoptions = new MutableLiveData<>(new ArrayList<>()); + private final MutableLiveData> stores = new MutableLiveData<>(new ArrayList<>()); + private final MutableLiveData isLoading = new MutableLiveData<>(false); + + private int currentPage = 0; + private boolean isLastPage = false; + private static final int PAGE_SIZE = 20; + + @Inject + public AdoptionListViewModel(AdoptionRepository adoptionRepository, StoreRepository storeRepository) { + this.adoptionRepository = adoptionRepository; + this.storeRepository = storeRepository; + } + + public LiveData> getAdoptions() { return adoptions; } + public LiveData> getStores() { return stores; } + public LiveData getIsLoading() { return isLoading; } + public boolean isLastPage() { return isLastPage; } + + public void loadAdoptions(boolean reset, String query, String status, Long storeId, String date, Long employeeId) { + 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, date, employeeId).observeForever(resource -> { + if (resource != null) { + if (resource.status == Resource.Status.SUCCESS && resource.data != null) { + List currentList = reset ? new ArrayList<>() : new ArrayList<>(adoptions.getValue()); + currentList.addAll(resource.data.getContent()); + adoptions.setValue(currentList); + isLastPage = resource.data.isLast(); + if (!isLastPage) currentPage++; + isLoading.setValue(false); + } else if (resource.status == Resource.Status.ERROR) { + isLoading.setValue(false); + } + } + }); + } + + public void loadStores() { + storeRepository.getAllStores(0, 100).observeForever(resource -> { + if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) { + stores.setValue(resource.data.getContent()); + } + }); + } + + public LiveData> bulkDeleteAdoptions(List ids) { + return adoptionRepository.bulkDeleteAdoptions(new BulkDeleteRequest(ids)); + } +} diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/AdoptionViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/AdoptionViewModel.java deleted file mode 100644 index 12eb9779..00000000 --- a/android/app/src/main/java/com/example/petstoremobile/viewmodels/AdoptionViewModel.java +++ /dev/null @@ -1,68 +0,0 @@ -package com.example.petstoremobile.viewmodels; - -import androidx.lifecycle.LiveData; -import androidx.lifecycle.ViewModel; - -import com.example.petstoremobile.dtos.AdoptionDTO; -import com.example.petstoremobile.dtos.BulkDeleteRequest; -import com.example.petstoremobile.dtos.PageResponse; -import com.example.petstoremobile.repositories.AdoptionRepository; -import com.example.petstoremobile.utils.Resource; - -import java.util.List; - -import javax.inject.Inject; - -import dagger.hilt.android.lifecycle.HiltViewModel; - -@HiltViewModel -public class AdoptionViewModel extends ViewModel { - private final AdoptionRepository repository; - - @Inject - public AdoptionViewModel(AdoptionRepository repository) { - this.repository = repository; - } - - /** - * Fetches a paginated list of all adoptions with filters. - */ - public LiveData>> getAllAdoptions(int page, int size, String query, String status, Long storeId, String date, Long employeeId) { - return repository.getAllAdoptions(page, size, query, status, storeId, date, employeeId); - } - - /** - * Retrieves a single adoption by its ID. - */ - public LiveData> getAdoptionById(Long id) { - return repository.getAdoptionById(id); - } - - /** - * Creates a new adoption record. - */ - public LiveData> createAdoption(AdoptionDTO adoption) { - return repository.createAdoption(adoption); - } - - /** - * Updates an existing adoption record by ID. - */ - public LiveData> updateAdoption(Long id, AdoptionDTO adoption) { - return repository.updateAdoption(id, adoption); - } - - /** - * Deletes an adoption record by ID. - */ - public LiveData> deleteAdoption(Long id) { - return repository.deleteAdoption(id); - } - - /** - * Deletes multiple adoption records. - */ - public LiveData> bulkDeleteAdoptions(List ids) { - return repository.bulkDeleteAdoptions(new BulkDeleteRequest(ids)); - } -} diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/AnalyticsViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/AnalyticsViewModel.java new file mode 100644 index 00000000..76c039ac --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/AnalyticsViewModel.java @@ -0,0 +1,163 @@ +package com.example.petstoremobile.viewmodels; + +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; +import androidx.lifecycle.ViewModel; + +import com.example.petstoremobile.dtos.SaleDTO; +import com.example.petstoremobile.repositories.SaleRepository; +import com.example.petstoremobile.utils.Resource; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; + +import javax.inject.Inject; + +import dagger.hilt.android.lifecycle.HiltViewModel; + +@HiltViewModel +public class AnalyticsViewModel extends ViewModel { + private final SaleRepository saleRepository; + + private final MutableLiveData analyticsData = new MutableLiveData<>(); + private final MutableLiveData isLoading = new MutableLiveData<>(false); + private final MutableLiveData errorMessage = new MutableLiveData<>(); + + @Inject + public AnalyticsViewModel(SaleRepository saleRepository) { + this.saleRepository = saleRepository; + } + + public LiveData getAnalyticsData() { return analyticsData; } + public LiveData getIsLoading() { return isLoading; } + public LiveData getErrorMessage() { return errorMessage; } + + public void loadAnalytics() { + isLoading.setValue(true); + errorMessage.setValue(null); + saleRepository.getAllSales(0, 1000, null, null, null, "saleDate,desc").observeForever(resource -> { + if (resource != null) { + if (resource.status == Resource.Status.SUCCESS && resource.data != null) { + computeAnalytics(resource.data.getContent()); + isLoading.setValue(false); + } else if (resource.status == Resource.Status.ERROR) { + errorMessage.setValue(resource.message); + isLoading.setValue(false); + } + } + }); + } + + private void computeAnalytics(List sales) { + List regularSales = new ArrayList<>(); + for (SaleDTO s : sales) { + if (!Boolean.TRUE.equals(s.getIsRefund())) + regularSales.add(s); + } + + AnalyticsData data = new AnalyticsData(); + + // Summary + BigDecimal totalRevenue = BigDecimal.ZERO; + int totalItems = 0; + for (SaleDTO s : regularSales) { + if (s.getTotalAmount() != null) totalRevenue = totalRevenue.add(s.getTotalAmount()); + if (s.getItems() != null) { + for (SaleDTO.SaleItemDTO item : s.getItems()) { + if (item.getQuantity() != null) totalItems += Math.abs(item.getQuantity()); + } + } + } + data.totalRevenue = totalRevenue; + data.totalTransactions = regularSales.size(); + data.avgTransaction = data.totalTransactions > 0 + ? totalRevenue.divide(BigDecimal.valueOf(data.totalTransactions), 2, RoundingMode.HALF_UP) + : BigDecimal.ZERO; + data.totalItems = totalItems; + + // Product Maps + Map revenueByProduct = new LinkedHashMap<>(); + Map quantityByProduct = new LinkedHashMap<>(); + Map paymentCount = new LinkedHashMap<>(); + Map employeeRevenue = new LinkedHashMap<>(); + + for (SaleDTO s : regularSales) { + // Payments + String method = s.getPaymentMethod() != null ? s.getPaymentMethod() : "Unknown"; + paymentCount.merge(method, 1, Integer::sum); + + // Employee + String emp = s.getEmployeeName() != null ? s.getEmployeeName() : "Unknown"; + if (s.getTotalAmount() != null) employeeRevenue.merge(emp, s.getTotalAmount(), BigDecimal::add); + + // Items + if (s.getItems() != null) { + for (SaleDTO.SaleItemDTO item : s.getItems()) { + String name = item.getProductName() != null ? item.getProductName() : "Unknown"; + int qty = item.getQuantity() != null ? Math.abs(item.getQuantity()) : 0; + BigDecimal lineTotal = item.getUnitPrice() != null + ? item.getUnitPrice().multiply(BigDecimal.valueOf(qty)) + : BigDecimal.ZERO; + revenueByProduct.merge(name, lineTotal, BigDecimal::add); + quantityByProduct.merge(name, qty, Integer::sum); + } + } + } + + // Sort Top Revenue + data.topRevenueProducts = new ArrayList<>(revenueByProduct.entrySet()); + data.topRevenueProducts.sort((a, b) -> b.getValue().compareTo(a.getValue())); + if (data.topRevenueProducts.size() > 5) data.topRevenueProducts = data.topRevenueProducts.subList(0, 5); + + // Sort Top Quantity + data.topQuantityProducts = new ArrayList<>(quantityByProduct.entrySet()); + data.topQuantityProducts.sort((a, b) -> b.getValue() - a.getValue()); + if (data.topQuantityProducts.size() > 5) data.topQuantityProducts = data.topQuantityProducts.subList(0, 5); + + // Payment Stats + data.paymentMethodStats = new ArrayList<>(paymentCount.entrySet()); + + // Employee Performance + data.employeePerformance = new ArrayList<>(employeeRevenue.entrySet()); + data.employeePerformance.sort((a, b) -> b.getValue().compareTo(a.getValue())); + + // Daily Revenue (last 7 days) + Map dailyMap = new TreeMap<>(); + for (int i = 6; i >= 0; i--) { + Calendar day = Calendar.getInstance(); + day.add(Calendar.DAY_OF_YEAR, -i); + String key = String.format("%04d-%02d-%02d", + day.get(Calendar.YEAR), day.get(Calendar.MONTH) + 1, day.get(Calendar.DAY_OF_MONTH)); + dailyMap.put(key, BigDecimal.ZERO); + } + for (SaleDTO s : regularSales) { + if (s.getSaleDate() != null && s.getTotalAmount() != null) { + String date = s.getSaleDate().length() >= 10 ? s.getSaleDate().substring(0, 10) : s.getSaleDate(); + if (dailyMap.containsKey(date)) dailyMap.merge(date, s.getTotalAmount(), BigDecimal::add); + } + } + data.dailyRevenue = new ArrayList<>(dailyMap.entrySet()); + + analyticsData.setValue(data); + } + + public static class AnalyticsData { + public BigDecimal totalRevenue; + public int totalTransactions; + public BigDecimal avgTransaction; + public int totalItems; + public List> topRevenueProducts; + public List> topQuantityProducts; + public List> paymentMethodStats; + public List> employeePerformance; + public List> dailyRevenue; + } +} diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/AppointmentDetailViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/AppointmentDetailViewModel.java new file mode 100644 index 00000000..685b645a --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/AppointmentDetailViewModel.java @@ -0,0 +1,402 @@ +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.DropdownDTO; +import com.example.petstoremobile.dtos.ServiceDTO; +import com.example.petstoremobile.repositories.AppointmentRepository; +import com.example.petstoremobile.repositories.CustomerRepository; +import com.example.petstoremobile.repositories.PetRepository; +import com.example.petstoremobile.repositories.ServiceRepository; +import com.example.petstoremobile.repositories.StoreRepository; +import com.example.petstoremobile.utils.DateTimeUtils; +import com.example.petstoremobile.utils.Resource; + +import java.util.ArrayList; +import java.util.List; + +import javax.inject.Inject; + +import dagger.hilt.android.lifecycle.HiltViewModel; + +/** + * ViewModel for managing appointment details and form state. + */ +@HiltViewModel +public class AppointmentDetailViewModel extends ViewModel { + private final AppointmentRepository repository; + private final CustomerRepository customerRepository; + private final StoreRepository storeRepository; + private final PetRepository petRepository; + private final ServiceRepository serviceRepository; + + private final MutableLiveData> customers = new MutableLiveData<>(); + private final MutableLiveData> stores = new MutableLiveData<>(); + private final MutableLiveData> services = new MutableLiveData<>(); + private final MutableLiveData> customerPets = new MutableLiveData<>(); + private final MutableLiveData> storeEmployees = new MutableLiveData<>(); + private final MutableLiveData viewState = new MutableLiveData<>(new ViewState()); + + private long appointmentId = -1; + private Long currentCustomerId; + private Long currentStoreId; + private Long currentPetId; + private Long currentServiceId; + private Long currentStaffId; + + /** + * Constructor for AppointmentDetailViewModel. + */ + @Inject + public AppointmentDetailViewModel( + AppointmentRepository repository, + CustomerRepository customerRepository, + StoreRepository storeRepository, + PetRepository petRepository, + ServiceRepository serviceRepository) { + this.repository = repository; + this.customerRepository = customerRepository; + this.storeRepository = storeRepository; + this.petRepository = petRepository; + this.serviceRepository = serviceRepository; + } + + // Initial Data Loading + + /** + * Loads initial dropdown data for customers, stores, and services. + */ + public void loadInitialFormData() { + customerRepository.getCustomerDropdowns().observeForever(r -> { + if (r.status == Resource.Status.SUCCESS) customers.setValue(r.data); + }); + storeRepository.getStoreDropdowns().observeForever(r -> { + if (r.status == Resource.Status.SUCCESS) stores.setValue(r.data); + }); + serviceRepository.getAllServices(0, 200, null, "serviceName").observeForever(r -> { + if (r.status == Resource.Status.SUCCESS && r.data != null) services.setValue(r.data.getContent()); + }); + } + + // LiveData Getters + + /** + * Returns the LiveData for the list of customers. + */ + public LiveData> getCustomers() { return customers; } + + /** + * Returns the LiveData for the list of stores. + */ + public LiveData> getStores() { return stores; } + + /** + * Returns the LiveData for the list of services. + */ + public LiveData> getServices() { return services; } + + /** + * Returns the LiveData for the list of pets for the current customer. + */ + public LiveData> getCustomerPets() { return customerPets; } + + /** + * Returns the LiveData for the list of employees for the current store. + */ + public LiveData> getStoreEmployees() { return storeEmployees; } + + /** + * Returns the LiveData for the view state. + */ + public LiveData getViewState() { return viewState; } + + //State Getters + + /** + * Returns the current appointment ID. + */ + public long getAppointmentId() { return appointmentId; } + + /** + * Sets the current appointment ID and updates the mode. + */ + public void setAppointmentId(long id) { + this.appointmentId = id; + initMode(id != -1); + } + + // Selection Handlers for spinners + + /** + * Handles customer selection and loads their pets. + */ + public void onCustomerSelected(int position) { + List list = customers.getValue(); + if (position > 0 && list != null && position <= list.size()) { + currentCustomerId = list.get(position - 1).getId(); + loadPetsForCustomer(currentCustomerId); + updateViewState(s -> { + s.selectedCustomerId = currentCustomerId; + s.isPetEnabled = !s.isEditing; + }); + } else { + currentCustomerId = null; + customerPets.setValue(new ArrayList<>()); + updateViewState(s -> { + s.selectedCustomerId = null; + s.isPetEnabled = false; + }); + } + } + + /** + * Handles store selection and loads its employees. + */ + public void onStoreSelected(int position) { + List list = stores.getValue(); + if (position > 0 && list != null && position <= list.size()) { + currentStoreId = list.get(position - 1).getId(); + loadEmployeesForStore(currentStoreId); + updateViewState(s -> { + s.selectedStoreId = currentStoreId; + s.isStaffEnabled = !s.isPast; + }); + } else { + currentStoreId = null; + storeEmployees.setValue(new ArrayList<>()); + updateViewState(s -> { + s.selectedStoreId = null; + s.isStaffEnabled = false; + }); + } + } + + /** + * Handles service selection. + */ + public void onServiceSelected(int position) { + List list = services.getValue(); + currentServiceId = (position > 0 && list != null && position <= list.size()) ? list.get(position - 1).getServiceId() : null; + updateViewState(s -> s.selectedServiceId = currentServiceId); + } + + /** + * Handles pet selection. + */ + public void onPetSelected(int position) { + List list = customerPets.getValue(); + currentPetId = (position > 0 && list != null && position <= list.size()) ? list.get(position - 1).getId() : null; + updateViewState(s -> s.selectedPetId = currentPetId); + } + + /** + * Handles staff selection. + */ + public void onStaffSelected(int position) { + List list = storeEmployees.getValue(); + currentStaffId = (position > 0 && list != null && position <= list.size()) ? list.get(position - 1).getId() : null; + updateViewState(s -> s.selectedStaffId = currentStaffId); + } + + /** + * Loads the list of pets for a specific customer. + */ + private void loadPetsForCustomer(Long customerId) { + petRepository.getCustomerPets(customerId).observeForever(r -> { + if (r.status == Resource.Status.SUCCESS) customerPets.setValue(r.data); + }); + } + + /** + * Loads the list of employees for a specific store. + */ + private void loadEmployeesForStore(Long storeId) { + storeRepository.getStoreEmployees(storeId).observeForever(r -> { + if (r.status == Resource.Status.SUCCESS) storeEmployees.setValue(r.data); + }); + } + + // Appointment Detail CRUD + + /** + * Fetches appointment details and populates internal state. + */ + public LiveData> loadAppointment() { + MutableLiveData> result = new MutableLiveData<>(); + repository.getAppointmentById(appointmentId).observeForever(resource -> { + if (resource.status == Resource.Status.SUCCESS && resource.data != null) { + AppointmentDTO a = resource.data; + currentCustomerId = a.getCustomerId(); + currentStoreId = a.getStoreId(); + currentPetId = a.getPetId(); + currentServiceId = a.getServiceId(); + currentStaffId = a.getEmployeeId(); + + updateViewState(s -> { + s.selectedCustomerId = currentCustomerId; + s.selectedStoreId = currentStoreId; + s.selectedPetId = currentPetId; + s.selectedServiceId = currentServiceId; + s.selectedStaffId = currentStaffId; + }); + + if (currentCustomerId != null) loadPetsForCustomer(currentCustomerId); + if (currentStoreId != null) loadEmployeesForStore(currentStoreId); + } + result.setValue(resource); + }); + return result; + } + + /** + * Saves the appointment by building the DTO from tracked state. + */ + public LiveData> saveAppointment(String date, String time, String status) { + AppointmentDTO dto = new AppointmentDTO(currentCustomerId, currentStoreId, currentServiceId, currentStaffId, date, time, status, currentPetId); + if (appointmentId != -1) { + return repository.updateAppointment(appointmentId, dto); + } else { + return repository.createAppointment(dto); + } + } + + /** + * Deletes the current appointment. + */ + public LiveData> deleteAppointment() { + return repository.deleteAppointment(appointmentId); + } + + // UI Logic + + /** + * Updates the UI state when date, time, or status changes. + */ + public void onDateOrTimeChanged(String date, String time, String currentStatus) { + updateViewState(s -> { + s.availableStatuses = calculateAvailableStatuses(s.isEditing, date, time, currentStatus); + boolean isPast = DateTimeUtils.isDateTimeInPast(date, time); + boolean isCancelled = "Cancelled".equalsIgnoreCase(currentStatus); + + if (isCancelled) { + s.isPast = true; + setAllFieldsEnabled(s, false); + s.isStatusEnabled = false; + s.isSaveVisible = false; + } else if (isPast) { + s.isPast = true; + setAllFieldsEnabled(s, false); + s.isStatusEnabled = true; + } else { + s.isPast = false; + if (!s.isEditing) { + s.isCustomerEnabled = true; + s.isStoreEnabled = true; + s.isServiceEnabled = true; + s.isPetEnabled = currentCustomerId != null; + } + s.isDateEnabled = true; + s.isTimeEnabled = true; + s.isStatusEnabled = true; + } + }); + } + + /** + * Calculates available appointment statuses based on the current context. + */ + private String[] calculateAvailableStatuses(boolean isEditing, String date, String currentTime, String currentStatus) { + if (!isEditing) return new String[]{"Booked"}; + if (date == null || date.isEmpty()) return new String[]{}; + if ("Cancelled".equalsIgnoreCase(currentStatus)) return new String[]{"Cancelled"}; + if (DateTimeUtils.isDateTimeInPast(date, currentTime)) return new String[]{"Completed", "Missed"}; + return new String[]{"Booked", "Cancelled"}; + } + + /** + * Helper method to enable or disable all fields. + */ + private void setAllFieldsEnabled(ViewState s, boolean enabled) { + s.isCustomerEnabled = enabled; + s.isStoreEnabled = enabled; + s.isPetEnabled = enabled; + s.isServiceEnabled = enabled; + s.isStaffEnabled = enabled; + s.isDateEnabled = enabled; + s.isTimeEnabled = enabled; + } + + /** + * Initializes the UI mode (Create vs Edit). + */ + public void initMode(boolean isEditing) { + updateViewState(s -> { + s.isEditing = isEditing; + s.isDeleteVisible = isEditing; + if (isEditing) { + s.isCustomerEnabled = false; + s.isStoreEnabled = false; + s.isPetEnabled = false; + s.isServiceEnabled = false; + } else { + s.isCustomerEnabled = true; + s.isStoreEnabled = true; + s.isServiceEnabled = true; + s.isPetEnabled = false; // until customer selected + s.isStaffEnabled = false; // until store selected + s.availableStatuses = new String[]{"Booked"}; + } + }); + } + + /** + * Validates if a booking is in the future. + */ + public boolean isValidFutureBooking(String status, String date, String time) { + return !"BOOKED".equalsIgnoreCase(status) || !DateTimeUtils.isDateTimeInPast(date, time); + } + + /** + * Helper to update the view state and notify observers. + */ + private void updateViewState(Action action) { + ViewState current = viewState.getValue(); + if (current != null) { + action.run(current); + viewState.setValue(current); + } + } + + private interface Action { + void run(T t); + } + + /** + * A Class to show the states of Appointment Detail Fragment. + */ + public static class ViewState { + public boolean isPast = false; + public boolean isEditing = false; + public boolean isSaveVisible = true; + public boolean isDeleteVisible = false; + public boolean isCustomerEnabled = true; + public boolean isStoreEnabled = true; + public boolean isPetEnabled = false; + public boolean isServiceEnabled = true; + public boolean isStaffEnabled = false; + public boolean isDateEnabled = true; + public boolean isTimeEnabled = true; + public boolean isStatusEnabled = true; + public String[] availableStatuses = new String[]{}; + + // Selected IDs + public Long selectedCustomerId = null; + public Long selectedStoreId = null; + public Long selectedPetId = null; + public Long selectedServiceId = null; + public Long selectedStaffId = null; + } +} diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/AppointmentListViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/AppointmentListViewModel.java new file mode 100644 index 00000000..8bdaf699 --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/AppointmentListViewModel.java @@ -0,0 +1,65 @@ +package com.example.petstoremobile.viewmodels; + +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; +import androidx.lifecycle.ViewModel; + +import com.example.petstoremobile.dtos.AppointmentDTO; +import com.example.petstoremobile.dtos.BulkDeleteRequest; +import com.example.petstoremobile.dtos.StoreDTO; +import com.example.petstoremobile.repositories.AppointmentRepository; +import com.example.petstoremobile.repositories.StoreRepository; +import com.example.petstoremobile.utils.Resource; + +import java.util.ArrayList; +import java.util.List; + +import javax.inject.Inject; + +import dagger.hilt.android.lifecycle.HiltViewModel; + +@HiltViewModel +public class AppointmentListViewModel extends ViewModel { + private final AppointmentRepository appointmentRepository; + private final StoreRepository storeRepository; + + private final MutableLiveData> appointments = new MutableLiveData<>(new ArrayList<>()); + private final MutableLiveData> stores = new MutableLiveData<>(new ArrayList<>()); + private final MutableLiveData isLoading = new MutableLiveData<>(false); + + @Inject + public AppointmentListViewModel(AppointmentRepository appointmentRepository, StoreRepository storeRepository) { + this.appointmentRepository = appointmentRepository; + this.storeRepository = storeRepository; + } + + public LiveData> getAppointments() { return appointments; } + public LiveData> getStores() { return stores; } + public LiveData getIsLoading() { return isLoading; } + + public void loadAppointments(String query, String status, Long storeId, String date, Long employeeId) { + isLoading.setValue(true); + appointmentRepository.getAllAppointments(0, 500, query, status, storeId, date, employeeId).observeForever(resource -> { + if (resource != null) { + if (resource.status == Resource.Status.SUCCESS && resource.data != null) { + appointments.setValue(resource.data.getContent()); + isLoading.setValue(false); + } else if (resource.status == Resource.Status.ERROR) { + isLoading.setValue(false); + } + } + }); + } + + public void loadStores() { + storeRepository.getAllStores(0, 100).observeForever(resource -> { + if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) { + stores.setValue(resource.data.getContent()); + } + }); + } + + public LiveData> bulkDeleteAppointments(List ids) { + return appointmentRepository.bulkDeleteAppointments(new BulkDeleteRequest(ids)); + } +} diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/AppointmentViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/AppointmentViewModel.java deleted file mode 100644 index 69f24c95..00000000 --- a/android/app/src/main/java/com/example/petstoremobile/viewmodels/AppointmentViewModel.java +++ /dev/null @@ -1,68 +0,0 @@ -package com.example.petstoremobile.viewmodels; - -import androidx.lifecycle.LiveData; -import androidx.lifecycle.ViewModel; - -import com.example.petstoremobile.dtos.AppointmentDTO; -import com.example.petstoremobile.dtos.BulkDeleteRequest; -import com.example.petstoremobile.dtos.PageResponse; -import com.example.petstoremobile.repositories.AppointmentRepository; -import com.example.petstoremobile.utils.Resource; - -import java.util.List; - -import javax.inject.Inject; - -import dagger.hilt.android.lifecycle.HiltViewModel; - -@HiltViewModel -public class AppointmentViewModel extends ViewModel { - private final AppointmentRepository repository; - - @Inject - public AppointmentViewModel(AppointmentRepository repository) { - this.repository = repository; - } - - /** - * Fetches a paginated list of all appointments with optional filters. - */ - public LiveData>> getAllAppointments(int page, int size, String query, String status, Long storeId, String date, Long employeeId) { - return repository.getAllAppointments(page, size, query, status, storeId, date, employeeId); - } - - /** - * Retrieves a single appointment by its ID. - */ - public LiveData> getAppointmentById(Long id) { - return repository.getAppointmentById(id); - } - - /** - * Creates a new appointment. - */ - public LiveData> createAppointment(AppointmentDTO appointment) { - return repository.createAppointment(appointment); - } - - /** - * Updates an existing appointment record by ID. - */ - public LiveData> updateAppointment(Long id, AppointmentDTO appointment) { - return repository.updateAppointment(id, appointment); - } - - /** - * Deletes an appointment record by ID. - */ - public LiveData> deleteAppointment(Long id) { - return repository.deleteAppointment(id); - } - - /** - * Deletes multiple appointment records. - */ - public LiveData> bulkDeleteAppointments(List ids) { - return repository.bulkDeleteAppointments(new BulkDeleteRequest(ids)); - } -} \ No newline at end of file diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/ChatListViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/ChatListViewModel.java new file mode 100644 index 00000000..0422561d --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/ChatListViewModel.java @@ -0,0 +1,174 @@ +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 java.util.stream.Collectors; + +import javax.inject.Inject; + +import dagger.hilt.android.lifecycle.HiltViewModel; +import okhttp3.MultipartBody; +import okhttp3.RequestBody; +import okhttp3.ResponseBody; + +@HiltViewModel +public class ChatListViewModel extends ViewModel { + private final ChatRepository chatRepository; + private final CustomerRepository customerRepository; + + private final MutableLiveData> activeChats = new MutableLiveData<>(new ArrayList<>()); + private final MutableLiveData> closedChats = new MutableLiveData<>(new ArrayList<>()); + private final MutableLiveData> messageList = new MutableLiveData<>(new ArrayList<>()); + private final Map customerNames = new HashMap<>(); + private final MutableLiveData isLoading = new MutableLiveData<>(false); + + private Long lastActiveConversationId = null; + + @Inject + public ChatListViewModel(ChatRepository chatRepository, CustomerRepository customerRepository) { + this.chatRepository = chatRepository; + this.customerRepository = customerRepository; + } + + public LiveData> getActiveChats() { return activeChats; } + public LiveData> getClosedChats() { return closedChats; } + public LiveData> getMessageList() { return messageList; } + public LiveData getIsLoading() { return isLoading; } + + public Long getLastActiveConversationId() { + return lastActiveConversationId; + } + + public void setLastActiveConversationId(Long conversationId) { + this.lastActiveConversationId = conversationId; + } + + public void loadCustomers() { + isLoading.setValue(true); + 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(); + } else if (resource != null && resource.status == Resource.Status.ERROR) { + isLoading.setValue(false); + } + }); + } + + public void loadConversations() { + isLoading.setValue(true); + chatRepository.getAllConversations().observeForever(resource -> { + if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) { + List active = new ArrayList<>(); + List closed = new ArrayList<>(); + for (ConversationDTO dto : resource.data) { + String name = customerNames.getOrDefault(dto.getCustomerId(), "Customer #" + dto.getCustomerId()); + Chat chat = new Chat(String.valueOf(dto.getId()), name, dto.getLastMessage(), dto.getCustomerId(), dto.getStaffId(), dto.getStatus()); + if ("CLOSED".equalsIgnoreCase(dto.getStatus())) { + closed.add(chat); + } else { + active.add(chat); + } + } + activeChats.setValue(active); + closedChats.setValue(closed); + isLoading.setValue(false); + } else if (resource != null && resource.status == Resource.Status.ERROR) { + isLoading.setValue(false); + } + }); + } + + public void loadMessageHistory(Long conversationId) { + isLoading.setValue(true); + chatRepository.getMessages(conversationId).observeForever(resource -> { + if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) { + List messages = new ArrayList<>(); + for (MessageDTO dto : resource.data) { + messages.add(dtoToModel(dto)); + } + messageList.setValue(messages); + isLoading.setValue(false); + } else if (resource != null && resource.status == Resource.Status.ERROR) { + isLoading.setValue(false); + } + }); + } + + public LiveData> sendMessage(Long conversationId, String text) { + return chatRepository.sendMessage(conversationId, new SendMessageRequest(text)); + } + + public LiveData> sendMessageWithAttachment(Long conversationId, MultipartBody.Part content, MultipartBody.Part attachment) { + return chatRepository.sendMessageWithAttachment(conversationId, content, attachment); + } + + public LiveData> downloadAttachment(Long messageId) { + return chatRepository.downloadAttachment(messageId); + } + + public void addMessageLocally(MessageDTO dto) { + List current = new ArrayList<>(messageList.getValue()); + current.add(dtoToModel(dto)); + messageList.setValue(current); + } + + public void updateConversationLocally(ConversationDTO dto) { + updateList(activeChats, dto); + updateList(closedChats, dto); + + loadConversations(); + } + + private void updateList(MutableLiveData> liveData, ConversationDTO dto) { + List current = new ArrayList<>(liveData.getValue()); + String name = customerNames.getOrDefault(dto.getCustomerId(), "Customer #" + dto.getCustomerId()); + boolean updated = false; + 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(), dto.getStatus())); + updated = true; + break; + } + } + if (updated) liveData.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.setAttachmentMimeType(dto.getAttachmentMimeType()); + m.setAttachmentSizeBytes(dto.getAttachmentSizeBytes()); + return m; + } + + public String getCustomerName(Long customerId) { + return customerNames.getOrDefault(customerId, "Customer #" + customerId); + } +} diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/ChatViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/ChatViewModel.java deleted file mode 100644 index 2b516490..00000000 --- a/android/app/src/main/java/com/example/petstoremobile/viewmodels/ChatViewModel.java +++ /dev/null @@ -1,58 +0,0 @@ -package com.example.petstoremobile.viewmodels; - -import androidx.lifecycle.LiveData; -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.repositories.ChatRepository; -import com.example.petstoremobile.utils.Resource; - -import java.util.List; - -import javax.inject.Inject; - -import dagger.hilt.android.lifecycle.HiltViewModel; -/** - * ViewModel for managing chat-related UI state and data operations. - */ -@HiltViewModel -public class ChatViewModel extends ViewModel { - private final ChatRepository repository; - - @Inject - public ChatViewModel(ChatRepository repository) { - this.repository = repository; - } - - /** - * Retrieves all chat conversations for the current user. - */ - public LiveData>> getAllConversations() { - return repository.getAllConversations(); - } - - /** - * Retrieves the message history for a specific conversation. - */ - public LiveData>> getMessages(Long conversationId) { - return repository.getMessages(conversationId); - } - - /** - * Sends a plain text message to a conversation. - */ - public LiveData> sendMessage(Long conversationId, SendMessageRequest request) { - return repository.sendMessage(conversationId, request); - } - - /** - * Fetches a paginated list of customers. - */ - public LiveData>> getAllCustomers(int page, int size) { - return repository.getAllCustomers(page, size); - } -} diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/CustomerViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/CustomerViewModel.java deleted file mode 100644 index 5ad7cc76..00000000 --- a/android/app/src/main/java/com/example/petstoremobile/viewmodels/CustomerViewModel.java +++ /dev/null @@ -1,37 +0,0 @@ -package com.example.petstoremobile.viewmodels; - -import androidx.lifecycle.LiveData; -import androidx.lifecycle.ViewModel; - -import com.example.petstoremobile.dtos.CustomerDTO; -import com.example.petstoremobile.dtos.PageResponse; -import com.example.petstoremobile.repositories.CustomerRepository; -import com.example.petstoremobile.utils.Resource; - -import javax.inject.Inject; - -import dagger.hilt.android.lifecycle.HiltViewModel; - -@HiltViewModel -public class CustomerViewModel extends ViewModel { - private final CustomerRepository repository; - - @Inject - public CustomerViewModel(CustomerRepository repository) { - this.repository = repository; - } - - /** - * Fetches a paginated list of all customers. - */ - public LiveData>> getAllCustomers(int page, int size) { - return repository.getAllCustomers(page, size); - } - - /** - * Retrieves a single customer by their ID. - */ - public LiveData> getCustomerById(Long id) { - return repository.getCustomerById(id); - } -} diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/EmployeeViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/EmployeeViewModel.java deleted file mode 100644 index 5454269e..00000000 --- a/android/app/src/main/java/com/example/petstoremobile/viewmodels/EmployeeViewModel.java +++ /dev/null @@ -1,43 +0,0 @@ -package com.example.petstoremobile.viewmodels; - -import androidx.lifecycle.LiveData; -import androidx.lifecycle.ViewModel; - -import com.example.petstoremobile.dtos.EmployeeDTO; -import com.example.petstoremobile.dtos.PageResponse; -import com.example.petstoremobile.repositories.EmployeeRepository; -import com.example.petstoremobile.utils.Resource; - -import javax.inject.Inject; - -import dagger.hilt.android.lifecycle.HiltViewModel; - -@HiltViewModel -public class EmployeeViewModel extends ViewModel { - private final EmployeeRepository employeeRepository; - - @Inject - public EmployeeViewModel(EmployeeRepository employeeRepository) { - this.employeeRepository = employeeRepository; - } - - public LiveData>> getAllEmployees(int page, int size) { - return employeeRepository.getAllEmployees(page, size); - } - - public LiveData> getEmployeeById(Long id) { - return employeeRepository.getEmployeeById(id); - } - - public LiveData> createEmployee(EmployeeDTO dto) { - return employeeRepository.createEmployee(dto); - } - - public LiveData> updateEmployee(Long id, EmployeeDTO dto) { - return employeeRepository.updateEmployee(id, dto); - } - - public LiveData> deleteEmployee(Long id) { - return employeeRepository.deleteEmployee(id); - } -} diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/InventoryDetailViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/InventoryDetailViewModel.java new file mode 100644 index 00000000..a76785af --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/InventoryDetailViewModel.java @@ -0,0 +1,79 @@ +package com.example.petstoremobile.viewmodels; + +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; +import androidx.lifecycle.ViewModel; + +import com.example.petstoremobile.dtos.DropdownDTO; +import com.example.petstoremobile.dtos.InventoryDTO; +import com.example.petstoremobile.dtos.PageResponse; +import com.example.petstoremobile.dtos.ProductDTO; +import com.example.petstoremobile.repositories.InventoryRepository; +import com.example.petstoremobile.repositories.ProductRepository; +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 InventoryDetailViewModel extends ViewModel { + private final InventoryRepository inventoryRepository; + private final StoreRepository storeRepository; + private final ProductRepository productRepository; + + private long inventoryId = -1; + private boolean isEditing = false; + + private final MutableLiveData> storeList = new MutableLiveData<>(new ArrayList<>()); + private final MutableLiveData> productList = new MutableLiveData<>(new ArrayList<>()); + + @Inject + public InventoryDetailViewModel(InventoryRepository inventoryRepository, StoreRepository storeRepository, ProductRepository productRepository) { + this.inventoryRepository = inventoryRepository; + this.storeRepository = storeRepository; + this.productRepository = productRepository; + } + + public void setInventoryId(long id) { + this.inventoryId = id; + this.isEditing = id != -1; + } + + public long getInventoryId() { return inventoryId; } + public boolean isEditing() { return isEditing; } + + public LiveData> loadInventory() { + return inventoryRepository.getInventoryById(inventoryId); + } + + public LiveData>> loadStores() { + return storeRepository.getStoreDropdowns(); + } + + public LiveData>> loadProducts() { + return productRepository.getAllProducts(null, null, 0, 500, "prodName"); + } + + public LiveData> saveInventory(InventoryDTO dto) { + if (isEditing) { + return inventoryRepository.updateInventory(inventoryId, dto); + } else { + return inventoryRepository.createInventory(dto); + } + } + + public LiveData> deleteInventory() { + return inventoryRepository.deleteInventory(inventoryId); + } + + public void setStoreList(List list) { storeList.setValue(list); } + public LiveData> getStoreList() { return storeList; } + + public void setProductList(List list) { productList.setValue(list); } + public LiveData> getProductList() { return productList; } +} diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/InventoryListViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/InventoryListViewModel.java new file mode 100644 index 00000000..c91f8337 --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/InventoryListViewModel.java @@ -0,0 +1,81 @@ +package com.example.petstoremobile.viewmodels; + +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; +import androidx.lifecycle.ViewModel; + +import com.example.petstoremobile.dtos.BulkDeleteRequest; +import com.example.petstoremobile.dtos.InventoryDTO; +import com.example.petstoremobile.dtos.StoreDTO; +import com.example.petstoremobile.repositories.InventoryRepository; +import com.example.petstoremobile.repositories.StoreRepository; +import com.example.petstoremobile.utils.Resource; + +import java.util.ArrayList; +import java.util.List; + +import javax.inject.Inject; + +import dagger.hilt.android.lifecycle.HiltViewModel; + +@HiltViewModel +public class InventoryListViewModel extends ViewModel { + private final InventoryRepository inventoryRepository; + private final StoreRepository storeRepository; + + private final MutableLiveData> inventory = new MutableLiveData<>(new ArrayList<>()); + private final MutableLiveData> stores = new MutableLiveData<>(new ArrayList<>()); + private final MutableLiveData isLoading = new MutableLiveData<>(false); + + private int currentPage = 0; + private boolean isLastPage = false; + private static final int PAGE_SIZE = 20; + + @Inject + public InventoryListViewModel(InventoryRepository inventoryRepository, StoreRepository storeRepository) { + this.inventoryRepository = inventoryRepository; + this.storeRepository = storeRepository; + } + + public LiveData> getInventory() { return inventory; } + public LiveData> getStores() { return stores; } + public LiveData getIsLoading() { return isLoading; } + public boolean isLastPage() { return isLastPage; } + + public void loadInventory(boolean reset, String query, Long storeId) { + if (isLoading.getValue() != null && isLoading.getValue() && !reset) return; + + if (reset) { + currentPage = 0; + isLastPage = false; + } + + isLoading.setValue(true); + inventoryRepository.getAllInventory(query, storeId, currentPage, PAGE_SIZE, "product.prodName").observeForever(resource -> { + if (resource != null) { + if (resource.status == Resource.Status.SUCCESS && resource.data != null) { + List currentList = reset ? new ArrayList<>() : new ArrayList<>(inventory.getValue()); + currentList.addAll(resource.data.getContent()); + inventory.setValue(currentList); + isLastPage = resource.data.isLast(); + if (!isLastPage) currentPage++; + isLoading.setValue(false); + } else if (resource.status == Resource.Status.ERROR) { + isLoading.setValue(false); + } + } + }); + } + + public void loadStores() { + storeRepository.getAllStores(0, 100).observeForever(resource -> { + if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) { + stores.setValue(resource.data.getContent()); + } + }); + } + + public LiveData> bulkDeleteInventory(List ids) { + return inventoryRepository.bulkDeleteInventory(new BulkDeleteRequest(ids)); + } +} diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/InventoryViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/InventoryViewModel.java deleted file mode 100644 index c7ccc070..00000000 --- a/android/app/src/main/java/com/example/petstoremobile/viewmodels/InventoryViewModel.java +++ /dev/null @@ -1,90 +0,0 @@ -package com.example.petstoremobile.viewmodels; - -import androidx.lifecycle.LiveData; -import androidx.lifecycle.ViewModel; - -import com.example.petstoremobile.dtos.BulkDeleteRequest; -import com.example.petstoremobile.dtos.CategoryDTO; -import com.example.petstoremobile.dtos.InventoryDTO; -import com.example.petstoremobile.dtos.PageResponse; -import com.example.petstoremobile.dtos.StoreDTO; -import com.example.petstoremobile.repositories.CategoryRepository; -import com.example.petstoremobile.repositories.InventoryRepository; -import com.example.petstoremobile.repositories.StoreRepository; -import com.example.petstoremobile.utils.Resource; - -import java.util.List; - -import javax.inject.Inject; - -import dagger.hilt.android.lifecycle.HiltViewModel; - -@HiltViewModel -public class InventoryViewModel extends ViewModel { - private final InventoryRepository inventoryRepository; - private final CategoryRepository categoryRepository; - private final StoreRepository storeRepository; - - @Inject - public InventoryViewModel(InventoryRepository inventoryRepository, CategoryRepository categoryRepository, StoreRepository storeRepository) { - this.inventoryRepository = inventoryRepository; - this.categoryRepository = categoryRepository; - this.storeRepository = storeRepository; - } - - /** - * Retrieves a paginated list of inventory items, with optional filtering and sorting. - */ - public LiveData>> getAllInventory(String query, Long storeId, int page, int size, String sort) { - return inventoryRepository.getAllInventory(query, storeId, page, size, sort); - } - - /** - * Retrieves a single inventory item by its ID. - */ - public LiveData> getInventoryById(Long id) { - return inventoryRepository.getInventoryById(id); - } - - /** - * Creates a new inventory record. - */ - public LiveData> createInventory(InventoryDTO request) { - return inventoryRepository.createInventory(request); - } - - /** - * Updates an existing inventory record by ID. - */ - public LiveData> updateInventory(Long id, InventoryDTO request) { - return inventoryRepository.updateInventory(id, request); - } - - /** - * Deletes an inventory record by ID. - */ - public LiveData> deleteInventory(Long id) { - return inventoryRepository.deleteInventory(id); - } - - /** - * Deletes multiple inventory records in a single request. - */ - public LiveData> bulkDeleteInventory(List ids) { - return inventoryRepository.bulkDeleteInventory(new BulkDeleteRequest(ids)); - } - - /** - * Retrieves a paginated list of categories. - */ - public LiveData>> getAllCategories(int page, int size) { - return categoryRepository.getAllCategories(page, size); - } - - /** - * Retrieves a paginated list of stores. - */ - public LiveData>> getAllStores(int page, int size) { - return storeRepository.getAllStores(page, size); - } -} diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/PetDetailViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/PetDetailViewModel.java new file mode 100644 index 00000000..68506f44 --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/PetDetailViewModel.java @@ -0,0 +1,103 @@ +package com.example.petstoremobile.viewmodels; + +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; +import androidx.lifecycle.ViewModel; + +import com.example.petstoremobile.dtos.DropdownDTO; +import com.example.petstoremobile.dtos.PetDTO; +import com.example.petstoremobile.repositories.CustomerRepository; +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 PetDetailViewModel extends ViewModel { + private final PetRepository petRepository; + private final CustomerRepository customerRepository; + private final StoreRepository storeRepository; + + private final MutableLiveData petState = new MutableLiveData<>(); + private final MutableLiveData> customerList = new MutableLiveData<>(new ArrayList<>()); + private final MutableLiveData> storeList = new MutableLiveData<>(new ArrayList<>()); + private final MutableLiveData isLoading = new MutableLiveData<>(false); + + private long petId = -1; + private boolean isEditing = false; + + @Inject + public PetDetailViewModel(PetRepository petRepository, CustomerRepository customerRepository, StoreRepository storeRepository) { + this.petRepository = petRepository; + this.customerRepository = customerRepository; + this.storeRepository = storeRepository; + } + + public void setPetId(long id) { + this.petId = id; + this.isEditing = id != -1; + } + + public long getPetId() { + return petId; + } + + public boolean isEditing() { + return isEditing; + } + + public LiveData> loadPet() { + return petRepository.getPetById(petId); + } + + public LiveData>> loadCustomers() { + return customerRepository.getCustomerDropdowns(); + } + + public LiveData>> loadStores() { + return storeRepository.getStoreDropdowns(); + } + + public LiveData> savePet(PetDTO petDTO) { + if (isEditing) { + petDTO.setPetId(petId); + return petRepository.updatePet(petId, petDTO); + } else { + return petRepository.createPet(petDTO); + } + } + + public LiveData> deletePet() { + return petRepository.deletePet(petId); + } + + public void setCustomerList(List list) { + customerList.setValue(list); + } + + public LiveData> getCustomerList() { + return customerList; + } + + public void setStoreList(List list) { + storeList.setValue(list); + } + + public LiveData> getStoreList() { + return storeList; + } + + public LiveData getIsLoading() { + return isLoading; + } + + public void setLoading(boolean loading) { + isLoading.setValue(loading); + } +} diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/PetListViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/PetListViewModel.java new file mode 100644 index 00000000..8a567450 --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/PetListViewModel.java @@ -0,0 +1,69 @@ +package com.example.petstoremobile.viewmodels; + +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; +import androidx.lifecycle.ViewModel; + +import com.example.petstoremobile.dtos.BulkDeleteRequest; +import com.example.petstoremobile.dtos.PageResponse; +import com.example.petstoremobile.dtos.PetDTO; +import com.example.petstoremobile.dtos.StoreDTO; +import com.example.petstoremobile.repositories.PetRepository; +import com.example.petstoremobile.repositories.StoreRepository; +import com.example.petstoremobile.utils.Resource; + +import java.util.ArrayList; +import java.util.List; + +import javax.inject.Inject; + +import dagger.hilt.android.lifecycle.HiltViewModel; + +@HiltViewModel +public class PetListViewModel extends ViewModel { + private final PetRepository petRepository; + private final StoreRepository storeRepository; + + private final MutableLiveData> pets = new MutableLiveData<>(new ArrayList<>()); + private final MutableLiveData> stores = new MutableLiveData<>(new ArrayList<>()); + private final MutableLiveData isLoading = new MutableLiveData<>(false); + + @Inject + public PetListViewModel(PetRepository petRepository, StoreRepository storeRepository) { + this.petRepository = petRepository; + this.storeRepository = storeRepository; + } + + public LiveData> getPets() { return pets; } + public LiveData> getStores() { return stores; } + public LiveData getIsLoading() { return isLoading; } + + public void loadPets(String query, String status, String species, Long storeId) { + if ("All Statuses".equals(status)) status = null; + if ("All Species".equals(species)) species = null; + + isLoading.setValue(true); + petRepository.getAllPets(0, 100, query, status, species, storeId, null, "petName").observeForever(resource -> { + if (resource != null) { + if (resource.status == Resource.Status.SUCCESS && resource.data != null) { + pets.setValue(resource.data.getContent()); + isLoading.setValue(false); + } else if (resource.status == Resource.Status.ERROR) { + isLoading.setValue(false); + } + } + }); + } + + public void loadStores() { + storeRepository.getAllStores(0, 100).observeForever(resource -> { + if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) { + stores.setValue(resource.data.getContent()); + } + }); + } + + public LiveData> bulkDeletePets(List ids) { + return petRepository.bulkDeletePets(new BulkDeleteRequest(ids)); + } +} diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/PetProfileViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/PetProfileViewModel.java new file mode 100644 index 00000000..1fb75f1c --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/PetProfileViewModel.java @@ -0,0 +1,35 @@ +package com.example.petstoremobile.viewmodels; + +import androidx.lifecycle.LiveData; +import androidx.lifecycle.ViewModel; + +import com.example.petstoremobile.dtos.PetDTO; +import com.example.petstoremobile.repositories.PetRepository; +import com.example.petstoremobile.utils.Resource; + +import javax.inject.Inject; + +import dagger.hilt.android.lifecycle.HiltViewModel; +import okhttp3.MultipartBody; + +@HiltViewModel +public class PetProfileViewModel extends ViewModel { + private final PetRepository repository; + + @Inject + public PetProfileViewModel(PetRepository repository) { + this.repository = repository; + } + + public LiveData> getPetById(Long id) { + return repository.getPetById(id); + } + + public LiveData> uploadPetImage(Long id, MultipartBody.Part image) { + return repository.uploadPetImage(id, image); + } + + public LiveData> deletePetImage(Long id) { + return repository.deletePetImage(id); + } +} diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/PetViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/PetViewModel.java deleted file mode 100644 index c75926a7..00000000 --- a/android/app/src/main/java/com/example/petstoremobile/viewmodels/PetViewModel.java +++ /dev/null @@ -1,83 +0,0 @@ -package com.example.petstoremobile.viewmodels; - -import androidx.lifecycle.LiveData; -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.repositories.PetRepository; -import com.example.petstoremobile.utils.Resource; - -import java.util.List; - -import javax.inject.Inject; - -import dagger.hilt.android.lifecycle.HiltViewModel; -import okhttp3.MultipartBody; - -@HiltViewModel -public class PetViewModel extends ViewModel { - private final PetRepository repository; - - @Inject - public PetViewModel(PetRepository repository) { - this.repository = repository; - } - - /** - * Fetches a paginated list of pets with filters. - */ - public LiveData>> getAllPets(int page, int size, String query, String status, String species, Long storeId, String sort) { - return repository.getAllPets(page, size, query, status, species, storeId, sort); - } - - /** - * Retrieves a single pet by its ID. - */ - public LiveData> getPetById(Long id) { - return repository.getPetById(id); - } - - /** - * Creates a new pet record. - */ - public LiveData> createPet(PetDTO pet) { - return repository.createPet(pet); - } - - /** - * Updates an existing pet record by ID. - */ - public LiveData> updatePet(Long id, PetDTO pet) { - return repository.updatePet(id, pet); - } - - /** - * Deletes a pet record by ID. - */ - public LiveData> deletePet(Long id) { - return repository.deletePet(id); - } - - /** - * Deletes multiple pet records. - */ - public LiveData> bulkDeletePets(List ids) { - return repository.bulkDeletePets(new BulkDeleteRequest(ids)); - } - - /** - * Uploads an image for a specific pet. - */ - public LiveData> uploadPetImage(Long id, MultipartBody.Part image) { - return repository.uploadPetImage(id, image); - } - - /** - * Deletes the image associated with a specific pet. - */ - public LiveData> deletePetImage(Long id) { - return repository.deletePetImage(id); - } -} diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/ProductDetailViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/ProductDetailViewModel.java new file mode 100644 index 00000000..9ec0628a --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/ProductDetailViewModel.java @@ -0,0 +1,85 @@ +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.PageResponse; +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; +import okhttp3.MultipartBody; + +@HiltViewModel +public class ProductDetailViewModel extends ViewModel { + private final ProductRepository productRepository; + private final CategoryRepository categoryRepository; + + private final MutableLiveData> categoryList = new MutableLiveData<>(new ArrayList<>()); + private long prodId = -1; + private boolean isEditing = false; + + @Inject + public ProductDetailViewModel(ProductRepository productRepository, CategoryRepository categoryRepository) { + this.productRepository = productRepository; + this.categoryRepository = categoryRepository; + } + + public void setProdId(long id) { + this.prodId = id; + this.isEditing = id != -1; + } + + public long getProdId() { + return prodId; + } + + public boolean isEditing() { + return isEditing; + } + + public LiveData>> loadCategories() { + return categoryRepository.getAllCategories(0, 100); + } + + public LiveData> loadProduct() { + return productRepository.getProductById(prodId); + } + + public LiveData> saveProduct(ProductDTO dto) { + if (isEditing) { + return productRepository.updateProduct(prodId, dto); + } else { + return productRepository.createProduct(dto); + } + } + + public LiveData> deleteProduct() { + return productRepository.deleteProduct(prodId); + } + + public LiveData> uploadProductImage(MultipartBody.Part image) { + return productRepository.uploadProductImage(prodId, image); + } + + public LiveData> deleteProductImage() { + return productRepository.deleteProductImage(prodId); + } + + public void setCategoryList(List list) { + categoryList.setValue(list); + } + + public LiveData> getCategoryList() { + return categoryList; + } +} diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/ProductListViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/ProductListViewModel.java new file mode 100644 index 00000000..ecd2d238 --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/ProductListViewModel.java @@ -0,0 +1,60 @@ +package com.example.petstoremobile.viewmodels; + +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; +import androidx.lifecycle.ViewModel; + +import com.example.petstoremobile.dtos.CategoryDTO; +import com.example.petstoremobile.dtos.ProductDTO; +import com.example.petstoremobile.repositories.CategoryRepository; +import com.example.petstoremobile.repositories.ProductRepository; +import com.example.petstoremobile.utils.Resource; + +import java.util.ArrayList; +import java.util.List; + +import javax.inject.Inject; + +import dagger.hilt.android.lifecycle.HiltViewModel; + +@HiltViewModel +public class ProductListViewModel extends ViewModel { + private final ProductRepository productRepository; + private final CategoryRepository categoryRepository; + + private final MutableLiveData> products = new MutableLiveData<>(new ArrayList<>()); + private final MutableLiveData> categories = new MutableLiveData<>(new ArrayList<>()); + private final MutableLiveData isLoading = new MutableLiveData<>(false); + + @Inject + public ProductListViewModel(ProductRepository productRepository, CategoryRepository categoryRepository) { + this.productRepository = productRepository; + this.categoryRepository = categoryRepository; + } + + public LiveData> getProducts() { return products; } + public LiveData> getCategories() { return categories; } + public LiveData getIsLoading() { return isLoading; } + + public void loadProducts(String query, Long categoryId) { + isLoading.setValue(true); + productRepository.getAllProducts(query, categoryId, 0, 100, "prodName").observeForever(resource -> { + if (resource != null) { + if (resource.status == Resource.Status.SUCCESS && resource.data != null) { + products.setValue(resource.data.getContent()); + isLoading.setValue(false); + } else if (resource.status == Resource.Status.ERROR) { + isLoading.setValue(false); + } + } + }); + } + + public void loadCategories() { + categoryRepository.getAllCategories(0, 100).observeForever(resource -> { + if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) { + categories.setValue(resource.data.getContent()); + } + }); + } +} diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/ProductSupplierDetailViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/ProductSupplierDetailViewModel.java new file mode 100644 index 00000000..552a99fc --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/ProductSupplierDetailViewModel.java @@ -0,0 +1,78 @@ +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.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 ProductSupplierDetailViewModel extends ViewModel { + private final ProductSupplierRepository psRepository; + private final ProductRepository productRepository; + private final SupplierRepository supplierRepository; + + private boolean isEditing = false; + private long editProductId = -1; + private long editSupplierId = -1; + + private final MutableLiveData> productList = new MutableLiveData<>(new ArrayList<>()); + private final MutableLiveData> supplierList = new MutableLiveData<>(new ArrayList<>()); + + @Inject + public ProductSupplierDetailViewModel(ProductSupplierRepository psRepository, ProductRepository productRepository, SupplierRepository supplierRepository) { + this.psRepository = psRepository; + this.productRepository = productRepository; + this.supplierRepository = supplierRepository; + } + + public void setEditMode(long productId, long supplierId) { + this.isEditing = true; + this.editProductId = productId; + this.editSupplierId = supplierId; + } + + public boolean isEditing() { return isEditing; } + public long getEditProductId() { return editProductId; } + public long getEditSupplierId() { return editSupplierId; } + + public LiveData>> loadProducts() { + return productRepository.getAllProducts(null, null, 0, 200, "prodName"); + } + + public LiveData>> loadSuppliers() { + return supplierRepository.getAllSuppliers(0, 200, null, "supCompany"); + } + + public LiveData> saveProductSupplier(ProductSupplierDTO dto) { + if (isEditing) { + return psRepository.updateProductSupplier(editProductId, editSupplierId, dto); + } else { + return psRepository.createProductSupplier(dto); + } + } + + public LiveData> deleteProductSupplier() { + return psRepository.deleteProductSupplier(editProductId, editSupplierId); + } + + public void setProductList(List list) { productList.setValue(list); } + public LiveData> getProductList() { return productList; } + + public void setSupplierList(List list) { supplierList.setValue(list); } + public LiveData> getSupplierList() { return supplierList; } +} diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/ProductSupplierListViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/ProductSupplierListViewModel.java new file mode 100644 index 00000000..cad846c4 --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/ProductSupplierListViewModel.java @@ -0,0 +1,77 @@ +package com.example.petstoremobile.viewmodels; + +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; +import androidx.lifecycle.ViewModel; + +import com.example.petstoremobile.dtos.BulkDeleteRequest; +import com.example.petstoremobile.dtos.ProductDTO; +import com.example.petstoremobile.dtos.ProductSupplierDTO; +import com.example.petstoremobile.dtos.SupplierDTO; +import com.example.petstoremobile.repositories.ProductRepository; +import com.example.petstoremobile.repositories.ProductSupplierRepository; +import com.example.petstoremobile.repositories.SupplierRepository; +import com.example.petstoremobile.utils.Resource; + +import java.util.ArrayList; +import java.util.List; + +import javax.inject.Inject; + +import dagger.hilt.android.lifecycle.HiltViewModel; + +@HiltViewModel +public class ProductSupplierListViewModel extends ViewModel { + private final ProductSupplierRepository psRepository; + private final ProductRepository productRepository; + private final SupplierRepository supplierRepository; + + private final MutableLiveData> productSuppliers = new MutableLiveData<>(new ArrayList<>()); + private final MutableLiveData> products = new MutableLiveData<>(new ArrayList<>()); + private final MutableLiveData> suppliers = new MutableLiveData<>(new ArrayList<>()); + private final MutableLiveData isLoading = new MutableLiveData<>(false); + + @Inject + public ProductSupplierListViewModel(ProductSupplierRepository psRepository, ProductRepository productRepository, SupplierRepository supplierRepository) { + this.psRepository = psRepository; + this.productRepository = productRepository; + this.supplierRepository = supplierRepository; + } + + public LiveData> getProductSuppliers() { return productSuppliers; } + public LiveData> getProducts() { return products; } + public LiveData> getSuppliers() { return suppliers; } + public LiveData getIsLoading() { return isLoading; } + + public void loadProductSuppliers(String query, Long productId, Long supplierId) { + isLoading.setValue(true); + psRepository.getAllProductSuppliers(0, 100, query, productId, supplierId, "productName").observeForever(resource -> { + if (resource != null) { + if (resource.status == Resource.Status.SUCCESS && resource.data != null) { + productSuppliers.setValue(resource.data.getContent()); + isLoading.setValue(false); + } else if (resource.status == Resource.Status.ERROR) { + isLoading.setValue(false); + } + } + }); + } + + public void loadFilterData() { + productRepository.getAllProducts(null, null, 0, 100, "prodName").observeForever(resource -> { + if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) { + products.setValue(resource.data.getContent()); + } + }); + + supplierRepository.getAllSuppliers(0, 100, null, "supCompany").observeForever(resource -> { + if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) { + suppliers.setValue(resource.data.getContent()); + } + }); + } + + public LiveData> bulkDeleteProductSuppliers(List ids) { + return psRepository.bulkDeleteProductSuppliers(new BulkDeleteRequest(ids)); + } +} diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/ProductSupplierViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/ProductSupplierViewModel.java deleted file mode 100644 index f4302225..00000000 --- a/android/app/src/main/java/com/example/petstoremobile/viewmodels/ProductSupplierViewModel.java +++ /dev/null @@ -1,58 +0,0 @@ -package com.example.petstoremobile.viewmodels; - -import androidx.lifecycle.LiveData; -import androidx.lifecycle.ViewModel; - -import com.example.petstoremobile.dtos.BulkDeleteRequest; -import com.example.petstoremobile.dtos.PageResponse; -import com.example.petstoremobile.dtos.ProductSupplierDTO; -import com.example.petstoremobile.repositories.ProductSupplierRepository; -import com.example.petstoremobile.utils.Resource; - -import java.util.List; - -import javax.inject.Inject; - -import dagger.hilt.android.lifecycle.HiltViewModel; - -@HiltViewModel -public class ProductSupplierViewModel extends ViewModel { - private final ProductSupplierRepository repository; - - @Inject - public ProductSupplierViewModel(ProductSupplierRepository repository) { - this.repository = repository; - } - - /** - * Fetches a paginated list of all product-supplier relationships. - */ - public LiveData>> getAllProductSuppliers(int page, int size, String query, Long productId, Long supplierId, String sort) { - return repository.getAllProductSuppliers(page, size, query, productId, supplierId, sort); - } - - /** - * Creates a new product-supplier relationship. - */ - public LiveData> createProductSupplier(ProductSupplierDTO dto) { - return repository.createProductSupplier(dto); - } - - /** - * Updates an existing product-supplier relationship. - */ - public LiveData> updateProductSupplier(Long productId, Long supplierId, ProductSupplierDTO dto) { - return repository.updateProductSupplier(productId, supplierId, dto); - } - - /** - * Deletes a product-supplier relationship by product and supplier IDs. - */ - public LiveData> deleteProductSupplier(Long productId, Long supplierId) { - return repository.deleteProductSupplier(productId, supplierId); - } - - public LiveData> bulkDeleteProductSuppliers(List ids) { - return repository.bulkDeleteProductSuppliers(new BulkDeleteRequest(ids)); - } -} diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/ProductViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/ProductViewModel.java deleted file mode 100644 index b44c08eb..00000000 --- a/android/app/src/main/java/com/example/petstoremobile/viewmodels/ProductViewModel.java +++ /dev/null @@ -1,84 +0,0 @@ -package com.example.petstoremobile.viewmodels; - -import androidx.lifecycle.LiveData; -import androidx.lifecycle.ViewModel; - -import com.example.petstoremobile.dtos.CategoryDTO; -import com.example.petstoremobile.dtos.PageResponse; -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 javax.inject.Inject; - -import dagger.hilt.android.lifecycle.HiltViewModel; -import okhttp3.MultipartBody; - -@HiltViewModel -public class ProductViewModel extends ViewModel { - private final ProductRepository productRepository; - private final CategoryRepository categoryRepository; - - @Inject - public ProductViewModel(ProductRepository productRepository, CategoryRepository categoryRepository) { - this.productRepository = productRepository; - this.categoryRepository = categoryRepository; - } - - /** - * Retrieves a paginated list of products, optionally filtered by a query string, category and sorted. - */ - public LiveData>> getAllProducts(String query, Long categoryId, int page, int size, String sort) { - return productRepository.getAllProducts(query, categoryId, page, size, sort); - } - - /** - * Retrieves a single product by its ID. - */ - public LiveData> getProductById(Long id) { - return productRepository.getProductById(id); - } - - /** - * Creates a new product. - */ - public LiveData> createProduct(ProductDTO product) { - return productRepository.createProduct(product); - } - - /** - * Updates an existing product by ID. - */ - public LiveData> updateProduct(Long id, ProductDTO product) { - return productRepository.updateProduct(id, product); - } - - /** - * Deletes a product by its ID. - */ - public LiveData> deleteProduct(Long id) { - return productRepository.deleteProduct(id); - } - - /** - * Uploads an image for a specific product. - */ - public LiveData> uploadProductImage(Long id, MultipartBody.Part image) { - return productRepository.uploadProductImage(id, image); - } - - /** - * Deletes the image associated with a specific product. - */ - public LiveData> deleteProductImage(Long id) { - return productRepository.deleteProductImage(id); - } - - /** - * Retrieves a paginated list of all product categories. - */ - public LiveData>> getAllCategories(int page, int size) { - return categoryRepository.getAllCategories(page, size); - } -} diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/PurchaseOrderDetailViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/PurchaseOrderDetailViewModel.java new file mode 100644 index 00000000..436cfa4c --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/PurchaseOrderDetailViewModel.java @@ -0,0 +1,26 @@ +package com.example.petstoremobile.viewmodels; + +import androidx.lifecycle.LiveData; +import androidx.lifecycle.ViewModel; + +import com.example.petstoremobile.dtos.PurchaseOrderDTO; +import com.example.petstoremobile.repositories.PurchaseOrderRepository; +import com.example.petstoremobile.utils.Resource; + +import javax.inject.Inject; + +import dagger.hilt.android.lifecycle.HiltViewModel; + +@HiltViewModel +public class PurchaseOrderDetailViewModel extends ViewModel { + private final PurchaseOrderRepository repository; + + @Inject + public PurchaseOrderDetailViewModel(PurchaseOrderRepository repository) { + this.repository = repository; + } + + public LiveData> loadPurchaseOrder(long id) { + return repository.getPurchaseOrderById(id); + } +} diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/PurchaseOrderListViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/PurchaseOrderListViewModel.java new file mode 100644 index 00000000..438f4198 --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/PurchaseOrderListViewModel.java @@ -0,0 +1,60 @@ +package com.example.petstoremobile.viewmodels; + +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; +import androidx.lifecycle.ViewModel; + +import com.example.petstoremobile.dtos.PurchaseOrderDTO; +import com.example.petstoremobile.dtos.StoreDTO; +import com.example.petstoremobile.repositories.PurchaseOrderRepository; +import com.example.petstoremobile.repositories.StoreRepository; +import com.example.petstoremobile.utils.Resource; + +import java.util.ArrayList; +import java.util.List; + +import javax.inject.Inject; + +import dagger.hilt.android.lifecycle.HiltViewModel; + +@HiltViewModel +public class PurchaseOrderListViewModel extends ViewModel { + private final PurchaseOrderRepository purchaseOrderRepository; + private final StoreRepository storeRepository; + + private final MutableLiveData> purchaseOrders = new MutableLiveData<>(new ArrayList<>()); + private final MutableLiveData> stores = new MutableLiveData<>(new ArrayList<>()); + private final MutableLiveData isLoading = new MutableLiveData<>(false); + + @Inject + public PurchaseOrderListViewModel(PurchaseOrderRepository purchaseOrderRepository, StoreRepository storeRepository) { + this.purchaseOrderRepository = purchaseOrderRepository; + this.storeRepository = storeRepository; + } + + public LiveData> getPurchaseOrders() { return purchaseOrders; } + public LiveData> getStores() { return stores; } + public LiveData getIsLoading() { return isLoading; } + + public void loadPurchaseOrders(String query, Long storeId) { + isLoading.setValue(true); + purchaseOrderRepository.getAllPurchaseOrders(0, 100, query, storeId, "purchaseOrderId,desc").observeForever(resource -> { + if (resource != null) { + if (resource.status == Resource.Status.SUCCESS && resource.data != null) { + purchaseOrders.setValue(resource.data.getContent()); + isLoading.setValue(false); + } else if (resource.status == Resource.Status.ERROR) { + isLoading.setValue(false); + } + } + }); + } + + public void loadStores() { + storeRepository.getAllStores(0, 100).observeForever(resource -> { + if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) { + stores.setValue(resource.data.getContent()); + } + }); + } +} diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/PurchaseOrderViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/PurchaseOrderViewModel.java deleted file mode 100644 index d9a24e5e..00000000 --- a/android/app/src/main/java/com/example/petstoremobile/viewmodels/PurchaseOrderViewModel.java +++ /dev/null @@ -1,37 +0,0 @@ -package com.example.petstoremobile.viewmodels; - -import androidx.lifecycle.LiveData; -import androidx.lifecycle.ViewModel; - -import com.example.petstoremobile.dtos.PageResponse; -import com.example.petstoremobile.dtos.PurchaseOrderDTO; -import com.example.petstoremobile.repositories.PurchaseOrderRepository; -import com.example.petstoremobile.utils.Resource; - -import javax.inject.Inject; - -import dagger.hilt.android.lifecycle.HiltViewModel; - -@HiltViewModel -public class PurchaseOrderViewModel extends ViewModel { - private final PurchaseOrderRepository repository; - - @Inject - public PurchaseOrderViewModel(PurchaseOrderRepository repository) { - this.repository = repository; - } - - /** - * Fetches a paginated list of all purchase orders. - */ - public LiveData>> getAllPurchaseOrders(int page, int size, String query, Long storeId, String sort) { - return repository.getAllPurchaseOrders(page, size, query, storeId, sort); - } - - /** - * Retrieves a single purchase order by its ID. - */ - public LiveData> getPurchaseOrderById(Long id) { - return repository.getPurchaseOrderById(id); - } -} diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/RefundViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/RefundViewModel.java new file mode 100644 index 00000000..d2b13732 --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/RefundViewModel.java @@ -0,0 +1,167 @@ +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.repositories.SaleRepository; +import com.example.petstoremobile.utils.Resource; + +import java.math.BigDecimal; +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 RefundViewModel extends ViewModel { + private final SaleRepository saleRepository; + + private final MutableLiveData> allSales = new MutableLiveData<>(new ArrayList<>()); + private final MutableLiveData currentSale = new MutableLiveData<>(); + private final MutableLiveData> availableItems = new MutableLiveData<>(new ArrayList<>()); + private final MutableLiveData> refundCart = new MutableLiveData<>(new ArrayList<>()); + + @Inject + public RefundViewModel(SaleRepository saleRepository) { + this.saleRepository = saleRepository; + } + + public LiveData>> loadAllSales() { + return saleRepository.getAllSales(0, 1000, null, null, null, "saleDate,desc"); + } + + public void setAllSales(List sales) { + allSales.setValue(sales); + } + + public List getAllSalesList() { + return allSales.getValue(); + } + + public void setCurrentSale(SaleDTO sale) { + currentSale.setValue(sale); + buildRefundableItems(); + } + + public SaleDTO getCurrentSale() { + return currentSale.getValue(); + } + + public LiveData> getAvailableItems() { + return availableItems; + } + + public LiveData> getRefundCart() { + return refundCart; + } + + private void buildRefundableItems() { + SaleDTO sale = currentSale.getValue(); + List sales = allSales.getValue(); + if (sale == null || sales == null || sale.getItems() == null) { + availableItems.setValue(new ArrayList<>()); + return; + } + + Map alreadyRefunded = new HashMap<>(); + for (SaleDTO s : sales) { + if (Boolean.TRUE.equals(s.getIsRefund()) + && sale.getSaleId().equals(s.getOriginalSaleId()) + && s.getItems() != null) { + for (SaleDTO.SaleItemDTO item : s.getItems()) { + if (item.getProdId() != null && item.getQuantity() != null) { + alreadyRefunded.merge(item.getProdId(), + Math.abs(item.getQuantity()), Integer::sum); + } + } + } + } + + List items = new ArrayList<>(); + for (SaleDTO.SaleItemDTO item : sale.getItems()) { + if (item.getProdId() == null || item.getQuantity() == null) continue; + int refunded = alreadyRefunded.getOrDefault(item.getProdId(), 0); + int remaining = item.getQuantity() - refunded; + if (remaining > 0) { + items.add(new RefundItem( + item.getProdId(), + item.getProductName() != null ? item.getProductName() : "Unknown", + remaining, + item.getUnitPrice() + )); + } + } + availableItems.setValue(items); + refundCart.setValue(new ArrayList<>()); + } + + public void addToCart(RefundItem item, int qty) { + List cart = new ArrayList<>(refundCart.getValue()); + boolean merged = false; + for (int i = 0; i < cart.size(); i++) { + if (cart.get(i).prodId == item.prodId) { + RefundItem existing = cart.get(i); + cart.set(i, new RefundItem(existing.prodId, existing.productName, existing.quantity + qty, existing.unitPrice)); + merged = true; + break; + } + } + if (!merged) { + cart.add(new RefundItem(item.prodId, item.productName, qty, item.unitPrice)); + } + refundCart.setValue(cart); + } + + public void removeFromCart(RefundItem item) { + List cart = new ArrayList<>(refundCart.getValue()); + cart.remove(item); + refundCart.setValue(cart); + } + + public LiveData> submitRefund(String paymentMethod) { + SaleDTO sale = currentSale.getValue(); + List cart = refundCart.getValue(); + if (sale == null || cart == null || cart.isEmpty()) return null; + + List items = new ArrayList<>(); + for (RefundItem item : cart) { + items.add(new SaleDTO.SaleItemDTO(item.prodId, -item.quantity)); + } + + SaleDTO dto = new SaleDTO( + sale.getStoreId(), + paymentMethod, + items, + true, + sale.getSaleId(), + null + ); + + return saleRepository.createSale(dto); + } + + public static class RefundItem { + public long prodId; + public String productName; + public int quantity; + public BigDecimal unitPrice; + + public RefundItem(long prodId, String productName, int quantity, BigDecimal unitPrice) { + this.prodId = prodId; + this.productName = productName; + this.quantity = quantity; + this.unitPrice = unitPrice; + } + + public BigDecimal getTotal() { + return unitPrice != null ? unitPrice.multiply(BigDecimal.valueOf(quantity)) : BigDecimal.ZERO; + } + } +} diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/SaleDetailViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/SaleDetailViewModel.java new file mode 100644 index 00000000..201fae9e --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/SaleDetailViewModel.java @@ -0,0 +1,118 @@ +package com.example.petstoremobile.viewmodels; + +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; +import androidx.lifecycle.ViewModel; + +import com.example.petstoremobile.dtos.DropdownDTO; +import com.example.petstoremobile.dtos.ProductDTO; +import com.example.petstoremobile.dtos.SaleDTO; +import com.example.petstoremobile.repositories.CustomerRepository; +import com.example.petstoremobile.repositories.ProductRepository; +import com.example.petstoremobile.repositories.SaleRepository; +import com.example.petstoremobile.repositories.StoreRepository; +import com.example.petstoremobile.utils.Resource; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.List; + +import javax.inject.Inject; + +import dagger.hilt.android.lifecycle.HiltViewModel; + +@HiltViewModel +public class SaleDetailViewModel extends ViewModel { + private final SaleRepository saleRepository; + private final StoreRepository storeRepository; + private final CustomerRepository customerRepository; + private final ProductRepository productRepository; + + private long saleId = -1; + private boolean viewOnly = false; + + private final MutableLiveData> storeList = new MutableLiveData<>(new ArrayList<>()); + private final MutableLiveData> customerList = new MutableLiveData<>(new ArrayList<>()); + private final MutableLiveData> productList = new MutableLiveData<>(new ArrayList<>()); + private final MutableLiveData> cartItems = new MutableLiveData<>(new ArrayList<>()); + private final MutableLiveData isLoading = new MutableLiveData<>(false); + + @Inject + public SaleDetailViewModel(SaleRepository saleRepository, StoreRepository storeRepository, + CustomerRepository customerRepository, ProductRepository productRepository) { + this.saleRepository = saleRepository; + this.storeRepository = storeRepository; + this.customerRepository = customerRepository; + this.productRepository = productRepository; + } + + public void setSaleId(long id, boolean viewOnly) { + this.saleId = id; + this.viewOnly = viewOnly; + } + + public long getSaleId() { return saleId; } + public boolean isViewOnly() { return viewOnly; } + + public LiveData> loadSaleDetails() { + return saleRepository.getSaleById(saleId); + } + + public LiveData>> loadStores() { + return storeRepository.getStoreDropdowns(); + } + + public LiveData>> loadCustomers() { + return customerRepository.getCustomerDropdowns(); + } + + public LiveData>> loadProducts() { + return productRepository.getAllProducts(null, null, 0, 200, null); + } + + public LiveData> createSale(SaleDTO sale) { + return saleRepository.createSale(sale); + } + + public void setStoreList(List list) { storeList.setValue(list); } + public LiveData> getStoreList() { return storeList; } + + public void setCustomerList(List list) { customerList.setValue(list); } + public LiveData> getCustomerList() { return customerList; } + + public void setProductList(List list) { productList.setValue(list); } + public LiveData> getProductList() { return productList; } + + public void addToCart(SaleDTO.SaleItemDTO item) { + List currentCart = new ArrayList<>(cartItems.getValue()); + currentCart.add(item); + cartItems.setValue(currentCart); + } + + public LiveData> getCartItems() { return cartItems; } + + public BigDecimal calculateSubtotal() { + BigDecimal total = BigDecimal.ZERO; + List items = cartItems.getValue(); + List products = productList.getValue(); + if (items != null && products != null) { + for (SaleDTO.SaleItemDTO item : items) { + for (ProductDTO p : products) { + if (p.getProdId().equals(item.getProdId()) && p.getProdPrice() != null) { + total = total.add(p.getProdPrice().multiply(BigDecimal.valueOf(item.getQuantity()))); + break; + } + } + } + } + return total; + } + + public LiveData getIsLoading() { + return isLoading; + } + + public void setLoading(boolean loading) { + isLoading.setValue(loading); + } +} diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/SaleListViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/SaleListViewModel.java new file mode 100644 index 00000000..a364a7d8 --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/SaleListViewModel.java @@ -0,0 +1,77 @@ +package com.example.petstoremobile.viewmodels; + +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; +import androidx.lifecycle.ViewModel; + +import com.example.petstoremobile.dtos.PageResponse; +import com.example.petstoremobile.dtos.SaleDTO; +import com.example.petstoremobile.dtos.StoreDTO; +import com.example.petstoremobile.repositories.SaleRepository; +import com.example.petstoremobile.repositories.StoreRepository; +import com.example.petstoremobile.utils.Resource; + +import java.util.ArrayList; +import java.util.List; + +import javax.inject.Inject; + +import dagger.hilt.android.lifecycle.HiltViewModel; + +@HiltViewModel +public class SaleListViewModel extends ViewModel { + private final SaleRepository saleRepository; + private final StoreRepository storeRepository; + + private final MutableLiveData> sales = new MutableLiveData<>(new ArrayList<>()); + private final MutableLiveData> stores = new MutableLiveData<>(new ArrayList<>()); + private final MutableLiveData isLoading = new MutableLiveData<>(false); + + private int currentPage = 0; + private boolean isLastPage = false; + private static final int PAGE_SIZE = 20; + + @Inject + public SaleListViewModel(SaleRepository saleRepository, StoreRepository storeRepository) { + this.saleRepository = saleRepository; + this.storeRepository = storeRepository; + } + + public LiveData> getSales() { return sales; } + public LiveData> getStores() { return stores; } + public LiveData getIsLoading() { return isLoading; } + public boolean isLastPage() { return isLastPage; } + + public void loadSales(boolean reset, String query, String paymentMethod, Long storeId) { + if (isLoading.getValue() != null && isLoading.getValue() && !reset) return; + + if (reset) { + currentPage = 0; + isLastPage = false; + } + + isLoading.setValue(true); + saleRepository.getAllSales(currentPage, PAGE_SIZE, query, paymentMethod, storeId, "saleDate,desc").observeForever(resource -> { + if (resource != null) { + if (resource.status == Resource.Status.SUCCESS && resource.data != null) { + List currentList = reset ? new ArrayList<>() : new ArrayList<>(sales.getValue()); + currentList.addAll(resource.data.getContent()); + sales.setValue(currentList); + isLastPage = resource.data.isLast(); + if (!isLastPage) currentPage++; + isLoading.setValue(false); + } else if (resource.status == Resource.Status.ERROR) { + isLoading.setValue(false); + } + } + }); + } + + public void loadStores() { + storeRepository.getAllStores(0, 100).observeForever(resource -> { + if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) { + stores.setValue(resource.data.getContent()); + } + }); + } +} diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/SaleViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/SaleViewModel.java deleted file mode 100644 index a02d3382..00000000 --- a/android/app/src/main/java/com/example/petstoremobile/viewmodels/SaleViewModel.java +++ /dev/null @@ -1,35 +0,0 @@ -package com.example.petstoremobile.viewmodels; - -import androidx.lifecycle.LiveData; -import androidx.lifecycle.ViewModel; - -import com.example.petstoremobile.dtos.PageResponse; -import com.example.petstoremobile.dtos.SaleDTO; -import com.example.petstoremobile.repositories.SaleRepository; -import com.example.petstoremobile.utils.Resource; - -import javax.inject.Inject; - -import dagger.hilt.android.lifecycle.HiltViewModel; - -@HiltViewModel -public class SaleViewModel extends ViewModel { - private final SaleRepository saleRepository; - - @Inject - public SaleViewModel(SaleRepository saleRepository) { - this.saleRepository = saleRepository; - } - - public LiveData>> getAllSales(int page, int size, String query, String paymentMethod, Long storeId, String sortBy) { - return saleRepository.getAllSales(page, size, query, paymentMethod, storeId, sortBy); - } - - public LiveData> getSaleById(Long id) { - return saleRepository.getSaleById(id); - } - - public LiveData> createSale(SaleDTO sale) { - return saleRepository.createSale(sale); - } -} diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/ServiceDetailViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/ServiceDetailViewModel.java new file mode 100644 index 00000000..fca74229 --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/ServiceDetailViewModel.java @@ -0,0 +1,54 @@ +package com.example.petstoremobile.viewmodels; + +import androidx.lifecycle.LiveData; +import androidx.lifecycle.ViewModel; + +import com.example.petstoremobile.dtos.ServiceDTO; +import com.example.petstoremobile.repositories.ServiceRepository; +import com.example.petstoremobile.utils.Resource; + +import javax.inject.Inject; + +import dagger.hilt.android.lifecycle.HiltViewModel; + +@HiltViewModel +public class ServiceDetailViewModel extends ViewModel { + private final ServiceRepository repository; + private long serviceId = -1; + private boolean isEditing = false; + + @Inject + public ServiceDetailViewModel(ServiceRepository repository) { + this.repository = repository; + } + + public void setServiceId(long id) { + this.serviceId = id; + this.isEditing = id != -1; + } + + public long getServiceId() { + return serviceId; + } + + public boolean isEditing() { + return isEditing; + } + + public LiveData> loadService() { + return repository.getServiceById(serviceId); + } + + public LiveData> saveService(ServiceDTO dto) { + if (isEditing) { + dto.setServiceId(serviceId); + return repository.updateService(serviceId, dto); + } else { + return repository.createService(dto); + } + } + + public LiveData> deleteService() { + return repository.deleteService(serviceId); + } +} diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/ServiceListViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/ServiceListViewModel.java new file mode 100644 index 00000000..d0fa121b --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/ServiceListViewModel.java @@ -0,0 +1,68 @@ +package com.example.petstoremobile.viewmodels; + +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; +import androidx.lifecycle.ViewModel; + +import com.example.petstoremobile.dtos.BulkDeleteRequest; +import com.example.petstoremobile.dtos.PageResponse; +import com.example.petstoremobile.dtos.ServiceDTO; +import com.example.petstoremobile.repositories.ServiceRepository; +import com.example.petstoremobile.utils.Resource; + +import java.util.ArrayList; +import java.util.List; + +import javax.inject.Inject; + +import dagger.hilt.android.lifecycle.HiltViewModel; + +@HiltViewModel +public class ServiceListViewModel extends ViewModel { + private final ServiceRepository repository; + + private final MutableLiveData> services = new MutableLiveData<>(new ArrayList<>()); + private final MutableLiveData isLoading = new MutableLiveData<>(false); + + private int currentPage = 0; + private boolean isLastPage = false; + private static final int PAGE_SIZE = 20; + + @Inject + public ServiceListViewModel(ServiceRepository repository) { + this.repository = repository; + } + + public LiveData> getServices() { return services; } + public LiveData getIsLoading() { return isLoading; } + public boolean isLastPage() { return isLastPage; } + + public void loadServices(boolean reset, String query) { + if (isLoading.getValue() != null && isLoading.getValue() && !reset) return; + + if (reset) { + currentPage = 0; + isLastPage = false; + } + + isLoading.setValue(true); + repository.getAllServices(currentPage, PAGE_SIZE, query, "serviceName").observeForever(resource -> { + if (resource != null) { + if (resource.status == Resource.Status.SUCCESS && resource.data != null) { + List currentList = reset ? new ArrayList<>() : new ArrayList<>(services.getValue()); + currentList.addAll(resource.data.getContent()); + services.setValue(currentList); + isLastPage = resource.data.isLast(); + if (!isLastPage) currentPage++; + isLoading.setValue(false); + } else if (resource.status == Resource.Status.ERROR) { + isLoading.setValue(false); + } + } + }); + } + + public LiveData> bulkDeleteServices(List ids) { + return repository.bulkDeleteServices(new BulkDeleteRequest(ids)); + } +} diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/ServiceViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/ServiceViewModel.java deleted file mode 100644 index ebd5c3b6..00000000 --- a/android/app/src/main/java/com/example/petstoremobile/viewmodels/ServiceViewModel.java +++ /dev/null @@ -1,68 +0,0 @@ -package com.example.petstoremobile.viewmodels; - -import androidx.lifecycle.LiveData; -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.List; - -import javax.inject.Inject; - -import dagger.hilt.android.lifecycle.HiltViewModel; - -@HiltViewModel -public class ServiceViewModel extends ViewModel { - private final ServiceRepository repository; - - @Inject - public ServiceViewModel(ServiceRepository repository) { - this.repository = repository; - } - - /** - * Fetches a paginated list of all services. - */ - public LiveData>> getAllServices(int page, int size, String query, String sort) { - return repository.getAllServices(page, size, query, sort); - } - - /** - * Retrieves a single service by its ID. - */ - public LiveData> getServiceById(Long id) { - return repository.getServiceById(id); - } - - /** - * Creates a new service. - */ - public LiveData> createService(ServiceDTO service) { - return repository.createService(service); - } - - /** - * Updates an existing service by ID. - */ - public LiveData> updateService(Long id, ServiceDTO service) { - return repository.updateService(id, service); - } - - /** - * Deletes a service by ID. - */ - public LiveData> deleteService(Long id) { - return repository.deleteService(id); - } - - /** - * Deletes multiple services. - */ - public LiveData> bulkDeleteServices(List ids) { - return repository.bulkDeleteServices(new BulkDeleteRequest(ids)); - } -} diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/StaffDetailViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/StaffDetailViewModel.java new file mode 100644 index 00000000..91162405 --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/StaffDetailViewModel.java @@ -0,0 +1,49 @@ +package com.example.petstoremobile.viewmodels; + +import androidx.lifecycle.LiveData; +import androidx.lifecycle.ViewModel; + +import com.example.petstoremobile.dtos.EmployeeDTO; +import com.example.petstoremobile.repositories.EmployeeRepository; +import com.example.petstoremobile.utils.Resource; + +import javax.inject.Inject; + +import dagger.hilt.android.lifecycle.HiltViewModel; + +@HiltViewModel +public class StaffDetailViewModel extends ViewModel { + private final EmployeeRepository repository; + private long employeeId = -1; + private boolean isEditing = false; + + @Inject + public StaffDetailViewModel(EmployeeRepository repository) { + this.repository = repository; + } + + public void setEmployeeId(long id, boolean isEditing) { + this.employeeId = id; + this.isEditing = isEditing; + } + + public long getEmployeeId() { + return employeeId; + } + + public boolean isEditing() { + return isEditing; + } + + public LiveData> saveEmployee(EmployeeDTO dto) { + if (isEditing && employeeId > 0) { + return repository.updateEmployee(employeeId, dto); + } else { + return repository.createEmployee(dto); + } + } + + public LiveData> deleteEmployee() { + return repository.deleteEmployee(employeeId); + } +} diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/StaffListViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/StaffListViewModel.java new file mode 100644 index 00000000..1bd317ca --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/StaffListViewModel.java @@ -0,0 +1,71 @@ +package com.example.petstoremobile.viewmodels; + +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; +import androidx.lifecycle.ViewModel; + +import com.example.petstoremobile.dtos.EmployeeDTO; +import com.example.petstoremobile.repositories.EmployeeRepository; +import com.example.petstoremobile.utils.Resource; + +import java.util.ArrayList; +import java.util.List; + +import javax.inject.Inject; + +import dagger.hilt.android.lifecycle.HiltViewModel; + +@HiltViewModel +public class StaffListViewModel extends ViewModel { + private final EmployeeRepository repository; + + private final MutableLiveData> employees = new MutableLiveData<>(new ArrayList<>()); + private final MutableLiveData> filteredEmployees = new MutableLiveData<>(new ArrayList<>()); + private final MutableLiveData isLoading = new MutableLiveData<>(false); + private String lastQuery = ""; + + @Inject + public StaffListViewModel(EmployeeRepository repository) { + this.repository = repository; + } + + public LiveData> getFilteredEmployees() { return filteredEmployees; } + public LiveData getIsLoading() { return isLoading; } + + public void loadStaff() { + isLoading.setValue(true); + repository.getAllEmployees(0, 100).observeForever(resource -> { + if (resource != null) { + if (resource.status == Resource.Status.SUCCESS && resource.data != null) { + employees.setValue(resource.data.getContent()); + filter(lastQuery); + isLoading.setValue(false); + } else if (resource.status == Resource.Status.ERROR) { + isLoading.setValue(false); + } + } + }); + } + + public void filter(String query) { + this.lastQuery = query; + List all = employees.getValue(); + if (all == null) return; + + if (query.isEmpty()) { + filteredEmployees.setValue(new ArrayList<>(all)); + } else { + List filtered = new ArrayList<>(); + String lower = query.toLowerCase(); + for (EmployeeDTO e : all) { + if ((e.getFullName() != null && e.getFullName().toLowerCase().contains(lower)) + || (e.getUsername() != null && e.getUsername().toLowerCase().contains(lower)) + || (e.getEmail() != null && e.getEmail().toLowerCase().contains(lower)) + || (e.getPhone() != null && e.getPhone().toLowerCase().contains(lower))) { + filtered.add(e); + } + } + filteredEmployees.setValue(filtered); + } + } +} diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/StoreViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/StoreViewModel.java deleted file mode 100644 index 83f4c3b3..00000000 --- a/android/app/src/main/java/com/example/petstoremobile/viewmodels/StoreViewModel.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.example.petstoremobile.viewmodels; - -import androidx.lifecycle.LiveData; -import androidx.lifecycle.ViewModel; - -import com.example.petstoremobile.dtos.PageResponse; -import com.example.petstoremobile.dtos.StoreDTO; -import com.example.petstoremobile.repositories.StoreRepository; -import com.example.petstoremobile.utils.Resource; - -import javax.inject.Inject; - -import dagger.hilt.android.lifecycle.HiltViewModel; - -@HiltViewModel -public class StoreViewModel extends ViewModel { - private final StoreRepository repository; - - @Inject - public StoreViewModel(StoreRepository repository) { - this.repository = repository; - } - - /** - * Fetches a paginated list of all stores. - */ - public LiveData>> getAllStores(int page, int size) { - return repository.getAllStores(page, size); - } -} diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/SupplierDetailViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/SupplierDetailViewModel.java new file mode 100644 index 00000000..591beb52 --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/SupplierDetailViewModel.java @@ -0,0 +1,54 @@ +package com.example.petstoremobile.viewmodels; + +import androidx.lifecycle.LiveData; +import androidx.lifecycle.ViewModel; + +import com.example.petstoremobile.dtos.SupplierDTO; +import com.example.petstoremobile.repositories.SupplierRepository; +import com.example.petstoremobile.utils.Resource; + +import javax.inject.Inject; + +import dagger.hilt.android.lifecycle.HiltViewModel; + +@HiltViewModel +public class SupplierDetailViewModel extends ViewModel { + private final SupplierRepository repository; + private long supId = -1; + private boolean isEditing = false; + + @Inject + public SupplierDetailViewModel(SupplierRepository repository) { + this.repository = repository; + } + + public void setSupId(long id) { + this.supId = id; + this.isEditing = id != -1; + } + + public long getSupId() { + return supId; + } + + public boolean isEditing() { + return isEditing; + } + + public LiveData> loadSupplier() { + return repository.getSupplierById(supId); + } + + public LiveData> saveSupplier(SupplierDTO dto) { + if (isEditing) { + dto.setSupId(supId); + return repository.updateSupplier(supId, dto); + } else { + return repository.createSupplier(dto); + } + } + + public LiveData> deleteSupplier() { + return repository.deleteSupplier(supId); + } +} diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/SupplierListViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/SupplierListViewModel.java new file mode 100644 index 00000000..072ad3bd --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/SupplierListViewModel.java @@ -0,0 +1,51 @@ +package com.example.petstoremobile.viewmodels; + +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; +import androidx.lifecycle.ViewModel; + +import com.example.petstoremobile.dtos.BulkDeleteRequest; +import com.example.petstoremobile.dtos.SupplierDTO; +import com.example.petstoremobile.repositories.SupplierRepository; +import com.example.petstoremobile.utils.Resource; + +import java.util.ArrayList; +import java.util.List; + +import javax.inject.Inject; + +import dagger.hilt.android.lifecycle.HiltViewModel; + +@HiltViewModel +public class SupplierListViewModel extends ViewModel { + private final SupplierRepository repository; + + private final MutableLiveData> suppliers = new MutableLiveData<>(new ArrayList<>()); + private final MutableLiveData isLoading = new MutableLiveData<>(false); + + @Inject + public SupplierListViewModel(SupplierRepository repository) { + this.repository = repository; + } + + public LiveData> getSuppliers() { return suppliers; } + public LiveData getIsLoading() { return isLoading; } + + public void loadSuppliers(String query) { + isLoading.setValue(true); + repository.getAllSuppliers(0, 100, query, "supCompany").observeForever(resource -> { + if (resource != null) { + if (resource.status == Resource.Status.SUCCESS && resource.data != null) { + suppliers.setValue(resource.data.getContent()); + isLoading.setValue(false); + } else if (resource.status == Resource.Status.ERROR) { + isLoading.setValue(false); + } + } + }); + } + + public LiveData> bulkDeleteSuppliers(List ids) { + return repository.bulkDeleteSuppliers(new BulkDeleteRequest(ids)); + } +} diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/SupplierViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/SupplierViewModel.java deleted file mode 100644 index 1486a562..00000000 --- a/android/app/src/main/java/com/example/petstoremobile/viewmodels/SupplierViewModel.java +++ /dev/null @@ -1,68 +0,0 @@ -package com.example.petstoremobile.viewmodels; - -import androidx.lifecycle.LiveData; -import androidx.lifecycle.ViewModel; - -import com.example.petstoremobile.dtos.BulkDeleteRequest; -import com.example.petstoremobile.dtos.PageResponse; -import com.example.petstoremobile.dtos.SupplierDTO; -import com.example.petstoremobile.repositories.SupplierRepository; -import com.example.petstoremobile.utils.Resource; - -import java.util.List; - -import javax.inject.Inject; - -import dagger.hilt.android.lifecycle.HiltViewModel; - -@HiltViewModel -public class SupplierViewModel extends ViewModel { - private final SupplierRepository repository; - - @Inject - public SupplierViewModel(SupplierRepository repository) { - this.repository = repository; - } - - /** - * Fetches a paginated list of all suppliers. - */ - public LiveData>> getAllSuppliers(int page, int size, String query, String sort) { - return repository.getAllSuppliers(page, size, query, sort); - } - - /** - * Retrieves a single supplier by its ID. - */ - public LiveData> getSupplierById(Long id) { - return repository.getSupplierById(id); - } - - /** - * Creates a new supplier record. - */ - public LiveData> createSupplier(SupplierDTO supplier) { - return repository.createSupplier(supplier); - } - - /** - * Updates an existing supplier record by ID. - */ - public LiveData> updateSupplier(Long id, SupplierDTO supplier) { - return repository.updateSupplier(id, supplier); - } - - /** - * Deletes a supplier record by ID. - */ - public LiveData> deleteSupplier(Long id) { - return repository.deleteSupplier(id); - } - - /** - * Deletes multiple supplier records. - */ - public LiveData> bulkDeleteSuppliers(List ids) { - return repository.bulkDeleteSuppliers(new BulkDeleteRequest(ids)); - } -} diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/UserViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/UserViewModel.java deleted file mode 100644 index d839f6c4..00000000 --- a/android/app/src/main/java/com/example/petstoremobile/viewmodels/UserViewModel.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.example.petstoremobile.viewmodels; - -import androidx.lifecycle.LiveData; -import androidx.lifecycle.ViewModel; - -import com.example.petstoremobile.dtos.PageResponse; -import com.example.petstoremobile.dtos.UserDTO; -import com.example.petstoremobile.repositories.UserRepository; -import com.example.petstoremobile.utils.Resource; - -import javax.inject.Inject; - -import dagger.hilt.android.lifecycle.HiltViewModel; - -@HiltViewModel -public class UserViewModel extends ViewModel { - private final UserRepository userRepository; - - @Inject - public UserViewModel(UserRepository userRepository) { - this.userRepository = userRepository; - } - - public LiveData>> getUsers(String role, int page, int size) { - return userRepository.getUsers(role, page, size); - } -} diff --git a/android/app/src/main/res/layout/dialog_full_screen_image.xml b/android/app/src/main/res/layout/dialog_full_screen_image.xml new file mode 100644 index 00000000..0767baa2 --- /dev/null +++ b/android/app/src/main/res/layout/dialog_full_screen_image.xml @@ -0,0 +1,40 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/android/app/src/main/res/layout/fragment_adoption_detail.xml b/android/app/src/main/res/layout/fragment_adoption_detail.xml index 45ac673f..0608f96f 100644 --- a/android/app/src/main/res/layout/fragment_adoption_detail.xml +++ b/android/app/src/main/res/layout/fragment_adoption_detail.xml @@ -1,214 +1,228 @@ - + android:layout_height="match_parent"> - - - - + @@ -65,6 +73,12 @@ + + diff --git a/desktop/src/main/resources/org/example/petshopdesktop/modelviews/analytics-view.fxml b/desktop/src/main/resources/org/example/petshopdesktop/modelviews/analytics-view.fxml index dec7a883..dba7511c 100644 --- a/desktop/src/main/resources/org/example/petshopdesktop/modelviews/analytics-view.fxml +++ b/desktop/src/main/resources/org/example/petshopdesktop/modelviews/analytics-view.fxml @@ -10,6 +10,7 @@ + @@ -25,6 +26,7 @@ + + + - + diff --git a/desktop/src/main/resources/org/example/petshopdesktop/modelviews/appointment-view.fxml b/desktop/src/main/resources/org/example/petshopdesktop/modelviews/appointment-view.fxml index 8e920be0..b4840087 100644 --- a/desktop/src/main/resources/org/example/petshopdesktop/modelviews/appointment-view.fxml +++ b/desktop/src/main/resources/org/example/petshopdesktop/modelviews/appointment-view.fxml @@ -51,6 +51,14 @@ + @@ -65,6 +73,12 @@ + + diff --git a/desktop/src/main/resources/org/example/petshopdesktop/modelviews/inventory-view.fxml b/desktop/src/main/resources/org/example/petshopdesktop/modelviews/inventory-view.fxml index 18e9ad5a..c3e1b9cf 100644 --- a/desktop/src/main/resources/org/example/petshopdesktop/modelviews/inventory-view.fxml +++ b/desktop/src/main/resources/org/example/petshopdesktop/modelviews/inventory-view.fxml @@ -51,6 +51,14 @@ + @@ -65,6 +73,12 @@ + + diff --git a/desktop/src/main/resources/org/example/petshopdesktop/modelviews/pet-view.fxml b/desktop/src/main/resources/org/example/petshopdesktop/modelviews/pet-view.fxml index d599e010..042a36f9 100644 --- a/desktop/src/main/resources/org/example/petshopdesktop/modelviews/pet-view.fxml +++ b/desktop/src/main/resources/org/example/petshopdesktop/modelviews/pet-view.fxml @@ -18,7 +18,7 @@ - + @@ -69,19 +77,25 @@ - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + diff --git a/desktop/src/main/resources/org/example/petshopdesktop/modelviews/product-supplier-view.fxml b/desktop/src/main/resources/org/example/petshopdesktop/modelviews/product-supplier-view.fxml index 19817faa..931c3f30 100644 --- a/desktop/src/main/resources/org/example/petshopdesktop/modelviews/product-supplier-view.fxml +++ b/desktop/src/main/resources/org/example/petshopdesktop/modelviews/product-supplier-view.fxml @@ -51,6 +51,14 @@ + @@ -65,6 +73,12 @@ + + diff --git a/desktop/src/main/resources/org/example/petshopdesktop/modelviews/product-view.fxml b/desktop/src/main/resources/org/example/petshopdesktop/modelviews/product-view.fxml index eeff74b4..96105c07 100644 --- a/desktop/src/main/resources/org/example/petshopdesktop/modelviews/product-view.fxml +++ b/desktop/src/main/resources/org/example/petshopdesktop/modelviews/product-view.fxml @@ -52,6 +52,14 @@ + @@ -67,6 +75,12 @@ + + diff --git a/desktop/src/main/resources/org/example/petshopdesktop/modelviews/purchase-order-view.fxml b/desktop/src/main/resources/org/example/petshopdesktop/modelviews/purchase-order-view.fxml index 74a00e6d..2d5d50a6 100644 --- a/desktop/src/main/resources/org/example/petshopdesktop/modelviews/purchase-order-view.fxml +++ b/desktop/src/main/resources/org/example/petshopdesktop/modelviews/purchase-order-view.fxml @@ -55,6 +55,12 @@ + + diff --git a/desktop/src/main/resources/org/example/petshopdesktop/modelviews/sale-view.fxml b/desktop/src/main/resources/org/example/petshopdesktop/modelviews/sale-view.fxml index 3a3c9608..947c1d42 100644 --- a/desktop/src/main/resources/org/example/petshopdesktop/modelviews/sale-view.fxml +++ b/desktop/src/main/resources/org/example/petshopdesktop/modelviews/sale-view.fxml @@ -9,36 +9,47 @@ + - - - - - - + + + + + + + + - - + @@ -98,16 +109,16 @@ - - + + - - - - + - + - - - - - - - - - - + + + + + + + + + + - + + + diff --git a/desktop/src/main/resources/org/example/petshopdesktop/modelviews/service-view.fxml b/desktop/src/main/resources/org/example/petshopdesktop/modelviews/service-view.fxml index 5353b0e6..bf3fc6a0 100644 --- a/desktop/src/main/resources/org/example/petshopdesktop/modelviews/service-view.fxml +++ b/desktop/src/main/resources/org/example/petshopdesktop/modelviews/service-view.fxml @@ -16,7 +16,7 @@ - + @@ -65,14 +73,20 @@ - - - - - - - - - + + + + + + + + + + + diff --git a/desktop/src/main/resources/org/example/petshopdesktop/modelviews/staff-accounts-view.fxml b/desktop/src/main/resources/org/example/petshopdesktop/modelviews/staff-accounts-view.fxml index c9de3cca..7aab7ad4 100644 --- a/desktop/src/main/resources/org/example/petshopdesktop/modelviews/staff-accounts-view.fxml +++ b/desktop/src/main/resources/org/example/petshopdesktop/modelviews/staff-accounts-view.fxml @@ -77,6 +77,12 @@ + +