Added Helper class and commented most fragments

This commit is contained in:
Alex
2026-04-05 17:16:40 -06:00
parent 0c75ffbf35
commit 65a8475e47
30 changed files with 1226 additions and 666 deletions

View File

@@ -38,6 +38,9 @@ public class HomeActivity extends AppCompatActivity {
}
});
/**
* Sets up the home screen, initializes bottom navigation, and handles incoming navigation intents.
*/
@Override
protected void onCreate(Bundle savedInstanceState) {
EdgeToEdge.enable(this);
@@ -71,12 +74,18 @@ public class HomeActivity extends AppCompatActivity {
requestNotificationPermission();
}
/**
* Handles new intents received while the activity is already running (like notifications).
*/
@Override
protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);
handleIntent(intent);
}
/**
* Processes the intent to determine if specific navigation (like opening a chat) is required.
*/
private void handleIntent(Intent intent) {
if (intent != null && "chat".equals(intent.getStringExtra("navigate_to"))) {
Bundle args = new Bundle();
@@ -90,14 +99,17 @@ public class HomeActivity extends AppCompatActivity {
}
}
// Function to start the notification service in the background
// to receive notifications when a new conversation is created
/**
* Starts the background service responsible for monitoring chat notifications.
*/
private void startNotificationService() {
Intent serviceIntent = new Intent(this, ChatNotificationService.class);
startService(serviceIntent);
}
//Function to request for notification permission
/**
* Requests POST_NOTIFICATIONS permission from the user if running on Android 13 and above.
*/
private void requestNotificationPermission() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
if (ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) {
@@ -105,4 +117,4 @@ public class HomeActivity extends AppCompatActivity {
}
}
}
}
}

View File

