diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index 6cd974d9..caaf8e17 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -58,41 +58,45 @@ android { } dependencies { + // Core AndroidX & UI implementation(libs.appcompat) implementation(libs.material) implementation(libs.activity) implementation(libs.constraintlayout) + implementation(libs.swiperefreshlayout) + implementation(libs.viewpager2) + // Hilt Dependency Injection implementation(libs.hilt.android) annotationProcessor(libs.hilt.compiler) + // Navigation Component implementation(libs.navigation.fragment) implementation(libs.navigation.ui) - implementation("com.squareup.retrofit2:retrofit:2.9.0") - implementation("com.squareup.retrofit2:converter-gson:2.9.0") - implementation("com.squareup.okhttp3:logging-interceptor:4.12.0") - implementation("com.squareup.okhttp3:okhttp:4.12.0") + // Networking + implementation(libs.retrofit) + implementation(libs.retrofit.gson) + implementation(libs.okhttp) + implementation(libs.okhttp.logging) - implementation("com.google.android.material:material:1.11.0") - implementation("androidx.viewpager2:viewpager2:1.1.0") + // CameraX + implementation(libs.camera.core) + implementation(libs.camera.camera2) + implementation(libs.camera.lifecycle) + implementation(libs.camera.view) - implementation("androidx.camera:camera-core:1.4.0") - implementation("androidx.camera:camera-camera2:1.4.0") - implementation("androidx.camera:camera-lifecycle:1.4.0") - implementation("androidx.camera:camera-view:1.4.0") - implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0") - implementation(libs.swiperefreshlayout) + // Image Loading + implementation(libs.glide) + annotationProcessor(libs.glide.compiler) + // Other Third-party Libraries implementation("com.github.NaikSoftware:StompProtocolAndroid:1.6.6") implementation("io.reactivex.rxjava2:rxjava:2.2.21") implementation("io.reactivex.rxjava2:rxandroid:2.1.1") - - implementation("com.github.bumptech.glide:glide:4.16.0") - annotationProcessor("com.github.bumptech.glide:compiler:4.16.0") - implementation("com.github.prolificinteractive:material-calendarview:2.0.1") + // Testing testImplementation(libs.junit) androidTestImplementation(libs.ext.junit) androidTestImplementation(libs.espresso.core) 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 0354a1fd..de6ccc04 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 @@ -3,9 +3,14 @@ package com.example.petstoremobile.adapters; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import android.widget.ImageView; import android.widget.TextView; import androidx.annotation.NonNull; import androidx.recyclerview.widget.RecyclerView; +import com.bumptech.glide.Glide; +import com.bumptech.glide.load.engine.DiskCacheStrategy; +import com.bumptech.glide.load.model.GlideUrl; +import com.bumptech.glide.load.model.LazyHeaders; import com.example.petstoremobile.R; import com.example.petstoremobile.models.Message; import java.util.List; @@ -17,6 +22,7 @@ public class MessageAdapter extends RecyclerView.Adapter messages; private Long currentUserId; + private String token; public MessageAdapter(List messages, Long currentUserId) { this.messages = messages; @@ -28,6 +34,10 @@ public class MessageAdapter extends RecyclerView.Adapter sendMessage(@Path("id") Long conversationId, @Body SendMessageRequest request); + + @Multipart + @POST("api/v1/chat/conversations/{id}/messages/attachment") + Call sendMessageWithAttachment( + @Path("id") Long conversationId, + @Part("content") RequestBody content, + @Part MultipartBody.Part file + ); } \ No newline at end of file diff --git a/android/app/src/main/java/com/example/petstoremobile/api/ProductSupplierApi.java b/android/app/src/main/java/com/example/petstoremobile/api/ProductSupplierApi.java index 32810b12..7ec9eb07 100644 --- a/android/app/src/main/java/com/example/petstoremobile/api/ProductSupplierApi.java +++ b/android/app/src/main/java/com/example/petstoremobile/api/ProductSupplierApi.java @@ -12,6 +12,11 @@ public interface ProductSupplierApi { @Query("page") int page, @Query("size") int size); + @GET("api/v1/product-suppliers/{productId}/{supplierId}") + Call getProductSupplierById( + @Path("productId") Long productId, + @Path("supplierId") Long supplierId); + @POST("api/v1/product-suppliers") Call createProductSupplier(@Body ProductSupplierDTO dto); 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 81383acc..5ed6ac6e 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 @@ -14,33 +14,35 @@ import androidx.activity.result.contract.ActivityResultContracts; import androidx.annotation.NonNull; 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; import com.example.petstoremobile.adapters.MessageAdapter; import com.example.petstoremobile.api.auth.TokenManager; -import com.example.petstoremobile.api.ChatApi; -import com.example.petstoremobile.api.CustomerApi; -import com.example.petstoremobile.api.MessageApi; import com.example.petstoremobile.databinding.FragmentChatBinding; import com.example.petstoremobile.dtos.ConversationDTO; -import com.example.petstoremobile.dtos.CustomerDTO; 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.RetrofitUtils; +import com.example.petstoremobile.utils.FileUtils; +import com.example.petstoremobile.utils.Resource; +import com.example.petstoremobile.viewmodels.ChatViewModel; import com.example.petstoremobile.websocket.StompChatManager; +import java.io.File; import java.util.*; -import java.util.stream.Collectors; import javax.inject.Inject; import javax.inject.Named; import dagger.hilt.android.AndroidEntryPoint; +import okhttp3.MediaType; +import okhttp3.MultipartBody; +import okhttp3.RequestBody; @AndroidEntryPoint public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickListener, StompChatManager.MessageListener, @@ -49,6 +51,7 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis private static final String TAG = "ChatFragment"; private FragmentChatBinding binding; + private ChatViewModel viewModel; // Adapters private ChatAdapter chatAdapter; @@ -60,10 +63,6 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis private final Map customerNames = new HashMap<>(); private Uri pendingAttachmentUri; - // APIs - @Inject ChatApi chatApi; - @Inject CustomerApi customerApi; - @Inject MessageApi messageApi; @Inject TokenManager tokenManager; @Inject @Named("baseUrl") String baseUrl; @@ -79,6 +78,7 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); + viewModel = new ViewModelProvider(this).get(ChatViewModel.class); attachmentLauncher = registerForActivityResult( new ActivityResultContracts.StartActivityForResult(), result -> { @@ -183,54 +183,53 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis } /** - * Fetches a list of customers from the API to display customer names for the chat list. + * Fetches a list of customers from the ViewModel to display customer names for the chat list. */ private void loadCustomers() { - customerApi.getAllCustomers(0, 100).enqueue(RetrofitUtils.createSilentCallback(TAG, result -> { - for (CustomerDTO c : result.getContent()) { - customerNames.put(c.getCustomerId(), c.getFullName()); + 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(); } - loadConversations(); - })); + }); } /** - * Retrieves all conversations for the current user and populates the chat drawer. + * Retrieves all conversations for the current user through the ViewModel and populates the chat drawer. */ private void loadConversations() { - chatApi.getAllConversations().enqueue(RetrofitUtils.createSilentCallback(TAG, result -> { - chatList.clear(); - List loaded = result.stream() - .map(dto -> { - String name = customerNames.getOrDefault( - dto.getCustomerId(), "Customer #" + dto.getCustomerId()); - return new Chat(String.valueOf(dto.getId()), - name, dto.getLastMessage(), - dto.getCustomerId(), dto.getStaffId()); - }) - .collect(Collectors.toList()); - chatList.addAll(loaded); - 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; + 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); } - if (stompChatManager != null) { - stompChatManager.subscribeToConversation(activeConversationId); - } - loadMessageHistory(activeConversationId); - } else { - messageList.clear(); - messageAdapter.notifyDataSetChanged(); - setConversationActive(false); } - })); + }); } /** @@ -251,21 +250,23 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis } /** - * Fetches the full message history for a specific conversation from the API. + * Fetches the full message history for a specific conversation from the ViewModel. */ private void loadMessageHistory(Long conversationId) { - messageApi.getMessages(conversationId).enqueue(RetrofitUtils.createSilentCallback(TAG, result -> { - messageList.clear(); - for (MessageDTO dto : result) { - messageList.add(dtoToModel(dto)); + 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(); } - messageAdapter.notifyDataSetChanged(); - scrollToBottom(); - })); + }); } /** - * Sends a plain text message to the currently active conversation. + * Sends a plain text message to the currently active conversation through the ViewModel. */ private void sendMessage() { //check if a chat is selected @@ -278,14 +279,15 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis //clear text field after sending binding.etMessage.setText(""); - //calls api to send the message - messageApi.sendMessage(activeConversationId, new SendMessageRequest(text)) - .enqueue(RetrofitUtils.createSilentCallback(TAG, result -> { - messageList.add(dtoToModel(result)); - messageAdapter.notifyItemInserted(messageList.size() - 1); - scrollToBottom(); - loadConversations(); - })); + //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(); + } + }); } /** @@ -351,13 +353,34 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis } /** - * Handles sending a message that includes a file attachment. + * Handles sending a message that includes a file attachment via the ViewModel. */ private void sendWithAttachment(Uri uri) { if (activeConversationId == null) return; + String text = binding.etMessage.getText().toString().trim(); + binding.etMessage.setText(""); + removeAttachment(); - //TODO: send the message with attachment when backend is done - Log.d(TAG, "Send with attachment happening"); + try { + File file = FileUtils.getFileFromUri(requireContext(), uri); + if (file == null) return; + + String mimeType = requireContext().getContentResolver().getType(uri); + RequestBody requestFile = RequestBody.create(file, MediaType.parse(mimeType != null ? mimeType : "application/octet-stream")); + MultipartBody.Part filePart = MultipartBody.Part.createFormData("file", file.getName(), requestFile); + RequestBody contentPart = RequestBody.create(text, MediaType.parse("text/plain")); + + viewModel.sendMessageWithAttachment(activeConversationId, contentPart, filePart).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(); + } + }); + } catch (Exception e) { + Log.e(TAG, "Error sending message with attachment", e); + } } /** @@ -376,8 +399,10 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis //else add the message to the active chat if it's not from the current user messageList.add(dtoToModel(dto)); - messageAdapter.notifyItemInserted(messageList.size() - 1); - scrollToBottom(); + requireActivity().runOnUiThread(() -> { + messageAdapter.notifyItemInserted(messageList.size() - 1); + scrollToBottom(); + }); } /** @@ -385,41 +410,43 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis */ @Override public void onConversationUpdated(ConversationDTO dto) { - boolean updated = false; - String name = customerNames.getOrDefault( - dto.getCustomerId(), "Customer #" + dto.getCustomerId()); + 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( + 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.notifyItemChanged(i); - updated = true; - break; + chatAdapter.notifyItemInserted(0); } - } - if (!updated) { - chatList.add(0, new Chat( - String.valueOf(dto.getId()), - name, - dto.getLastMessage(), - dto.getCustomerId(), - dto.getStaffId() - )); - chatAdapter.notifyItemInserted(0); - } - - if (activeConversationId != null && activeConversationId.equals(dto.getId())) { - setConversationActive(true); - binding.tvChatTitle.setText(name); - } + if (activeConversationId != null && activeConversationId.equals(dto.getId())) { + setConversationActive(true); + binding.tvChatTitle.setText(name); + } + }); } /** @@ -430,10 +457,12 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis if (!isAdded()) { return; } - loadConversations(); - if (activeConversationId != null) { - loadMessageHistory(activeConversationId); - } + requireActivity().runOnUiThread(() -> { + loadConversations(); + if (activeConversationId != null) { + loadMessageHistory(activeConversationId); + } + }); } /** @@ -444,7 +473,7 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis if (!isAdded()) { return; } - loadConversations(); + requireActivity().runOnUiThread(this::loadConversations); } /** @@ -455,10 +484,12 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis if (!isAdded()) { return; } - loadConversations(); - if (activeConversationId != null) { - loadMessageHistory(activeConversationId); - } + requireActivity().runOnUiThread(() -> { + loadConversations(); + if (activeConversationId != null) { + loadMessageHistory(activeConversationId); + } + }); } /** @@ -495,21 +526,23 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis if (conversationId == null) { return; } - 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; + 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; + } } - } + }); } /** 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 9d8a472f..c80a82ae 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 @@ -4,8 +4,10 @@ import android.net.Uri; import android.os.Bundle; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.appcompat.app.AlertDialog; import androidx.fragment.app.Fragment; +import androidx.lifecycle.ViewModelProvider; import android.util.Log; import android.view.LayoutInflater; @@ -25,8 +27,9 @@ import com.example.petstoremobile.utils.FileUtils; import com.example.petstoremobile.utils.GlideUtils; import com.example.petstoremobile.utils.ImagePickerHelper; import com.example.petstoremobile.utils.InputValidator; -import com.example.petstoremobile.utils.RetrofitUtils; +import com.example.petstoremobile.utils.Resource; import com.example.petstoremobile.utils.UIUtils; +import com.example.petstoremobile.viewmodels.AuthViewModel; import java.io.File; import java.util.HashMap; @@ -48,9 +51,9 @@ public class ProfileFragment extends Fragment { private FragmentProfileBinding binding; private UserDTO currentUser; + private AuthViewModel viewModel; private boolean hasImage = false; - @Inject AuthApi authApi; @Inject TokenManager tokenManager; @Inject @Named("baseUrl") String baseUrl; @@ -62,6 +65,7 @@ public class ProfileFragment extends Fragment { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); + viewModel = new ViewModelProvider(this).get(AuthViewModel.class); imagePickerHelper = new ImagePickerHelper(this, "profile_photo.jpg", new ImagePickerHelper.ImagePickerListener() { @Override @@ -178,31 +182,36 @@ public class ProfileFragment extends Fragment { * Fetches current user profile data from the API and then updates the UI. */ private void loadProfileData() { - authApi.getMe().enqueue(RetrofitUtils.createCallback(requireContext(), "PROFILE", null, result -> { - currentUser = result; + viewModel.getMe().observe(getViewLifecycleOwner(), resource -> { + if (resource == null) return; + if (resource.status == Resource.Status.SUCCESS && resource.data != null) { + currentUser = resource.data; - //set the user data to the view - binding.tvProfileName.setText(currentUser.getFullName()); - binding.tvProfileEmail.setText(currentUser.getEmail()); - binding.tvProfilePhone.setText(currentUser.getPhone()); - binding.tvProfileRole.setText(currentUser.getRole()); + //set the user data to the view + binding.tvProfileName.setText(currentUser.getFullName()); + binding.tvProfileEmail.setText(currentUser.getEmail()); + binding.tvProfilePhone.setText(currentUser.getPhone()); + binding.tvProfileRole.setText(currentUser.getRole()); - // get the avatar endpoint to load profile image and the token for authorization - String avatarUrl = baseUrl + AuthApi.AVATAR_FILE_PATH; - String token = tokenManager.getToken(); + // get the avatar endpoint to load profile image and the token for authorization + String avatarUrl = baseUrl + AuthApi.AVATAR_FILE_PATH; + String token = tokenManager.getToken(); - GlideUtils.loadImageWithToken(requireContext(), binding.imgProfile, avatarUrl, token, R.drawable.placeholder, new GlideUtils.ImageLoadListener() { - @Override - public void onResourceReady() { - hasImage = true; - } + GlideUtils.loadImageWithToken(requireContext(), binding.imgProfile, avatarUrl, token, R.drawable.placeholder, new GlideUtils.ImageLoadListener() { + @Override + public void onResourceReady() { + hasImage = true; + } - @Override - public void onLoadFailed() { - hasImage = false; - } - }); - })); + @Override + public void onLoadFailed() { + hasImage = false; + } + }); + } else if (resource.status == Resource.Status.ERROR) { + Toast.makeText(getContext(), "Failed to load profile: " + resource.message, Toast.LENGTH_SHORT).show(); + } + }); } /** @@ -218,11 +227,17 @@ public class ProfileFragment extends Fragment { MultipartBody.Part body = MultipartBody.Part.createFormData("avatar", file.getName(), requestFile); //Call the backend to upload the avatar - authApi.uploadAvatar(body).enqueue(RetrofitUtils.createCallback(requireContext(), "UPLOAD_AVATAR", "Avatar updated successfully", result -> { - currentUser = result; - // Reload image after successful upload - loadProfileData(); - })); + viewModel.uploadAvatar(body).observe(getViewLifecycleOwner(), resource -> { + if (resource == null) return; + if (resource.status == Resource.Status.SUCCESS) { + currentUser = resource.data; + Toast.makeText(getContext(), "Avatar updated successfully", Toast.LENGTH_SHORT).show(); + // Reload image after successful upload + loadProfileData(); + } else if (resource.status == Resource.Status.ERROR) { + Toast.makeText(getContext(), "Upload failed: " + resource.message, Toast.LENGTH_SHORT).show(); + } + }); } catch (Exception e) { Log.e("UPLOAD_AVATAR", "Error: " + e.getMessage()); } @@ -232,10 +247,16 @@ public class ProfileFragment extends Fragment { * Sends a request to the API to delete the current user's avatar image. */ private void deleteAvatar() { - authApi.deleteAvatar().enqueue(RetrofitUtils.createCallback(requireContext(), "DELETE_AVATAR", "Avatar removed successfully", result -> { - hasImage = false; - binding.imgProfile.setImageResource(R.drawable.placeholder); - })); + viewModel.deleteAvatar().observe(getViewLifecycleOwner(), resource -> { + if (resource == null) return; + if (resource.status == Resource.Status.SUCCESS) { + hasImage = false; + binding.imgProfile.setImageResource(R.drawable.placeholder); + Toast.makeText(getContext(), "Avatar removed successfully", Toast.LENGTH_SHORT).show(); + } else if (resource.status == Resource.Status.ERROR) { + Toast.makeText(getContext(), "Removal failed: " + resource.message, Toast.LENGTH_SHORT).show(); + } + }); } /** @@ -245,11 +266,17 @@ public class ProfileFragment extends Fragment { Map updates = new HashMap<>(); updates.put(fieldName, value); - authApi.updateMe(updates).enqueue(RetrofitUtils.createCallback(requireContext(), "UPDATE_PROFILE", "Profile updated successfully", result -> { - currentUser = result; - // Update the view with the new data from backend - binding.tvProfileEmail.setText(currentUser.getEmail()); - binding.tvProfilePhone.setText(currentUser.getPhone()); - })); + viewModel.updateMe(updates).observe(getViewLifecycleOwner(), resource -> { + if (resource == null) return; + if (resource.status == Resource.Status.SUCCESS && resource.data != null) { + currentUser = resource.data; + Toast.makeText(getContext(), "Profile updated successfully", Toast.LENGTH_SHORT).show(); + // Update the view with the new data from backend + binding.tvProfileEmail.setText(currentUser.getEmail()); + binding.tvProfilePhone.setText(currentUser.getPhone()); + } else if (resource.status == Resource.Status.ERROR) { + Toast.makeText(getContext(), "Update failed: " + resource.message, 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 04713d86..fafd0d93 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 @@ -239,10 +239,6 @@ public class AdoptionFragment extends Fragment implements AdoptionAdapter.OnAdop if (position != -1) { AdoptionDTO a = filteredList.get(position); args.putLong("adoptionId", a.getAdoptionId()); - args.putLong("petId", a.getPetId() != null ? a.getPetId() : -1); - args.putLong("customerId", a.getCustomerId() != null ? a.getCustomerId() : -1); - args.putString("adoptionDate", a.getAdoptionDate()); - args.putString("adoptionStatus", a.getAdoptionStatus()); } NavHostFragment.findNavController(this).navigate(R.id.nav_adoption_detail, args); 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 fb1455ae..c93b9e89 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 @@ -22,13 +22,8 @@ import com.example.petstoremobile.R; import com.example.petstoremobile.adapters.AppointmentAdapter; import com.example.petstoremobile.databinding.FragmentAppointmentBinding; import com.example.petstoremobile.dtos.AppointmentDTO; -import com.example.petstoremobile.dtos.ServiceDTO; -import com.example.petstoremobile.dtos.PetDTO; import com.example.petstoremobile.fragments.ListFragment; import com.example.petstoremobile.viewmodels.AppointmentViewModel; -import com.example.petstoremobile.viewmodels.PetViewModel; -import com.example.petstoremobile.viewmodels.ServiceViewModel; -import com.example.petstoremobile.utils.Resource; import com.example.petstoremobile.utils.EventDecorator; import com.prolificinteractive.materialcalendarview.CalendarDay; import com.prolificinteractive.materialcalendarview.CalendarMode; @@ -50,13 +45,9 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter. private FragmentAppointmentBinding binding; private List appointmentList = new ArrayList<>(); private List filteredList = new ArrayList<>(); - private List petList = new ArrayList<>(); - private List serviceList = new ArrayList<>(); private AppointmentAdapter adapter; private AppointmentViewModel appointmentViewModel; - private PetViewModel petViewModel; - private ServiceViewModel serviceViewModel; private CalendarDay selectedCalendarDay; private boolean isMonthMode = false; @@ -69,8 +60,6 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter. public void onCreate(@Nullable 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); } /** @@ -86,8 +75,6 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter. setupSwipeRefresh(); setupCalendar(); loadAppointmentData(); - loadPets(); - loadServices(); binding.fabAddAppointment.setOnClickListener(v -> openAppointmentDetails(-1)); @@ -226,37 +213,13 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter. */ private void openAppointmentDetails(int position) { Bundle args = new Bundle(); - if (position != -1) { AppointmentDTO a = filteredList.get(position); args.putLong("appointmentId", a.getAppointmentId()); - args.putString("appointmentDate", a.getAppointmentDate()); - args.putString("appointmentTime", a.getAppointmentTime()); - args.putString("appointmentStatus", a.getAppointmentStatus()); - // IDs for pre-selecting spinners - if (a.getPetID() != null) args.putLong("petId", a.getPetID()); - if (a.getServiceId() != null) args.putLong("serviceId", a.getServiceId()); - if (a.getCustomerId() != null) args.putLong("customerId", a.getCustomerId()); - if (a.getStoreId() != null) args.putLong("storeId", a.getStoreId()); } - NavHostFragment.findNavController(this).navigate(R.id.nav_appointment_detail, args); } - /** - * Reloads data when an appointment is saved. - */ - public void onAppointmentSaved(int position, AppointmentDTO appointment) { - loadAppointmentData(); - } - - /** - * Reloads data when an appointment is deleted. - */ - public void onAppointmentDeleted(int position) { - loadAppointmentData(); - } - /** * Handles item click in the appointment list. */ @@ -299,30 +262,6 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter. }); } - /** - * Fetches the full list of pets from the server. - */ - private void loadPets() { - petViewModel.getAllPets(0, 100).observe(getViewLifecycleOwner(), resource -> { - if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) { - petList.clear(); - petList.addAll(resource.data.getContent()); - } - }); - } - - /** - * Fetches the full list of services from the server. - */ - private void loadServices() { - serviceViewModel.getAllServices(0, 100).observe(getViewLifecycleOwner(), resource -> { - if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) { - serviceList.clear(); - serviceList.addAll(resource.data.getContent()); - } - }); - } - /** * Initializes the RecyclerView for displaying appointments. */ 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 07e08aba..029dc446 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 @@ -341,10 +341,6 @@ public class InventoryFragment extends Fragment implements InventoryAdapter.OnIn if (inv != null) { args.putLong("inventoryId", inv.getInventoryId()); - args.putLong("prodId", inv.getProdId() != null ? inv.getProdId() : -1); - args.putString("productName", inv.getProductName()); - args.putString("categoryName", inv.getCategoryName()); - args.putInt("quantity", inv.getQuantity() != null ? inv.getQuantity() : 0); } NavHostFragment.findNavController(this).navigate(R.id.nav_inventory_detail, args); 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 0c71593a..01331b19 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 @@ -166,18 +166,7 @@ public class PetFragment extends Fragment implements PetAdapter.OnPetClickListen private void openPetProfile(int position) { Bundle args = new Bundle(); PetDTO pet = filteredList.get(position); - args.putInt("petId", pet.getPetId().intValue()); - args.putString("petName", pet.getPetName()); - args.putString("petSpecies", pet.getPetSpecies()); - args.putString("petBreed", pet.getPetBreed()); - args.putInt("petAge", pet.getPetAge()); - args.putString("petStatus", pet.getPetStatus()); - - try { - args.putDouble("petPrice", Double.parseDouble(pet.getPetPrice())); - } catch (Exception e) { - args.putDouble("petPrice", 0.0); - } + args.putLong("petId", pet.getPetId()); NavHostFragment.findNavController(this).navigate(R.id.nav_pet_profile, args); } 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 a459262f..6103918e 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 @@ -170,10 +170,6 @@ public class ProductFragment extends Fragment implements ProductAdapter.OnProduc if (position != -1) { ProductDTO p = filteredList.get(position); args.putLong("prodId", p.getProdId()); - args.putString("prodName", p.getProdName()); - args.putString("prodDesc", p.getProdDesc() != null ? p.getProdDesc() : ""); - args.putString("prodPrice", p.getProdPrice() != null ? p.getProdPrice().toString() : ""); - args.putLong("categoryId", p.getCategoryId() != null ? p.getCategoryId() : -1); } NavHostFragment.findNavController(this).navigate(R.id.nav_product_detail, args); } 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 49eccc94..4c8e08d0 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 @@ -166,9 +166,6 @@ public class ProductSupplierFragment extends Fragment ProductSupplierDTO ps = filteredList.get(position); args.putLong("productId", ps.getProductId()); args.putLong("supplierId", ps.getSupplierId()); - args.putString("productName", ps.getProductName()); - args.putString("supplierName", ps.getSupplierName()); - args.putString("cost", ps.getCost() != null ? ps.getCost().toString() : ""); } NavHostFragment.findNavController(this).navigate(R.id.nav_product_supplier_detail, args); } 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 078ef06c..78fcd981 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 @@ -166,9 +166,6 @@ public class PurchaseOrderFragment extends Fragment Bundle args = new Bundle(); PurchaseOrderDTO po = filteredList.get(position); args.putLong("purchaseOrderId", po.getPurchaseOrderId()); - args.putString("supplierName", po.getSupplierName()); - args.putString("orderDate", po.getOrderDate()); - args.putString("status", po.getStatus()); NavHostFragment.findNavController(this).navigate(R.id.nav_purchase_order_detail, args); } 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 8ae3c5a5..ff8e1bb5 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 @@ -128,16 +128,11 @@ public class ServiceFragment extends Fragment implements ServiceAdapter.OnServic private void openServiceDetails(int position) { //Make a bundle to pass data to the detail fragment Bundle args = new Bundle(); - args.putInt("position", position); - //if editing a service, add the service data to the bundle + //if editing a service, add the service id to the bundle if (position != -1) { ServiceDTO service = filteredList.get(position); - args.putInt("serviceId", service.getServiceId().intValue()); - args.putString("serviceName", service.getServiceName()); - args.putString("serviceDesc", service.getServiceDesc()); - args.putInt("serviceDuration", service.getServiceDuration()); - args.putDouble("servicePrice", service.getServicePrice()); + args.putLong("serviceId", service.getServiceId()); } NavHostFragment.findNavController(this).navigate(R.id.nav_service_detail, args); 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 4018c27a..e3a0c131 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 @@ -129,17 +129,11 @@ public class SupplierFragment extends Fragment implements SupplierAdapter.OnSupp private void openSupplierDetails(int position) { //Make a bundle to pass data to the detail fragment Bundle args = new Bundle(); - args.putInt("position", position); - //if editing a supplier, add the supplier data to the bundle + //if editing a supplier, add the supplier id to the bundle if (position != -1) { SupplierDTO supplier = filteredList.get(position); - args.putInt("supId", supplier.getSupId().intValue()); - args.putString("supCompany", supplier.getSupCompany()); - args.putString("supContactFirstName", supplier.getSupContactFirstName()); - args.putString("supContactLastName", supplier.getSupContactLastName()); - args.putString("supEmail", supplier.getSupEmail()); - args.putString("supPhone", supplier.getSupPhone()); + args.putLong("supId", supplier.getSupId()); } NavHostFragment.findNavController(this).navigate(R.id.nav_supplier_detail, args); 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 3702e168..1bdaba71 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 @@ -5,6 +5,7 @@ import android.os.Bundle; import android.view.*; import android.widget.*; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; import androidx.lifecycle.ViewModelProvider; import androidx.navigation.fragment.NavHostFragment; @@ -56,15 +57,20 @@ public class AdoptionDetailFragment extends Fragment { public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { binding = FragmentAdoptionDetailBinding.inflate(inflater, container, false); + return binding.getRoot(); + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); setupSpinners(); setupDatePicker(); - loadData(); + loadSpinnersData(); handleArguments(); binding.btnAdoptionBack.setOnClickListener(v -> navigateBack()); binding.btnSaveAdoption.setOnClickListener(v -> saveAdoption()); binding.btnDeleteAdoption.setOnClickListener(v -> confirmDelete()); - return binding.getRoot(); } @Override @@ -96,9 +102,9 @@ public class AdoptionDetailFragment extends Fragment { } /** - * Fetches required data (pets and customers) from the backend. + * Fetches required data for spinners from the backend. */ - private void loadData() { + private void loadSpinnersData() { loadPets(); loadCustomers(); } @@ -110,13 +116,20 @@ public class AdoptionDetailFragment extends Fragment { petViewModel.getAllPets(0, 200).observe(getViewLifecycleOwner(), resource -> { if (resource.status == Resource.Status.SUCCESS && resource.data != null) { petList = resource.data.getContent(); - SpinnerUtils.populateSpinner(requireContext(), binding.spinnerAdoptionPet, petList, - PetDTO::getPetName, "-- Select Pet --", - preselectedPetId, PetDTO::getPetId); + refreshPetSpinner(); } }); } + /** + * Populates the pet selection spinner with data. + */ + private void refreshPetSpinner() { + SpinnerUtils.populateSpinner(requireContext(), binding.spinnerAdoptionPet, petList, + PetDTO::getPetName, "-- Select Pet --", + preselectedPetId, PetDTO::getPetId); + } + /** * Loads the list of customers from the API. */ @@ -124,14 +137,21 @@ public class AdoptionDetailFragment extends Fragment { customerViewModel.getAllCustomers(0, 200).observe(getViewLifecycleOwner(), resource -> { if (resource.status == Resource.Status.SUCCESS && resource.data != null) { customerList = resource.data.getContent(); - SpinnerUtils.populateSpinner(requireContext(), binding.spinnerAdoptionCustomer, customerList, - item -> item.getFirstName() + " " + item.getLastName(), - "-- Select Customer --", - preselectedCustomerId, CustomerDTO::getCustomerId); + 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); + } + /** * Handles arguments to determine if the fragment is in edit or add mode. */ @@ -139,18 +159,12 @@ public class AdoptionDetailFragment extends Fragment { Bundle a = getArguments(); if (a != null && a.containsKey("adoptionId")) { isEditing = true; - adoptionId = a.getLong("adoptionId"); - preselectedPetId = a.getLong("petId", -1); - preselectedCustomerId = a.getLong("customerId", -1); - + adoptionId = a.getLong("adoptionId"); binding.tvAdoptionMode.setText("Edit Adoption"); binding.tvAdoptionId.setText("ID: " + adoptionId); binding.tvAdoptionId.setVisibility(View.VISIBLE); - binding.etAdoptionDate.setText(a.getString("adoptionDate")); binding.btnDeleteAdoption.setVisibility(View.VISIBLE); - - // Pre-fill status - SpinnerUtils.setSelectionByValue(binding.spinnerAdoptionStatus, a.getString("adoptionStatus", "Pending")); + loadAdoptionData(); } else { binding.tvAdoptionMode.setText("Add Adoption"); binding.btnDeleteAdoption.setVisibility(View.GONE); @@ -158,6 +172,27 @@ public class AdoptionDetailFragment extends Fragment { } } + /** + * Fetches specific adoption details from the backend using the ID. + */ + private void loadAdoptionData() { + adoptionViewModel.getAdoptionById(adoptionId).observe(getViewLifecycleOwner(), resource -> { + if (resource == null) return; + if (resource.status == Resource.Status.SUCCESS && resource.data != null) { + AdoptionDTO a = resource.data; + preselectedPetId = a.getPetId() != null ? a.getPetId() : -1; + preselectedCustomerId = a.getCustomerId() != null ? a.getCustomerId() : -1; + binding.etAdoptionDate.setText(a.getAdoptionDate()); + SpinnerUtils.setSelectionByValue(binding.spinnerAdoptionStatus, a.getAdoptionStatus()); + + refreshPetSpinner(); + refreshCustomerSpinner(); + } 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. */ 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 83e70852..cc419a69 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 @@ -6,6 +6,7 @@ import android.util.Log; import android.view.*; import android.widget.*; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; import androidx.lifecycle.ViewModelProvider; import androidx.navigation.fragment.NavHostFragment; @@ -68,15 +69,20 @@ public class AppointmentDetailFragment extends Fragment { @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { binding = FragmentAppointmentDetailBinding.inflate(inflater, container, false); + return binding.getRoot(); + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); setupSpinners(); setupDatePicker(); - loadData(); + loadSpinnersData(); handleArguments(); binding.btnApptBack.setOnClickListener(v -> navigateBack()); binding.btnSaveAppointment.setOnClickListener(v -> saveAppointment()); binding.btnDeleteAppointment.setOnClickListener(v -> confirmDelete()); - return binding.getRoot(); } @Override @@ -115,9 +121,9 @@ public class AppointmentDetailFragment extends Fragment { } /** - * Fetches all required data from the backend to populate the fragment. + * Fetches all required data for spinners from the backend. */ - private void loadData() { + private void loadSpinnersData() { loadPets(); loadServices(); loadCustomers(); @@ -131,13 +137,20 @@ public class AppointmentDetailFragment extends Fragment { petViewModel.getAllPets(0, 200).observe(getViewLifecycleOwner(), resource -> { if (resource.status == Resource.Status.SUCCESS && resource.data != null) { petList = resource.data.getContent(); - SpinnerUtils.populateSpinner(requireContext(), binding.spinnerPet, petList, - PetDTO::getPetName, "-- Select Pet --", - preselectedPetId, PetDTO::getPetId); + refreshPetSpinner(); } }); } + /** + * Populates the pet selection spinner. + */ + private void refreshPetSpinner() { + SpinnerUtils.populateSpinner(requireContext(), binding.spinnerPet, petList, + PetDTO::getPetName, "-- Select Pet --", + preselectedPetId, PetDTO::getPetId); + } + /** * Loads the list of services from the API. */ @@ -145,13 +158,20 @@ public class AppointmentDetailFragment extends Fragment { serviceViewModel.getAllServices(0, 200).observe(getViewLifecycleOwner(), resource -> { if (resource.status == Resource.Status.SUCCESS && resource.data != null) { serviceList = resource.data.getContent(); - SpinnerUtils.populateSpinner(requireContext(), binding.spinnerService, serviceList, - ServiceDTO::getServiceName, "-- Select Service --", - preselectedServiceId, ServiceDTO::getServiceId); + refreshServiceSpinner(); } }); } + /** + * 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. */ @@ -159,14 +179,21 @@ public class AppointmentDetailFragment extends Fragment { customerViewModel.getAllCustomers(0, 200).observe(getViewLifecycleOwner(), resource -> { if (resource.status == Resource.Status.SUCCESS && resource.data != null) { customerList = resource.data.getContent(); - SpinnerUtils.populateSpinner(requireContext(), binding.spinnerCustomer, customerList, - item -> item.getFirstName() + " " + item.getLastName(), - "-- Select Customer --", - preselectedCustomerId, CustomerDTO::getCustomerId); + 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. */ @@ -174,13 +201,20 @@ public class AppointmentDetailFragment extends Fragment { storeViewModel.getAllStores(0, 50).observe(getViewLifecycleOwner(), resource -> { if (resource.status == Resource.Status.SUCCESS && resource.data != null) { storeList = resource.data.getContent(); - SpinnerUtils.populateSpinner(requireContext(), binding.spinnerStore, storeList, - StoreDTO::getStoreName, "-- Select Store --", - preselectedStoreId, StoreDTO::getStoreId); + refreshStoreSpinner(); } }); } + /** + * Populates the store selection spinner. + */ + private void refreshStoreSpinner() { + SpinnerUtils.populateSpinner(requireContext(), binding.spinnerStore, storeList, + StoreDTO::getStoreName, "-- Select Store --", + preselectedStoreId, StoreDTO::getStoreId); + } + /** * Handles arguments to determine if the fragment is in edit or add mode. */ @@ -188,34 +222,12 @@ public class AppointmentDetailFragment extends Fragment { Bundle a = getArguments(); if (a != null && a.containsKey("appointmentId")) { isEditing = true; - appointmentId = a.getLong("appointmentId"); - preselectedPetId = a.getLong("petId", -1); - preselectedServiceId= a.getLong("serviceId", -1); - preselectedCustomerId = a.getLong("customerId", -1); - preselectedStoreId = a.getLong("storeId", -1); - + appointmentId = a.getLong("appointmentId"); binding.tvApptMode.setText("Edit Appointment"); binding.tvAppointmentId.setText("ID: " + appointmentId); binding.tvAppointmentId.setVisibility(View.VISIBLE); - binding.etAppointmentDate.setText(a.getString("appointmentDate")); binding.btnDeleteAppointment.setVisibility(View.VISIBLE); - - // Pre-fill time spinners - String time = a.getString("appointmentTime", "09:00"); - if (time.length() > 5) time = time.substring(0, 5); - String[] parts = time.split(":"); - if (parts.length == 2) { - 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; } - } - - // Pre-fill status - SpinnerUtils.setSelectionByValue(binding.spinnerAppointmentStatus, a.getString("appointmentStatus", "Booked")); - + loadAppointmentData(); } else { binding.tvApptMode.setText("Add Appointment"); binding.btnDeleteAppointment.setVisibility(View.GONE); @@ -223,6 +235,48 @@ public class AppointmentDetailFragment extends Fragment { } } + /** + * Fetches specific appointment details from the backend using the ID. + */ + private void loadAppointmentData() { + appointmentViewModel.getAppointmentById(appointmentId).observe(getViewLifecycleOwner(), resource -> { + if (resource == null) return; + 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; + + 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) {} + } + + SpinnerUtils.setSelectionByValue(binding.spinnerAppointmentStatus, a.getAppointmentStatus()); + + refreshPetSpinner(); + refreshServiceSpinner(); + refreshCustomerSpinner(); + refreshStoreSpinner(); + } else if (resource.status == Resource.Status.ERROR) { + Toast.makeText(getContext(), "Failed to load appointment: " + resource.message, Toast.LENGTH_SHORT).show(); + } + }); + } + /** * Validates input and saves the appointment to the backend. */ @@ -307,6 +361,9 @@ public class AppointmentDetailFragment extends Fragment { } } + /** + * Handles errors that occur during the saving process. + */ private void handleSaveError(String errorMessage) { if (errorMessage != null) { Log.e("APPT_SAVE", "Error: " + errorMessage); 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 bafa71c5..cd1e84f4 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 @@ -12,6 +12,7 @@ import android.widget.ArrayAdapter; 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; @@ -19,6 +20,7 @@ import androidx.navigation.fragment.NavHostFragment; import com.example.petstoremobile.adapters.BlackTextArrayAdapter; import com.example.petstoremobile.databinding.FragmentInventoryDetailBinding; +import com.example.petstoremobile.dtos.InventoryDTO; import com.example.petstoremobile.dtos.InventoryRequest; import com.example.petstoremobile.dtos.ProductDTO; import com.example.petstoremobile.utils.InputValidator; @@ -56,6 +58,9 @@ public class InventoryDetailFragment extends Fragment { private final List productSuggestions = new ArrayList<>(); private ArrayAdapter dropdownAdapter; + /** + * Initializes the view models. + */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -63,10 +68,22 @@ public class InventoryDetailFragment extends Fragment { productViewModel = new ViewModelProvider(this).get(ProductViewModel.class); } + /** + * Inflates the layout. + */ @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { binding = FragmentInventoryDetailBinding.inflate(inflater, container, false); + 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); setupProductSearch(); handleArguments(); @@ -80,27 +97,26 @@ public class InventoryDetailFragment extends Fragment { android.R.layout.simple_dropdown_item_1line, new ArrayList<>()); binding.etProductSearch.setAdapter(dropdownAdapter); binding.etProductSearch.setThreshold(1); // start showing after 1 character - - return binding.getRoot(); } @Override public void onDestroyView() { super.onDestroyView(); + if (searchRunnable != null) { + searchHandler.removeCallbacks(searchRunnable); + } binding = null; } /** - * setup the product search dropdown. + * Sets up the product search dropdown. */ private void setupProductSearch() { binding.etProductSearch.addTextChangedListener(new TextWatcher() { - @Override - public void beforeTextChanged(CharSequence s, int i, int i1, int i2) { + @Override public void beforeTextChanged(CharSequence s, int i, int i1, int i2) { } - @Override - public void afterTextChanged(Editable s) { + @Override public void afterTextChanged(Editable s) { } @Override @@ -137,6 +153,7 @@ public class InventoryDetailFragment extends Fragment { * Searches for products matching the query from the backend. */ private void searchProducts(String query) { + if (getView() == null) return; productViewModel.getAllProducts(query, 0, 20).observe(getViewLifecycleOwner(), resource -> { if (resource.status == Resource.Status.SUCCESS && resource.data != null) { productSuggestions.clear(); @@ -157,7 +174,7 @@ public class InventoryDetailFragment extends Fragment { } /** - * arguments to set up edit or add mode. + * Handles fragment arguments to determine if we are in edit or add mode. */ private void handleArguments() { Bundle args = getArguments(); @@ -168,27 +185,10 @@ public class InventoryDetailFragment extends Fragment { binding.tvInventoryMode.setText("Edit Inventory"); binding.tvInventoryId.setText("Inventory ID: " + inventoryId); binding.tvInventoryId.setVisibility(View.VISIBLE); - - // Pre-fill search box with existing product name - String productName = args.getString("productName", ""); - long prodId = args.getLong("prodId", -1); - binding.etProductSearch.setText(productName); - - // Show existing product info - if (prodId != -1) { - binding.tvProductInfo.setText( - "ID: " + prodId - + " • " + args.getString("categoryName", "")); - binding.tvProductInfo.setVisibility(View.VISIBLE); - - // Build a minimal ProductDTO so selectedProduct is not null on save - selectedProduct = new ProductDTO(productName, null, null, null); - selectedProduct.setProdId(prodId); - } - - binding.etQuantity.setText(String.valueOf(args.getInt("quantity", 0))); binding.btnDeleteInventory.setVisibility(View.VISIBLE); binding.btnSaveInventory.setText("Save"); + + loadInventoryData(); } else { isEditing = false; binding.tvInventoryMode.setText("Add Inventory"); @@ -200,7 +200,35 @@ public class InventoryDetailFragment extends Fragment { } /** - * Saves the current inventory item details to the backend. + * Loads existing inventory data from the backend. + */ + private void loadInventoryData() { + inventoryViewModel.getInventoryById(inventoryId).observe(getViewLifecycleOwner(), resource -> { + if (resource == null) return; + if (resource.status == Resource.Status.SUCCESS && resource.data != null) { + InventoryDTO inv = resource.data; + binding.etProductSearch.setText(inv.getProductName()); + binding.etQuantity.setText(String.valueOf(inv.getQuantity())); + + if (inv.getProdId() != null) { + binding.tvProductInfo.setText( + "ID: " + inv.getProdId() + + " • " + inv.getCategoryName()); + binding.tvProductInfo.setVisibility(View.VISIBLE); + + selectedProduct = new ProductDTO(); + selectedProduct.setProdId(inv.getProdId()); + selectedProduct.setProdName(inv.getProductName()); + selectedProduct.setCategoryName(inv.getCategoryName()); + } + } else if (resource.status == Resource.Status.ERROR) { + Toast.makeText(getContext(), "Failed to load inventory: " + resource.message, Toast.LENGTH_SHORT).show(); + } + }); + } + + /** + * Validates input and saves the current inventory item details to the backend. */ private void saveInventory() { if (selectedProduct == null) { 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 9af2b79d..eb34869c 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 @@ -3,6 +3,7 @@ package com.example.petstoremobile.fragments.listfragments.detailfragments; import android.os.Bundle; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; import androidx.lifecycle.ViewModelProvider; import androidx.navigation.fragment.NavHostFragment; @@ -31,7 +32,7 @@ import dagger.hilt.android.AndroidEntryPoint; public class PetDetailFragment extends Fragment { private FragmentPetDetailBinding binding; - private int petId; + private long petId; private boolean isEditing = false; private PetViewModel viewModel; @@ -46,6 +47,12 @@ public class PetDetailFragment extends Fragment { public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { binding = FragmentPetDetailBinding.inflate(inflater, container, false); + return binding.getRoot(); + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); setupSpinner(); handleArguments(); @@ -54,8 +61,6 @@ public class PetDetailFragment extends Fragment { binding.btnBack.setOnClickListener(v -> navigateBack()); binding.btnSavePet.setOnClickListener(v -> savePet()); binding.btnDeletePet.setOnClickListener(v -> deletePet()); - - return binding.getRoot(); } @Override @@ -95,10 +100,10 @@ public class PetDetailFragment extends Fragment { //check if the pet is being edited or added if (isEditing) { // Update existing pet - petDTO.setPetId((long) petId); - viewModel.updatePet((long) petId, petDTO).observe(getViewLifecycleOwner(), resource -> { + petDTO.setPetId(petId); + viewModel.updatePet(petId, petDTO).observe(getViewLifecycleOwner(), resource -> { if (resource.status == Resource.Status.SUCCESS) { - ActivityLogger.logChange(requireContext(), "Pet", "UPDATED", petId); + ActivityLogger.logChange(requireContext(), "Pet", "UPDATED", (int) petId); Toast.makeText(getContext(), "Pet updated successfully!", Toast.LENGTH_SHORT).show(); navigateToPetList(); } else if (resource.status == Resource.Status.ERROR) { @@ -124,9 +129,9 @@ public class PetDetailFragment extends Fragment { */ private void deletePet() { DialogUtils.showDeleteConfirmDialog(requireContext(), "Pet", () -> - viewModel.deletePet((long) petId).observe(getViewLifecycleOwner(), resource -> { + viewModel.deletePet(petId).observe(getViewLifecycleOwner(), resource -> { if (resource.status == Resource.Status.SUCCESS) { - ActivityLogger.logChange(requireContext(), "Pet", "DELETED", petId); + ActivityLogger.logChange(requireContext(), "Pet", "DELETED", (int) petId); Toast.makeText(getContext(), "Pet deleted successfully!", Toast.LENGTH_SHORT).show(); navigateToPetList(); } else if (resource.status == Resource.Status.ERROR) { @@ -157,16 +162,12 @@ public class PetDetailFragment extends Fragment { if (getArguments() != null && getArguments().containsKey("petId")) { // Get pet data from arguments and populate fields isEditing = true; - petId = getArguments().getInt("petId"); + petId = getArguments().getLong("petId"); binding.tvMode.setText("Edit Pet"); binding.tvPetId.setText("ID: " + petId); - binding.etPetName.setText(getArguments().getString("petName")); - binding.etPetSpecies.setText(getArguments().getString("petSpecies")); - binding.etPetBreed.setText(getArguments().getString("petBreed")); - binding.etPetAge.setText(String.valueOf(getArguments().getInt("petAge"))); - binding.etPetPrice.setText(String.valueOf(getArguments().getDouble("petPrice"))); - SpinnerUtils.setSelectionByValue(binding.spinnerPetStatus, getArguments().getString("petStatus")); + binding.tvPetId.setVisibility(View.VISIBLE); binding.btnDeletePet.setVisibility(View.VISIBLE); + loadPetData(); } else { // Pet is being added // Set default values for add a new pet @@ -178,6 +179,26 @@ public class PetDetailFragment extends Fragment { } } + /** + * Fetches specific pet details from the backend using the ID. + */ + private void loadPetData() { + viewModel.getPetById(petId).observe(getViewLifecycleOwner(), resource -> { + if (resource == null) return; + if (resource.status == Resource.Status.SUCCESS && resource.data != null) { + PetDTO p = resource.data; + binding.etPetName.setText(p.getPetName()); + binding.etPetSpecies.setText(p.getPetSpecies()); + binding.etPetBreed.setText(p.getPetBreed()); + binding.etPetAge.setText(String.valueOf(p.getPetAge())); + binding.etPetPrice.setText(p.getPetPrice()); + SpinnerUtils.setSelectionByValue(binding.spinnerPetStatus, p.getPetStatus()); + } else if (resource.status == Resource.Status.ERROR) { + Toast.makeText(getContext(), "Failed to load pet: " + resource.message, Toast.LENGTH_SHORT).show(); + } + }); + } + /** * Initializes the spinner for pet status selection. */ 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 4784fd03..48a988d2 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 @@ -5,6 +5,7 @@ import android.os.Bundle; import android.view.*; import android.widget.*; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; import androidx.lifecycle.ViewModelProvider; import androidx.navigation.fragment.NavHostFragment; @@ -21,6 +22,7 @@ import com.example.petstoremobile.utils.FileUtils; import com.example.petstoremobile.utils.GlideUtils; import com.example.petstoremobile.utils.ImagePickerHelper; import com.example.petstoremobile.utils.InputValidator; +import com.example.petstoremobile.utils.Resource; import com.example.petstoremobile.utils.SpinnerUtils; import java.io.File; @@ -89,12 +91,21 @@ public class ProductDetailFragment extends Fragment { } /** - * Inflates the layout and initializes UI components and listeners. + * Inflates the layout. */ @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { binding = FragmentProductDetailBinding.inflate(inflater, container, false); + 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(); handleArguments(); @@ -103,7 +114,6 @@ public class ProductDetailFragment extends Fragment { binding.btnSaveProduct.setOnClickListener(v -> saveProduct()); binding.btnDeleteProduct.setOnClickListener(v -> confirmDelete()); binding.ivProductImage.setOnClickListener(v -> imagePickerHelper.showImagePickerDialog("Select Product Image", hasImage)); - return binding.getRoot(); } @Override @@ -117,7 +127,7 @@ public class ProductDetailFragment extends Fragment { */ private void loadCategories() { viewModel.getAllCategories(0, 100).observe(getViewLifecycleOwner(), resource -> { - if (resource != null && resource.status == com.example.petstoremobile.utils.Resource.Status.SUCCESS && resource.data != null) { + 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 --", @@ -134,16 +144,11 @@ public class ProductDetailFragment extends Fragment { if (a != null && a.containsKey("prodId")) { isEditing = true; prodId = a.getLong("prodId"); - preselectedCategoryId = a.getLong("categoryId", -1); - hasImage = true; - binding.tvProductMode.setText("Edit Product"); binding.tvProductId.setText("ID: " + prodId); binding.tvProductId.setVisibility(View.VISIBLE); - binding.etProductName.setText(a.getString("prodName")); - binding.etProductDesc.setText(a.getString("prodDesc")); - binding.etProductPrice.setText(a.getString("prodPrice")); binding.btnDeleteProduct.setVisibility(View.VISIBLE); + loadProductData(); loadProductImage(); } else { binding.tvProductMode.setText("Add Product"); @@ -153,6 +158,31 @@ public class ProductDetailFragment extends Fragment { } } + /** + * Loads the product data from the backend. + */ + private void loadProductData() { + viewModel.getProductById(prodId).observe(getViewLifecycleOwner(), resource -> { + if (resource == null) return; + 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); + } + } 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. */ @@ -179,8 +209,8 @@ public class ProductDetailFragment extends Fragment { private void performPendingImageActions(String successMsg) { if (isImageRemoved) { viewModel.deleteProductImage(prodId).observe(getViewLifecycleOwner(), resource -> { - if (resource != null && resource.status != com.example.petstoremobile.utils.Resource.Status.LOADING) { - if (resource.status == com.example.petstoremobile.utils.Resource.Status.SUCCESS) { + if (resource != null && resource.status != Resource.Status.LOADING) { + if (resource.status == Resource.Status.SUCCESS) { Toast.makeText(getContext(), successMsg, Toast.LENGTH_SHORT).show(); } else { Toast.makeText(getContext(), successMsg + " (but image removal failed)", Toast.LENGTH_SHORT).show(); @@ -211,8 +241,8 @@ public class ProductDetailFragment extends Fragment { MultipartBody.Part body = MultipartBody.Part.createFormData("image", file.getName(), requestFile); viewModel.uploadProductImage(prodId, body).observe(getViewLifecycleOwner(), resource -> { - if (resource != null && resource.status != com.example.petstoremobile.utils.Resource.Status.LOADING) { - if (resource.status == com.example.petstoremobile.utils.Resource.Status.SUCCESS) { + if (resource != null && resource.status != Resource.Status.LOADING) { + if (resource.status == Resource.Status.SUCCESS) { Toast.makeText(getContext(), successMsg, Toast.LENGTH_SHORT).show(); } else { Toast.makeText(getContext(), successMsg + " (but image upload failed)", Toast.LENGTH_SHORT).show(); @@ -246,8 +276,8 @@ public class ProductDetailFragment extends Fragment { if (isEditing) { viewModel.updateProduct(prodId, dto).observe(getViewLifecycleOwner(), resource -> { - if (resource != null && resource.status != com.example.petstoremobile.utils.Resource.Status.LOADING) { - if (resource.status == com.example.petstoremobile.utils.Resource.Status.SUCCESS) { + 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(); @@ -256,8 +286,8 @@ public class ProductDetailFragment extends Fragment { }); } else { viewModel.createProduct(dto).observe(getViewLifecycleOwner(), resource -> { - if (resource != null && resource.status != com.example.petstoremobile.utils.Resource.Status.LOADING) { - if (resource.status == com.example.petstoremobile.utils.Resource.Status.SUCCESS && resource.data != null) { + if (resource != null && resource.status != Resource.Status.LOADING) { + if (resource.status == Resource.Status.SUCCESS && resource.data != null) { prodId = resource.data.getProdId(); performPendingImageActions("Saved"); } else { @@ -274,9 +304,9 @@ public class ProductDetailFragment extends Fragment { private void confirmDelete() { DialogUtils.showDeleteConfirmDialog(requireContext(), "Product", () -> viewModel.deleteProduct(prodId).observe(getViewLifecycleOwner(), resource -> { - if (resource != null && resource.status == com.example.petstoremobile.utils.Resource.Status.SUCCESS) { + if (resource != null && resource.status == Resource.Status.SUCCESS) { navigateBack(); - } else if (resource != null && resource.status == com.example.petstoremobile.utils.Resource.Status.ERROR) { + } else if (resource != null && 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/fragments/listfragments/detailfragments/ProductSupplierDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ProductSupplierDetailFragment.java index 4fd23fa0..495c0f6d 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 @@ -4,6 +4,7 @@ import android.os.Bundle; import android.view.*; import android.widget.*; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; import androidx.lifecycle.ViewModelProvider; import androidx.navigation.fragment.NavHostFragment; @@ -56,13 +57,18 @@ public class ProductSupplierDetailFragment extends Fragment { public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { binding = FragmentProductSupplierDetailBinding.inflate(inflater, container, false); - loadData(); + return binding.getRoot(); + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + loadSpinnersData(); handleArguments(); binding.btnPSBack.setOnClickListener(v -> navigateBack()); binding.btnSavePS.setOnClickListener(v -> save()); binding.btnDeletePS.setOnClickListener(v -> confirmDelete()); - return binding.getRoot(); } @Override @@ -74,7 +80,7 @@ public class ProductSupplierDetailFragment extends Fragment { /** * Fetches products and suppliers to populate the spinners. */ - private void loadData() { + private void loadSpinnersData() { loadProducts(); loadSuppliers(); } @@ -86,13 +92,17 @@ public class ProductSupplierDetailFragment extends Fragment { productViewModel.getAllProducts(null, 0, 200).observe(getViewLifecycleOwner(), resource -> { if (resource.status == Resource.Status.SUCCESS && resource.data != null) { productList = resource.data.getContent(); - SpinnerUtils.populateSpinner(requireContext(), binding.spinnerPSProduct, productList, - ProductDTO::getProdName, "-- Select Product --", - preselectedProductId, ProductDTO::getProdId); + refreshProductSpinner(); } }); } + private void refreshProductSpinner() { + SpinnerUtils.populateSpinner(requireContext(), binding.spinnerPSProduct, productList, + ProductDTO::getProdName, "-- Select Product --", + preselectedProductId, ProductDTO::getProdId); + } + /** * Loads the list of suppliers from the API. */ @@ -100,33 +110,39 @@ public class ProductSupplierDetailFragment extends Fragment { supplierViewModel.getAllSuppliers(0, 200).observe(getViewLifecycleOwner(), resource -> { if (resource.status == Resource.Status.SUCCESS && resource.data != null) { supplierList = resource.data.getContent(); - SpinnerUtils.populateSpinner(requireContext(), binding.spinnerPSSupplier, supplierList, - SupplierDTO::getSupCompany, "-- Select Supplier --", - preselectedSupplierId, SupplierDTO::getSupId); + refreshSupplierSpinner(); } }); } + private void refreshSupplierSpinner() { + SpinnerUtils.populateSpinner(requireContext(), binding.spinnerPSSupplier, supplierList, + 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")) { + if (a != null && a.containsKey("productId") && a.containsKey("supplierId")) { isEditing = true; - editProductId = a.getLong("productId"); - editSupplierId = a.getLong("supplierId"); - preselectedProductId = editProductId; + editProductId = a.getLong("productId"); + editSupplierId = a.getLong("supplierId"); + preselectedProductId = editProductId; preselectedSupplierId = editSupplierId; - binding.etPSCost.setText(a.getString("cost")); + 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. */ 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 5d9cf5a9..4a30e4cd 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 @@ -3,11 +3,18 @@ package com.example.petstoremobile.fragments.listfragments.detailfragments; import android.graphics.Color; import android.os.Bundle; import android.view.*; +import android.widget.Toast; + import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; +import androidx.lifecycle.ViewModelProvider; 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 dagger.hilt.android.AndroidEntryPoint; @@ -18,40 +25,72 @@ import dagger.hilt.android.AndroidEntryPoint; public class PurchaseOrderDetailFragment extends Fragment { private FragmentPurchaseOrderDetailBinding binding; + private PurchaseOrderViewModel viewModel; + private long purchaseOrderId; + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + viewModel = new ViewModelProvider(this).get(PurchaseOrderViewModel.class); + } /** - * Inflates the layout, initializes views, and populates order data from arguments. + * Inflates the layout. */ @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { binding = FragmentPurchaseOrderDetailBinding.inflate(inflater, container, false); + return binding.getRoot(); + } - Bundle a = getArguments(); - if (a != null) { - binding.tvPODetailId.setText("PO #" + a.getLong("purchaseOrderId")); - binding.tvPODetailSupplier.setText(a.getString("supplierName")); - binding.tvPODetailDate.setText(a.getString("orderDate")); + /** + * Initializes views and populates order data from backend after the view is created. + */ + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); - String status = a.getString("status", ""); - binding.tvPODetailStatus.setText(status); - switch (status) { - case "Completed": - binding.tvPODetailStatus.setTextColor(Color.parseColor("#4CAF50")); break; - case "Pending": - binding.tvPODetailStatus.setTextColor(Color.parseColor("#FF9800")); break; - case "Cancelled": - binding.tvPODetailStatus.setTextColor(Color.parseColor("#F44336")); break; - default: - binding.tvPODetailStatus.setTextColor(Color.parseColor("#9E9E9E")); break; - } - } + handleArguments(); binding.btnPOBack.setOnClickListener(v -> { NavHostFragment.findNavController(this).popBackStack(); }); + } - return binding.getRoot(); + private void handleArguments() { + Bundle a = getArguments(); + if (a != null && a.containsKey("purchaseOrderId")) { + purchaseOrderId = a.getLong("purchaseOrderId"); + loadPurchaseOrderData(); + } + } + + private void loadPurchaseOrderData() { + viewModel.getPurchaseOrderById(purchaseOrderId).observe(getViewLifecycleOwner(), resource -> { + if (resource == null) return; + if (resource.status == Resource.Status.SUCCESS && resource.data != null) { + PurchaseOrderDTO po = resource.data; + binding.tvPODetailId.setText("PO #" + po.getPurchaseOrderId()); + binding.tvPODetailSupplier.setText(po.getSupplierName()); + binding.tvPODetailDate.setText(po.getOrderDate()); + + String status = po.getStatus() != null ? po.getStatus() : ""; + binding.tvPODetailStatus.setText(status); + switch (status) { + case "Completed": + binding.tvPODetailStatus.setTextColor(Color.parseColor("#4CAF50")); break; + case "Pending": + binding.tvPODetailStatus.setTextColor(Color.parseColor("#FF9800")); break; + case "Cancelled": + binding.tvPODetailStatus.setTextColor(Color.parseColor("#F44336")); break; + default: + binding.tvPODetailStatus.setTextColor(Color.parseColor("#9E9E9E")); break; + } + } else if (resource.status == Resource.Status.ERROR) { + Toast.makeText(getContext(), "Failed to load order: " + resource.message, Toast.LENGTH_SHORT).show(); + } + }); } @Override diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/RefundDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/RefundDetailFragment.java index f4ce5f91..ce532696 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/RefundDetailFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/RefundDetailFragment.java @@ -2,6 +2,7 @@ package com.example.petstoremobile.fragments.listfragments.detailfragments; import android.os.Bundle; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; import androidx.navigation.fragment.NavHostFragment; @@ -38,15 +39,18 @@ public class RefundDetailFragment extends Fragment { public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { binding = FragmentRefundDetailBinding.inflate(inflater, container, false); + return binding.getRoot(); + } + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); setupSpinner(); handleArguments(); binding.btnRefundBack.setOnClickListener(v -> goBack()); binding.btnLoadSale.setOnClickListener(v -> loadSaleDetails()); binding.btnProcessRefund.setOnClickListener(v -> processRefund()); - - return binding.getRoot(); } @Override 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 7f9c053b..49c51141 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 @@ -3,6 +3,7 @@ package com.example.petstoremobile.fragments.listfragments.detailfragments; import android.os.Bundle; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; import androidx.lifecycle.ViewModelProvider; import androidx.navigation.fragment.NavHostFragment; @@ -30,7 +31,7 @@ import dagger.hilt.android.AndroidEntryPoint; public class ServiceDetailFragment extends Fragment { private FragmentServiceDetailBinding binding; - private int serviceId; + private long serviceId; private boolean isEditing = false; private ServiceViewModel viewModel; @@ -45,6 +46,12 @@ public class ServiceDetailFragment extends Fragment { public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { binding = FragmentServiceDetailBinding.inflate(inflater, container, false); + return binding.getRoot(); + } + + @Override + 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(); @@ -53,8 +60,6 @@ public class ServiceDetailFragment extends Fragment { binding.btnBack.setOnClickListener(v -> navigateBack()); binding.btnSaveService.setOnClickListener(v -> saveService()); binding.btnDeleteService.setOnClickListener(v -> deleteService()); - - return binding.getRoot(); } @Override @@ -89,10 +94,10 @@ public class ServiceDetailFragment extends Fragment { //check if the service is being edited or added if (isEditing) { // Update existing service - serviceDTO.setServiceId((long) serviceId); - viewModel.updateService((long) serviceId, serviceDTO).observe(getViewLifecycleOwner(), resource -> { + serviceDTO.setServiceId(serviceId); + viewModel.updateService(serviceId, serviceDTO).observe(getViewLifecycleOwner(), resource -> { if (resource.status == Resource.Status.SUCCESS) { - ActivityLogger.logChange(requireContext(), "Service", "UPDATED", serviceId); + ActivityLogger.logChange(requireContext(), "Service", "UPDATED", (int) serviceId); Toast.makeText(getContext(), "Service updated successfully!", Toast.LENGTH_SHORT).show(); navigateBack(); } else if (resource.status == Resource.Status.ERROR) { @@ -117,9 +122,9 @@ public class ServiceDetailFragment extends Fragment { */ private void deleteService() { DialogUtils.showDeleteConfirmDialog(requireContext(), "Service", () -> - viewModel.deleteService((long) serviceId).observe(getViewLifecycleOwner(), resource -> { + viewModel.deleteService(serviceId).observe(getViewLifecycleOwner(), resource -> { if (resource.status == Resource.Status.SUCCESS) { - ActivityLogger.logChange(requireContext(), "Service", "DELETED", serviceId); + ActivityLogger.logChange(requireContext(), "Service", "DELETED", (int) serviceId); Toast.makeText(getContext(), "Service deleted successfully!", Toast.LENGTH_SHORT).show(); navigateBack(); } else if (resource.status == Resource.Status.ERROR) { @@ -143,14 +148,11 @@ public class ServiceDetailFragment extends Fragment { if (getArguments() != null && getArguments().containsKey("serviceId")) { // Get service data from arguments and populate fields isEditing = true; - serviceId = getArguments().getInt("serviceId"); + serviceId = getArguments().getLong("serviceId"); binding.tvMode.setText("Edit Service"); binding.tvServiceId.setText("ID: " + serviceId); - binding.etServiceName.setText(getArguments().getString("serviceName")); - binding.etServiceDesc.setText(getArguments().getString("serviceDesc")); - binding.etServiceDuration.setText(String.valueOf(getArguments().getInt("serviceDuration"))); - binding.etServicePrice.setText(String.valueOf(getArguments().getDouble("servicePrice"))); binding.btnDeleteService.setVisibility(View.VISIBLE); + loadServiceData(); } else { // Service is being added // Set default values for add a new service @@ -161,4 +163,22 @@ public class ServiceDetailFragment extends Fragment { binding.btnSaveService.setText("Add"); } } + + /** + * Fetches specific service details from the backend using the ID. + */ + private void loadServiceData() { + viewModel.getServiceById(serviceId).observe(getViewLifecycleOwner(), resource -> { + if (resource == null) return; + if (resource.status == Resource.Status.SUCCESS && resource.data != null) { + ServiceDTO s = resource.data; + binding.etServiceName.setText(s.getServiceName()); + binding.etServiceDesc.setText(s.getServiceDesc()); + binding.etServiceDuration.setText(String.valueOf(s.getServiceDuration())); + binding.etServicePrice.setText(String.valueOf(s.getServicePrice())); + } else if (resource.status == Resource.Status.ERROR) { + Toast.makeText(getContext(), "Failed to load service: " + resource.message, Toast.LENGTH_SHORT).show(); + } + }); + } } 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 5d52606d..4935cb8b 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 @@ -3,6 +3,7 @@ package com.example.petstoremobile.fragments.listfragments.detailfragments; import android.os.Bundle; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; import androidx.lifecycle.ViewModelProvider; import androidx.navigation.fragment.NavHostFragment; @@ -30,7 +31,7 @@ import dagger.hilt.android.AndroidEntryPoint; public class SupplierDetailFragment extends Fragment { private FragmentSupplierDetailBinding binding; - private int supId; + private long supId; private boolean isEditing = false; private SupplierViewModel viewModel; @@ -45,6 +46,12 @@ public class SupplierDetailFragment extends Fragment { public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { binding = FragmentSupplierDetailBinding.inflate(inflater, container, false); + return binding.getRoot(); + } + + @Override + 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); @@ -55,8 +62,6 @@ public class SupplierDetailFragment extends Fragment { binding.btnBack.setOnClickListener(v -> navigateBack()); binding.btnSaveSupplier.setOnClickListener(v -> saveSupplier()); binding.btnDeleteSupplier.setOnClickListener(v -> deleteSupplier()); - - return binding.getRoot(); } @Override @@ -94,10 +99,10 @@ public class SupplierDetailFragment extends Fragment { //check if the supplier is being edited or added if (isEditing) { // Update existing supplier - supplierDTO.setSupId((long) supId); - viewModel.updateSupplier((long) supId, supplierDTO).observe(getViewLifecycleOwner(), resource -> { + supplierDTO.setSupId(supId); + viewModel.updateSupplier(supId, supplierDTO).observe(getViewLifecycleOwner(), resource -> { if (resource.status == Resource.Status.SUCCESS) { - ActivityLogger.logChange(requireContext(), "Supplier", "UPDATED", supId); + ActivityLogger.logChange(requireContext(), "Supplier", "UPDATED", (int) supId); Toast.makeText(getContext(), "Supplier updated successfully!", Toast.LENGTH_SHORT).show(); navigateBack(); } else if (resource.status == Resource.Status.ERROR) { @@ -123,9 +128,9 @@ public class SupplierDetailFragment extends Fragment { */ private void deleteSupplier() { DialogUtils.showDeleteConfirmDialog(requireContext(), "Supplier", () -> - viewModel.deleteSupplier((long) supId).observe(getViewLifecycleOwner(), resource -> { + viewModel.deleteSupplier(supId).observe(getViewLifecycleOwner(), resource -> { if (resource.status == Resource.Status.SUCCESS) { - ActivityLogger.logChange(requireContext(), "Supplier", "DELETED", supId); + ActivityLogger.logChange(requireContext(), "Supplier", "DELETED", (int) supId); Toast.makeText(getContext(), "Supplier deleted successfully!", Toast.LENGTH_SHORT).show(); navigateBack(); } else if (resource.status == Resource.Status.ERROR) { @@ -149,15 +154,12 @@ public class SupplierDetailFragment extends Fragment { if (getArguments() != null && getArguments().containsKey("supId")) { // Get supplier data from arguments and populate fields isEditing = true; - supId = getArguments().getInt("supId"); + supId = getArguments().getLong("supId"); binding.tvMode.setText("Edit Supplier"); binding.tvSupId.setText("ID: " + supId); - binding.etSupCompany.setText(getArguments().getString("supCompany")); - binding.etSupContactFirstName.setText(getArguments().getString("supContactFirstName")); - binding.etSupContactLastName.setText(getArguments().getString("supContactLastName")); - binding.etSupEmail.setText(getArguments().getString("supEmail")); - binding.etSupPhone.setText(getArguments().getString("supPhone")); + binding.tvSupId.setVisibility(View.VISIBLE); binding.btnDeleteSupplier.setVisibility(View.VISIBLE); + loadSupplierData(); } else { // Supplier is being added // Set default values for add a new supplier @@ -168,4 +170,23 @@ public class SupplierDetailFragment extends Fragment { binding.btnSaveSupplier.setText("Add"); } } + + /** + * Fetches specific supplier details from the backend using the ID. + */ + private void loadSupplierData() { + viewModel.getSupplierById(supId).observe(getViewLifecycleOwner(), resource -> { + if (resource == null) return; + if (resource.status == Resource.Status.SUCCESS && resource.data != null) { + SupplierDTO s = resource.data; + binding.etSupCompany.setText(s.getSupCompany()); + binding.etSupContactFirstName.setText(s.getSupContactFirstName()); + binding.etSupContactLastName.setText(s.getSupContactLastName()); + binding.etSupEmail.setText(s.getSupEmail()); + binding.etSupPhone.setText(s.getSupPhone()); + } else if (resource.status == Resource.Status.ERROR) { + Toast.makeText(getContext(), "Failed to load supplier: " + resource.message, Toast.LENGTH_SHORT).show(); + } + }); + } } 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 0ddc0511..69391412 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 @@ -3,24 +3,27 @@ package com.example.petstoremobile.fragments.listfragments.listprofilefragments; import android.net.Uri; import android.os.Bundle; +import androidx.annotation.NonNull; import androidx.fragment.app.Fragment; +import androidx.lifecycle.ViewModelProvider; import androidx.navigation.fragment.NavHostFragment; import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import android.widget.Button; -import android.widget.ImageView; -import android.widget.TextView; +import android.widget.Toast; import com.example.petstoremobile.R; import com.example.petstoremobile.api.PetApi; import com.example.petstoremobile.api.auth.TokenManager; +import com.example.petstoremobile.databinding.FragmentPetProfileBinding; +import com.example.petstoremobile.dtos.PetDTO; import com.example.petstoremobile.utils.FileUtils; import com.example.petstoremobile.utils.GlideUtils; import com.example.petstoremobile.utils.ImagePickerHelper; -import com.example.petstoremobile.utils.RetrofitUtils; +import com.example.petstoremobile.utils.Resource; +import com.example.petstoremobile.viewmodels.PetViewModel; import java.io.File; import java.util.Locale; @@ -36,16 +39,14 @@ import okhttp3.RequestBody; @AndroidEntryPoint public class PetProfileFragment extends Fragment { - private TextView tvPetName, tvPetSpecies, tvPetBreed, tvPetAge, tvPetPrice; - private Button btnBack, btnEditPet, btnChangePhoto; - private ImageView imgPet; - private int petId; + private FragmentPetProfileBinding binding; + private long petId; private boolean hasImage = false; - @Inject PetApi petApi; @Inject @Named("baseUrl") String baseUrl; @Inject TokenManager tokenManager; + private PetViewModel viewModel; private ImagePickerHelper imagePickerHelper; @@ -55,6 +56,7 @@ public class PetProfileFragment extends Fragment { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); + viewModel = new ViewModelProvider(this).get(PetViewModel.class); imagePickerHelper = new ImagePickerHelper(this, "pet_photo.jpg", new ImagePickerHelper.ImagePickerListener() { @Override @@ -70,65 +72,77 @@ public class PetProfileFragment extends Fragment { } /** - * Inflates the layout, initializes views, and sets up click listeners. + * Inflates the layout using view binding, initializes views, and sets up click listeners. */ @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, + public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - View view = inflater.inflate(R.layout.fragment_pet_profile, container, false); - - // Initialize views - tvPetName = view.findViewById(R.id.tvPetName); - tvPetSpecies = view.findViewById(R.id.tvPetSpecies); - tvPetBreed = view.findViewById(R.id.tvPetBreed); - tvPetAge = view.findViewById(R.id.tvPetAge); - tvPetPrice = view.findViewById(R.id.tvPetPrice); - btnBack = view.findViewById(R.id.btnBack); - btnEditPet = view.findViewById(R.id.btnEditPet); - btnChangePhoto = view.findViewById(R.id.btnChangePhoto); - imgPet = view.findViewById(R.id.imgPet); - + binding = FragmentPetProfileBinding.inflate(inflater, container, false); // Set pet details to display if (getArguments() != null) { - petId = getArguments().getInt("petId"); - tvPetName.setText(getArguments().getString("petName")); - tvPetSpecies.setText(getArguments().getString("petSpecies")); - tvPetBreed.setText(getArguments().getString("petBreed")); - tvPetAge.setText(String.format(Locale.getDefault(), "%d yr(s)", getArguments().getInt("petAge"))); - tvPetPrice.setText(String.format(Locale.getDefault(), "$%.2f", getArguments().getDouble("petPrice"))); - - // Load pet image from backend - loadPetImage(petId); + petId = getArguments().getLong("petId"); + loadPetData(); + loadPetImage((int) petId); } //set button click listeners - btnBack.setOnClickListener(v -> { + binding.btnBack.setOnClickListener(v -> { NavHostFragment.findNavController(this).popBackStack(); }); //Make the edit button go to the pet detail view - btnEditPet.setOnClickListener(v -> { - if (getArguments() == null) return; - NavHostFragment.findNavController(this).navigate(R.id.nav_pet_detail, getArguments()); + 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 - btnChangePhoto.setOnClickListener(v -> { + binding.btnChangePhoto.setOnClickListener(v -> { imagePickerHelper.showImagePickerDialog("Change Pet Photo", hasImage); }); - return view; + return binding.getRoot(); + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + binding = null; } /** - * Fetches and displays the pet's image from the server. + * Fetches current pet data from the backend and updates the UI. + */ + private void loadPetData() { + viewModel.getPetById(petId).observe(getViewLifecycleOwner(), resource -> { + if (resource == null) return; + if (resource.status == Resource.Status.SUCCESS && resource.data != null) { + PetDTO pet = resource.data; + binding.tvPetName.setText(pet.getPetName()); + binding.tvPetSpecies.setText(pet.getPetSpecies()); + binding.tvPetBreed.setText(pet.getPetBreed()); + binding.tvPetAge.setText(String.format(Locale.getDefault(), "%d yr(s)", pet.getPetAge())); + try { + binding.tvPetPrice.setText(String.format(Locale.getDefault(), "$%.2f", Double.parseDouble(pet.getPetPrice()))); + } catch (Exception e) { + binding.tvPetPrice.setText("$0.00"); + } + } else if (resource.status == Resource.Status.ERROR) { + Toast.makeText(getContext(), "Failed to load pet data: " + resource.message, Toast.LENGTH_SHORT).show(); + } + }); + } + + /** + * 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(); - GlideUtils.loadImageWithToken(requireContext(), imgPet, imageUrl, token, R.drawable.placeholder, new GlideUtils.ImageLoadListener() { + GlideUtils.loadImageWithToken(requireContext(), binding.imgPet, imageUrl, token, R.drawable.placeholder, new GlideUtils.ImageLoadListener() { @Override public void onResourceReady() { hasImage = true; @@ -142,7 +156,7 @@ public class PetProfileFragment extends Fragment { } /** - * Uploads a selected or captured image a pet photo through the API. + * Uploads a selected or captured image a pet photo through the ViewModel. */ private void uploadPetImage(Uri uri) { try { @@ -153,30 +167,36 @@ public class PetProfileFragment extends Fragment { RequestBody requestFile = RequestBody.create(file, MediaType.parse(requireContext().getContentResolver().getType(uri))); MultipartBody.Part body = MultipartBody.Part.createFormData("image", file.getName(), requestFile); - // Call the backend to upload the image - petApi.uploadPetImage((long) petId, body).enqueue(RetrofitUtils.createCallback( - requireContext(), - "UPLOAD_PET_IMAGE", - "Pet photo updated successfully", - result -> loadPetImage(petId) - )); + // 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(); + } + } + }); } catch (Exception e) { Log.e("UPLOAD_PET_IMAGE", "Error: " + e.getMessage()); } } /** - * Sends a request to the API to remove the current pet photo. + * Sends a request to the ViewModel to remove the current pet photo. */ private void deletePetImage() { - petApi.deletePetImage((long) petId).enqueue(RetrofitUtils.createCallback( - requireContext(), - "DELETE_PET_IMAGE", - "Pet photo removed", - result -> { + 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; - imgPet.setImageResource(R.drawable.placeholder); + binding.imgPet.setImageResource(R.drawable.placeholder); + } else { + Toast.makeText(getContext(), "Delete failed: " + resource.message, Toast.LENGTH_SHORT).show(); } - )); + } + }); } } diff --git a/android/app/src/main/java/com/example/petstoremobile/repositories/AdoptionRepository.java b/android/app/src/main/java/com/example/petstoremobile/repositories/AdoptionRepository.java index f5a5d2c5..0e73b706 100644 --- a/android/app/src/main/java/com/example/petstoremobile/repositories/AdoptionRepository.java +++ b/android/app/src/main/java/com/example/petstoremobile/repositories/AdoptionRepository.java @@ -1,24 +1,22 @@ package com.example.petstoremobile.repositories; import androidx.lifecycle.LiveData; -import androidx.lifecycle.MutableLiveData; import com.example.petstoremobile.api.AdoptionApi; import com.example.petstoremobile.dtos.AdoptionDTO; import com.example.petstoremobile.dtos.PageResponse; import com.example.petstoremobile.utils.Resource; -import com.example.petstoremobile.utils.RetrofitUtils; import javax.inject.Inject; import javax.inject.Singleton; @Singleton -public class AdoptionRepository { - private static final String TAG = "AdoptionRepository"; +public class AdoptionRepository extends BaseRepository { private final AdoptionApi adoptionApi; @Inject public AdoptionRepository(AdoptionApi adoptionApi) { + super("AdoptionRepository"); this.adoptionApi = adoptionApi; } @@ -26,44 +24,34 @@ public class AdoptionRepository { * Retrieves a paginated list of all adoptions from the API. */ public LiveData>> getAllAdoptions(int page, int size) { - MutableLiveData>> data = new MutableLiveData<>(); - RetrofitUtils.enqueue(adoptionApi.getAllAdoptions(page, size), data, TAG); - return data; + return executeCall(adoptionApi.getAllAdoptions(page, size)); } /** * Retrieves a specific adoption record by its ID from the API. */ public LiveData> getAdoptionById(Long id) { - MutableLiveData> data = new MutableLiveData<>(); - RetrofitUtils.enqueue(adoptionApi.getAdoptionById(id), data, TAG); - return data; + return executeCall(adoptionApi.getAdoptionById(id)); } /** * Sends a request to the API to create a new adoption record. */ public LiveData> createAdoption(AdoptionDTO adoption) { - MutableLiveData> data = new MutableLiveData<>(); - RetrofitUtils.enqueue(adoptionApi.createAdoption(adoption), data, TAG); - return data; + return executeCall(adoptionApi.createAdoption(adoption)); } /** * Sends a request to the API to update an existing adoption record by ID. */ public LiveData> updateAdoption(Long id, AdoptionDTO adoption) { - MutableLiveData> data = new MutableLiveData<>(); - RetrofitUtils.enqueue(adoptionApi.updateAdoption(id, adoption), data, TAG); - return data; + return executeCall(adoptionApi.updateAdoption(id, adoption)); } /** * Sends a request to the API to delete a specific adoption record. */ public LiveData> deleteAdoption(Long id) { - MutableLiveData> data = new MutableLiveData<>(); - RetrofitUtils.enqueue(adoptionApi.deleteAdoption(id), data, TAG); - return data; + return executeCall(adoptionApi.deleteAdoption(id)); } } diff --git a/android/app/src/main/java/com/example/petstoremobile/repositories/AppointmentRepository.java b/android/app/src/main/java/com/example/petstoremobile/repositories/AppointmentRepository.java index 61f56842..30e25d0e 100644 --- a/android/app/src/main/java/com/example/petstoremobile/repositories/AppointmentRepository.java +++ b/android/app/src/main/java/com/example/petstoremobile/repositories/AppointmentRepository.java @@ -1,24 +1,22 @@ package com.example.petstoremobile.repositories; import androidx.lifecycle.LiveData; -import androidx.lifecycle.MutableLiveData; import com.example.petstoremobile.api.AppointmentApi; import com.example.petstoremobile.dtos.AppointmentDTO; import com.example.petstoremobile.dtos.PageResponse; import com.example.petstoremobile.utils.Resource; -import com.example.petstoremobile.utils.RetrofitUtils; import javax.inject.Inject; import javax.inject.Singleton; @Singleton -public class AppointmentRepository { - private static final String TAG = "AppointmentRepository"; +public class AppointmentRepository extends BaseRepository { private final AppointmentApi appointmentApi; @Inject public AppointmentRepository(AppointmentApi appointmentApi) { + super("AppointmentRepository"); this.appointmentApi = appointmentApi; } @@ -26,44 +24,34 @@ public class AppointmentRepository { * Retrieves a paginated list of all appointments from the API. */ public LiveData>> getAllAppointments(int page, int size) { - MutableLiveData>> data = new MutableLiveData<>(); - RetrofitUtils.enqueue(appointmentApi.getAllAppointments(page, size), data, TAG); - return data; + return executeCall(appointmentApi.getAllAppointments(page, size)); } /** * Retrieves a specific appointment by its ID from the API. */ public LiveData> getAppointmentById(Long id) { - MutableLiveData> data = new MutableLiveData<>(); - RetrofitUtils.enqueue(appointmentApi.getAppointmentById(id), data, TAG); - return data; + return executeCall(appointmentApi.getAppointmentById(id)); } /** * Sends a request to the API to create a new appointment record. */ public LiveData> createAppointment(AppointmentDTO appointment) { - MutableLiveData> data = new MutableLiveData<>(); - RetrofitUtils.enqueue(appointmentApi.createAppointment(appointment), data, TAG); - return data; + return executeCall(appointmentApi.createAppointment(appointment)); } /** * Sends a request to the API to update an existing appointment record by ID. */ public LiveData> updateAppointment(Long id, AppointmentDTO appointment) { - MutableLiveData> data = new MutableLiveData<>(); - RetrofitUtils.enqueue(appointmentApi.updateAppointment(id, appointment), data, TAG); - return data; + return executeCall(appointmentApi.updateAppointment(id, appointment)); } /** * Sends a request to the API to delete a specific appointment record. */ public LiveData> deleteAppointment(Long id) { - MutableLiveData> data = new MutableLiveData<>(); - RetrofitUtils.enqueue(appointmentApi.deleteAppointment(id), data, TAG); - return data; + return executeCall(appointmentApi.deleteAppointment(id)); } } diff --git a/android/app/src/main/java/com/example/petstoremobile/repositories/AuthRepository.java b/android/app/src/main/java/com/example/petstoremobile/repositories/AuthRepository.java index dec9613e..2ec410f9 100644 --- a/android/app/src/main/java/com/example/petstoremobile/repositories/AuthRepository.java +++ b/android/app/src/main/java/com/example/petstoremobile/repositories/AuthRepository.java @@ -1,7 +1,5 @@ package com.example.petstoremobile.repositories; -import android.util.Log; - import androidx.annotation.NonNull; import androidx.lifecycle.LiveData; import androidx.lifecycle.MutableLiveData; @@ -12,7 +10,6 @@ import com.example.petstoremobile.dtos.AuthDTO; import com.example.petstoremobile.dtos.UserDTO; import com.example.petstoremobile.utils.ErrorUtils; import com.example.petstoremobile.utils.Resource; -import com.example.petstoremobile.utils.RetrofitUtils; import java.util.Map; @@ -25,13 +22,13 @@ import retrofit2.Callback; import retrofit2.Response; @Singleton -public class AuthRepository { - private static final String TAG = "AuthRepository"; +public class AuthRepository extends BaseRepository { private final AuthApi authApi; private final TokenManager tokenManager; @Inject public AuthRepository(AuthApi authApi, TokenManager tokenManager) { + super("AuthRepository"); this.authApi = authApi; this.tokenManager = tokenManager; } @@ -62,7 +59,7 @@ public class AuthRepository { @Override public void onFailure(@NonNull Call call, @NonNull Throwable t) { - data.setValue(Resource.error("Network error: " + t.getMessage(), null)); + data.setValue(Resource.error(ErrorUtils.getFailureMessage(t), null)); } }); @@ -73,36 +70,28 @@ public class AuthRepository { * Retrieves the current user's profile information from the API. */ public LiveData> getMe() { - MutableLiveData> data = new MutableLiveData<>(); - RetrofitUtils.enqueue(authApi.getMe(), data, TAG); - return data; + return executeCall(authApi.getMe()); } /** * Updates the current user's profile details. */ public LiveData> updateMe(Map updates) { - MutableLiveData> data = new MutableLiveData<>(); - RetrofitUtils.enqueue(authApi.updateMe(updates), data, TAG); - return data; + return executeCall(authApi.updateMe(updates)); } /** * Uploads a multipart image to be used as the current user's avatar. */ public LiveData> uploadAvatar(MultipartBody.Part avatar) { - MutableLiveData> data = new MutableLiveData<>(); - RetrofitUtils.enqueue(authApi.uploadAvatar(avatar), data, TAG); - return data; + return executeCall(authApi.uploadAvatar(avatar)); } /** * Sends a request to the API to remove the current user's avatar. */ public LiveData> deleteAvatar() { - MutableLiveData> data = new MutableLiveData<>(); - RetrofitUtils.enqueue(authApi.deleteAvatar(), data, TAG); - return data; + return executeCall(authApi.deleteAvatar()); } /** diff --git a/android/app/src/main/java/com/example/petstoremobile/repositories/BaseRepository.java b/android/app/src/main/java/com/example/petstoremobile/repositories/BaseRepository.java new file mode 100644 index 00000000..cf98dfe8 --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/repositories/BaseRepository.java @@ -0,0 +1,29 @@ +package com.example.petstoremobile.repositories; + +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; + +import com.example.petstoremobile.utils.Resource; +import com.example.petstoremobile.utils.RetrofitUtils; + +import retrofit2.Call; + +/** + * Base class for all repositories to provide common functionality for API calls. + */ +public abstract class BaseRepository { + protected final String TAG; + + protected BaseRepository(String tag) { + this.TAG = tag; + } + + /** + * Executes a Retrofit call and returns a LiveData containing the Resource. + */ + protected LiveData> executeCall(Call call) { + MutableLiveData> data = new MutableLiveData<>(); + RetrofitUtils.enqueue(call, data, TAG); + return data; + } +} diff --git a/android/app/src/main/java/com/example/petstoremobile/repositories/CategoryRepository.java b/android/app/src/main/java/com/example/petstoremobile/repositories/CategoryRepository.java index 74516e2b..8d11511b 100644 --- a/android/app/src/main/java/com/example/petstoremobile/repositories/CategoryRepository.java +++ b/android/app/src/main/java/com/example/petstoremobile/repositories/CategoryRepository.java @@ -1,24 +1,22 @@ package com.example.petstoremobile.repositories; import androidx.lifecycle.LiveData; -import androidx.lifecycle.MutableLiveData; import com.example.petstoremobile.api.CategoryApi; import com.example.petstoremobile.dtos.CategoryDTO; import com.example.petstoremobile.dtos.PageResponse; import com.example.petstoremobile.utils.Resource; -import com.example.petstoremobile.utils.RetrofitUtils; import javax.inject.Inject; import javax.inject.Singleton; @Singleton -public class CategoryRepository { - private static final String TAG = "CategoryRepository"; +public class CategoryRepository extends BaseRepository { private final CategoryApi categoryApi; @Inject public CategoryRepository(CategoryApi categoryApi) { + super("CategoryRepository"); this.categoryApi = categoryApi; } @@ -26,8 +24,6 @@ public class CategoryRepository { * Retrieves a paginated list of all product categories from the API. */ public LiveData>> getAllCategories(int page, int size) { - MutableLiveData>> data = new MutableLiveData<>(); - RetrofitUtils.enqueue(categoryApi.getAllCategories(page, size), data, TAG); - return data; + return executeCall(categoryApi.getAllCategories(page, size)); } } 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 new file mode 100644 index 00000000..8301797d --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/repositories/ChatRepository.java @@ -0,0 +1,74 @@ +package com.example.petstoremobile.repositories; + +import androidx.lifecycle.LiveData; + +import com.example.petstoremobile.api.ChatApi; +import com.example.petstoremobile.api.CustomerApi; +import com.example.petstoremobile.api.MessageApi; +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.utils.Resource; + +import java.util.List; + +import javax.inject.Inject; +import javax.inject.Singleton; + +import okhttp3.MultipartBody; +import okhttp3.RequestBody; + +/** + * Repository for handling chat-related data operations. + */ +@Singleton +public class ChatRepository extends BaseRepository { + private final ChatApi chatApi; + private final MessageApi messageApi; + private final CustomerApi customerApi; + + @Inject + public ChatRepository(ChatApi chatApi, MessageApi messageApi, CustomerApi customerApi) { + super("ChatRepository"); + this.chatApi = chatApi; + this.messageApi = messageApi; + this.customerApi = customerApi; + } + + /** + * Retrieves all chat conversations for the current user. + */ + public LiveData>> getAllConversations() { + return executeCall(chatApi.getAllConversations()); + } + + /** + * Retrieves the message history for a specific conversation. + */ + public LiveData>> getMessages(Long conversationId) { + return executeCall(messageApi.getMessages(conversationId)); + } + + /** + * Sends a plain text message to a conversation. + */ + public LiveData> sendMessage(Long conversationId, SendMessageRequest request) { + return executeCall(messageApi.sendMessage(conversationId, request)); + } + + /** + * Sends a message with a file attachment to a conversation. + */ + public LiveData> sendMessageWithAttachment(Long conversationId, RequestBody content, MultipartBody.Part file) { + return executeCall(messageApi.sendMessageWithAttachment(conversationId, content, file)); + } + + /** + * Fetches a paginated list of customers. + */ + public LiveData>> getAllCustomers(int page, int size) { + return executeCall(customerApi.getAllCustomers(page, size)); + } +} 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 834f9197..4006ae69 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 @@ -1,24 +1,22 @@ package com.example.petstoremobile.repositories; import androidx.lifecycle.LiveData; -import androidx.lifecycle.MutableLiveData; import com.example.petstoremobile.api.CustomerApi; import com.example.petstoremobile.dtos.CustomerDTO; import com.example.petstoremobile.dtos.PageResponse; import com.example.petstoremobile.utils.Resource; -import com.example.petstoremobile.utils.RetrofitUtils; import javax.inject.Inject; import javax.inject.Singleton; @Singleton -public class CustomerRepository { - private static final String TAG = "CustomerRepository"; +public class CustomerRepository extends BaseRepository { private final CustomerApi customerApi; @Inject public CustomerRepository(CustomerApi customerApi) { + super("CustomerRepository"); this.customerApi = customerApi; } @@ -26,17 +24,13 @@ public class CustomerRepository { * Retrieves a paginated list of all customers from the API. */ public LiveData>> getAllCustomers(int page, int size) { - MutableLiveData>> data = new MutableLiveData<>(); - RetrofitUtils.enqueue(customerApi.getAllCustomers(page, size), data, TAG); - return data; + return executeCall(customerApi.getAllCustomers(page, size)); } /** * Retrieves a specific customer by their ID. */ public LiveData> getCustomerById(Long id) { - MutableLiveData> data = new MutableLiveData<>(); - RetrofitUtils.enqueue(customerApi.getCustomerById(id), data, TAG); - return data; + return executeCall(customerApi.getCustomerById(id)); } } diff --git a/android/app/src/main/java/com/example/petstoremobile/repositories/InventoryRepository.java b/android/app/src/main/java/com/example/petstoremobile/repositories/InventoryRepository.java index 4fd2e66d..5513d0e3 100644 --- a/android/app/src/main/java/com/example/petstoremobile/repositories/InventoryRepository.java +++ b/android/app/src/main/java/com/example/petstoremobile/repositories/InventoryRepository.java @@ -1,7 +1,6 @@ package com.example.petstoremobile.repositories; import androidx.lifecycle.LiveData; -import androidx.lifecycle.MutableLiveData; import com.example.petstoremobile.api.InventoryApi; import com.example.petstoremobile.dtos.BulkDeleteRequest; @@ -9,20 +8,17 @@ import com.example.petstoremobile.dtos.InventoryDTO; import com.example.petstoremobile.dtos.InventoryRequest; import com.example.petstoremobile.dtos.PageResponse; import com.example.petstoremobile.utils.Resource; -import com.example.petstoremobile.utils.RetrofitUtils; - -import java.util.List; import javax.inject.Inject; import javax.inject.Singleton; @Singleton -public class InventoryRepository { - private static final String TAG = "InventoryRepository"; +public class InventoryRepository extends BaseRepository { private final InventoryApi inventoryApi; @Inject public InventoryRepository(InventoryApi inventoryApi) { + super("InventoryRepository"); this.inventoryApi = inventoryApi; } @@ -30,47 +26,35 @@ public class InventoryRepository { * Retrieves a paginated list of inventory items from the API with optional search and sort. */ public LiveData>> getAllInventory(String query, int page, int size, String sort) { - MutableLiveData>> data = new MutableLiveData<>(); - RetrofitUtils.enqueue(inventoryApi.getAllInventory(query, page, size, sort), data, TAG); - return data; + return executeCall(inventoryApi.getAllInventory(query, page, size, sort)); } /** * Retrieves a specific inventory item by its ID from the API. */ public LiveData> getInventoryById(Long id) { - MutableLiveData> data = new MutableLiveData<>(); - RetrofitUtils.enqueue(inventoryApi.getInventoryById(id), data, TAG); - return data; + return executeCall(inventoryApi.getInventoryById(id)); } /** * Sends a request to the API to create a new inventory record. */ public LiveData> createInventory(InventoryRequest request) { - MutableLiveData> data = new MutableLiveData<>(); - RetrofitUtils.enqueue(inventoryApi.createInventory(request), data, TAG); - return data; + return executeCall(inventoryApi.createInventory(request)); } public LiveData> updateInventory(Long id, InventoryRequest request) { - MutableLiveData> data = new MutableLiveData<>(); - RetrofitUtils.enqueue(inventoryApi.updateInventory(id, request), data, TAG); - return data; + return executeCall(inventoryApi.updateInventory(id, request)); } /** * Sends a request to the API to delete a specific inventory record. */ public LiveData> deleteInventory(Long id) { - MutableLiveData> data = new MutableLiveData<>(); - RetrofitUtils.enqueue(inventoryApi.deleteInventory(id), data, TAG); - return data; + return executeCall(inventoryApi.deleteInventory(id)); } public LiveData> bulkDeleteInventory(BulkDeleteRequest request) { - MutableLiveData> data = new MutableLiveData<>(); - RetrofitUtils.enqueue(inventoryApi.bulkDeleteInventory(request), data, TAG); - return data; + return executeCall(inventoryApi.bulkDeleteInventory(request)); } } 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 f46bb9e3..64f23b78 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 @@ -1,13 +1,11 @@ package com.example.petstoremobile.repositories; import androidx.lifecycle.LiveData; -import androidx.lifecycle.MutableLiveData; import com.example.petstoremobile.api.PetApi; import com.example.petstoremobile.dtos.PageResponse; import com.example.petstoremobile.dtos.PetDTO; import com.example.petstoremobile.utils.Resource; -import com.example.petstoremobile.utils.RetrofitUtils; import javax.inject.Inject; import javax.inject.Singleton; @@ -15,12 +13,12 @@ import javax.inject.Singleton; import okhttp3.MultipartBody; @Singleton -public class PetRepository { - private static final String TAG = "PetRepository"; +public class PetRepository extends BaseRepository { private final PetApi petApi; @Inject public PetRepository(PetApi petApi) { + super("PetRepository"); this.petApi = petApi; } @@ -28,62 +26,48 @@ public class PetRepository { * Retrieves a paginated list of all pets from the API. */ public LiveData>> getAllPets(int page, int size) { - MutableLiveData>> data = new MutableLiveData<>(); - RetrofitUtils.enqueue(petApi.getAllPets(page, size), data, TAG); - return data; + return executeCall(petApi.getAllPets(page, size)); } /** * Retrieves a specific pet by its ID from the API. */ public LiveData> getPetById(Long id) { - MutableLiveData> data = new MutableLiveData<>(); - RetrofitUtils.enqueue(petApi.getPetById(id), data, TAG); - return data; + return executeCall(petApi.getPetById(id)); } /** * Sends a request to the API to create a new pet record. */ public LiveData> createPet(PetDTO pet) { - MutableLiveData> data = new MutableLiveData<>(); - RetrofitUtils.enqueue(petApi.createPet(pet), data, TAG); - return data; + return executeCall(petApi.createPet(pet)); } /** * Sends a request to the API to update an existing pet record. */ public LiveData> updatePet(Long id, PetDTO pet) { - MutableLiveData> data = new MutableLiveData<>(); - RetrofitUtils.enqueue(petApi.updatePet(id, pet), data, TAG); - return data; + return executeCall(petApi.updatePet(id, pet)); } /** * Sends a request to the API to delete a specific pet record. */ public LiveData> deletePet(Long id) { - MutableLiveData> data = new MutableLiveData<>(); - RetrofitUtils.enqueue(petApi.deletePet(id), data, TAG); - return data; + return executeCall(petApi.deletePet(id)); } /** * Uploads an image file for a specific pet via the API. */ public LiveData> uploadPetImage(Long id, MultipartBody.Part image) { - MutableLiveData> data = new MutableLiveData<>(); - RetrofitUtils.enqueue(petApi.uploadPetImage(id, image), data, TAG); - return data; + return executeCall(petApi.uploadPetImage(id, image)); } /** * Sends a request to the API to delete the image of a specific pet. */ public LiveData> deletePetImage(Long id) { - MutableLiveData> data = new MutableLiveData<>(); - RetrofitUtils.enqueue(petApi.deletePetImage(id), data, TAG); - return data; + return executeCall(petApi.deletePetImage(id)); } } diff --git a/android/app/src/main/java/com/example/petstoremobile/repositories/ProductRepository.java b/android/app/src/main/java/com/example/petstoremobile/repositories/ProductRepository.java index 0baf5c4a..5ed95c8a 100644 --- a/android/app/src/main/java/com/example/petstoremobile/repositories/ProductRepository.java +++ b/android/app/src/main/java/com/example/petstoremobile/repositories/ProductRepository.java @@ -1,13 +1,11 @@ package com.example.petstoremobile.repositories; import androidx.lifecycle.LiveData; -import androidx.lifecycle.MutableLiveData; import com.example.petstoremobile.api.ProductApi; import com.example.petstoremobile.dtos.PageResponse; import com.example.petstoremobile.dtos.ProductDTO; import com.example.petstoremobile.utils.Resource; -import com.example.petstoremobile.utils.RetrofitUtils; import javax.inject.Inject; import javax.inject.Singleton; @@ -15,12 +13,12 @@ import javax.inject.Singleton; import okhttp3.MultipartBody; @Singleton -public class ProductRepository { - private static final String TAG = "ProductRepository"; +public class ProductRepository extends BaseRepository { private final ProductApi productApi; @Inject public ProductRepository(ProductApi productApi) { + super("ProductRepository"); this.productApi = productApi; } @@ -28,62 +26,48 @@ public class ProductRepository { * Retrieves a paginated list of products from the API, filtered by an optional query. */ public LiveData>> getAllProducts(String query, int page, int size) { - MutableLiveData>> data = new MutableLiveData<>(); - RetrofitUtils.enqueue(productApi.getAllProducts(query, page, size), data, TAG); - return data; + return executeCall(productApi.getAllProducts(query, page, size)); } /** * Retrieves a specific product by its ID from the API. */ public LiveData> getProductById(Long id) { - MutableLiveData> data = new MutableLiveData<>(); - RetrofitUtils.enqueue(productApi.getProductById(id), data, TAG); - return data; + return executeCall(productApi.getProductById(id)); } /** * Sends a request to the API to create a new product. */ public LiveData> createProduct(ProductDTO product) { - MutableLiveData> data = new MutableLiveData<>(); - RetrofitUtils.enqueue(productApi.createProduct(product), data, TAG); - return data; + return executeCall(productApi.createProduct(product)); } /** * Sends a request to the API to update an existing product by ID. */ public LiveData> updateProduct(Long id, ProductDTO product) { - MutableLiveData> data = new MutableLiveData<>(); - RetrofitUtils.enqueue(productApi.updateProduct(id, product), data, TAG); - return data; + return executeCall(productApi.updateProduct(id, product)); } /** * Sends a request to the API to delete a specific product. */ public LiveData> deleteProduct(Long id) { - MutableLiveData> data = new MutableLiveData<>(); - RetrofitUtils.enqueue(productApi.deleteProduct(id), data, TAG); - return data; + return executeCall(productApi.deleteProduct(id)); } /** * Uploads an image file for a specific product via the API. */ public LiveData> uploadProductImage(Long id, MultipartBody.Part image) { - MutableLiveData> data = new MutableLiveData<>(); - RetrofitUtils.enqueue(productApi.uploadProductImage(id, image), data, TAG); - return data; + return executeCall(productApi.uploadProductImage(id, image)); } /** * Sends a request to the API to delete the image of a specific product. */ public LiveData> deleteProductImage(Long id) { - MutableLiveData> data = new MutableLiveData<>(); - RetrofitUtils.enqueue(productApi.deleteProductImage(id), data, TAG); - return data; + return executeCall(productApi.deleteProductImage(id)); } } diff --git a/android/app/src/main/java/com/example/petstoremobile/repositories/ProductSupplierRepository.java b/android/app/src/main/java/com/example/petstoremobile/repositories/ProductSupplierRepository.java index e92472c0..72182918 100644 --- a/android/app/src/main/java/com/example/petstoremobile/repositories/ProductSupplierRepository.java +++ b/android/app/src/main/java/com/example/petstoremobile/repositories/ProductSupplierRepository.java @@ -1,24 +1,22 @@ package com.example.petstoremobile.repositories; import androidx.lifecycle.LiveData; -import androidx.lifecycle.MutableLiveData; import com.example.petstoremobile.api.ProductSupplierApi; import com.example.petstoremobile.dtos.PageResponse; import com.example.petstoremobile.dtos.ProductSupplierDTO; import com.example.petstoremobile.utils.Resource; -import com.example.petstoremobile.utils.RetrofitUtils; import javax.inject.Inject; import javax.inject.Singleton; @Singleton -public class ProductSupplierRepository { - private static final String TAG = "ProductSupplierRepository"; +public class ProductSupplierRepository extends BaseRepository { private final ProductSupplierApi api; @Inject public ProductSupplierRepository(ProductSupplierApi api) { + super("ProductSupplierRepository"); this.api = api; } @@ -26,35 +24,34 @@ public class ProductSupplierRepository { * Retrieves a paginated list of all product-supplier relationships from the API. */ public LiveData>> getAllProductSuppliers(int page, int size) { - MutableLiveData>> data = new MutableLiveData<>(); - RetrofitUtils.enqueue(api.getAllProductSuppliers(page, size), data, TAG); - return data; + return executeCall(api.getAllProductSuppliers(page, size)); + } + + /** + * Retrieves a single product-supplier relationship by product and supplier IDs. + */ + public LiveData> getProductSupplierById(Long productId, Long supplierId) { + return executeCall(api.getProductSupplierById(productId, supplierId)); } /** * Sends a request to the API to create a new product-supplier relationship. */ public LiveData> createProductSupplier(ProductSupplierDTO dto) { - MutableLiveData> data = new MutableLiveData<>(); - RetrofitUtils.enqueue(api.createProductSupplier(dto), data, TAG); - return data; + return executeCall(api.createProductSupplier(dto)); } /** * Sends a request to the API to update an existing product-supplier relationship. */ public LiveData> updateProductSupplier(Long productId, Long supplierId, ProductSupplierDTO dto) { - MutableLiveData> data = new MutableLiveData<>(); - RetrofitUtils.enqueue(api.updateProductSupplier(productId, supplierId, dto), data, TAG); - return data; + return executeCall(api.updateProductSupplier(productId, supplierId, dto)); } /** * Sends a request to the API to delete a specific product-supplier relationship. */ public LiveData> deleteProductSupplier(Long productId, Long supplierId) { - MutableLiveData> data = new MutableLiveData<>(); - RetrofitUtils.enqueue(api.deleteProductSupplier(productId, supplierId), data, TAG); - return data; + return executeCall(api.deleteProductSupplier(productId, supplierId)); } } diff --git a/android/app/src/main/java/com/example/petstoremobile/repositories/PurchaseOrderRepository.java b/android/app/src/main/java/com/example/petstoremobile/repositories/PurchaseOrderRepository.java index f00400e5..bd1b224e 100644 --- a/android/app/src/main/java/com/example/petstoremobile/repositories/PurchaseOrderRepository.java +++ b/android/app/src/main/java/com/example/petstoremobile/repositories/PurchaseOrderRepository.java @@ -1,24 +1,22 @@ package com.example.petstoremobile.repositories; import androidx.lifecycle.LiveData; -import androidx.lifecycle.MutableLiveData; import com.example.petstoremobile.api.PurchaseOrderApi; import com.example.petstoremobile.dtos.PageResponse; import com.example.petstoremobile.dtos.PurchaseOrderDTO; import com.example.petstoremobile.utils.Resource; -import com.example.petstoremobile.utils.RetrofitUtils; import javax.inject.Inject; import javax.inject.Singleton; @Singleton -public class PurchaseOrderRepository { - private static final String TAG = "PurchaseOrderRepo"; +public class PurchaseOrderRepository extends BaseRepository { private final PurchaseOrderApi api; @Inject public PurchaseOrderRepository(PurchaseOrderApi api) { + super("PurchaseOrderRepo"); this.api = api; } @@ -26,17 +24,13 @@ public class PurchaseOrderRepository { * Retrieves a paginated list of all purchase orders from the API. */ public LiveData>> getAllPurchaseOrders(int page, int size) { - MutableLiveData>> data = new MutableLiveData<>(); - RetrofitUtils.enqueue(api.getAllPurchaseOrders(page, size), data, TAG); - return data; + return executeCall(api.getAllPurchaseOrders(page, size)); } /** * Retrieves a specific purchase order by its ID from the API. */ public LiveData> getPurchaseOrderById(Long id) { - MutableLiveData> data = new MutableLiveData<>(); - RetrofitUtils.enqueue(api.getPurchaseOrderById(id), data, TAG); - return data; + return executeCall(api.getPurchaseOrderById(id)); } } diff --git a/android/app/src/main/java/com/example/petstoremobile/repositories/ServiceRepository.java b/android/app/src/main/java/com/example/petstoremobile/repositories/ServiceRepository.java index ca6021e6..eac8bb32 100644 --- a/android/app/src/main/java/com/example/petstoremobile/repositories/ServiceRepository.java +++ b/android/app/src/main/java/com/example/petstoremobile/repositories/ServiceRepository.java @@ -1,24 +1,22 @@ package com.example.petstoremobile.repositories; import androidx.lifecycle.LiveData; -import androidx.lifecycle.MutableLiveData; import com.example.petstoremobile.api.ServiceApi; import com.example.petstoremobile.dtos.PageResponse; import com.example.petstoremobile.dtos.ServiceDTO; import com.example.petstoremobile.utils.Resource; -import com.example.petstoremobile.utils.RetrofitUtils; import javax.inject.Inject; import javax.inject.Singleton; @Singleton -public class ServiceRepository { - private static final String TAG = "ServiceRepository"; +public class ServiceRepository extends BaseRepository { private final ServiceApi serviceApi; @Inject public ServiceRepository(ServiceApi serviceApi) { + super("ServiceRepository"); this.serviceApi = serviceApi; } @@ -26,44 +24,34 @@ public class ServiceRepository { * Retrieves a paginated list of all services from the API. */ public LiveData>> getAllServices(int page, int size) { - MutableLiveData>> data = new MutableLiveData<>(); - RetrofitUtils.enqueue(serviceApi.getAllServices(page, size), data, TAG); - return data; + return executeCall(serviceApi.getAllServices(page, size)); } /** * Retrieves a specific service by its ID from the API. */ public LiveData> getServiceById(Long id) { - MutableLiveData> data = new MutableLiveData<>(); - RetrofitUtils.enqueue(serviceApi.getServiceById(id), data, TAG); - return data; + return executeCall(serviceApi.getServiceById(id)); } /** * Sends a request to the API to create a new service. */ public LiveData> createService(ServiceDTO service) { - MutableLiveData> data = new MutableLiveData<>(); - RetrofitUtils.enqueue(serviceApi.createService(service), data, TAG); - return data; + return executeCall(serviceApi.createService(service)); } /** * Sends a request to the API to update an existing service by ID. */ public LiveData> updateService(Long id, ServiceDTO service) { - MutableLiveData> data = new MutableLiveData<>(); - RetrofitUtils.enqueue(serviceApi.updateService(id, service), data, TAG); - return data; + return executeCall(serviceApi.updateService(id, service)); } /** * Sends a request to the API to delete a specific service. */ public LiveData> deleteService(Long id) { - MutableLiveData> data = new MutableLiveData<>(); - RetrofitUtils.enqueue(serviceApi.deleteService(id), data, TAG); - return data; + return executeCall(serviceApi.deleteService(id)); } } 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 a71a7ffe..44781a32 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 @@ -1,24 +1,22 @@ package com.example.petstoremobile.repositories; import androidx.lifecycle.LiveData; -import androidx.lifecycle.MutableLiveData; import com.example.petstoremobile.api.StoreApi; import com.example.petstoremobile.dtos.PageResponse; import com.example.petstoremobile.dtos.StoreDTO; import com.example.petstoremobile.utils.Resource; -import com.example.petstoremobile.utils.RetrofitUtils; import javax.inject.Inject; import javax.inject.Singleton; @Singleton -public class StoreRepository { - private static final String TAG = "StoreRepository"; +public class StoreRepository extends BaseRepository { private final StoreApi storeApi; @Inject public StoreRepository(StoreApi storeApi) { + super("StoreRepository"); this.storeApi = storeApi; } @@ -26,8 +24,6 @@ public class StoreRepository { * Retrieves a paginated list of all stores from the API. */ public LiveData>> getAllStores(int page, int size) { - MutableLiveData>> data = new MutableLiveData<>(); - RetrofitUtils.enqueue(storeApi.getAllStores(page, size), data, TAG); - return data; + return executeCall(storeApi.getAllStores(page, size)); } } diff --git a/android/app/src/main/java/com/example/petstoremobile/repositories/SupplierRepository.java b/android/app/src/main/java/com/example/petstoremobile/repositories/SupplierRepository.java index f169f415..ec0489ca 100644 --- a/android/app/src/main/java/com/example/petstoremobile/repositories/SupplierRepository.java +++ b/android/app/src/main/java/com/example/petstoremobile/repositories/SupplierRepository.java @@ -1,24 +1,22 @@ package com.example.petstoremobile.repositories; import androidx.lifecycle.LiveData; -import androidx.lifecycle.MutableLiveData; import com.example.petstoremobile.api.SupplierApi; import com.example.petstoremobile.dtos.PageResponse; import com.example.petstoremobile.dtos.SupplierDTO; import com.example.petstoremobile.utils.Resource; -import com.example.petstoremobile.utils.RetrofitUtils; import javax.inject.Inject; import javax.inject.Singleton; @Singleton -public class SupplierRepository { - private static final String TAG = "SupplierRepository"; +public class SupplierRepository extends BaseRepository { private final SupplierApi supplierApi; @Inject public SupplierRepository(SupplierApi supplierApi) { + super("SupplierRepository"); this.supplierApi = supplierApi; } @@ -26,44 +24,34 @@ public class SupplierRepository { * Retrieves a paginated list of all suppliers from the API. */ public LiveData>> getAllSuppliers(int page, int size) { - MutableLiveData>> data = new MutableLiveData<>(); - RetrofitUtils.enqueue(supplierApi.getAllSuppliers(page, size), data, TAG); - return data; + return executeCall(supplierApi.getAllSuppliers(page, size)); } /** * Retrieves a specific supplier by its ID from the API. */ public LiveData> getSupplierById(Long id) { - MutableLiveData> data = new MutableLiveData<>(); - RetrofitUtils.enqueue(supplierApi.getSupplierById(id), data, TAG); - return data; + return executeCall(supplierApi.getSupplierById(id)); } /** * Sends a request to the API to create a new supplier record. */ public LiveData> createSupplier(SupplierDTO supplier) { - MutableLiveData> data = new MutableLiveData<>(); - RetrofitUtils.enqueue(supplierApi.createSupplier(supplier), data, TAG); - return data; + return executeCall(supplierApi.createSupplier(supplier)); } /** * Sends a request to the API to update an existing supplier record by ID. */ public LiveData> updateSupplier(Long id, SupplierDTO supplier) { - MutableLiveData> data = new MutableLiveData<>(); - RetrofitUtils.enqueue(supplierApi.updateSupplier(id, supplier), data, TAG); - return data; + return executeCall(supplierApi.updateSupplier(id, supplier)); } /** * Sends a request to the API to delete a specific supplier record. */ public LiveData> deleteSupplier(Long id) { - MutableLiveData> data = new MutableLiveData<>(); - RetrofitUtils.enqueue(supplierApi.deleteSupplier(id), data, TAG); - return data; + return executeCall(supplierApi.deleteSupplier(id)); } } diff --git a/android/app/src/main/java/com/example/petstoremobile/utils/ErrorUtils.java b/android/app/src/main/java/com/example/petstoremobile/utils/ErrorUtils.java index a1e69f5f..82a10cf7 100644 --- a/android/app/src/main/java/com/example/petstoremobile/utils/ErrorUtils.java +++ b/android/app/src/main/java/com/example/petstoremobile/utils/ErrorUtils.java @@ -5,12 +5,19 @@ import android.util.Log; import android.widget.Toast; import com.example.petstoremobile.dtos.ErrorResponse; import com.google.gson.Gson; +import java.io.IOException; +import java.net.ConnectException; +import java.net.SocketTimeoutException; +import java.net.UnknownHostException; import retrofit2.Response; /** * Utility class for handling API error responses. */ public class ErrorUtils { + private static final String TAG = "ErrorUtils"; + private static final Gson gson = new Gson(); + /** * Shows an error message to toast based on the response. */ @@ -19,20 +26,46 @@ public class ErrorUtils { } /** - * Extracts the error message from the response body. + * Extracts a user-friendly error message from the response body or status code. */ public static String getErrorMessage(Response response, String defaultMessage) { + if (response == null) return defaultMessage; + try { - if (response != null && response.errorBody() != null) { + if (response.errorBody() != null) { String errorJson = response.errorBody().string(); - ErrorResponse errorResponse = new Gson().fromJson(errorJson, ErrorResponse.class); + ErrorResponse errorResponse = gson.fromJson(errorJson, ErrorResponse.class); if (errorResponse != null && errorResponse.getMessage() != null) { return errorResponse.getMessage(); } } } catch (Exception e) { - Log.e("ErrorUtils", "Error parsing error body", e); + Log.e(TAG, "Error parsing error body", e); + } + + // Handle specific status codes if no message was provided by the API + switch (response.code()) { + case 401: return "Unauthorized. Please login again."; + case 403: return "Access denied."; + case 404: return "Resource not found."; + case 500: return "Internal server error. Please try again later."; + case 503: return "Service unavailable. The server might be down."; + default: return defaultMessage + " (Code: " + response.code() + ")"; + } + } + + /** + * Converts a Throwable (from onFailure) into a user-friendly network error message. + */ + public static String getFailureMessage(Throwable t) { + if (t instanceof UnknownHostException || t instanceof ConnectException) { + return "No internet connection. Please check your settings."; + } else if (t instanceof SocketTimeoutException) { + return "The connection timed out. Please try again."; + } else if (t instanceof IOException) { + return "Network error occurred. Please try again."; + } else { + return "An unexpected error occurred: " + t.getLocalizedMessage(); } - return defaultMessage; } } diff --git a/android/app/src/main/java/com/example/petstoremobile/utils/RetrofitUtils.java b/android/app/src/main/java/com/example/petstoremobile/utils/RetrofitUtils.java index 3584f845..f86a6307 100644 --- a/android/app/src/main/java/com/example/petstoremobile/utils/RetrofitUtils.java +++ b/android/app/src/main/java/com/example/petstoremobile/utils/RetrofitUtils.java @@ -25,7 +25,7 @@ public class RetrofitUtils { } /** - * call and updates the provided MutableLiveData with Resource states. + * Enqueues a Retrofit call and updates the provided MutableLiveData with Resource states. */ public static void enqueue(@NonNull Call call, @NonNull MutableLiveData> data, String tag) { data.setValue(Resource.loading(null)); @@ -43,8 +43,8 @@ public class RetrofitUtils { @Override public void onFailure(@NonNull Call call, @NonNull Throwable t) { - String errorMsg = "Network Error: " + t.getMessage(); - Log.e(tag, errorMsg); + String errorMsg = ErrorUtils.getFailureMessage(t); + Log.e(tag, "Network Error: " + t.getMessage(), t); data.setValue(Resource.error(errorMsg, null)); } }); @@ -52,6 +52,7 @@ public class RetrofitUtils { /** * Creates a callback for Retrofit calls that handles errors and logging. + * @deprecated Use {@link #enqueue(Call, MutableLiveData, String)} for LiveData-based architecture. */ @Deprecated public static Callback createCallback(Context context, String tag, String successMsg, SuccessCallback successCallback) { @@ -73,8 +74,9 @@ public class RetrofitUtils { @Override public void onFailure(@NonNull Call call, @NonNull Throwable t) { - Log.e(tag, "Network Error: " + t.getMessage()); - Toast.makeText(context, "Network error. Please try again.", Toast.LENGTH_SHORT).show(); + String errorMsg = ErrorUtils.getFailureMessage(t); + Log.e(tag, "Network Error: " + t.getMessage(), t); + Toast.makeText(context, errorMsg, Toast.LENGTH_SHORT).show(); } }; } @@ -96,7 +98,7 @@ public class RetrofitUtils { @Override public void onFailure(@NonNull Call call, @NonNull Throwable t) { - Log.e(tag, "Network Error: " + t.getMessage()); + Log.e(tag, "Network Error: " + t.getMessage(), t); } }; } 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 new file mode 100644 index 00000000..51435f82 --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/ChatViewModel.java @@ -0,0 +1,68 @@ +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; +import okhttp3.MultipartBody; +import okhttp3.RequestBody; + +/** + * 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); + } + + /** + * Sends a message with a file attachment to a conversation. + */ + public LiveData> sendMessageWithAttachment(Long conversationId, RequestBody content, MultipartBody.Part file) { + return repository.sendMessageWithAttachment(conversationId, content, file); + } + + /** + * Fetches a paginated list of customers. + */ + public LiveData>> getAllCustomers(int page, int size) { + return repository.getAllCustomers(page, size); + } +} diff --git a/android/gradle/libs.versions.toml b/android/gradle/libs.versions.toml index c7a01abf..f1b9518b 100644 --- a/android/gradle/libs.versions.toml +++ b/android/gradle/libs.versions.toml @@ -5,11 +5,16 @@ junitVersion = "1.3.0" espressoCore = "3.7.0" appcompat = "1.7.1" material = "1.13.0" -activity = "1.12.4" +activity = "1.13.0" constraintlayout = "2.2.1" swiperefreshlayout = "1.2.0" hilt = "2.51.1" navigation = "2.8.8" +retrofit = "2.11.0" +okhttp = "4.12.0" +glide = "4.16.0" +viewpager2 = "1.1.0" +camera = "1.4.1" [libraries] junit = { group = "junit", name = "junit", version.ref = "junit" } @@ -25,6 +30,23 @@ hilt-compiler = { group = "com.google.dagger", name = "hilt-compiler", version.r navigation-fragment = { group = "androidx.navigation", name = "navigation-fragment", version.ref = "navigation" } navigation-ui = { group = "androidx.navigation", name = "navigation-ui", version.ref = "navigation" } +# Networking +retrofit = { group = "com.squareup.retrofit2", name = "retrofit", version.ref = "retrofit" } +retrofit-gson = { group = "com.squareup.retrofit2", name = "converter-gson", version.ref = "retrofit" } +okhttp = { group = "com.squareup.okhttp3", name = "okhttp", version.ref = "okhttp" } +okhttp-logging = { group = "com.squareup.okhttp3", name = "logging-interceptor", version.ref = "okhttp" } + +# UI Components +viewpager2 = { group = "androidx.viewpager2", name = "viewpager2", version.ref = "viewpager2" } +glide = { group = "com.github.bumptech.glide", name = "glide", version.ref = "glide" } +glide-compiler = { group = "com.github.bumptech.glide", name = "compiler", version.ref = "glide" } + +# CameraX +camera-core = { group = "androidx.camera", name = "camera-core", version.ref = "camera" } +camera-camera2 = { group = "androidx.camera", name = "camera-camera2", version.ref = "camera" } +camera-lifecycle = { group = "androidx.camera", name = "camera-lifecycle", version.ref = "camera" } +camera-view = { group = "androidx.camera", name = "camera-view", version.ref = "camera" } + [plugins] android-application = { id = "com.android.application", version.ref = "agp" } hilt = { id = "com.google.dagger.hilt.android", version.ref = "hilt" } diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties index 7e694390..5ef50837 100644 --- a/android/gradle/wrapper/gradle-wrapper.properties +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -1,8 +1,7 @@ #Sun Mar 01 14:36:37 MST 2026 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionSha256Sum=a17ddd85a26b6a7f5ddb71ff8b05fc5104c0202c6e64782429790c933686c806 -distributionUrl=https\://services.gradle.org/distributions/gradle-9.1.0-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME