diff --git a/android/app/src/main/java/com/example/petstoremobile/activities/HomeActivity.java b/android/app/src/main/java/com/example/petstoremobile/activities/HomeActivity.java index 09bcea9d..947b98e1 100644 --- a/android/app/src/main/java/com/example/petstoremobile/activities/HomeActivity.java +++ b/android/app/src/main/java/com/example/petstoremobile/activities/HomeActivity.java @@ -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 { } } } -} \ No newline at end of file +} diff --git a/android/app/src/main/java/com/example/petstoremobile/activities/MainActivity.java b/android/app/src/main/java/com/example/petstoremobile/activities/MainActivity.java index d090a09e..3d27aab3 100644 --- a/android/app/src/main/java/com/example/petstoremobile/activities/MainActivity.java +++ b/android/app/src/main/java/com/example/petstoremobile/activities/MainActivity.java @@ -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) { diff --git a/android/app/src/main/java/com/example/petstoremobile/adapters/PetAdapter.java b/android/app/src/main/java/com/example/petstoremobile/adapters/PetAdapter.java index bec941f0..35c323eb 100644 --- a/android/app/src/main/java/com/example/petstoremobile/adapters/PetAdapter.java +++ b/android/app/src/main/java/com/example/petstoremobile/adapters/PetAdapter.java @@ -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 { @@ -93,25 +90,10 @@ public class PetAdapter extends RecyclerView.Adapter { 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); } diff --git a/android/app/src/main/java/com/example/petstoremobile/adapters/ProductAdapter.java b/android/app/src/main/java/com/example/petstoremobile/adapters/ProductAdapter.java index 4e7c5fea..ad1cf678 100644 --- a/android/app/src/main/java/com/example/petstoremobile/adapters/ProductAdapter.java +++ b/android/app/src/main/java/com/example/petstoremobile/adapters/ProductAdapter.java @@ -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 { @@ -72,22 +69,7 @@ public class ProductAdapter extends RecyclerView.Adapter 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>() { @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>() { @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>() { @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(); } -} \ No newline at end of file +} diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/ListFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/ListFragment.java index af3de053..5a2dcfc6 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/ListFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/ListFragment.java @@ -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); } diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/ProfileFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/ProfileFragment.java index f470ab31..b4f15e69 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/ProfileFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/ProfileFragment.java @@ -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 galleryLauncher; - private ActivityResultLauncher cameraLauncher; - private ActivityResultLauncher 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 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() { @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() { - @Override - public boolean onLoadFailed(@androidx.annotation.Nullable com.bumptech.glide.load.engine.GlideException e, Object model, com.bumptech.glide.request.target.Target target, boolean isFirstResource) { - hasImage = false; - return false; - } - - @Override - public boolean onResourceReady(android.graphics.drawable.Drawable resource, Object model, com.bumptech.glide.request.target.Target 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() { @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 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 { } }); } -} \ No newline at end of file +} diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AdoptionFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AdoptionFragment.java index 69bcee15..cae5fd2f 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AdoptionFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AdoptionFragment.java @@ -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 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); } } diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AppointmentFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AppointmentFragment.java index b284a2bf..f62475c0 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AppointmentFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AppointmentFragment.java @@ -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 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); diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/InventoryFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/InventoryFragment.java index e77ae958..799fe39f 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/InventoryFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/InventoryFragment.java @@ -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 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 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 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) { diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/PetFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/PetFragment.java index fdbc38f4..44198344 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/PetFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/PetFragment.java @@ -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); diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ProductFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ProductFragment.java index d56da91a..61adfcf5 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ProductFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ProductFragment.java @@ -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); } } diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ProductSupplierFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ProductSupplierFragment.java index a7b16298..d77331ca 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ProductSupplierFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ProductSupplierFragment.java @@ -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); } } diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/PurchaseOrderFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/PurchaseOrderFragment.java index 35366e83..36050661 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/PurchaseOrderFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/PurchaseOrderFragment.java @@ -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); diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ServiceFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ServiceFragment.java index 1c12cbf8..ff0819b2 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ServiceFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ServiceFragment.java @@ -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); diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/SupplierFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/SupplierFragment.java index 5e26a432..baa67b5b 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/SupplierFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/SupplierFragment.java @@ -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); diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/AdoptionDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/AdoptionDetailFragment.java index a9167678..a135e36e 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/AdoptionDetailFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/AdoptionDetailFragment.java @@ -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>() { @@ -110,6 +129,9 @@ public class AdoptionDetailFragment extends Fragment { }); } + /** + * Populates the pet selection spinner. + */ private void populatePetSpinner() { List 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>() { @@ -141,6 +166,9 @@ public class AdoptionDetailFragment extends Fragment { }); } + /** + * Populates the customer selection spinner. + */ private void populateCustomerSpinner() { List 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 simpleCallback(String msg) { return new Callback<>() { public void onResponse(Call c, Response 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 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() { - public void onResponse(Call c, Response r) { - navigateBack(); - } + public void onResponse(Call c, Response r) { navigateBack(); } public void onFailure(Call 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(); } diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/AppointmentDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/AppointmentDetailFragment.java index 45ffe85a..0f192dce 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/AppointmentDetailFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/AppointmentDetailFragment.java @@ -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>() { @@ -134,6 +153,9 @@ public class AppointmentDetailFragment extends Fragment { }); } + /** + * Populates the pet selection spinner. + */ private void populatePetSpinner() { List 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>() { @@ -164,6 +189,9 @@ public class AppointmentDetailFragment extends Fragment { }); } + /** + * Populates the service selection spinner. + */ private void populateServiceSpinner() { List 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>() { @@ -194,6 +225,9 @@ public class AppointmentDetailFragment extends Fragment { }); } + /** + * Populates the customer spinner. + */ private void populateCustomerSpinner() { List 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>() { @@ -225,6 +262,9 @@ public class AppointmentDetailFragment extends Fragment { }); } + /** + * Populates the store spinner. + */ private void populateStoreSpinner() { List 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>() { @@ -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 simpleCallback(String msg) { return new Callback<>() { public void onResponse(Call c, Response 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(); } diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/InventoryDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/InventoryDetailFragment.java index 92fecd47..ee22a536 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/InventoryDetailFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/InventoryDetailFragment.java @@ -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 productSuggestions = new ArrayList<>(); private ArrayAdapter 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>() { @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() { @@ -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); diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/PetDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/PetDetailFragment.java index 8aa3b319..83ce6d51 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/PetDetailFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/PetDetailFragment.java @@ -3,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 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 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 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 adapter = new BlackTextArrayAdapter<>(requireContext(), android.R.layout.simple_spinner_item, diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ProductDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ProductDetailFragment.java index 0eecece6..8baa9646 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ProductDetailFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ProductDetailFragment.java @@ -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 categoryList = new ArrayList<>(); private Uri photoUri; private ProductViewModel viewModel; + private ImagePickerHelper imagePickerHelper; @Inject @Named("baseUrl") String baseUrl; @Inject TokenManager tokenManager; - private ActivityResultLauncher galleryLauncher; - private ActivityResultLauncher cameraLauncher; - private ActivityResultLauncher 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 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 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(); } diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ProductSupplierDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ProductSupplierDetailFragment.java index d934f64f..f1559957 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ProductSupplierDetailFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ProductSupplierDetailFragment.java @@ -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>() { @@ -88,6 +101,9 @@ public class ProductSupplierDetailFragment extends Fragment { }); } + /** + * Populates the product spinner. + */ private void populateProductSpinner() { List 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>() { @@ -119,6 +138,9 @@ public class ProductSupplierDetailFragment extends Fragment { }); } + /** + * Populates the supplier spinner. + */ private void populateSupplierSpinner() { List 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 simpleCallback(String msg) { return new Callback<>() { public void onResponse(Call c, Response 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 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(); } diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/PurchaseOrderDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/PurchaseOrderDetailFragment.java index 38da2aa4..e4f05a26 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/PurchaseOrderDetailFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/PurchaseOrderDetailFragment.java @@ -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) { diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ServiceDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ServiceDetailFragment.java index fc65a1e1..0842f6bc 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ServiceDetailFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ServiceDetailFragment.java @@ -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 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 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 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); diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/SupplierDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/SupplierDetailFragment.java index e1651c1a..e4b41598 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/SupplierDetailFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/SupplierDetailFragment.java @@ -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 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 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 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); diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/listprofilefragments/PetProfileFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/listprofilefragments/PetProfileFragment.java index 6e6943d1..57915e29 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/listprofilefragments/PetProfileFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/listprofilefragments/PetProfileFragment.java @@ -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 galleryLauncher; - private ActivityResultLauncher cameraLauncher; - private ActivityResultLauncher 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 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() { - @Override - public boolean onLoadFailed(@androidx.annotation.Nullable com.bumptech.glide.load.engine.GlideException e, Object model, com.bumptech.glide.request.target.Target target, boolean isFirstResource) { - hasImage = false; - return false; - } - - @Override - public boolean onResourceReady(android.graphics.drawable.Drawable resource, Object model, com.bumptech.glide.request.target.Target 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() { @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); - } } diff --git a/android/app/src/main/java/com/example/petstoremobile/utils/ErrorUtils.java b/android/app/src/main/java/com/example/petstoremobile/utils/ErrorUtils.java new file mode 100644 index 00000000..941f9662 --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/utils/ErrorUtils.java @@ -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(); + } +} diff --git a/android/app/src/main/java/com/example/petstoremobile/utils/GlideUtils.java b/android/app/src/main/java/com/example/petstoremobile/utils/GlideUtils.java new file mode 100644 index 00000000..d5810dc1 --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/utils/GlideUtils.java @@ -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() { + @Override + public boolean onLoadFailed(@Nullable GlideException e, Object model, Target target, boolean isFirstResource) { + if (listener != null) listener.onLoadFailed(); + return false; + } + + @Override + public boolean onResourceReady(Drawable resource, Object model, Target 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() { + @Override + public boolean onLoadFailed(@Nullable GlideException e, Object model, Target target, boolean isFirstResource) { + if (listener != null) listener.onLoadFailed(); + return false; + } + + @Override + public boolean onResourceReady(Drawable resource, Object model, Target target, DataSource dataSource, boolean isFirstResource) { + if (listener != null) listener.onResourceReady(); + return false; + } + }) + .into(imageView); + } +} diff --git a/android/app/src/main/java/com/example/petstoremobile/utils/ImagePickerHelper.java b/android/app/src/main/java/com/example/petstoremobile/utils/ImagePickerHelper.java new file mode 100644 index 00000000..4d1e9bf9 --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/utils/ImagePickerHelper.java @@ -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 galleryLauncher; + private final ActivityResultLauncher cameraLauncher; + private final ActivityResultLauncher 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 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(); + } +} diff --git a/android/app/src/main/java/com/example/petstoremobile/utils/UIUtils.java b/android/app/src/main/java/com/example/petstoremobile/utils/UIUtils.java new file mode 100644 index 00000000..22b8d4e0 --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/utils/UIUtils.java @@ -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)}); + } +}