@@ -39,6 +39,9 @@ public class MainActivity extends AppCompatActivity {
@Inject TokenManager tokenManager;
@Inject @Named("baseUrl") String baseUrl;
/**
* Initializes the activity, sets up the UI, and checks for an existing login session.
*/
@Override
protected void onCreate(Bundle savedInstanceState) {
EdgeToEdge.enable(this);
@@ -98,6 +101,9 @@ public class MainActivity extends AppCompatActivity {
});
}
/**
* Executes the login process using the AuthViewModel and handles the authentication response.
*/
private void performLogin(String username, String password) {
viewModel.login(username, password).observe(this, resource -> {
if (resource == null) return;
@@ -129,6 +135,9 @@ public class MainActivity extends AppCompatActivity {
});
}
/**
* Retrieves the logged-in user's profile information to save their ID before navigating to the home screen.
*/
private void fetchUserIdAndNavigate() {
viewModel.getMe().observe(this, resource -> {
if (resource != null && resource.status != Resource.Status.LOADING) {

View File

@@ -9,13 +9,10 @@ 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.api.PetApi;
import com.example.petstoremobile.dtos.PetDTO;
import com.example.petstoremobile.utils.GlideUtils;
import java.util.List;
public class PetAdapter extends RecyclerView.Adapter<PetAdapter.PetViewHolder> {
@@ -93,25 +90,10 @@ public class PetAdapter extends RecyclerView.Adapter<PetAdapter.PetViewHolder> {
holder.tvPetStatus.setBackgroundColor(Color.parseColor("#F44336"));
}
// Load pet image using Glide with circle crop
// Load pet image using Glide
if (baseUrl != null) {
String imageUrl = baseUrl + String.format(PetApi.PET_IMAGE_PATH, pet.getPetId());
Object loadTarget = imageUrl;
if (token != null) {
loadTarget = new GlideUrl(imageUrl, new LazyHeaders.Builder()
.addHeader("Authorization", "Bearer " + token)
.build());
}
Glide.with(holder.itemView.getContext())
.load(loadTarget)
.circleCrop()
.skipMemoryCache(true)
.diskCacheStrategy(DiskCacheStrategy.NONE)
.placeholder(R.drawable.placeholder)
.error(R.drawable.placeholder)
.into(holder.ivPetProfile);
GlideUtils.loadImageWithTokenCircle(holder.itemView.getContext(), holder.ivPetProfile, imageUrl, token, R.drawable.placeholder);
} else {
holder.ivPetProfile.setImageResource(R.drawable.placeholder);
}

View File

@@ -6,13 +6,10 @@ 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.api.ProductApi;
import com.example.petstoremobile.dtos.ProductDTO;
import com.example.petstoremobile.utils.GlideUtils;
import java.util.List;
public class ProductAdapter extends RecyclerView.Adapter<ProductAdapter.ProductViewHolder> {
@@ -72,22 +69,7 @@ public class ProductAdapter extends RecyclerView.Adapter<ProductAdapter.ProductV
// Load product image using Glide
if (baseUrl != null) {
String imageUrl = baseUrl + String.format(ProductApi.PRODUCT_IMAGE_PATH, p.getProdId());
Object loadTarget = imageUrl;
if (token != null) {
loadTarget = new GlideUrl(imageUrl, new LazyHeaders.Builder()
.addHeader("Authorization", "Bearer " + token)
.build());
}
Glide.with(holder.itemView.getContext())
.load(loadTarget)
.circleCrop()
.skipMemoryCache(true)
.diskCacheStrategy(DiskCacheStrategy.NONE)
.placeholder(R.drawable.placeholder)
.error(R.drawable.placeholder)
.into(holder.ivProductImage);
GlideUtils.loadImageWithTokenCircle(holder.itemView.getContext(), holder.ivProductImage, imageUrl, token, R.drawable.placeholder);
} else {
holder.ivProductImage.setImageResource(R.drawable.placeholder);
}

View File

@@ -88,7 +88,9 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis
private StompChatManager stompChatManager;
private ActivityResultLauncher<Intent> attachmentLauncher;
/**
* Initializes the attachment launcher to handle file selection from the gallery.
*/
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
@@ -105,6 +107,9 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis
);
}
/**
* Inflates the layout, initializes UI components, and sets up click listeners for messaging.
*/
@Override
public View onCreateView(@NonNull LayoutInflater inflater,
ViewGroup container, Bundle savedInstanceState) {
@@ -155,7 +160,9 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis
return view;
}
// Helper function to setup recycler views for chat and messages
/**
* Configures the RecyclerViews for the conversation list and the message history.
*/
private void setupRecyclerViews() {
// Set up Drawer menu to select conversation
chatAdapter = new ChatAdapter(chatList, this);
@@ -171,7 +178,9 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis
setConversationActive(false);
}
//Helper function to load token and user id then connect to websocket
/**
* Loads authentication tokens and user info, then initializes the Stomp WebSocket connection.
*/
private void loadInitialData() {
String token = tokenManager.getToken();
currentUserId = tokenManager.getUserId();
@@ -198,7 +207,9 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis
loadCustomers();
}
//Helper function to load customer names for it to be displayed on drawer menu
/**
* Fetches a list of customers from the API to display customer names for the chat list.
*/
private void loadCustomers() {
customerApi.getAllCustomers(0, 100).enqueue(new Callback<PageResponse<CustomerDTO>>() {
@Override
@@ -220,7 +231,9 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis
});
}
//helper function to load conversations entities to display with customer names in drawer menu
/**
* Retrieves all conversations for the current user and populates the chat drawer.
*/
private void loadConversations() {
chatApi.getAllConversations().enqueue(new Callback<List<ConversationDTO>>() {
@Override
@@ -268,8 +281,9 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis
});
}
// Called when user taps a chat in the drawer
// Loads messages for that chat selected
/**
* Handles selection of a chat from the drawer, updating the UI and subscribing to the WebSocket.
*/
@Override
public void onChatClick(Chat chat) {
activeConversationId = Long.parseLong(chat.getChatId());
@@ -284,7 +298,9 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis
loadMessageHistory(activeConversationId);
}
//helper function to load messages for selected chat
/**
* Fetches the full message history for a specific conversation from the API.
*/
private void loadMessageHistory(Long conversationId) {
messageApi.getMessages(conversationId).enqueue(new Callback<List<MessageDTO>>() {
@Override
@@ -307,7 +323,9 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis
});
}
//Helper function to send a message to the chat
/**
* Sends a plain text message to the currently active conversation.
*/
private void sendMessage() {
//check if a chat is selected
if (activeConversationId == null) return;
@@ -340,14 +358,18 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis
});
}
//Helper function to open file picker when the attachment button is clicked
/**
* Launches a file picker intent to select an attachment for the message.
*/
private void selectAttachment() {
Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
intent.setType("*/*");
attachmentLauncher.launch(intent);
}
//Helper function to show the attachment preview
/**
* Displays a preview of the selected attachment in the UI.
*/
private void showAttachmentPreview(Uri uri) {
pendingAttachmentUri = uri;
layoutAttachmentPreview.setVisibility(View.VISIBLE);
@@ -365,13 +387,17 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis
}
}
//Helper function to remove the attachment
/**
* Clears the current attachment selection and hides the preview UI.
*/
private void removeAttachment() {
pendingAttachmentUri = null;
layoutAttachmentPreview.setVisibility(View.GONE);
}
//Helper function to get the file name from the uri to display in attachment preview
/**
* Show the display name of the file from its Uri.
*/
private String getFileName(Uri uri) {
String result = null;
if (uri.getScheme().equals("content")) {
@@ -394,7 +420,9 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis
return result;
}
//Helper function to send the message with attachment
/**
* Handles sending a message that includes a file attachment.
*/
private void sendWithAttachment(Uri uri) {
if (activeConversationId == null) return;
@@ -402,7 +430,9 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis
Log.d(TAG, "Send with attachment happening");
}
// When a message is received updates the chat preview
/**
* Callback triggered when a new message is received via the WebSocket.
*/
@Override
public void onMessageReceived(MessageDTO dto) {
//if there is no active selected conversation or the message received is for another chat, then just update the preview of last message
@@ -420,7 +450,9 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis
scrollToBottom();
}
// When a new conversation is added, updates the chat preview
/**
* Callback triggered when a conversation is created or updated via the WebSocket.
*/
@Override
public void onConversationUpdated(ConversationDTO dto) {
boolean updated = false;
@@ -460,6 +492,9 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis
}
}
/**
* Callback triggered when the WebSocket connection is successfully opened.
*/
@Override
public void onSocketOpened() {
if (!isAdded()) {
@@ -471,6 +506,9 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis
}
}
/**
* Callback triggered when the WebSocket connection is closed.
*/
@Override
public void onSocketClosed() {
if (!isAdded()) {
@@ -479,6 +517,9 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis
loadConversations();
}
/**
* Callback triggered when a WebSocket connection error occurs.
*/
@Override
public void onSocketError() {
if (!isAdded()) {
@@ -490,7 +531,9 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis
}
}
// Helper function to convert DTO to message
/**
* Converts a MessageDTO into a Message object.
*/
private Message dtoToModel(MessageDTO dto) {
Message m = new Message();
m.setId(dto.getId());
@@ -505,7 +548,9 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis
return m;
}
//Helper function to scroll to bottom of the chat
/**
* Scrolls the message history RecyclerView to the most recent message.
*/
private void scrollToBottom() {
if (!messageList.isEmpty()) {
rvMessages.post(() ->
@@ -513,7 +558,9 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis
}
}
// Helper function to update the chat preview last message
/**
* Updates the preview snippet of the last message for a specific conversation in the drawer.
*/
private void updateConversationPreview(Long conversationId, String lastMessage) {
if (conversationId == null) {
return;
@@ -535,7 +582,9 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis
}
}
//Helper function to enable or disable the send button when there is no active chat
/**
* Toggles the UI state based on whether a conversation is currently selected.
*/
private void setConversationActive(boolean active) {
btnSend.setEnabled(active);
etMessage.setEnabled(active);
@@ -558,11 +607,13 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis
}
}
// When fragment is destroyed, disconnect from websocket
/**
* Disconnects the WebSocket manager when the fragment view is destroyed.
*/
@Override
public void onDestroyView() {
super.onDestroyView();
ChatNotificationService.activeConversationIdInUi = null;
if (stompChatManager != null) stompChatManager.disconnect();
}
}
}

View File

@@ -38,6 +38,9 @@ public class ListFragment extends Fragment {
@Inject TokenManager tokenManager;
/**
* Inflates the fragment layout, initializes navigation drawers, and applies role-based access control.
*/
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
@@ -106,6 +109,9 @@ public class ListFragment extends Fragment {
return view;
}
/**
* Initializes the NavController for the internal fragment container.
*/
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
@@ -116,6 +122,9 @@ public class ListFragment extends Fragment {
}
}
/**
* Navigates to a specific inner destination and closes all drawers.
*/
private void navigateTo(int destinationId) {
if (innerNavController != null) {
innerNavController.navigate(destinationId);
@@ -123,7 +132,9 @@ public class ListFragment extends Fragment {
drawerLayout.closeDrawers();
}
//helper function to open the drawer
/**
* Programmatically opens the navigation drawer.
*/
public void openDrawer() {
drawerLayout.openDrawer(GravityCompat.START);
}

View File

@@ -1,21 +1,11 @@
package com.example.petstoremobile.fragments;
import android.Manifest;
import android.app.Activity;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Bundle;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.appcompat.app.AlertDialog;
import androidx.core.content.ContextCompat;
import androidx.core.content.FileProvider;
import androidx.fragment.app.Fragment;
import android.provider.MediaStore;
import android.text.InputType;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
@@ -26,26 +16,21 @@ import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast;
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.activities.MainActivity;
import com.example.petstoremobile.api.auth.AuthApi;
import com.example.petstoremobile.api.auth.TokenManager;
import com.example.petstoremobile.dtos.ErrorResponse;
import com.example.petstoremobile.dtos.UserDTO;
import com.example.petstoremobile.services.ChatNotificationService;
import com.example.petstoremobile.utils.ErrorUtils;
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.google.gson.Gson;
import com.example.petstoremobile.utils.UIUtils;
import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.inject.Inject;
@@ -59,13 +44,15 @@ import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
/**
* Fragment that displays and allows editing of the user's profile information.
*/
@AndroidEntryPoint
public class ProfileFragment extends Fragment {
//initialize the view/controls
private ImageView imgProfile;
private TextView tvProfileName, tvProfileEmail, tvProfilePhone, tvProfileRole;
private Uri photoUri;
private UserDTO currentUser;
private boolean hasImage = false;
@@ -73,71 +60,31 @@ public class ProfileFragment extends Fragment {
@Inject TokenManager tokenManager;
@Inject @Named("baseUrl") String baseUrl;
//Initialize the launchers for camera and gallery
private ActivityResultLauncher<Intent> galleryLauncher;
private ActivityResultLauncher<Uri> cameraLauncher;
private ActivityResultLauncher<String> permissionLauncher;
private ImagePickerHelper imagePickerHelper;
//Called when the fragment is created, sets up the launchers is set profile image
/**
* Initializes activity launchers and the ImagePickerHelper for camera and gallary.
*/
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Launcher to open gallery to select profile image
galleryLauncher = registerForActivityResult(
//open gallery
new ActivityResultContracts.StartActivityForResult(),
result -> {
//if the user selects an image and its not null
if (result.getResultCode() == Activity.RESULT_OK
&& result.getData() != null) {
//get the selected image and set the image to the profile
Uri selectedImage = result.getData().getData();
uploadAvatar(selectedImage);
}
}
);
imagePickerHelper = new ImagePickerHelper(this, "profile_photo.jpg", new ImagePickerHelper.ImagePickerListener() {
@Override
public void onImagePicked(Uri uri) {
uploadAvatar(uri);
}
// Launcher for camera to open and capture profile image
cameraLauncher = registerForActivityResult(
//open camera
new ActivityResultContracts.TakePicture(),
success -> {
//if a photo is taken set the image profile to it otherwise do nothing
if (success) {
uploadAvatar(photoUri);
}
}
);
// Launcher to request camera permission
permissionLauncher = registerForActivityResult(
//ask user for camera permission
new ActivityResultContracts.RequestPermission(),
granted -> {
//if the permission is granted launch the camera
if (granted) {
launchCamera();
}
else {
//if the permission is denied then tell the user to grant it
new AlertDialog.Builder(requireContext())
.setTitle("Permission Permission Required")
.setMessage("Please grant camera permission to use this feature")
.setPositiveButton("Open Settings", (dialog, which) ->{
//open the settings page to grant the permission when they click open settings
Intent intent = new Intent(android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
intent.setData(Uri.fromParts("package", requireContext().getPackageName(), null));
startActivity(intent);
})
//close the dialog when the user clicks cancel
.setNegativeButton("Cancel", null)
.show();
}
}
);
@Override
public void onImageRemoved() {
deleteAvatar();
}
});
}
/**
* Inflates the fragment layout and sets up listeners for profile.
*/
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
@@ -160,39 +107,7 @@ public class ProfileFragment extends Fragment {
//Set up listeners for the buttons
//Change photo button
btnChangePhoto.setOnClickListener(v -> {
List<String> options = new ArrayList<>();
options.add("Take Photo");
options.add("Choose from Gallery");
if (hasImage) {
options.add("Remove Photo");
}
//Show alert dialog to user to select from gallery or camera
new AlertDialog.Builder(requireContext())
.setTitle("Change Profile Photo")
//set the options for the alert dialog
.setItems(options.toArray(new String[0]), (dialog, which) -> {
String selected = options.get(which);
if (selected.equals("Take Photo")) {
// Choose Camera
//Checks if the user has granted the camera permission already
if (ContextCompat.checkSelfPermission(requireContext(), Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED) {
//if the permission is already granted then launch the camera
launchCamera();
} else {
//otherwise request the permission
permissionLauncher.launch(Manifest.permission.CAMERA);
}
} else if (selected.equals("Choose from Gallery")) {
// Choose Gallery
Intent intent = new Intent(Intent.ACTION_PICK,
MediaStore.Images.Media.EXTERNAL_CONTENT_URI);
galleryLauncher.launch(intent);
} else if (selected.equals("Remove Photo")) {
deleteAvatar();
}
})
.show();
imagePickerHelper.showImagePickerDialog("Change Profile Photo", hasImage);
});
//Edit email button
@@ -231,11 +146,10 @@ public class ProfileFragment extends Fragment {
input.setText(tvProfilePhone.getText().toString());
//set input type to phone number
input.setInputType(InputType.TYPE_CLASS_PHONE);
input.setInputType(android.view.inputmethod.EditorInfo.TYPE_CLASS_PHONE);
//add canada phone number formatting to input (XXX) XXX-XXXX
input.addTextChangedListener(new android.telephony.PhoneNumberFormattingTextWatcher("CA"));
input.setFilters(new android.text.InputFilter[]{new android.text.InputFilter.LengthFilter(14)});
//add canada phone number formatting to input
UIUtils.formatPhoneInput(input);
//Show alert dialog to user to enter new phone
@@ -256,13 +170,13 @@ public class ProfileFragment extends Fragment {
//Logout button
btnLogout.setOnClickListener(v -> {
// Stop notification service before logging out so notifications stop
Intent serviceIntent = new Intent(requireContext(), ChatNotificationService.class);
android.content.Intent serviceIntent = new android.content.Intent(requireContext(), ChatNotificationService.class);
requireContext().stopService(serviceIntent);
tokenManager.clearLoginData(); // clear the token for next login
//get the intent to the main activity and clear the back stack so the back button won't allow the user to go back to the previous screen
Intent intent = new Intent(getActivity(), MainActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK);
android.content.Intent intent = new android.content.Intent(getActivity(), MainActivity.class);
intent.addFlags(android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP | android.content.Intent.FLAG_ACTIVITY_NEW_TASK);
//start the activity to go to login page and finish the current activity
startActivity(intent);
requireActivity().finish();
@@ -271,17 +185,9 @@ public class ProfileFragment extends Fragment {
return view;
}
//Helper function create a file in the cache directory to store the photo in then launch the camera to capture the photo
private void launchCamera() {
//create a file in the cache directory to store the photo in
File photoFile = new File(requireContext().getCacheDir(), "profile_photo.jpg");
//get the uri for the file made
photoUri = FileProvider.getUriForFile(requireContext(), requireContext().getPackageName() + ".fileprovider", photoFile);
//launch the camera to capture the photo and save the photo to photoUri
cameraLauncher.launch(photoUri);
}
//Helper function to call the backend to get profile data and load it to the view
/**
* Fetches current user profile data from the API and then updates the UI.
*/
private void loadProfileData() {
authApi.getMe().enqueue(new Callback<UserDTO>() {
@Override
@@ -300,44 +206,21 @@ public class ProfileFragment extends Fragment {
String avatarUrl = baseUrl + AuthApi.AVATAR_FILE_PATH;
String token = tokenManager.getToken();
if (token != null) {
// Create GlideUrl with token to fetch the image
GlideUrl glideUrl = new GlideUrl(avatarUrl, new LazyHeaders.Builder()
.addHeader("Authorization", "Bearer " + token)
.build());
GlideUtils.loadImageWithToken(requireContext(), imgProfile, avatarUrl, token, R.drawable.placeholder, new GlideUtils.ImageLoadListener() {
@Override
public void onResourceReady() {
hasImage = true;
}
// Load image using Glide
Glide.with(ProfileFragment.this)
.load(glideUrl)
.diskCacheStrategy(DiskCacheStrategy.NONE)
.skipMemoryCache(true)
.placeholder(R.drawable.placeholder)
.error(R.drawable.placeholder)
.listener(new com.bumptech.glide.request.RequestListener<android.graphics.drawable.Drawable>() {
@Override
public boolean onLoadFailed(@androidx.annotation.Nullable com.bumptech.glide.load.engine.GlideException e, Object model, com.bumptech.glide.request.target.Target<android.graphics.drawable.Drawable> target, boolean isFirstResource) {
hasImage = false;
return false;
}
@Override
public boolean onResourceReady(android.graphics.drawable.Drawable resource, Object model, com.bumptech.glide.request.target.Target<android.graphics.drawable.Drawable> target, com.bumptech.glide.load.DataSource dataSource, boolean isFirstResource) {
hasImage = true;
return false;
}
})
.into(imgProfile);
} else {
// load placeholder image if token is null
hasImage = false;
Glide.with(ProfileFragment.this)
.load(R.drawable.placeholder)
.into(imgProfile);
}
@Override
public void onLoadFailed() {
hasImage = false;
}
});
}
else {
Log.e("onResponse: ", response.message());
Toast.makeText(getContext(), "Failed to load profile: ", Toast.LENGTH_SHORT).show();
ErrorUtils.showErrorMessage(getContext(), response, "Failed to load profile");
}
}
@@ -349,10 +232,12 @@ public class ProfileFragment extends Fragment {
});
}
//Helper function to call the backend to upload a profile image
/**
* Uploads the selected or captured image as the user's new avatar.
*/
private void uploadAvatar(Uri uri) {
try {
File file = getFileFromUri(uri);
File file = FileUtils.getFileFromUri(requireContext(), uri);
if (file == null) return;
// Create RequestBody for file upload
@@ -369,7 +254,7 @@ public class ProfileFragment extends Fragment {
// Reload image after successful upload
loadProfileData();
} else {
Toast.makeText(requireContext(), "Failed to upload avatar", Toast.LENGTH_SHORT).show();
ErrorUtils.showErrorMessage(getContext(), response, "Failed to upload avatar");
}
}
@@ -384,6 +269,9 @@ 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(new Callback<Void>() {
@Override
@@ -393,7 +281,7 @@ public class ProfileFragment extends Fragment {
hasImage = false;
imgProfile.setImageResource(R.drawable.placeholder);
} else {
Toast.makeText(requireContext(), "Failed to remove avatar", Toast.LENGTH_SHORT).show();
ErrorUtils.showErrorMessage(getContext(), response, "Failed to remove avatar");
}
}
@@ -405,27 +293,9 @@ public class ProfileFragment extends Fragment {
});
}
// Helper function to create a temporary File object from a Uri for uploading the avatar
private File getFileFromUri(Uri uri) {
try {
InputStream inputStream = requireContext().getContentResolver().openInputStream(uri);
File tempFile = new File(requireContext().getCacheDir(), "upload_avatar.jpg");
FileOutputStream outputStream = new FileOutputStream(tempFile);
byte[] buffer = new byte[1024];
int length;
while ((length = inputStream.read(buffer)) > 0) {
outputStream.write(buffer, 0, length);
}
outputStream.close();
inputStream.close();
return tempFile;
} catch (Exception e) {
Log.e("FILE_UTILS", "Error creating temp file", e);
return null;
}
}
//Helper function to update a profile field in the backend
/**
* Updates a specific profile field (like email or phone) by sending a request to the API.
*/
private void updateProfileField(String fieldName, String value) {
Map<String, String> updates = new HashMap<>();
updates.put(fieldName, value);
@@ -440,15 +310,7 @@ public class ProfileFragment extends Fragment {
tvProfilePhone.setText(currentUser.getPhone());
Toast.makeText(requireContext(), "Profile updated successfully", Toast.LENGTH_SHORT).show();
} else {
try {
String errorJson = response.errorBody().string();
ErrorResponse errorResponse = new Gson().fromJson(errorJson, ErrorResponse.class);
String errorMessage = errorResponse.getMessage();
Toast.makeText(requireContext(), errorMessage, Toast.LENGTH_LONG).show();
} catch (Exception e) {
Log.e("UPDATE_PROFILE", "Error parsing error body", e);
Toast.makeText(requireContext(), "Failed to update profile", Toast.LENGTH_SHORT).show();
}
ErrorUtils.showErrorMessage(getContext(), response, "Failed to update profile");
}
}
@@ -459,4 +321,4 @@ public class ProfileFragment extends Fragment {
}
});
}
}
}

View File

@@ -48,12 +48,18 @@ public class AdoptionFragment extends Fragment implements AdoptionAdapter.OnAdop
private boolean isMonthMode = false;
private final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd", Locale.getDefault());
/**
* Initializes the fragment and its ViewModel.
*/
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
viewModel = new ViewModelProvider(this).get(AdoptionViewModel.class);
}
/**
* Sets up the fragment's UI components, including RecyclerView, Search, SwipeRefresh, and Calendar.
*/
@Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
@@ -87,6 +93,9 @@ public class AdoptionFragment extends Fragment implements AdoptionAdapter.OnAdop
return view;
}
/**
* Toggles the calendar display between week and month modes.
*/
private void toggleCalendarMode() {
isMonthMode = !isMonthMode;
calendarView.state().edit()
@@ -94,6 +103,9 @@ public class AdoptionFragment extends Fragment implements AdoptionAdapter.OnAdop
.commit();
}
/**
* Sets up the date selection listener for the calendar.
*/
private void setupCalendar() {
calendarView.setOnDateChangedListener(new OnDateSelectedListener() {
@Override
@@ -113,6 +125,9 @@ public class AdoptionFragment extends Fragment implements AdoptionAdapter.OnAdop
});
}
/**
* Updates the calendar decorators to highlight days with adoptions.
*/
private void updateCalendarDecorators() {
HashSet<CalendarDay> datesWithAdoptions = new HashSet<>();
for (AdoptionDTO adoption : adoptionList) {
@@ -133,6 +148,9 @@ public class AdoptionFragment extends Fragment implements AdoptionAdapter.OnAdop
calendarView.addDecorator(new EventDecorator(Color.RED, datesWithAdoptions));
}
/**
* Initializes the RecyclerView for displaying adoptions.
*/
private void setupRecyclerView(View view) {
RecyclerView rv = view.findViewById(R.id.recyclerViewAdoptions);
adapter = new AdoptionAdapter(filteredList, this);
@@ -140,6 +158,9 @@ public class AdoptionFragment extends Fragment implements AdoptionAdapter.OnAdop
rv.setAdapter(adapter);
}
/**
* Sets up the search bar for filtering
*/
private void setupSearch(View view) {
etSearch = view.findViewById(R.id.etSearchAdoption);
etSearch.addTextChangedListener(new TextWatcher() {
@@ -151,11 +172,17 @@ public class AdoptionFragment extends Fragment implements AdoptionAdapter.OnAdop
});
}
/**
* Sets up the SwipeRefreshLayout to reload adoption data.
*/
private void setupSwipeRefresh(View view) {
swipeRefresh = view.findViewById(R.id.swipeRefreshAdoption);
swipeRefresh.setOnRefreshListener(this::loadAdoptions);
}
/**
* Filters the adoption list based on search query and selected calendar date.
*/
private void filter(String query) {
filteredList.clear();
String lowerQuery = query.toLowerCase();
@@ -182,7 +209,9 @@ public class AdoptionFragment extends Fragment implements AdoptionAdapter.OnAdop
adapter.notifyDataSetChanged();
}
// Helper function to get a list of all adoptions from the backend
/**
* Fetches the adoption list from the server through the ViewModel.
*/
private void loadAdoptions() {
//Load all adoptions from the backend using viewModel
viewModel.getAllAdoptions(0, 500).observe(getViewLifecycleOwner(), resource -> {
@@ -214,6 +243,9 @@ public class AdoptionFragment extends Fragment implements AdoptionAdapter.OnAdop
});
}
/**
* Navigates to the adoption detail screen for a specific adoption or to create a new one.
*/
private void openDetail(int position) {
Bundle args = new Bundle();
@@ -229,6 +261,9 @@ public class AdoptionFragment extends Fragment implements AdoptionAdapter.OnAdop
NavHostFragment.findNavController(this).navigate(R.id.nav_adoption_detail, args);
}
/**
* Handles item click in the adoption list.
*/
@Override
public void onAdoptionClick(int position) { openDetail(position); }
}

View File

@@ -71,6 +71,9 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter.
private boolean isMonthMode = false;
private final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd", Locale.getDefault());
/**
* Initializes the fragment and its associated ViewModels.
*/
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
@@ -79,6 +82,9 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter.
serviceViewModel = new ViewModelProvider(this).get(ServiceViewModel.class);
}
/**
* Sets up the fragment's UI, including RecyclerView, search, swipe-to-refresh, and calendar.
*/
@Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
@@ -114,7 +120,9 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter.
return view;
}
// Toggle Calendar Mode from week to month and other way around
/**
* Toggles the calendar between week and month display modes.
*/
private void toggleCalendarMode() {
isMonthMode = !isMonthMode;
calendarView.state().edit()
@@ -122,6 +130,9 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter.
.commit();
}
/**
* Sets up the date selection listener for the calendar.
*/
private void setupCalendar() {
calendarView.setOnDateChangedListener(new OnDateSelectedListener() {
@Override
@@ -141,7 +152,9 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter.
});
}
//Set indicators for dates with appointments on the calendar
/**
* Updates calendar indicators to highlight dates that have scheduled appointments.
*/
private void updateCalendarDecorators() {
HashSet<CalendarDay> datesWithAppointments = new HashSet<>();
for (AppointmentDTO appointment : appointmentList) {
@@ -163,6 +176,9 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter.
calendarView.addDecorator(new EventDecorator(Color.RED, datesWithAppointments));
}
/**
* Configures the search bar for filtering.
*/
private void setupSearch(View view) {
etSearch = view.findViewById(R.id.etSearchAppointment);
etSearch.addTextChangedListener(new TextWatcher() {
@@ -181,6 +197,9 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter.
});
}
/**
* Filters the appointment list based on the search query and selected calendar date.
*/
private void filterAppointments(String query) {
filteredList.clear();
String lowerQuery = query.toLowerCase();
@@ -207,11 +226,17 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter.
adapter.notifyDataSetChanged();
}
/**
* Initializes the SwipeRefreshLayout to allow manual data refreshing.
*/
private void setupSwipeRefresh(View view) {
swipeRefreshLayout = view.findViewById(R.id.swipeRefreshAppointment);
swipeRefreshLayout.setOnRefreshListener(this::loadAppointmentData);
}
/**
* Navigates to the appointment detail screen for editing or creating an appointment.
*/
private void openAppointmentDetails(int position) {
Bundle args = new Bundle();
@@ -230,20 +255,32 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter.
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.
*/
@Override
public void onAppointmentClick(int position) {
openAppointmentDetails(position);
}
// Helper function to get a list of all appointments from the backend
/**
* Fetches all appointment data from the server.
*/
private void loadAppointmentData() {
//Load all appointments from the backend using viewModel
appointmentViewModel.getAllAppointments(0, 500).observe(getViewLifecycleOwner(), resource -> {
@@ -275,7 +312,9 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter.
});
}
// Load Pets
/**
* 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) {
@@ -285,7 +324,9 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter.
});
}
// Load Services
/**
* 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) {
@@ -295,6 +336,9 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter.
});
}
/**
* Get a pet's name based on its ID.
*/
private String getPetName(Long id) {
for (PetDTO p : petList) {
if (p.getPetId().equals(id)) return p.getPetName();
@@ -303,6 +347,9 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter.
return "";
}
/**
* Get a service's name based on its ID.
*/
private String getServiceName(Long id) {
for (ServiceDTO s : serviceList) {
if (s.getServiceId().equals(id))return s.getServiceName();
@@ -310,6 +357,9 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter.
return "";
}
/**
* Initializes the RecyclerView for displaying appointments.
*/
private void setupRecyclerView(View view) {
RecyclerView recyclerView = view.findViewById(R.id.recyclerViewAppointments);
adapter = new AppointmentAdapter(filteredList, this);

View File

@@ -74,12 +74,18 @@ public class InventoryFragment extends Fragment implements InventoryAdapter.OnIn
// Prevent spinner from firing on initial load
private boolean spinnerReady = false;
/**
* Initializes the fragment and its ViewModel.
*/
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
viewModel = new ViewModelProvider(this).get(InventoryViewModel.class);
}
/**
* Sets up the fragment's UI components, including the inventory list, search, and category filter.
*/
@Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
@@ -114,7 +120,9 @@ public class InventoryFragment extends Fragment implements InventoryAdapter.OnIn
return view;
}
// Categories
/**
* Fetches all product categories to populate the filter spinner.
*/
private void loadCategories() {
viewModel.getAllCategories(0, 100).observe(getViewLifecycleOwner(), resource -> {
if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) {
@@ -128,6 +136,9 @@ public class InventoryFragment extends Fragment implements InventoryAdapter.OnIn
});
}
/**
* Setup the category filter spinner.
*/
private void setupCategorySpinner() {
// First item is always "All Categories"
List<String> categoryNames = new ArrayList<>();
@@ -167,8 +178,9 @@ public class InventoryFragment extends Fragment implements InventoryAdapter.OnIn
}
}
// Search
/**
* Sets up the search bar for filtering.
*/
private void setupSearch(View view) {
etSearch = view.findViewById(R.id.etSearchInventory);
etSearch.addTextChangedListener(new TextWatcher() {
@@ -193,7 +205,9 @@ public class InventoryFragment extends Fragment implements InventoryAdapter.OnIn
});
}
// RecyclerView + infinite scroll
/**
* Initializes the RecyclerView with a layout manager, and adapter.
*/
private void setupRecyclerView(View view) {
RecyclerView rv = view.findViewById(R.id.recyclerViewInventory);
adapter = new InventoryAdapter(inventoryList, this);
@@ -218,12 +232,17 @@ public class InventoryFragment extends Fragment implements InventoryAdapter.OnIn
});
}
/**
* Sets up the SwipeRefreshLayout to reload the first page of inventory items.
*/
private void setupSwipeRefresh(View view) {
swipeRefreshLayout = view.findViewById(R.id.swipeRefreshInventory);
swipeRefreshLayout.setOnRefreshListener(() -> loadInventory(true));
}
// Helper function to get a list of all inventory items from the backend
/**
* Fetches a page of inventory items from the API.
*/
private void loadInventory(boolean reset) {
if (isLoading)
return;
@@ -270,7 +289,9 @@ public class InventoryFragment extends Fragment implements InventoryAdapter.OnIn
});
}
// Combines search text and category into one query string for ?q=
/**
* Constructs a query string based on the current search text and selected category.
*/
private String buildQuery() {
String q = null;
if (!currentQuery.isEmpty() && selectedCategory != null) {
@@ -284,7 +305,9 @@ public class InventoryFragment extends Fragment implements InventoryAdapter.OnIn
return q;
}
// Bulk delete
/**
* Displays a confirmation dialog before performing a bulk deletion of selected items.
*/
private void confirmBulkDelete() {
List<Long> ids = adapter.getSelectedIds();
if (ids.isEmpty())
@@ -298,6 +321,9 @@ public class InventoryFragment extends Fragment implements InventoryAdapter.OnIn
.show();
}
/**
* Executes the bulk deletion of inventory items through the ViewModel.
*/
private void bulkDelete(List<Long> ids) {
viewModel.bulkDeleteInventory(ids).observe(getViewLifecycleOwner(), resource -> {
if (resource != null && resource.status != Resource.Status.LOADING) {
@@ -313,6 +339,9 @@ public class InventoryFragment extends Fragment implements InventoryAdapter.OnIn
});
}
/**
* Hides the bulk deletion UI bar.
*/
private void hideBulkDeleteBar() {
if (btnBulkDelete != null)
btnBulkDelete.setVisibility(View.GONE);
@@ -320,7 +349,9 @@ public class InventoryFragment extends Fragment implements InventoryAdapter.OnIn
tvSelectionCount.setVisibility(View.GONE);
}
// Navigation
/**
* Navigates to the inventory detail screen for a specific item or to add a new one.
*/
private void openDetail(InventoryDTO inv) {
Bundle args = new Bundle();
@@ -335,12 +366,16 @@ public class InventoryFragment extends Fragment implements InventoryAdapter.OnIn
NavHostFragment.findNavController(this).navigate(R.id.nav_inventory_detail, args);
}
/**
* Reloads inventory data when changes occur.
*/
public void onInventoryChanged() {
loadInventory(true);
}
// Adapter callbacks
/**
* Handles item click in the inventory list.
*/
@Override
public void onInventoryClick(int position) {
if (position >= 0 && position < inventoryList.size()) {
@@ -348,6 +383,9 @@ public class InventoryFragment extends Fragment implements InventoryAdapter.OnIn
}
}
/**
* Updates the bulk deletion UI visibility and count when items are selected or deselected.
*/
@Override
public void onSelectionChanged(int selectedCount) {
if (selectedCount > 0) {

View File

@@ -2,7 +2,6 @@ package com.example.petstoremobile.fragments.listfragments;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.ViewModelProvider;
@@ -29,7 +28,6 @@ import com.example.petstoremobile.adapters.PetAdapter;
import com.example.petstoremobile.dtos.PetDTO;
import com.example.petstoremobile.fragments.ListFragment;
import com.example.petstoremobile.viewmodels.PetViewModel;
import com.example.petstoremobile.utils.Resource;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
import java.util.ArrayList;
@@ -53,12 +51,18 @@ public class PetFragment extends Fragment implements PetAdapter.OnPetClickListen
private EditText etSearch;
private Spinner spinnerStatus;
/**
* Initializes the fragment and its associated PetViewModel.
*/
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
viewModel = new ViewModelProvider(this).get(PetViewModel.class);
}
/**
* Sets up the fragment's UI components, including RecyclerView, search, status filter, and swipe-to-refresh.
*/
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
@@ -72,7 +76,7 @@ public class PetFragment extends Fragment implements PetAdapter.OnPetClickListen
setupSwipeRefresh(view);
FloatingActionButton fabAddPet = view.findViewById(R.id.fabAddPet);
fabAddPet.setOnClickListener(v -> openPetDetails(-1));
fabAddPet.setOnClickListener(v -> openPetDetails());
hamburger.setOnClickListener(v -> {
Fragment parent = getParentFragment();
@@ -87,12 +91,18 @@ public class PetFragment extends Fragment implements PetAdapter.OnPetClickListen
return view;
}
/**
* Reloads pet data every time the fragment becomes visible.
*/
@Override
public void onResume() {
super.onResume();
loadPetData();
}
/**
* Configures the search bar with a for filtering.
*/
private void setupSearch(View view) {
etSearch = view.findViewById(R.id.etSearchPet);
etSearch.addTextChangedListener(new TextWatcher() {
@@ -104,6 +114,9 @@ public class PetFragment extends Fragment implements PetAdapter.OnPetClickListen
});
}
/**
* Configures the status filter spinner.
*/
private void setupStatusFilter(View view) {
spinnerStatus = view.findViewById(R.id.spinnerStatus);
String[] statuses = {"All Statuses", "Available", "Adopted"};
@@ -122,6 +135,9 @@ public class PetFragment extends Fragment implements PetAdapter.OnPetClickListen
});
}
/**
* Filters the pet list based on both the search query and the selected status.
*/
private void filterPets() {
String query = etSearch.getText().toString().toLowerCase();
String selectedStatus = spinnerStatus.getSelectedItem().toString();
@@ -143,11 +159,17 @@ public class PetFragment extends Fragment implements PetAdapter.OnPetClickListen
adapter.notifyDataSetChanged();
}
/**
* Sets up the SwipeRefreshLayout to allow manual re-fetching of pet data.
*/
private void setupSwipeRefresh(View view) {
swipeRefreshLayout = view.findViewById(R.id.swipeRefreshPet);
swipeRefreshLayout.setOnRefreshListener(this::loadPetData);
}
/**
* Navigates to the pet profile screen for a specific pet.
*/
private void openPetProfile(int position) {
Bundle args = new Bundle();
PetDTO pet = filteredList.get(position);
@@ -167,16 +189,24 @@ public class PetFragment extends Fragment implements PetAdapter.OnPetClickListen
NavHostFragment.findNavController(this).navigate(R.id.nav_pet_profile, args);
}
private void openPetDetails(int position) {
/**
* Navigates to the pet detail screen. (Only used for adding a new pet on this screen)
*/
private void openPetDetails() {
NavHostFragment.findNavController(this).navigate(R.id.nav_pet_detail);
}
/**
* Handles clicks on individual pet items in the list.
*/
@Override
public void onPetClick(int position) {
openPetProfile(position);
}
// Helper function to get a list of all pets from the backend
/**
* Fetches all pet data from the server via the ViewModel and updates the UI.
*/
private void loadPetData() {
//Load all pets from the backend using viewModel
viewModel.getAllPets(0, 100).observe(getViewLifecycleOwner(), resource -> {
@@ -207,6 +237,9 @@ public class PetFragment extends Fragment implements PetAdapter.OnPetClickListen
});
}
/**
* Initializes the RecyclerView with a layout manager and adapter for displaying pets.
*/
private void setupRecyclerView(View view) {
RecyclerView recyclerView = view.findViewById(R.id.recyclerViewPets);
adapter = new PetAdapter(filteredList, this);

View File

@@ -41,12 +41,18 @@ public class ProductFragment extends Fragment implements ProductAdapter.OnProduc
@Inject @Named("baseUrl") String baseUrl;
@Inject TokenManager tokenManager;
/**
* Initializes the fragment and its associated ProductViewModel.
*/
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
viewModel = new ViewModelProvider(this).get(ProductViewModel.class);
}
/**
* Sets up the fragment's UI components, including the product list, search, and swipe-to-refresh.
*/
@Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
@@ -75,6 +81,9 @@ public class ProductFragment extends Fragment implements ProductAdapter.OnProduc
return view;
}
/**
* Initializes the RecyclerView with a layout manager and adapter for displaying products.
*/
private void setupRecyclerView(View view) {
RecyclerView rv = view.findViewById(R.id.recyclerViewProducts);
adapter = new ProductAdapter(filteredList, this);
@@ -84,6 +93,9 @@ public class ProductFragment extends Fragment implements ProductAdapter.OnProduc
rv.setAdapter(adapter);
}
/**
* Configures the search bar for filtering.
*/
private void setupSearch(View view) {
etSearch = view.findViewById(R.id.etSearchProduct);
etSearch.addTextChangedListener(new TextWatcher() {
@@ -95,11 +107,17 @@ public class ProductFragment extends Fragment implements ProductAdapter.OnProduc
});
}
/**
* Sets up the SwipeRefreshLayout to allow manual re-fetching of product data.
*/
private void setupSwipeRefresh(View view) {
swipeRefresh = view.findViewById(R.id.swipeRefreshProduct);
swipeRefresh.setOnRefreshListener(this::loadProducts);
}
/**
* Filters the product list based on the search query across name, category, and description.
*/
private void filter() {
String query = etSearch.getText().toString().toLowerCase();
@@ -117,7 +135,9 @@ public class ProductFragment extends Fragment implements ProductAdapter.OnProduc
adapter.notifyDataSetChanged();
}
// Helper function to get a list of all products from the backend
/**
* Fetches all product data from the server through the ViewModel and updates the UI.
*/
private void loadProducts() {
//Load all products from the backend using viewModel
viewModel.getAllProducts(null, 0, 100).observe(getViewLifecycleOwner(), resource -> {
@@ -148,6 +168,9 @@ public class ProductFragment extends Fragment implements ProductAdapter.OnProduc
});
}
/**
* Navigates to the product detail screen for a specific product or to add a new one.
*/
private void openDetail(int position) {
Bundle args = new Bundle();
if (position != -1) {
@@ -161,6 +184,9 @@ public class ProductFragment extends Fragment implements ProductAdapter.OnProduc
NavHostFragment.findNavController(this).navigate(R.id.nav_product_detail, args);
}
/**
* Handles item click in the product list.
*/
@Override
public void onProductClick(int position) { openDetail(position); }
}

View File

@@ -35,12 +35,18 @@ public class ProductSupplierFragment extends Fragment
private EditText etSearch;
private ProductSupplierViewModel viewModel;
/**
* Initializes the fragment and its associated ProductSupplierViewModel.
*/
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
viewModel = new ViewModelProvider(this).get(ProductSupplierViewModel.class);
}
/**
* Sets up the fragment's UI components, including the RecyclerView, search, and swipe-to-refresh.
*/
@Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
@@ -68,6 +74,9 @@ public class ProductSupplierFragment extends Fragment
return view;
}
/**
* Initializes the RecyclerView with a layout manager and adapter for product-supplier data.
*/
private void setupRecyclerView(View view) {
RecyclerView rv = view.findViewById(R.id.recyclerViewPS);
adapter = new ProductSupplierAdapter(filteredList, this);
@@ -75,6 +84,9 @@ public class ProductSupplierFragment extends Fragment
rv.setAdapter(adapter);
}
/**
* Configures the search bar for filtering.
*/
private void setupSearch(View view) {
etSearch = view.findViewById(R.id.etSearchPS);
etSearch.addTextChangedListener(new TextWatcher() {
@@ -86,11 +98,17 @@ public class ProductSupplierFragment extends Fragment
});
}
/**
* Sets up the SwipeRefreshLayout to allow manual reloading of product-supplier data.
*/
private void setupSwipeRefresh(View view) {
swipeRefresh = view.findViewById(R.id.swipeRefreshPS);
swipeRefresh.setOnRefreshListener(this::loadData);
}
/**
* Filters the product-supplier list based on the search query.
*/
private void filter(String query) {
filteredList.clear();
if (query.isEmpty()) {
@@ -107,7 +125,9 @@ public class ProductSupplierFragment extends Fragment
adapter.notifyDataSetChanged();
}
// Helper function to get a list of all product suppliers from the backend
/**
* Fetches all product-supplier data from the server through the ViewModel.
*/
private void loadData() {
//Load all product suppliers from the backend using viewModel
viewModel.getAllProductSuppliers(0, 100).observe(getViewLifecycleOwner(), resource -> {
@@ -138,6 +158,9 @@ public class ProductSupplierFragment extends Fragment
});
}
/**
* Navigates to the product-supplier detail screen for a specific item or to add a new record.
*/
private void openDetail(int position) {
Bundle args = new Bundle();
if (position != -1) {
@@ -151,6 +174,9 @@ public class ProductSupplierFragment extends Fragment
NavHostFragment.findNavController(this).navigate(R.id.nav_product_supplier_detail, args);
}
/**
* Handles item click in the product-supplier list.
*/
@Override
public void onProductSupplierClick(int position) { openDetail(position); }
}

View File

@@ -34,12 +34,18 @@ public class PurchaseOrderFragment extends Fragment
private EditText etSearch;
private PurchaseOrderViewModel viewModel;
/**
* Initializes the fragment and its associated PurchaseOrderViewModel.
*/
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
viewModel = new ViewModelProvider(this).get(PurchaseOrderViewModel.class);
}
/**
* Sets up the fragment's UI components, including RecyclerView, search, and swipe-to-refresh.
*/
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
@@ -64,6 +70,9 @@ public class PurchaseOrderFragment extends Fragment
return view;
}
/**
* Initializes the RecyclerView with a layout manager and adapter for purchase orders.
*/
private void setupRecyclerView(View view) {
RecyclerView rv = view.findViewById(R.id.recyclerViewPO);
adapter = new PurchaseOrderAdapter(filteredList, this);
@@ -71,6 +80,9 @@ public class PurchaseOrderFragment extends Fragment
rv.setAdapter(adapter);
}
/**
* Configures the search bar for filtering.
*/
private void setupSearch(View view) {
etSearch = view.findViewById(R.id.etSearchPO);
etSearch.addTextChangedListener(new TextWatcher() {
@@ -86,11 +98,17 @@ public class PurchaseOrderFragment extends Fragment
});
}
/**
* Sets up the SwipeRefreshLayout to allow manual reloading of purchase order data.
*/
private void setupSwipeRefresh(View view) {
swipeRefresh = view.findViewById(R.id.swipeRefreshPO);
swipeRefresh.setOnRefreshListener(this::loadData);
}
/**
* Filters the purchase order list based on the search query.
*/
private void filter(String query) {
filteredList.clear();
if (query.isEmpty()) {
@@ -107,7 +125,9 @@ public class PurchaseOrderFragment extends Fragment
adapter.notifyDataSetChanged();
}
// Helper function to get a list of all purchase orders from the backend
/**
* Fetches all purchase order data from the server through the ViewModel and updates the UI.
*/
private void loadData() {
//Load all purchase orders from the backend using viewModel
viewModel.getAllPurchaseOrders(0, 100).observe(getViewLifecycleOwner(), resource -> {
@@ -138,6 +158,9 @@ public class PurchaseOrderFragment extends Fragment
});
}
/**
* Navigates to the purchase order detail screen for a specific record.
*/
private void openDetail(int position) {
Bundle args = new Bundle();
PurchaseOrderDTO po = filteredList.get(position);
@@ -148,6 +171,9 @@ public class PurchaseOrderFragment extends Fragment
NavHostFragment.findNavController(this).navigate(R.id.nav_purchase_order_detail, args);
}
/**
* Handles item click in the purchase order list.
*/
@Override
public void onPurchaseOrderClick(int position) {
openDetail(position);

View File

@@ -45,13 +45,18 @@ public class ServiceFragment extends Fragment implements ServiceAdapter.OnServic
private SwipeRefreshLayout swipeRefreshLayout;
private EditText etSearch;
/**
* Initializes the fragment and its associated ServiceViewModel.
*/
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
viewModel = new ViewModelProvider(this).get(ServiceViewModel.class);
}
//load service view
/**
* Sets up the fragment's UI components, including RecyclerView, search, and swipe-to-refresh.
*/
@Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
@@ -82,6 +87,9 @@ public class ServiceFragment extends Fragment implements ServiceAdapter.OnServic
return view;
}
/**
* Configures the search bar for filtering.
*/
private void setupSearch(View view) {
etSearch = view.findViewById(R.id.etSearchService);
etSearch.addTextChangedListener(new TextWatcher() {
@@ -93,6 +101,9 @@ public class ServiceFragment extends Fragment implements ServiceAdapter.OnServic
});
}
/**
* Filters the service list based on the search query across name and description fields.
*/
private void filterServices(String query) {
filteredList.clear();
if (query.isEmpty()) {
@@ -109,12 +120,17 @@ public class ServiceFragment extends Fragment implements ServiceAdapter.OnServic
adapter.notifyDataSetChanged();
}
/**
* Sets up the SwipeRefreshLayout to allow manual reloading of service data.
*/
private void setupSwipeRefresh(View view) {
swipeRefreshLayout = view.findViewById(R.id.swipeRefreshService);
swipeRefreshLayout.setOnRefreshListener(this::loadServiceData);
}
//Open the service detail view depending on the mode
/**
* Navigates to the service detail screen for editing an existing service or adding a new one.
*/
private void openServiceDetails(int position) {
//Make a bundle to pass data to the detail fragment
Bundle args = new Bundle();
@@ -133,13 +149,17 @@ public class ServiceFragment extends Fragment implements ServiceAdapter.OnServic
NavHostFragment.findNavController(this).navigate(R.id.nav_service_detail, args);
}
// Called by ServiceAdapter when a row is clicked to open the details view
/**
* Handles item click in the service list.
*/
@Override
public void onServiceClick(int position) {
openServiceDetails(position);
}
// Helper function to get a list of all services from the backend
/**
* Fetches all service data from the server through the ViewModel and updates the UI.
*/
private void loadServiceData() {
//Load all services from the backend using viewModel
viewModel.getAllServices(0, 100).observe(getViewLifecycleOwner(), resource -> {
@@ -172,7 +192,9 @@ public class ServiceFragment extends Fragment implements ServiceAdapter.OnServic
});
}
//set up the recyclerview and adapter
/**
* Initializes the RecyclerView with a layout manager and adapter for services.
*/
private void setupRecyclerView(View view) {
RecyclerView recyclerView = view.findViewById(R.id.recyclerViewServices);
adapter = new ServiceAdapter(filteredList, this);

View File

@@ -45,13 +45,18 @@ public class SupplierFragment extends Fragment implements SupplierAdapter.OnSupp
private SwipeRefreshLayout swipeRefreshLayout;
private EditText etSearch;
/**
* Initializes the fragment and its associated SupplierViewModel.
*/
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
viewModel = new ViewModelProvider(this).get(SupplierViewModel.class);
}
//load supplier view
/**
* Sets up the fragment's UI components, including RecyclerView, search, and swipe-to-refresh.
*/
@Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
@@ -82,6 +87,9 @@ public class SupplierFragment extends Fragment implements SupplierAdapter.OnSupp
return view;
}
/**
* Configures the search bar for filtering.
*/
private void setupSearch(View view) {
etSearch = view.findViewById(R.id.etSearchSupplier);
etSearch.addTextChangedListener(new TextWatcher() {
@@ -93,6 +101,9 @@ public class SupplierFragment extends Fragment implements SupplierAdapter.OnSupp
});
}
/**
* Filters the supplier list based on the search query across company name and contact person.
*/
private void filterSuppliers(String query) {
filteredList.clear();
if (query.isEmpty()) {
@@ -110,12 +121,17 @@ public class SupplierFragment extends Fragment implements SupplierAdapter.OnSupp
adapter.notifyDataSetChanged();
}
/**
* Sets up the SwipeRefreshLayout to allow manual reloading of supplier data.
*/
private void setupSwipeRefresh(View view) {
swipeRefreshLayout = view.findViewById(R.id.swipeRefreshSupplier);
swipeRefreshLayout.setOnRefreshListener(this::loadSupplierData);
}
//Open the supplier detail view depending on the mode
/**
* Navigates to the supplier detail screen for editing an existing record or adding a new one.
*/
private void openSupplierDetails(int position) {
//Make a bundle to pass data to the detail fragment
Bundle args = new Bundle();
@@ -136,13 +152,17 @@ public class SupplierFragment extends Fragment implements SupplierAdapter.OnSupp
}
// Called by SupplierAdapter when a row is clicked to open the details view
/**
* Handles item click in the supplier list.
*/
@Override
public void onSupplierClick(int position) {
openSupplierDetails(position);
}
// Helper function to get a list of all suppliers from the backend
/**
* Fetches all supplier data from the server through the ViewModel and updates the UI.
*/
private void loadSupplierData() {
//Load all suppliers from the backend using viewModel
viewModel.getAllSuppliers(0, 100).observe(getViewLifecycleOwner(), resource -> {
@@ -175,7 +195,9 @@ public class SupplierFragment extends Fragment implements SupplierAdapter.OnSupp
});
}
//set up the recyclerview and adapter
/**
* Initializes the RecyclerView with a layout manager and adapter for displaying suppliers.
*/
private void setupRecyclerView(View view) {
RecyclerView recyclerView = view.findViewById(R.id.recyclerViewSuppliers);
adapter = new SupplierAdapter(filteredList, this);

View File

@@ -14,7 +14,8 @@ import com.example.petstoremobile.R;
import com.example.petstoremobile.adapters.BlackTextArrayAdapter;
import com.example.petstoremobile.api.*;
import com.example.petstoremobile.dtos.*;
import com.example.petstoremobile.fragments.ListFragment;
import com.example.petstoremobile.utils.ErrorUtils;
import java.util.*;
import javax.inject.Inject;
@@ -22,6 +23,9 @@ import javax.inject.Inject;
import dagger.hilt.android.AndroidEntryPoint;
import retrofit2.*;
/**
* Fragment for displaying and editing adoption request details.
*/
@AndroidEntryPoint
public class AdoptionDetailFragment extends Fragment {
@@ -60,6 +64,9 @@ public class AdoptionDetailFragment extends Fragment {
return view;
}
/**
* Initializes UI components from the layout.
*/
private void initViews(View v) {
tvMode = v.findViewById(R.id.tvAdoptionMode);
tvAdoptionId = v.findViewById(R.id.tvAdoptionId);
@@ -72,11 +79,17 @@ public class AdoptionDetailFragment extends Fragment {
btnBack = v.findViewById(R.id.btnAdoptionBack);
}
/**
* Configures the spinner for adoption status.
*/
private void setupSpinners() {
spinnerStatus.setAdapter(new BlackTextArrayAdapter<>(requireContext(),
android.R.layout.simple_spinner_item, STATUSES));
}
/**
* Configures the date picker dialog for the adoption date field.
*/
private void setupDatePicker() {
etAdoptionDate.setOnClickListener(v -> {
Calendar c = Calendar.getInstance();
@@ -89,11 +102,17 @@ public class AdoptionDetailFragment extends Fragment {
});
}
/**
* Fetches required data (pets and customers) from the backend.
*/
private void loadData() {
loadPets();
loadCustomers();
}
/**
* Loads the list of pets from the API.
*/
private void loadPets() {
petApi.getAllPets(0, 200)
.enqueue(new Callback<PageResponse<PetDTO>>() {
@@ -110,6 +129,9 @@ public class AdoptionDetailFragment extends Fragment {
});
}
/**
* Populates the pet selection spinner.
*/
private void populatePetSpinner() {
List<String> names = new ArrayList<>();
names.add("-- Select Pet --");
@@ -125,6 +147,9 @@ public class AdoptionDetailFragment extends Fragment {
}
}
/**
* Loads the list of customers from the API.
*/
private void loadCustomers() {
customerApi.getAllCustomers(0, 200)
.enqueue(new Callback<PageResponse<CustomerDTO>>() {
@@ -141,6 +166,9 @@ public class AdoptionDetailFragment extends Fragment {
});
}
/**
* Populates the customer selection spinner.
*/
private void populateCustomerSpinner() {
List<String> names = new ArrayList<>();
names.add("-- Select Customer --");
@@ -157,6 +185,9 @@ public class AdoptionDetailFragment extends Fragment {
}
}
/**
* Handles arguments to determine if the fragment is in edit or add mode.
*/
private void handleArguments() {
Bundle a = getArguments();
if (a != null && a.containsKey("adoptionId")) {
@@ -185,6 +216,9 @@ public class AdoptionDetailFragment extends Fragment {
}
}
/**
* Validates input and saves the adoption request to the backend.
*/
private void saveAdoption() {
if (spinnerCustomer.getSelectedItemPosition() == 0) {
Toast.makeText(getContext(), "Select a customer", Toast.LENGTH_SHORT).show(); return;
@@ -219,6 +253,9 @@ public class AdoptionDetailFragment extends Fragment {
}
}
/**
* callback for adoption save/update operations.
*/
private Callback<AdoptionDTO> simpleCallback(String msg) {
return new Callback<>() {
public void onResponse(Call<AdoptionDTO> c, Response<AdoptionDTO> r) {
@@ -227,13 +264,7 @@ public class AdoptionDetailFragment extends Fragment {
Toast.makeText(getContext(), msg, Toast.LENGTH_SHORT).show();
navigateBack();
} else {
try {
String err = r.errorBody().string();
Log.e("ADOPTION_SAVE", "Error: " + err);
Toast.makeText(getContext(), "Error " + r.code(), Toast.LENGTH_SHORT).show();
} catch (Exception e) {
Log.e("ADOPTION_SAVE", "Failed to read error");
}
ErrorUtils.showErrorMessage(getContext(), r, "Error " + r.code());
}
}
public void onFailure(Call<AdoptionDTO> c, Throwable t) {
@@ -243,15 +274,16 @@ public class AdoptionDetailFragment extends Fragment {
};
}
/**
* Shows a confirmation dialog before deleting an adoption request.
*/
private void confirmDelete() {
new AlertDialog.Builder(requireContext())
.setTitle("Delete Adoption?")
.setPositiveButton("Yes", (d, w) ->
adoptionApi.deleteAdoption(adoptionId)
.enqueue(new Callback<Void>() {
public void onResponse(Call<Void> c, Response<Void> r) {
navigateBack();
}
public void onResponse(Call<Void> c, Response<Void> r) { navigateBack(); }
public void onFailure(Call<Void> c, Throwable t) {
Toast.makeText(getContext(), "Delete failed",
Toast.LENGTH_SHORT).show();
@@ -260,6 +292,9 @@ public class AdoptionDetailFragment extends Fragment {
.setNegativeButton("No", null).show();
}
/**
* Navigates back to the previous fragment.
*/
private void navigateBack() {
NavHostFragment.findNavController(this).popBackStack();
}

View File

@@ -14,7 +14,8 @@ import com.example.petstoremobile.R;
import com.example.petstoremobile.adapters.BlackTextArrayAdapter;
import com.example.petstoremobile.api.*;
import com.example.petstoremobile.dtos.*;
import com.example.petstoremobile.fragments.ListFragment;
import com.example.petstoremobile.utils.ErrorUtils;
import java.util.*;
import javax.inject.Inject;
@@ -22,6 +23,9 @@ import javax.inject.Inject;
import dagger.hilt.android.AndroidEntryPoint;
import retrofit2.*;
/**
* Fragment for displaying and editing appointment details.
*/
@AndroidEntryPoint
public class AppointmentDetailFragment extends Fragment {
@@ -69,6 +73,9 @@ public class AppointmentDetailFragment extends Fragment {
return view;
}
/**
* Initializes UI components from the layout.
*/
private void initViews(View v) {
tvMode = v.findViewById(R.id.tvApptMode);
tvAppointmentId = v.findViewById(R.id.tvAppointmentId);
@@ -85,6 +92,9 @@ public class AppointmentDetailFragment extends Fragment {
btnBack = v.findViewById(R.id.btnApptBack);
}
/**
* Configures the adapters for spinners.
*/
private void setupSpinners() {
spinnerStatus.setAdapter(new BlackTextArrayAdapter<>(requireContext(),
android.R.layout.simple_spinner_item, STATUSES));
@@ -98,6 +108,9 @@ public class AppointmentDetailFragment extends Fragment {
android.R.layout.simple_spinner_item, new String[]{"00","15","30","45"}));
}
/**
* Configures the date picker dialog for the appointment date field.
*/
private void setupDatePicker() {
etAppointmentDate.setOnClickListener(v -> {
Calendar c = Calendar.getInstance();
@@ -111,6 +124,9 @@ public class AppointmentDetailFragment extends Fragment {
});
}
/**
* Fetches all required data from the backend to populate the fragment.
*/
private void loadData() {
loadPets();
loadServices();
@@ -119,6 +135,9 @@ public class AppointmentDetailFragment extends Fragment {
loadAllAppointments();
}
/**
* Loads the list of pets from the API.
*/
private void loadPets() {
petApi.getAllPets(0, 200)
.enqueue(new Callback<PageResponse<PetDTO>>() {
@@ -134,6 +153,9 @@ public class AppointmentDetailFragment extends Fragment {
});
}
/**
* Populates the pet selection spinner.
*/
private void populatePetSpinner() {
List<String> names = new ArrayList<>();
names.add("-- Select Pet --");
@@ -149,6 +171,9 @@ public class AppointmentDetailFragment extends Fragment {
}
}
/**
* Loads the list of services from the API.
*/
private void loadServices() {
serviceApi.getAllServices(0, 200)
.enqueue(new Callback<PageResponse<ServiceDTO>>() {
@@ -164,6 +189,9 @@ public class AppointmentDetailFragment extends Fragment {
});
}
/**
* Populates the service selection spinner.
*/
private void populateServiceSpinner() {
List<String> names = new ArrayList<>();
names.add("-- Select Service --");
@@ -179,6 +207,9 @@ public class AppointmentDetailFragment extends Fragment {
}
}
/**
* Loads the list of customers from the API.
*/
private void loadCustomers() {
customerApi.getAllCustomers(0, 200)
.enqueue(new Callback<PageResponse<CustomerDTO>>() {
@@ -194,6 +225,9 @@ public class AppointmentDetailFragment extends Fragment {
});
}
/**
* Populates the customer spinner.
*/
private void populateCustomerSpinner() {
List<String> names = new ArrayList<>();
names.add("-- Select Customer --");
@@ -210,6 +244,9 @@ public class AppointmentDetailFragment extends Fragment {
}
}
/**
* Loads the list of stores from the API.
*/
private void loadStores() {
storeApi.getAllStores(0, 50)
.enqueue(new Callback<PageResponse<StoreDTO>>() {
@@ -225,6 +262,9 @@ public class AppointmentDetailFragment extends Fragment {
});
}
/**
* Populates the store spinner.
*/
private void populateStoreSpinner() {
List<String> names = new ArrayList<>();
names.add("-- Select Store --");
@@ -240,6 +280,9 @@ public class AppointmentDetailFragment extends Fragment {
}
}
/**
* Loads all appointments from the API.
*/
private void loadAllAppointments() {
appointmentApi.getAllAppointments(0, 500)
.enqueue(new Callback<PageResponse<AppointmentDTO>>() {
@@ -251,6 +294,9 @@ public class AppointmentDetailFragment extends Fragment {
});
}
/**
* Handles arguments to determine if the fragment is in edit or add mode.
*/
private void handleArguments() {
Bundle a = getArguments();
if (a != null && a.containsKey("appointmentId")) {
@@ -292,6 +338,9 @@ public class AppointmentDetailFragment extends Fragment {
}
}
/**
* Validates input and saves the appointment to the backend.
*/
private void saveAppointment() {
if (spinnerCustomer.getSelectedItemPosition() == 0) {
Toast.makeText(getContext(), "Select a customer", Toast.LENGTH_SHORT).show(); return;
@@ -370,6 +419,9 @@ public class AppointmentDetailFragment extends Fragment {
}
}
/**
* callback for appointment save/update operations.
*/
private Callback<AppointmentDTO> simpleCallback(String msg) {
return new Callback<>() {
public void onResponse(Call<AppointmentDTO> c, Response<AppointmentDTO> r) {
@@ -387,22 +439,12 @@ public class AppointmentDetailFragment extends Fragment {
showErrorDialog("Invalid Date/Time",
"Booked appointments must be scheduled in the future. " +
"Please select a future date and time.");
//------------------------------------------
} else if (errorBody.toLowerCase().contains("not available") ||
errorBody.toLowerCase().contains("time is not available")) {
showNoAvailabilityDialog();
} else if (r.code() == 404) {
showErrorDialog("Not Found",
"The selected pet, customer or service was not found.");
} else if (r.code() == 403) {
showErrorDialog("Access Denied",
"You don't have permission to perform this action.");
} else if (r.code() == 400) {
showErrorDialog("Invalid Request", errorBody);
} else {
showErrorDialog("Error", "Something went wrong. Please try again.");
ErrorUtils.showErrorMessage(getContext(), r, "Something went wrong. Please try again.");
}
//-----------------------------
} catch (Exception e) {
Log.e("APPT_SAVE", "Failed to read error body");
showErrorDialog("Error", "Something went wrong. Please try again.");
@@ -417,6 +459,9 @@ public class AppointmentDetailFragment extends Fragment {
};
}
/**
* Shows a specialized dialog when a time slot is not available.
*/
private void showNoAvailabilityDialog() {
new AlertDialog.Builder(requireContext())
.setTitle("No Availability")
@@ -427,6 +472,9 @@ public class AppointmentDetailFragment extends Fragment {
.show();
}
/**
* Shows a generic error dialog with a title and message.
*/
private void showErrorDialog(String title, String message) {
new AlertDialog.Builder(requireContext())
.setTitle(title)
@@ -434,6 +482,10 @@ public class AppointmentDetailFragment extends Fragment {
.setPositiveButton("OK", null)
.show();
}
/**
* Shows a confirmation dialog and handles the deletion of an appointment.
*/
private void confirmDelete() {
new AlertDialog.Builder(requireContext())
.setTitle("Delete Appointment?")
@@ -448,6 +500,9 @@ public class AppointmentDetailFragment extends Fragment {
.setNegativeButton("No", null).show();
}
/**
* Navigates back to the previous screen.
*/
private void navigateBack() {
NavHostFragment.findNavController(this).popBackStack();
}

View File

@@ -26,8 +26,8 @@ import com.example.petstoremobile.dtos.InventoryDTO;
import com.example.petstoremobile.dtos.InventoryRequest;
import com.example.petstoremobile.dtos.PageResponse;
import com.example.petstoremobile.dtos.ProductDTO;
import com.example.petstoremobile.fragments.ListFragment;
import com.example.petstoremobile.fragments.listfragments.InventoryFragment;
import com.example.petstoremobile.utils.ErrorUtils;
import java.util.ArrayList;
import java.util.List;
@@ -39,6 +39,9 @@ import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
/**
* Fragment for displaying and editing inventory item details.
*/
@AndroidEntryPoint
public class InventoryDetailFragment extends Fragment {
@@ -65,6 +68,9 @@ public class InventoryDetailFragment extends Fragment {
private final List<ProductDTO> productSuggestions = new ArrayList<>();
private ArrayAdapter<String> dropdownAdapter;
/**
* Sets the parent inventory fragment to notify of changes.
*/
public void setInventoryFragment(InventoryFragment fragment) {
this.inventoryFragment = fragment;
}
@@ -85,6 +91,9 @@ public class InventoryDetailFragment extends Fragment {
return view;
}
/**
* get the layout view and set adapter.
*/
private void initViews(View view) {
tvMode = view.findViewById(R.id.tvInventoryMode);
tvInventoryId = view.findViewById(R.id.tvInventoryId);
@@ -102,7 +111,9 @@ public class InventoryDetailFragment extends Fragment {
etProductSearch.setThreshold(1); // start showing after 1 character
}
// Product search dropdown
/**
* setup the product search dropdown.
*/
private void setupProductSearch() {
etProductSearch.addTextChangedListener(new TextWatcher() {
@Override
@@ -143,6 +154,9 @@ public class InventoryDetailFragment extends Fragment {
});
}
/**
* Searches for products matching the query from the backend.
*/
private void searchProducts(String query) {
productApi.getAllProducts(query, 0, 20).enqueue(new Callback<PageResponse<ProductDTO>>() {
@Override
@@ -172,8 +186,9 @@ public class InventoryDetailFragment extends Fragment {
});
}
// Arguments (edit mode)
/**
* arguments to set up edit or add mode.
*/
private void handleArguments() {
Bundle args = getArguments();
if (args != null && args.containsKey("inventoryId")) {
@@ -214,7 +229,9 @@ public class InventoryDetailFragment extends Fragment {
}
}
// Save
/**
* Saves the current inventory item details to the backend.
*/
private void saveInventory() {
if (selectedProduct == null) {
etProductSearch.setError("Please select a product from the list");
@@ -255,7 +272,7 @@ public class InventoryDetailFragment extends Fragment {
Toast.makeText(getContext(), "Inventory updated", Toast.LENGTH_SHORT).show();
notifyParentAndGoBack();
} else {
Toast.makeText(getContext(), "Update failed: " + response.code(), Toast.LENGTH_SHORT).show();
ErrorUtils.showErrorMessage(getContext(), response, "Update failed");
}
}
@@ -274,7 +291,7 @@ public class InventoryDetailFragment extends Fragment {
Toast.makeText(getContext(), "Inventory created", Toast.LENGTH_SHORT).show();
notifyParentAndGoBack();
} else {
Toast.makeText(getContext(), "Create failed: " + response.code(), Toast.LENGTH_SHORT).show();
ErrorUtils.showErrorMessage(getContext(), response, "Create failed");
}
}
@@ -287,7 +304,9 @@ public class InventoryDetailFragment extends Fragment {
}
}
// Delete
/**
* Shows a confirmation dialog before deleting an inventory item.
*/
private void confirmDelete() {
new AlertDialog.Builder(requireContext())
.setTitle("Delete inventory item?")
@@ -297,6 +316,9 @@ public class InventoryDetailFragment extends Fragment {
.show();
}
/**
* Sends a request to the API to delete the inventory item.
*/
private void deleteInventory() {
setButtonsEnabled(false);
inventoryApi.deleteInventory(inventoryId).enqueue(new Callback<Void>() {
@@ -307,7 +329,7 @@ public class InventoryDetailFragment extends Fragment {
Toast.makeText(getContext(), "Inventory deleted", Toast.LENGTH_SHORT).show();
notifyParentAndGoBack();
} else {
Toast.makeText(getContext(), "Delete failed: " + response.code(), Toast.LENGTH_SHORT).show();
ErrorUtils.showErrorMessage(getContext(), response, "Delete failed");
}
}
@@ -319,18 +341,25 @@ public class InventoryDetailFragment extends Fragment {
});
}
// Helpers
/**
* Notifies the parent fragment of a change and navigates back.
*/
private void notifyParentAndGoBack() {
if (inventoryFragment != null)
inventoryFragment.onInventoryChanged();
navigateBack();
}
/**
* Navigates back to the previous fragment.
*/
private void navigateBack() {
NavHostFragment.findNavController(this).popBackStack();
}
/**
* Enables or disables action buttons.
*/
private void setButtonsEnabled(boolean enabled) {
btnSave.setEnabled(enabled);
btnDelete.setEnabled(enabled);

View File

@@ -3,9 +3,7 @@ package com.example.petstoremobile.fragments.listfragments.detailfragments;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.core.content.ContextCompat;
import androidx.fragment.app.Fragment;
import androidx.navigation.fragment.NavHostFragment;
@@ -13,7 +11,6 @@ import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.EditText;
import android.widget.Spinner;
@@ -24,9 +21,8 @@ import com.example.petstoremobile.R;
import com.example.petstoremobile.adapters.BlackTextArrayAdapter;
import com.example.petstoremobile.api.PetApi;
import com.example.petstoremobile.dtos.PetDTO;
import com.example.petstoremobile.fragments.ListFragment;
import com.example.petstoremobile.fragments.listfragments.PetFragment;
import com.example.petstoremobile.utils.ActivityLogger;
import com.example.petstoremobile.utils.ErrorUtils;
import com.example.petstoremobile.utils.InputValidator;
import javax.inject.Inject;
@@ -36,6 +32,9 @@ import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
/**
* Fragment for displaying and editing pet details.
*/
@AndroidEntryPoint
public class PetDetailFragment extends Fragment {
@@ -66,7 +65,9 @@ public class PetDetailFragment extends Fragment {
return view;
}
//Method to Update or Add a pet
/**
* Handles the saving of pet data (adding/updating).
*/
private void savePet() {
// Validates all fields using InputValidator
if (!InputValidator.isNotEmpty(etPetName, "Pet Name")) return;
@@ -104,7 +105,7 @@ public class PetDetailFragment extends Fragment {
Toast.makeText(getContext(), "Pet updated successfully!", Toast.LENGTH_SHORT).show();
navigateBack();
} else {
Toast.makeText(getContext(), "Failed to update pet: " + response.code(), Toast.LENGTH_SHORT).show();
ErrorUtils.showErrorMessage(getContext(), response, "Failed to update pet");
}
}
@@ -112,7 +113,7 @@ public class PetDetailFragment extends Fragment {
public void onFailure(Call<PetDTO> call, Throwable t) {
ActivityLogger.logException(requireContext(), "PetDetailFragment.updatePet", new Exception(t));
Log.e("PetDetailFragment", "Error updating pet", t);
Toast.makeText(getContext(), "Error: " + t.getMessage(), Toast.LENGTH_SHORT).show();
Toast.makeText(getContext(), "Network error: " + t.getMessage(), Toast.LENGTH_SHORT).show();
}
});
} else {
@@ -125,7 +126,7 @@ public class PetDetailFragment extends Fragment {
Toast.makeText(getContext(), "Pet added successfully!", Toast.LENGTH_SHORT).show();
navigateBack();
} else {
Toast.makeText(getContext(), "Failed to add pet: " + response.code(), Toast.LENGTH_SHORT).show();
ErrorUtils.showErrorMessage(getContext(), response, "Failed to add pet");
}
}
@@ -133,13 +134,15 @@ public class PetDetailFragment extends Fragment {
public void onFailure(Call<PetDTO> call, Throwable t) {
ActivityLogger.logException(requireContext(), "PetDetailFragment.createPet", new Exception(t));
Log.e("PetDetailFragment", "Error adding pet", t);
Toast.makeText(getContext(), "Error: " + t.getMessage(), Toast.LENGTH_SHORT).show();
Toast.makeText(getContext(), "Network error: " + t.getMessage(), Toast.LENGTH_SHORT).show();
}
});
}
}
//Method to Delete a pet
/**
* Displays a confirmation dialog and handles the deletion of a pet.
*/
private void deletePet() {
//Alert the user to confirm the delete
new AlertDialog.Builder(requireContext())
@@ -155,7 +158,7 @@ public class PetDetailFragment extends Fragment {
Toast.makeText(getContext(), "Pet deleted successfully!", Toast.LENGTH_SHORT).show();
navigateBack();
} else {
Toast.makeText(getContext(), "Failed to delete pet: " + response.code(), Toast.LENGTH_SHORT).show();
ErrorUtils.showErrorMessage(getContext(), response, "Failed to delete pet");
}
}
@@ -163,7 +166,7 @@ public class PetDetailFragment extends Fragment {
public void onFailure(Call<Void> call, Throwable t) {
ActivityLogger.logException(requireContext(), "PetDetailFragment.deletePet", new Exception(t));
Log.e("PetDetailFragment", "Error deleting pet", t);
Toast.makeText(getContext(), "Error: " + t.getMessage(), Toast.LENGTH_SHORT).show();
Toast.makeText(getContext(), "Network error: " + t.getMessage(), Toast.LENGTH_SHORT).show();
}
});
})
@@ -171,12 +174,16 @@ public class PetDetailFragment extends Fragment {
.show();
}
//Helper method to navigate back to the list
/**
* Navigates back to the previous screen.
*/
private void navigateBack() {
NavHostFragment.findNavController(this).popBackStack();
}
//helper function to check if pet is being edited or added and show the view accordingly
/**
* Handles arguments passed to the fragment to determine if it's in edit or add mode.
*/
private void handleArguments() {
// Pet is being edited if the bundle contains a petId
if (getArguments() != null && getArguments().containsKey("petId")) {
@@ -208,7 +215,9 @@ public class PetDetailFragment extends Fragment {
}
}
//helper function to get controls from layout
/**
* Binds UI components from the layout.
*/
private void initViews(View view) {
tvMode = view.findViewById(R.id.tvMode);
tvPetId = view.findViewById(R.id.tvPetId);
@@ -223,7 +232,9 @@ public class PetDetailFragment extends Fragment {
btnBack = view.findViewById(R.id.btnBack);
}
//helper function to set up the spinner menu for pet status
/**
* Initializes the spinner for pet status selection.
*/
private void setupSpinner() {
BlackTextArrayAdapter<String> adapter = new BlackTextArrayAdapter<>(requireContext(),
android.R.layout.simple_spinner_item,

View File

@@ -1,38 +1,25 @@
package com.example.petstoremobile.fragments.listfragments.detailfragments;
import android.Manifest;
import android.app.Activity;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Bundle;
import android.provider.MediaStore;
import android.util.Log;
import android.view.*;
import android.widget.*;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.core.content.ContextCompat;
import androidx.core.content.FileProvider;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.ViewModelProvider;
import androidx.navigation.fragment.NavHostFragment;
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.adapters.BlackTextArrayAdapter;
import com.example.petstoremobile.api.*;
import com.example.petstoremobile.api.auth.TokenManager;
import com.example.petstoremobile.dtos.*;
import com.example.petstoremobile.viewmodels.ProductViewModel;
import com.example.petstoremobile.utils.Resource;
import com.example.petstoremobile.utils.ErrorUtils;
import com.example.petstoremobile.utils.FileUtils;
import com.example.petstoremobile.utils.GlideUtils;
import com.example.petstoremobile.utils.ImagePickerHelper;
import java.io.File;
import java.math.BigDecimal;
@@ -46,6 +33,9 @@ import okhttp3.MediaType;
import okhttp3.MultipartBody;
import okhttp3.RequestBody;
/**
* Fragment for displaying and editing product details, including image selection.
*/
@AndroidEntryPoint
public class ProductDetailFragment extends Fragment {
@@ -65,52 +55,43 @@ public class ProductDetailFragment extends Fragment {
private List<CategoryDTO> categoryList = new ArrayList<>();
private Uri photoUri;
private ProductViewModel viewModel;
private ImagePickerHelper imagePickerHelper;
@Inject @Named("baseUrl") String baseUrl;
@Inject TokenManager tokenManager;
private ActivityResultLauncher<Intent> galleryLauncher;
private ActivityResultLauncher<Uri> cameraLauncher;
private ActivityResultLauncher<String> permissionLauncher;
/**
* Initializes activity launchers and the ImagePickerHelper.
*/
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
viewModel = new ViewModelProvider(this).get(ProductViewModel.class);
galleryLauncher = registerForActivityResult(
new ActivityResultContracts.StartActivityForResult(),
result -> {
if (result.getResultCode() == Activity.RESULT_OK && result.getData() != null) {
Uri selectedImage = result.getData().getData();
Glide.with(this).load(selectedImage).into(ivProductImage);
photoUri = selectedImage;
hasImage = true;
isImageChanged = true;
isImageRemoved = false;
}
}
);
cameraLauncher = registerForActivityResult(
new ActivityResultContracts.TakePicture(),
success -> {
if (success) {
Glide.with(this).load(photoUri).into(ivProductImage);
hasImage = true;
isImageChanged = true;
isImageRemoved = false;
}
}
);
permissionLauncher = registerForActivityResult(
new ActivityResultContracts.RequestPermission(),
granted -> {
if (granted) launchCamera();
else Toast.makeText(getContext(), "Camera permission denied", Toast.LENGTH_SHORT).show();
}
);
imagePickerHelper = new ImagePickerHelper(this, "product_photo.jpg", new ImagePickerHelper.ImagePickerListener() {
@Override
public void onImagePicked(Uri uri) {
photoUri = uri;
Glide.with(ProductDetailFragment.this).load(uri).into(ivProductImage);
hasImage = true;
isImageChanged = true;
isImageRemoved = false;
}
@Override
public void onImageRemoved() {
photoUri = null;
hasImage = false;
isImageChanged = false;
isImageRemoved = true;
ivProductImage.setImageResource(R.drawable.placeholder2);
}
});
}
/**
* Inflates the layout and initializes UI components and listeners.
*/
@Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
@@ -122,10 +103,13 @@ public class ProductDetailFragment extends Fragment {
btnBack.setOnClickListener(v -> navigateBack());
btnSave.setOnClickListener(v -> saveProduct());
btnDelete.setOnClickListener(v -> confirmDelete());
ivProductImage.setOnClickListener(v -> showImagePickerDialog());
ivProductImage.setOnClickListener(v -> imagePickerHelper.showImagePickerDialog("Select Product Image", hasImage));
return view;
}
/**
* get the UI components from the layout.
*/
private void initViews(View v) {
tvMode = v.findViewById(R.id.tvProductMode);
tvProductId = v.findViewById(R.id.tvProductId);
@@ -139,63 +123,21 @@ public class ProductDetailFragment extends Fragment {
ivProductImage = v.findViewById(R.id.ivProductImage);
}
// Helper function to show the image picker dialog
private void showImagePickerDialog() {
List<String> options = new ArrayList<>();
options.add("Take Photo");
options.add("Choose from Gallery");
if (hasImage) {
options.add("Remove Photo");
}
new AlertDialog.Builder(requireContext())
.setTitle("Select Product Image")
.setItems(options.toArray(new String[0]), (dialog, which) -> {
String selectedOption = options.get(which);
if (selectedOption.equals("Take Photo")) {
if (ContextCompat.checkSelfPermission(requireContext(), Manifest.permission.CAMERA)
== PackageManager.PERMISSION_GRANTED) {
launchCamera();
} else {
permissionLauncher.launch(Manifest.permission.CAMERA);
}
} else if (selectedOption.equals("Choose from Gallery")) {
Intent intent = new Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI);
galleryLauncher.launch(intent);
} else if (selectedOption.equals("Remove Photo")) {
removePhoto();
}
})
.show();
}
// Helper function to remove the photo locally
private void removePhoto() {
photoUri = null;
hasImage = false;
isImageChanged = false;
isImageRemoved = true;
Glide.with(this).load(R.drawable.placeholder2).into(ivProductImage);
}
// Helper function to launch the camera
private void launchCamera() {
File photoFile = new File(requireContext().getCacheDir(), "product_photo.jpg");
photoUri = FileProvider.getUriForFile(requireContext(), requireContext().getPackageName() + ".fileprovider", photoFile);
cameraLauncher.launch(photoUri);
}
// Helper function to load categories from the backend for the spinner
/**
* Fetches all product categories for the selection spinner.
*/
private void loadCategories() {
viewModel.getAllCategories(0, 100).observe(getViewLifecycleOwner(), resource -> {
if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) {
if (resource != null && resource.status == com.example.petstoremobile.utils.Resource.Status.SUCCESS && resource.data != null) {
categoryList = resource.data.getContent();
populateCategorySpinner();
}
});
}
// Helper function to populate the category spinner
/**
* Fills the spinner with category names.
*/
private void populateCategorySpinner() {
List<String> names = new ArrayList<>();
names.add("-- Select Category --");
@@ -211,6 +153,9 @@ public class ProductDetailFragment extends Fragment {
}
}
/**
* Checks if the fragment was opened with existing product data for editing.
*/
private void handleArguments() {
Bundle a = getArguments();
if (a != null && a.containsKey("prodId")) {
@@ -235,34 +180,34 @@ public class ProductDetailFragment extends Fragment {
}
}
//load the product image from the backend
/**
* Loads the product image from the backend.
*/
private void loadProductImage() {
String imageUrl = baseUrl + String.format(Locale.US, ProductApi.PRODUCT_IMAGE_PATH, prodId);
String token = tokenManager.getToken();
Object loadTarget = imageUrl;
if (token != null) {
loadTarget = new GlideUrl(imageUrl, new LazyHeaders.Builder()
.addHeader("Authorization", "Bearer " + token)
.build());
}
GlideUtils.loadImageWithToken(requireContext(), ivProductImage, imageUrl, token, R.drawable.placeholder2, new GlideUtils.ImageLoadListener() {
@Override
public void onResourceReady() {
hasImage = true;
}
Glide.with(this)
.load(loadTarget)
.skipMemoryCache(true)
.diskCacheStrategy(DiskCacheStrategy.NONE)
.placeholder(R.drawable.placeholder2)
.error(R.drawable.placeholder2)
.into(ivProductImage);
@Override
public void onLoadFailed() {
hasImage = false;
}
});
}
// Function to check any changes to the image and perform the appropriate action
// updating/adding photo, removing photo or no change
/**
* Performs image related actions (upload/delete) after product details are saved.
*/
private void performPendingImageActions(String successMsg) {
if (isImageRemoved) {
viewModel.deleteProductImage(prodId).observe(getViewLifecycleOwner(), resource -> {
if (resource != null && resource.status != Resource.Status.LOADING) {
if (resource.status == Resource.Status.SUCCESS) {
if (resource != null && resource.status != com.example.petstoremobile.utils.Resource.Status.LOADING) {
if (resource.status == com.example.petstoremobile.utils.Resource.Status.SUCCESS) {
Toast.makeText(getContext(), successMsg, Toast.LENGTH_SHORT).show();
} else {
Toast.makeText(getContext(), successMsg + " (but image removal failed)", Toast.LENGTH_SHORT).show();
@@ -278,8 +223,9 @@ public class ProductDetailFragment extends Fragment {
}
}
// Helper function to upload the product image by calling the backend
// and then navigate back to the previous screen
/**
* Uploads the selected image file to the server.
*/
private void uploadProductImageAndNavigate(Uri uri, String successMsg) {
File file = FileUtils.getFileFromUri(requireContext(), uri);
if (file == null) {
@@ -292,8 +238,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 != Resource.Status.LOADING) {
if (resource.status == Resource.Status.SUCCESS) {
if (resource != null && resource.status != com.example.petstoremobile.utils.Resource.Status.LOADING) {
if (resource.status == com.example.petstoremobile.utils.Resource.Status.SUCCESS) {
Toast.makeText(getContext(), successMsg, Toast.LENGTH_SHORT).show();
} else {
Toast.makeText(getContext(), successMsg + " (but image upload failed)", Toast.LENGTH_SHORT).show();
@@ -303,6 +249,9 @@ public class ProductDetailFragment extends Fragment {
});
}
/**
* Validates input fields and saves product information to the backend.
*/
private void saveProduct() {
String name = etProductName.getText().toString().trim();
String desc = etProductDesc.getText().toString().trim();
@@ -330,8 +279,8 @@ public class ProductDetailFragment extends Fragment {
if (isEditing) {
viewModel.updateProduct(prodId, dto).observe(getViewLifecycleOwner(), resource -> {
if (resource != null && resource.status != Resource.Status.LOADING) {
if (resource.status == Resource.Status.SUCCESS) {
if (resource != null && resource.status != com.example.petstoremobile.utils.Resource.Status.LOADING) {
if (resource.status == com.example.petstoremobile.utils.Resource.Status.SUCCESS) {
performPendingImageActions("Updated");
} else {
Toast.makeText(getContext(), "Error: " + resource.message, Toast.LENGTH_SHORT).show();
@@ -340,8 +289,8 @@ public class ProductDetailFragment extends Fragment {
});
} else {
viewModel.createProduct(dto).observe(getViewLifecycleOwner(), resource -> {
if (resource != null && resource.status != Resource.Status.LOADING) {
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
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) {
prodId = resource.data.getProdId();
performPendingImageActions("Saved");
} else {
@@ -352,21 +301,26 @@ public class ProductDetailFragment extends Fragment {
}
}
// Function to delete the product from the server
/**
* Displays a confirmation dialog before deleting the product.
*/
private void confirmDelete() {
new AlertDialog.Builder(requireContext())
new androidx.appcompat.app.AlertDialog.Builder(requireContext())
.setTitle("Delete Product?")
.setPositiveButton("Yes", (d, w) ->
viewModel.deleteProduct(prodId).observe(getViewLifecycleOwner(), resource -> {
if (resource != null && resource.status == Resource.Status.SUCCESS) {
if (resource != null && resource.status == com.example.petstoremobile.utils.Resource.Status.SUCCESS) {
navigateBack();
} else if (resource != null && resource.status == Resource.Status.ERROR) {
} else if (resource != null && resource.status == com.example.petstoremobile.utils.Resource.Status.ERROR) {
Toast.makeText(getContext(), "Delete failed: " + resource.message, Toast.LENGTH_SHORT).show();
}
}))
.setNegativeButton("No", null).show();
}
/**
* Navigates back to the previous fragment.
*/
private void navigateBack() {
NavHostFragment.findNavController(this).popBackStack();
}

View File

@@ -13,7 +13,8 @@ import com.example.petstoremobile.R;
import com.example.petstoremobile.adapters.BlackTextArrayAdapter;
import com.example.petstoremobile.api.*;
import com.example.petstoremobile.dtos.*;
import com.example.petstoremobile.fragments.ListFragment;
import com.example.petstoremobile.utils.ErrorUtils;
import java.math.BigDecimal;
import java.util.*;
@@ -22,6 +23,9 @@ import javax.inject.Inject;
import dagger.hilt.android.AndroidEntryPoint;
import retrofit2.*;
/**
* Fragment for displaying and editing the relationship between products and suppliers,
*/
@AndroidEntryPoint
public class ProductSupplierDetailFragment extends Fragment {
@@ -57,6 +61,9 @@ public class ProductSupplierDetailFragment extends Fragment {
return view;
}
/**
* Initializes UI components from the layout.
*/
private void initViews(View v) {
tvMode = v.findViewById(R.id.tvPSMode);
spinnerProduct = v.findViewById(R.id.spinnerPSProduct);
@@ -67,11 +74,17 @@ public class ProductSupplierDetailFragment extends Fragment {
btnBack = v.findViewById(R.id.btnPSBack);
}
/**
* Fetches products and suppliers to populate the spinners.
*/
private void loadData() {
loadProducts();
loadSuppliers();
}
/**
* Loads the list of products from the API.
*/
private void loadProducts() {
productApi.getAllProducts(null, 0, 200)
.enqueue(new Callback<PageResponse<ProductDTO>>() {
@@ -88,6 +101,9 @@ public class ProductSupplierDetailFragment extends Fragment {
});
}
/**
* Populates the product spinner.
*/
private void populateProductSpinner() {
List<String> names = new ArrayList<>();
names.add("-- Select Product --");
@@ -103,6 +119,9 @@ public class ProductSupplierDetailFragment extends Fragment {
}
}
/**
* Loads the list of suppliers from the API.
*/
private void loadSuppliers() {
supplierApi.getAllSuppliers(0, 200)
.enqueue(new Callback<PageResponse<SupplierDTO>>() {
@@ -119,6 +138,9 @@ public class ProductSupplierDetailFragment extends Fragment {
});
}
/**
* Populates the supplier spinner.
*/
private void populateSupplierSpinner() {
List<String> names = new ArrayList<>();
names.add("-- Select Supplier --");
@@ -134,6 +156,9 @@ public class ProductSupplierDetailFragment extends Fragment {
}
}
/**
* 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")) {
@@ -151,6 +176,9 @@ public class ProductSupplierDetailFragment extends Fragment {
}
}
/**
* Validates input and saves the product-supplier to the backend.
*/
private void save() {
if (spinnerProduct.getSelectedItemPosition() == 0) {
Toast.makeText(getContext(), "Select a product", Toast.LENGTH_SHORT).show(); return;
@@ -183,6 +211,9 @@ public class ProductSupplierDetailFragment extends Fragment {
}
}
/**
* callback for product-supplier save/update operations.
*/
private Callback<ProductSupplierDTO> simpleCallback(String msg) {
return new Callback<>() {
public void onResponse(Call<ProductSupplierDTO> c, Response<ProductSupplierDTO> r) {
@@ -190,13 +221,7 @@ public class ProductSupplierDetailFragment extends Fragment {
Toast.makeText(getContext(), msg, Toast.LENGTH_SHORT).show();
navigateBack();
} else {
try {
String err = r.errorBody().string();
Log.e("PS_SAVE", "Error: " + err);
Toast.makeText(getContext(), "Error " + r.code(), Toast.LENGTH_SHORT).show();
} catch (Exception e) {
Log.e("PS_SAVE", "Failed to read error");
}
ErrorUtils.showErrorMessage(getContext(), r, "Error " + r.code());
}
}
public void onFailure(Call<ProductSupplierDTO> c, Throwable t) {
@@ -206,6 +231,9 @@ public class ProductSupplierDetailFragment extends Fragment {
};
}
/**
* Shows a confirmation dialog before deleting a product-supplier relationship.
*/
private void confirmDelete() {
new AlertDialog.Builder(requireContext())
.setTitle("Delete?")
@@ -223,6 +251,9 @@ public class ProductSupplierDetailFragment extends Fragment {
.setNegativeButton("No", null).show();
}
/**
* Navigates back to the previous screen.
*/
private void navigateBack() {
NavHostFragment.findNavController(this).popBackStack();
}

View File

@@ -9,16 +9,21 @@ import androidx.fragment.app.Fragment;
import androidx.navigation.fragment.NavHostFragment;
import com.example.petstoremobile.R;
import com.example.petstoremobile.fragments.ListFragment;
import dagger.hilt.android.AndroidEntryPoint;
/**
* Fragment for displaying the information of a purchase order.
*/
@AndroidEntryPoint
public class PurchaseOrderDetailFragment extends Fragment {
private TextView tvId, tvSupplier, tvDate, tvStatus;
private Button btnBack;
/**
* Inflates the layout, initializes views, and populates order data from arguments.
*/
@Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {

View File

@@ -20,6 +20,7 @@ import com.example.petstoremobile.api.ServiceApi;
import com.example.petstoremobile.dtos.ServiceDTO;
import com.example.petstoremobile.fragments.listfragments.ServiceFragment;
import com.example.petstoremobile.utils.ActivityLogger;
import com.example.petstoremobile.utils.ErrorUtils;
import com.example.petstoremobile.utils.InputValidator;
import javax.inject.Inject;
@@ -29,6 +30,9 @@ import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
/**
* Fragment for displaying and editing service details.
*/
@AndroidEntryPoint
public class ServiceDetailFragment extends Fragment {
@@ -41,7 +45,9 @@ public class ServiceDetailFragment extends Fragment {
@Inject ServiceApi serviceApi;
//set the service fragment to the parent so we refer back to service view when save or delete is done
/**
* Sets the parent service fragment to notify of changes.
*/
public void setServiceFragment(ServiceFragment fragment) {
this.serviceFragment = fragment;
}
@@ -63,7 +69,9 @@ public class ServiceDetailFragment extends Fragment {
return view;
}
//Method to Update or Add a service
/**
* Handles the saving of service data (adding or updating).
*/
private void saveService() {
// Validates all fields using InputValidator
if (!InputValidator.isNotEmpty(etServiceName, "Service Name")) return;
@@ -96,7 +104,7 @@ public class ServiceDetailFragment extends Fragment {
Toast.makeText(getContext(), "Service updated successfully!", Toast.LENGTH_SHORT).show();
navigateBack();
} else {
Toast.makeText(getContext(), "Failed to update service: " + response.code(), Toast.LENGTH_SHORT).show();
ErrorUtils.showErrorMessage(getContext(), response, "Failed to update service");
}
}
@@ -104,7 +112,7 @@ public class ServiceDetailFragment extends Fragment {
public void onFailure(Call<ServiceDTO> call, Throwable t) {
ActivityLogger.logException(requireContext(), "ServiceDetailFragment.updateService", new Exception(t));
Log.e("ServiceDetailFragment", "Error updating service", t);
Toast.makeText(getContext(), "Error: " + t.getMessage(), Toast.LENGTH_SHORT).show();
Toast.makeText(getContext(), "Network error: " + t.getMessage(), Toast.LENGTH_SHORT).show();
}
});
} else {
@@ -117,7 +125,7 @@ public class ServiceDetailFragment extends Fragment {
Toast.makeText(getContext(), "Service added successfully!", Toast.LENGTH_SHORT).show();
navigateBack();
} else {
Toast.makeText(getContext(), "Failed to add service: " + response.code(), Toast.LENGTH_SHORT).show();
ErrorUtils.showErrorMessage(getContext(), response, "Failed to add service");
}
}
@@ -125,13 +133,15 @@ public class ServiceDetailFragment extends Fragment {
public void onFailure(Call<ServiceDTO> call, Throwable t) {
ActivityLogger.logException(requireContext(), "ServiceDetailFragment.createService", new Exception(t));
Log.e("ServiceDetailFragment", "Error adding service", t);
Toast.makeText(getContext(), "Error: " + t.getMessage(), Toast.LENGTH_SHORT).show();
Toast.makeText(getContext(), "Network error: " + t.getMessage(), Toast.LENGTH_SHORT).show();
}
});
}
}
//Method to Delete a service
/**
* Displays a confirmation dialog and handles the deletion of a service.
*/
private void deleteService() {
//Alert the user to confirm the delete
new AlertDialog.Builder(requireContext())
@@ -146,7 +156,7 @@ public class ServiceDetailFragment extends Fragment {
Toast.makeText(getContext(), "Service deleted successfully!", Toast.LENGTH_SHORT).show();
navigateBack();
} else {
Toast.makeText(getContext(), "Failed to delete service: " + response.code(), Toast.LENGTH_SHORT).show();
ErrorUtils.showErrorMessage(getContext(), response, "Failed to delete service");
}
}
@@ -154,7 +164,7 @@ public class ServiceDetailFragment extends Fragment {
public void onFailure(Call<Void> call, Throwable t) {
ActivityLogger.logException(requireContext(), "ServiceDetailFragment.deleteService", new Exception(t));
Log.e("ServiceDetailFragment", "Error deleting service", t);
Toast.makeText(getContext(), "Error: " + t.getMessage(), Toast.LENGTH_SHORT).show();
Toast.makeText(getContext(), "Network error: " + t.getMessage(), Toast.LENGTH_SHORT).show();
}
});
})
@@ -162,12 +172,16 @@ public class ServiceDetailFragment extends Fragment {
.show();
}
//Helper method to navigate back to the list
/**
* Navigates back to the previous screen.
*/
private void navigateBack() {
NavHostFragment.findNavController(this).popBackStack();
}
//helper function to check if service is being edited or added and show the view accordingly
/**
* Handles arguments passed to the fragment to determine if it's in edit or add mode.
*/
private void handleArguments() {
// Service is being edited if the bundle contains a serviceId
if (getArguments() != null && getArguments().containsKey("serviceId")) {
@@ -192,7 +206,9 @@ public class ServiceDetailFragment extends Fragment {
}
}
//helper function to get controls from layout
/**
* Set UI components from the layout.
*/
private void initViews(View view) {
tvMode = view.findViewById(R.id.tvMode);
tvServiceId = view.findViewById(R.id.tvServiceId);

View File

@@ -19,7 +19,9 @@ import com.example.petstoremobile.R;
import com.example.petstoremobile.api.SupplierApi;
import com.example.petstoremobile.dtos.SupplierDTO;
import com.example.petstoremobile.utils.ActivityLogger;
import com.example.petstoremobile.utils.ErrorUtils;
import com.example.petstoremobile.utils.InputValidator;
import com.example.petstoremobile.utils.UIUtils;
import javax.inject.Inject;
@@ -28,6 +30,9 @@ import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
/**
* Fragment for displaying and editing supplier details.
*/
@AndroidEntryPoint
public class SupplierDetailFragment extends Fragment {
@@ -56,7 +61,9 @@ public class SupplierDetailFragment extends Fragment {
return view;
}
//Method to Update or Add a supplier
/**
* Handles the saving of supplier data (adding or updating).
*/
private void saveSupplier() {
// Validates all fields using InputValidator
if (!InputValidator.isNotEmpty(etSupCompany, "Company Name")) return;
@@ -92,7 +99,7 @@ public class SupplierDetailFragment extends Fragment {
Toast.makeText(getContext(), "Supplier updated successfully!", Toast.LENGTH_SHORT).show();
navigateBack();
} else {
Toast.makeText(getContext(), "Failed to update supplier: " + response.code(), Toast.LENGTH_SHORT).show();
ErrorUtils.showErrorMessage(getContext(), response, "Failed to update supplier");
}
}
@@ -100,7 +107,7 @@ public class SupplierDetailFragment extends Fragment {
public void onFailure(Call<SupplierDTO> call, Throwable t) {
ActivityLogger.logException(requireContext(), "SupplierDetailFragment.updateSupplier", new Exception(t));
Log.e("SupplierDetailFragment", "Error updating supplier", t);
Toast.makeText(getContext(), "Error: " + t.getMessage(), Toast.LENGTH_SHORT).show();
Toast.makeText(getContext(), "Network error: " + t.getMessage(), Toast.LENGTH_SHORT).show();
}
});
} else {
@@ -113,7 +120,7 @@ public class SupplierDetailFragment extends Fragment {
Toast.makeText(getContext(), "Supplier added successfully!", Toast.LENGTH_SHORT).show();
navigateBack();
} else {
Toast.makeText(getContext(), "Failed to add supplier: " + response.code(), Toast.LENGTH_SHORT).show();
ErrorUtils.showErrorMessage(getContext(), response, "Failed to add supplier");
}
}
@@ -121,13 +128,15 @@ public class SupplierDetailFragment extends Fragment {
public void onFailure(Call<SupplierDTO> call, Throwable t) {
ActivityLogger.logException(requireContext(), "SupplierDetailFragment.createSupplier", new Exception(t));
Log.e("SupplierDetailFragment", "Error adding supplier", t);
Toast.makeText(getContext(), "Error: " + t.getMessage(), Toast.LENGTH_SHORT).show();
Toast.makeText(getContext(), "Network error: " + t.getMessage(), Toast.LENGTH_SHORT).show();
}
});
}
}
//Method to Delete a supplier
/**
* Displays a confirmation dialog and handles the deletion of a supplier.
*/
private void deleteSupplier() {
//Alert the user to confirm the delete
new AlertDialog.Builder(requireContext())
@@ -142,7 +151,7 @@ public class SupplierDetailFragment extends Fragment {
Toast.makeText(getContext(), "Supplier deleted successfully!", Toast.LENGTH_SHORT).show();
navigateBack();
} else {
Toast.makeText(getContext(), "Failed to delete supplier: " + response.code(), Toast.LENGTH_SHORT).show();
ErrorUtils.showErrorMessage(getContext(), response, "Failed to delete supplier");
}
}
@@ -150,7 +159,7 @@ public class SupplierDetailFragment extends Fragment {
public void onFailure(Call<Void> call, Throwable t) {
ActivityLogger.logException(requireContext(), "SupplierDetailFragment.deleteSupplier", new Exception(t));
Log.e("SupplierDetailFragment", "Error deleting supplier", t);
Toast.makeText(getContext(), "Error: " + t.getMessage(), Toast.LENGTH_SHORT).show();
Toast.makeText(getContext(), "Network error: " + t.getMessage(), Toast.LENGTH_SHORT).show();
}
});
})
@@ -158,12 +167,16 @@ public class SupplierDetailFragment extends Fragment {
.show();
}
//Helper method to navigate back to the list
/**
* Navigates back to the previous screen.
*/
private void navigateBack() {
NavHostFragment.findNavController(this).popBackStack();
}
//helper function to check if supplier is being edited or added and show the view accordingly
/**
* Handles arguments passed to the fragment to determine if it's in edit or add mode.
*/
private void handleArguments() {
// Supplier is being edited if the bundle contains a supId
if (getArguments() != null && getArguments().containsKey("supId")) {
@@ -189,7 +202,9 @@ public class SupplierDetailFragment extends Fragment {
}
}
//helper function to get controls from layout
/**
* Initializes the UI components and sets up formatting for phone input.
*/
private void initViews(View view) {
tvMode = view.findViewById(R.id.tvMode);
tvSupId = view.findViewById(R.id.tvSupId);
@@ -200,8 +215,7 @@ public class SupplierDetailFragment extends Fragment {
etSupPhone = view.findViewById(R.id.etSupPhone);
// Add phone number formatting (CA) and limit length to 14 characters
etSupPhone.addTextChangedListener(new android.telephony.PhoneNumberFormattingTextWatcher("CA"));
etSupPhone.setFilters(new android.text.InputFilter[]{new android.text.InputFilter.LengthFilter(14)});
UIUtils.formatPhoneInput(etSupPhone);
btnSaveSupplier = view.findViewById(R.id.btnSaveSupplier);
btnDeleteSupplier = view.findViewById(R.id.btnDeleteSupplier);

View File

@@ -1,22 +1,11 @@
package com.example.petstoremobile.fragments.listfragments.listprofilefragments;
import android.Manifest;
import android.app.Activity;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.graphics.Color;
import android.net.Uri;
import android.os.Bundle;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.appcompat.app.AlertDialog;
import androidx.core.content.ContextCompat;
import androidx.core.content.FileProvider;
import androidx.fragment.app.Fragment;
import androidx.navigation.fragment.NavHostFragment;
import android.provider.MediaStore;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
@@ -26,21 +15,14 @@ import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast;
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.api.PetApi;
import com.example.petstoremobile.api.auth.TokenManager;
import com.example.petstoremobile.fragments.ListFragment;
import com.example.petstoremobile.fragments.listfragments.detailfragments.PetDetailFragment;
import com.example.petstoremobile.utils.FileUtils;
import com.example.petstoremobile.utils.GlideUtils;
import com.example.petstoremobile.utils.ImagePickerHelper;
import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import javax.inject.Inject;
@@ -60,7 +42,6 @@ public class PetProfileFragment extends Fragment {
private TextView tvPetName, tvPetSpecies, tvPetBreed, tvPetAge, tvPetPrice;
private Button btnBack, btnEditPet, btnChangePhoto;
private ImageView imgPet;
private Uri photoUri;
private int petId;
private boolean hasImage = false;
@@ -68,59 +49,32 @@ public class PetProfileFragment extends Fragment {
@Inject @Named("baseUrl") String baseUrl;
@Inject TokenManager tokenManager;
// launchers for camera and gallery
private ActivityResultLauncher<Intent> galleryLauncher;
private ActivityResultLauncher<Uri> cameraLauncher;
private ActivityResultLauncher<String> permissionLauncher;
private ImagePickerHelper imagePickerHelper;
/**
* Initializes activity launchers for gallery, camera, and permissions.
*/
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Launcher to open gallery to select image
galleryLauncher = registerForActivityResult(
new ActivityResultContracts.StartActivityForResult(),
result -> {
if (result.getResultCode() == Activity.RESULT_OK && result.getData() != null) {
Uri selectedImage = result.getData().getData();
uploadPetImage(selectedImage);
}
}
);
imagePickerHelper = new ImagePickerHelper(this, "pet_photo.jpg", new ImagePickerHelper.ImagePickerListener() {
@Override
public void onImagePicked(Uri uri) {
uploadPetImage(uri);
}
// Launcher for camera to open and capture image
cameraLauncher = registerForActivityResult(
new ActivityResultContracts.TakePicture(),
success -> {
if (success) {
uploadPetImage(photoUri);
}
}
);
// Launcher to request camera permission
permissionLauncher = registerForActivityResult(
new ActivityResultContracts.RequestPermission(),
granted -> {
if (granted) {
launchCamera();
} else {
new AlertDialog.Builder(requireContext())
.setTitle("Permission Required")
.setMessage("Please grant camera permission to use this feature")
.setPositiveButton("Open Settings", (dialog, which) -> {
Intent intent = new Intent(android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
intent.setData(Uri.fromParts("package", requireContext().getPackageName(), null));
startActivity(intent);
})
.setNegativeButton("Cancel", null)
.show();
}
}
);
@Override
public void onImageRemoved() {
deletePetImage();
}
});
}
/**
* Inflates the layout, initializes views, and sets up click listeners.
*/
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
@@ -164,78 +118,38 @@ public class PetProfileFragment extends Fragment {
//Make change photo button ask user to select a new photo
btnChangePhoto.setOnClickListener(v -> {
List<String> options = new ArrayList<>();
options.add("Take Photo");
options.add("Choose from Gallery");
if (hasImage) {
options.add("Remove Photo");
}
new AlertDialog.Builder(requireContext())
.setTitle("Change Pet Photo")
.setItems(options.toArray(new String[0]), (dialog, which) -> {
String selected = options.get(which);
if (selected.equals("Take Photo")) {
// Choose Camera
//Checks if the user has granted the camera permission already
if (ContextCompat.checkSelfPermission(requireContext(), Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED) {
//if the permission is already granted then launch the camera
launchCamera();
} else {
//otherwise request the permission
permissionLauncher.launch(Manifest.permission.CAMERA);
}
} else if (selected.equals("Choose from Gallery")) {
Intent intent = new Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI);
galleryLauncher.launch(intent);
} else if (selected.equals("Remove Photo")) {
deletePetImage();
}
})
.show();
imagePickerHelper.showImagePickerDialog("Change Pet Photo", hasImage);
});
return view;
}
// Helper function to load pet image from backend
/**
* 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();
Object loadTarget = imageUrl;
if (token != null) {
loadTarget = new GlideUrl(imageUrl, new LazyHeaders.Builder()
.addHeader("Authorization", "Bearer " + token)
.build());
}
GlideUtils.loadImageWithToken(requireContext(), imgPet, imageUrl, token, R.drawable.placeholder, new GlideUtils.ImageLoadListener() {
@Override
public void onResourceReady() {
hasImage = true;
}
Glide.with(this)
.load(loadTarget)
.skipMemoryCache(true)
.diskCacheStrategy(DiskCacheStrategy.NONE)
.placeholder(R.drawable.placeholder)
.error(R.drawable.placeholder)
.listener(new com.bumptech.glide.request.RequestListener<android.graphics.drawable.Drawable>() {
@Override
public boolean onLoadFailed(@androidx.annotation.Nullable com.bumptech.glide.load.engine.GlideException e, Object model, com.bumptech.glide.request.target.Target<android.graphics.drawable.Drawable> target, boolean isFirstResource) {
hasImage = false;
return false;
}
@Override
public boolean onResourceReady(android.graphics.drawable.Drawable resource, Object model, com.bumptech.glide.request.target.Target<android.graphics.drawable.Drawable> target, com.bumptech.glide.load.DataSource dataSource, boolean isFirstResource) {
hasImage = true;
return false;
}
})
.into(imgPet);
@Override
public void onLoadFailed() {
hasImage = false;
}
});
}
// Helper function to upload pet image to backend
/**
* Uploads a selected or captured image a pet photo through the API.
*/
private void uploadPetImage(Uri uri) {
try {
File file = getFileFromUri(uri);
File file = FileUtils.getFileFromUri(requireContext(), uri);
if (file == null) return;
// Create RequestBody for file upload
@@ -266,6 +180,9 @@ public class PetProfileFragment extends Fragment {
}
}
/**
* Sends a request to the API to remove the current pet photo.
*/
private void deletePetImage() {
petApi.deletePetImage((long) petId).enqueue(new Callback<Void>() {
@Override
@@ -286,30 +203,4 @@ public class PetProfileFragment extends Fragment {
}
});
}
// Helper function to create a temporary File object from a Uri for uploading
private File getFileFromUri(Uri uri) {
try {
InputStream inputStream = requireContext().getContentResolver().openInputStream(uri);
File tempFile = new File(requireContext().getCacheDir(), "upload_pet_image.jpg");
FileOutputStream outputStream = new FileOutputStream(tempFile);
byte[] buffer = new byte[1024];
int length;
while ((length = inputStream.read(buffer)) > 0) {
outputStream.write(buffer, 0, length);
}
outputStream.close();
inputStream.close();
return tempFile;
} catch (Exception e) {
Log.e("FILE_UTILS", "Error creating temp file", e);
return null;
}
}
private void launchCamera() {
File photoFile = new File(requireContext().getCacheDir(), "pet_photo.jpg");
photoUri = FileProvider.getUriForFile(requireContext(), requireContext().getPackageName() + ".fileprovider", photoFile);
cameraLauncher.launch(photoUri);
}
}

View File

@@ -0,0 +1,32 @@
package com.example.petstoremobile.utils;
import android.content.Context;
import android.util.Log;
import android.widget.Toast;
import com.example.petstoremobile.dtos.ErrorResponse;
import com.google.gson.Gson;
import retrofit2.Response;
/**
* Utility class for handling API error responses.
*/
public class ErrorUtils {
/**
* Shows an error message to toast based on the response.
*/
public static void showErrorMessage(Context context, Response<?> response, String defaultMessage) {
try {
if (response.errorBody() != null) {
String errorJson = response.errorBody().string();
ErrorResponse errorResponse = new Gson().fromJson(errorJson, ErrorResponse.class);
if (errorResponse != null && errorResponse.getMessage() != null) {
Toast.makeText(context, errorResponse.getMessage(), Toast.LENGTH_LONG).show();
return;
}
}
} catch (Exception e) {
Log.e("ErrorUtils", "Error parsing error body", e);
}
Toast.makeText(context, defaultMessage, Toast.LENGTH_SHORT).show();
}
}

View File

@@ -0,0 +1,122 @@
package com.example.petstoremobile.utils;
import android.content.Context;
import android.graphics.drawable.Drawable;
import android.widget.ImageView;
import androidx.annotation.Nullable;
import com.bumptech.glide.Glide;
import com.bumptech.glide.load.DataSource;
import com.bumptech.glide.load.engine.DiskCacheStrategy;
import com.bumptech.glide.load.engine.GlideException;
import com.bumptech.glide.load.model.GlideUrl;
import com.bumptech.glide.load.model.LazyHeaders;
import com.bumptech.glide.request.RequestListener;
import com.bumptech.glide.request.target.Target;
import com.example.petstoremobile.R;
/**
* Utility class for loading images using Glide with authentication tokens.
*/
public class GlideUtils {
/**
* interface to check the status of the image load.
*/
public interface ImageLoadListener {
void onResourceReady();
void onLoadFailed();
}
/**
* Loads an image from a URL into an ImageView with token.
*/
public static void loadImageWithToken(Context context, ImageView imageView, String url, String token, int placeholder) {
loadImageWithToken(context, imageView, url, token, placeholder, null);
}
/**
* Loads an image from a URL into an ImageView with token and listener.
*/
public static void loadImageWithToken(Context context, ImageView imageView, String url, String token, int placeholder, ImageLoadListener listener) {
if (url == null) {
imageView.setImageResource(placeholder);
if (listener != null) listener.onLoadFailed();
return;
}
Object loadTarget = url;
if (token != null && url.startsWith("http")) {
loadTarget = new GlideUrl(url, new LazyHeaders.Builder()
.addHeader("Authorization", "Bearer " + token)
.build());
}
Glide.with(context)
.load(loadTarget)
.diskCacheStrategy(DiskCacheStrategy.NONE)
.skipMemoryCache(true)
.placeholder(placeholder)
.error(placeholder)
.listener(new RequestListener<Drawable>() {
@Override
public boolean onLoadFailed(@Nullable GlideException e, Object model, Target<Drawable> target, boolean isFirstResource) {
if (listener != null) listener.onLoadFailed();
return false;
}
@Override
public boolean onResourceReady(Drawable resource, Object model, Target<Drawable> target, DataSource dataSource, boolean isFirstResource) {
if (listener != null) listener.onResourceReady();
return false;
}
})
.into(imageView);
}
/**
* Loads an image from a URL into an ImageView with token and applies circle cropping for image.
*/
public static void loadImageWithTokenCircle(Context context, ImageView imageView, String url, String token, int placeholder) {
loadImageWithTokenCircle(context, imageView, url, token, placeholder, null);
}
/**
* Loads an image from a URL into an ImageView with token, circle cropping, and listener.
*/
public static void loadImageWithTokenCircle(Context context, ImageView imageView, String url, String token, int placeholder, ImageLoadListener listener) {
if (url == null) {
imageView.setImageResource(placeholder);
if (listener != null) listener.onLoadFailed();
return;
}
Object loadTarget = url;
if (token != null && url.startsWith("http")) {
loadTarget = new GlideUrl(url, new LazyHeaders.Builder()
.addHeader("Authorization", "Bearer " + token)
.build());
}
Glide.with(context)
.load(loadTarget)
.circleCrop()
.diskCacheStrategy(DiskCacheStrategy.NONE)
.skipMemoryCache(true)
.placeholder(placeholder)
.error(placeholder)
.listener(new RequestListener<Drawable>() {
@Override
public boolean onLoadFailed(@Nullable GlideException e, Object model, Target<Drawable> target, boolean isFirstResource) {
if (listener != null) listener.onLoadFailed();
return false;
}
@Override
public boolean onResourceReady(Drawable resource, Object model, Target<Drawable> target, DataSource dataSource, boolean isFirstResource) {
if (listener != null) listener.onResourceReady();
return false;
}
})
.into(imageView);
}
}

View File

@@ -0,0 +1,160 @@
package com.example.petstoremobile.utils;
import android.Manifest;
import android.app.Activity;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.provider.MediaStore;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.appcompat.app.AlertDialog;
import androidx.core.content.ContextCompat;
import androidx.core.content.FileProvider;
import androidx.fragment.app.Fragment;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
/**
* Helper class to handle image picking from camera or gallery.
*/
public class ImagePickerHelper {
/**
* Listener interface to handle the results of image picking.
*/
public interface ImagePickerListener {
/**
* Called when an image has been successfully selected or captured.
*/
void onImagePicked(Uri uri);
/**
* Called when the user chooses to remove the existing image.
*/
void onImageRemoved();
}
private final Fragment fragment;
private final ImagePickerListener listener;
private final ActivityResultLauncher<Intent> galleryLauncher;
private final ActivityResultLauncher<Uri> cameraLauncher;
private final ActivityResultLauncher<String> permissionLauncher;
private Uri photoUri;
private final String tempFileName;
/**
* Constructor for ImagePickerHelper.
* Registers activity launchers for gallery, camera, and permissions.
*/
public ImagePickerHelper(Fragment fragment, String tempFileName, ImagePickerListener listener) {
this.fragment = fragment;
this.tempFileName = tempFileName;
this.listener = listener;
// Launcher to open gallery to select image
galleryLauncher = fragment.registerForActivityResult(
new ActivityResultContracts.StartActivityForResult(),
result -> {
if (result.getResultCode() == Activity.RESULT_OK && result.getData() != null) {
Uri selectedImage = result.getData().getData();
if (selectedImage != null) {
listener.onImagePicked(selectedImage);
}
}
}
);
// Launcher for camera to open and capture image
cameraLauncher = fragment.registerForActivityResult(
new ActivityResultContracts.TakePicture(),
success -> {
if (success && photoUri != null) {
listener.onImagePicked(photoUri);
}
}
);
// Launcher to request camera permission
permissionLauncher = fragment.registerForActivityResult(
new ActivityResultContracts.RequestPermission(),
granted -> {
if (granted) {
launchCamera();
} else {
showPermissionDeniedDialog();
}
}
);
}
/**
* Shows a dialog to choose between camera, gallery, and optionally remove photo.
*/
public void showImagePickerDialog(String title, boolean hasImage) {
List<String> options = new ArrayList<>();
options.add("Take Photo");
options.add("Choose from Gallery");
if (hasImage) {
options.add("Remove Photo");
}
new AlertDialog.Builder(fragment.requireContext())
.setTitle(title)
.setItems(options.toArray(new String[0]), (dialog, which) -> {
String selected = options.get(which);
if (selected.equals("Take Photo")) {
checkCameraPermission();
} else if (selected.equals("Choose from Gallery")) {
launchGallery();
} else if (selected.equals("Remove Photo")) {
listener.onImageRemoved();
}
})
.show();
}
/**
* Checks if camera permission is granted and launches camera or requests permission.
*/
private void checkCameraPermission() {
if (ContextCompat.checkSelfPermission(fragment.requireContext(), Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED) {
launchCamera();
} else {
permissionLauncher.launch(Manifest.permission.CAMERA);
}
}
/**
* Prepares a temporary file and launches the camera app.
*/
private void launchCamera() {
File photoFile = new File(fragment.requireContext().getCacheDir(), tempFileName);
photoUri = FileProvider.getUriForFile(fragment.requireContext(), fragment.requireContext().getPackageName() + ".fileprovider", photoFile);
cameraLauncher.launch(photoUri);
}
/**
* Launches the gallery app to select an existing image.
*/
private void launchGallery() {
Intent intent = new Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI);
galleryLauncher.launch(intent);
}
/**
* Shows a dialog explaining why camera permission is needed when denied.
*/
private void showPermissionDeniedDialog() {
new AlertDialog.Builder(fragment.requireContext())
.setTitle("Permission Required")
.setMessage("Please grant camera permission to use this feature")
.setPositiveButton("Open Settings", (dialog, which) -> {
Intent intent = new Intent(android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
intent.setData(Uri.fromParts("package", fragment.requireContext().getPackageName(), null));
fragment.startActivity(intent);
})
.setNegativeButton("Cancel", null)
.show();
}
}

View File

@@ -0,0 +1,18 @@
package com.example.petstoremobile.utils;
import android.telephony.PhoneNumberFormattingTextWatcher;
import android.text.InputFilter;
import android.widget.EditText;
/**
* Utility class for shared UI component logic and formatting.
*/
public class UIUtils {
/**
* Formats an EditText for to phone format
*/
public static void formatPhoneInput(EditText editText) {
editText.addTextChangedListener(new PhoneNumberFormattingTextWatcher("CA"));
editText.setFilters(new InputFilter[]{new InputFilter.LengthFilter(14)});
}
}