AttachmentsToChat #145
@@ -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 {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -9,13 +9,10 @@ import android.widget.TextView;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.bumptech.glide.Glide;
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy;
|
||||
import com.bumptech.glide.load.model.GlideUrl;
|
||||
import com.bumptech.glide.load.model.LazyHeaders;
|
||||
import com.example.petstoremobile.R;
|
||||
import com.example.petstoremobile.api.PetApi;
|
||||
import com.example.petstoremobile.dtos.PetDTO;
|
||||
import com.example.petstoremobile.utils.GlideUtils;
|
||||
import java.util.List;
|
||||
|
||||
public class PetAdapter extends RecyclerView.Adapter<PetAdapter.PetViewHolder> {
|
||||
@@ -93,25 +90,10 @@ public class PetAdapter extends RecyclerView.Adapter<PetAdapter.PetViewHolder> {
|
||||
holder.tvPetStatus.setBackgroundColor(Color.parseColor("#F44336"));
|
||||
}
|
||||
|
||||
// Load pet image using Glide with circle crop
|
||||
// Load pet image using Glide
|
||||
if (baseUrl != null) {
|
||||
String imageUrl = baseUrl + String.format(PetApi.PET_IMAGE_PATH, pet.getPetId());
|
||||
|
||||
Object loadTarget = imageUrl;
|
||||
if (token != null) {
|
||||
loadTarget = new GlideUrl(imageUrl, new LazyHeaders.Builder()
|
||||
.addHeader("Authorization", "Bearer " + token)
|
||||
.build());
|
||||
}
|
||||
|
||||
Glide.with(holder.itemView.getContext())
|
||||
.load(loadTarget)
|
||||
.circleCrop()
|
||||
.skipMemoryCache(true)
|
||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||
.placeholder(R.drawable.placeholder)
|
||||
.error(R.drawable.placeholder)
|
||||
.into(holder.ivPetProfile);
|
||||
GlideUtils.loadImageWithTokenCircle(holder.itemView.getContext(), holder.ivPetProfile, imageUrl, token, R.drawable.placeholder);
|
||||
} else {
|
||||
holder.ivPetProfile.setImageResource(R.drawable.placeholder);
|
||||
}
|
||||
|
||||
@@ -6,13 +6,10 @@ import android.widget.TextView;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.bumptech.glide.Glide;
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy;
|
||||
import com.bumptech.glide.load.model.GlideUrl;
|
||||
import com.bumptech.glide.load.model.LazyHeaders;
|
||||
import com.example.petstoremobile.R;
|
||||
import com.example.petstoremobile.api.ProductApi;
|
||||
import com.example.petstoremobile.dtos.ProductDTO;
|
||||
import com.example.petstoremobile.utils.GlideUtils;
|
||||
import java.util.List;
|
||||
|
||||
public class ProductAdapter extends RecyclerView.Adapter<ProductAdapter.ProductViewHolder> {
|
||||
@@ -72,22 +69,7 @@ public class ProductAdapter extends RecyclerView.Adapter<ProductAdapter.ProductV
|
||||
// Load product image using Glide
|
||||
if (baseUrl != null) {
|
||||
String imageUrl = baseUrl + String.format(ProductApi.PRODUCT_IMAGE_PATH, p.getProdId());
|
||||
|
||||
Object loadTarget = imageUrl;
|
||||
if (token != null) {
|
||||
loadTarget = new GlideUrl(imageUrl, new LazyHeaders.Builder()
|
||||
.addHeader("Authorization", "Bearer " + token)
|
||||
.build());
|
||||
}
|
||||
|
||||
Glide.with(holder.itemView.getContext())
|
||||
.load(loadTarget)
|
||||
.circleCrop()
|
||||
.skipMemoryCache(true)
|
||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||
.placeholder(R.drawable.placeholder)
|
||||
.error(R.drawable.placeholder)
|
||||
.into(holder.ivProductImage);
|
||||
GlideUtils.loadImageWithTokenCircle(holder.itemView.getContext(), holder.ivProductImage, imageUrl, token, R.drawable.placeholder);
|
||||
} else {
|
||||
holder.ivProductImage.setImageResource(R.drawable.placeholder);
|
||||
}
|
||||
|
||||
@@ -88,7 +88,9 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis
|
||||
private StompChatManager stompChatManager;
|
||||
private ActivityResultLauncher<Intent> attachmentLauncher;
|
||||
|
||||
|
||||
/**
|
||||
* Initializes the attachment launcher to handle file selection from the gallery.
|
||||
*/
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
@@ -105,6 +107,9 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Inflates the layout, initializes UI components, and sets up click listeners for messaging.
|
||||
*/
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater,
|
||||
ViewGroup container, Bundle savedInstanceState) {
|
||||
@@ -155,7 +160,9 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis
|
||||
return view;
|
||||
}
|
||||
|
||||
// Helper function to setup recycler views for chat and messages
|
||||
/**
|
||||
* Configures the RecyclerViews for the conversation list and the message history.
|
||||
*/
|
||||
private void setupRecyclerViews() {
|
||||
// Set up Drawer menu to select conversation
|
||||
chatAdapter = new ChatAdapter(chatList, this);
|
||||
@@ -171,7 +178,9 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis
|
||||
setConversationActive(false);
|
||||
}
|
||||
|
||||
//Helper function to load token and user id then connect to websocket
|
||||
/**
|
||||
* Loads authentication tokens and user info, then initializes the Stomp WebSocket connection.
|
||||
*/
|
||||
private void loadInitialData() {
|
||||
String token = tokenManager.getToken();
|
||||
currentUserId = tokenManager.getUserId();
|
||||
@@ -198,7 +207,9 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis
|
||||
loadCustomers();
|
||||
}
|
||||
|
||||
//Helper function to load customer names for it to be displayed on drawer menu
|
||||
/**
|
||||
* Fetches a list of customers from the API to display customer names for the chat list.
|
||||
*/
|
||||
private void loadCustomers() {
|
||||
customerApi.getAllCustomers(0, 100).enqueue(new Callback<PageResponse<CustomerDTO>>() {
|
||||
@Override
|
||||
@@ -220,7 +231,9 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis
|
||||
});
|
||||
}
|
||||
|
||||
//helper function to load conversations entities to display with customer names in drawer menu
|
||||
/**
|
||||
* Retrieves all conversations for the current user and populates the chat drawer.
|
||||
*/
|
||||
private void loadConversations() {
|
||||
chatApi.getAllConversations().enqueue(new Callback<List<ConversationDTO>>() {
|
||||
@Override
|
||||
@@ -268,8 +281,9 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis
|
||||
});
|
||||
}
|
||||
|
||||
// Called when user taps a chat in the drawer
|
||||
// Loads messages for that chat selected
|
||||
/**
|
||||
* Handles selection of a chat from the drawer, updating the UI and subscribing to the WebSocket.
|
||||
*/
|
||||
@Override
|
||||
public void onChatClick(Chat chat) {
|
||||
activeConversationId = Long.parseLong(chat.getChatId());
|
||||
@@ -284,7 +298,9 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis
|
||||
loadMessageHistory(activeConversationId);
|
||||
}
|
||||
|
||||
//helper function to load messages for selected chat
|
||||
/**
|
||||
* Fetches the full message history for a specific conversation from the API.
|
||||
*/
|
||||
private void loadMessageHistory(Long conversationId) {
|
||||
messageApi.getMessages(conversationId).enqueue(new Callback<List<MessageDTO>>() {
|
||||
@Override
|
||||
@@ -307,7 +323,9 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis
|
||||
});
|
||||
}
|
||||
|
||||
//Helper function to send a message to the chat
|
||||
/**
|
||||
* Sends a plain text message to the currently active conversation.
|
||||
*/
|
||||
private void sendMessage() {
|
||||
//check if a chat is selected
|
||||
if (activeConversationId == null) return;
|
||||
@@ -340,14 +358,18 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis
|
||||
});
|
||||
}
|
||||
|
||||
//Helper function to open file picker when the attachment button is clicked
|
||||
/**
|
||||
* Launches a file picker intent to select an attachment for the message.
|
||||
*/
|
||||
private void selectAttachment() {
|
||||
Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
|
||||
intent.setType("*/*");
|
||||
attachmentLauncher.launch(intent);
|
||||
}
|
||||
|
||||
//Helper function to show the attachment preview
|
||||
/**
|
||||
* Displays a preview of the selected attachment in the UI.
|
||||
*/
|
||||
private void showAttachmentPreview(Uri uri) {
|
||||
pendingAttachmentUri = uri;
|
||||
layoutAttachmentPreview.setVisibility(View.VISIBLE);
|
||||
@@ -365,13 +387,17 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis
|
||||
}
|
||||
}
|
||||
|
||||
//Helper function to remove the attachment
|
||||
/**
|
||||
* Clears the current attachment selection and hides the preview UI.
|
||||
*/
|
||||
private void removeAttachment() {
|
||||
pendingAttachmentUri = null;
|
||||
layoutAttachmentPreview.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
//Helper function to get the file name from the uri to display in attachment preview
|
||||
/**
|
||||
* Show the display name of the file from its Uri.
|
||||
*/
|
||||
private String getFileName(Uri uri) {
|
||||
String result = null;
|
||||
if (uri.getScheme().equals("content")) {
|
||||
@@ -394,7 +420,9 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis
|
||||
return result;
|
||||
}
|
||||
|
||||
//Helper function to send the message with attachment
|
||||
/**
|
||||
* Handles sending a message that includes a file attachment.
|
||||
*/
|
||||
private void sendWithAttachment(Uri uri) {
|
||||
if (activeConversationId == null) return;
|
||||
|
||||
@@ -402,7 +430,9 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis
|
||||
Log.d(TAG, "Send with attachment happening");
|
||||
}
|
||||
|
||||
// When a message is received updates the chat preview
|
||||
/**
|
||||
* Callback triggered when a new message is received via the WebSocket.
|
||||
*/
|
||||
@Override
|
||||
public void onMessageReceived(MessageDTO dto) {
|
||||
//if there is no active selected conversation or the message received is for another chat, then just update the preview of last message
|
||||
@@ -420,7 +450,9 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis
|
||||
scrollToBottom();
|
||||
}
|
||||
|
||||
// When a new conversation is added, updates the chat preview
|
||||
/**
|
||||
* Callback triggered when a conversation is created or updated via the WebSocket.
|
||||
*/
|
||||
@Override
|
||||
public void onConversationUpdated(ConversationDTO dto) {
|
||||
boolean updated = false;
|
||||
@@ -460,6 +492,9 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback triggered when the WebSocket connection is successfully opened.
|
||||
*/
|
||||
@Override
|
||||
public void onSocketOpened() {
|
||||
if (!isAdded()) {
|
||||
@@ -471,6 +506,9 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback triggered when the WebSocket connection is closed.
|
||||
*/
|
||||
@Override
|
||||
public void onSocketClosed() {
|
||||
if (!isAdded()) {
|
||||
@@ -479,6 +517,9 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis
|
||||
loadConversations();
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback triggered when a WebSocket connection error occurs.
|
||||
*/
|
||||
@Override
|
||||
public void onSocketError() {
|
||||
if (!isAdded()) {
|
||||
@@ -490,7 +531,9 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to convert DTO to message
|
||||
/**
|
||||
* Converts a MessageDTO into a Message object.
|
||||
*/
|
||||
private Message dtoToModel(MessageDTO dto) {
|
||||
Message m = new Message();
|
||||
m.setId(dto.getId());
|
||||
@@ -505,7 +548,9 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis
|
||||
return m;
|
||||
}
|
||||
|
||||
//Helper function to scroll to bottom of the chat
|
||||
/**
|
||||
* Scrolls the message history RecyclerView to the most recent message.
|
||||
*/
|
||||
private void scrollToBottom() {
|
||||
if (!messageList.isEmpty()) {
|
||||
rvMessages.post(() ->
|
||||
@@ -513,7 +558,9 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to update the chat preview last message
|
||||
/**
|
||||
* Updates the preview snippet of the last message for a specific conversation in the drawer.
|
||||
*/
|
||||
private void updateConversationPreview(Long conversationId, String lastMessage) {
|
||||
if (conversationId == null) {
|
||||
return;
|
||||
@@ -535,7 +582,9 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis
|
||||
}
|
||||
}
|
||||
|
||||
//Helper function to enable or disable the send button when there is no active chat
|
||||
/**
|
||||
* Toggles the UI state based on whether a conversation is currently selected.
|
||||
*/
|
||||
private void setConversationActive(boolean active) {
|
||||
btnSend.setEnabled(active);
|
||||
etMessage.setEnabled(active);
|
||||
@@ -558,11 +607,13 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis
|
||||
}
|
||||
}
|
||||
|
||||
// When fragment is destroyed, disconnect from websocket
|
||||
/**
|
||||
* Disconnects the WebSocket manager when the fragment view is destroyed.
|
||||
*/
|
||||
@Override
|
||||
public void onDestroyView() {
|
||||
super.onDestroyView();
|
||||
ChatNotificationService.activeConversationIdInUi = null;
|
||||
if (stompChatManager != null) stompChatManager.disconnect();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -1,21 +1,11 @@
|
||||
package com.example.petstoremobile.fragments;
|
||||
|
||||
import android.Manifest;
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
|
||||
import androidx.activity.result.ActivityResultLauncher;
|
||||
import androidx.activity.result.contract.ActivityResultContracts;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.core.content.FileProvider;
|
||||
import androidx.fragment.app.Fragment;
|
||||
|
||||
import android.provider.MediaStore;
|
||||
import android.text.InputType;
|
||||
import android.util.Log;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
@@ -26,26 +16,21 @@ import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.bumptech.glide.Glide;
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy;
|
||||
import com.bumptech.glide.load.model.GlideUrl;
|
||||
import com.bumptech.glide.load.model.LazyHeaders;
|
||||
import com.example.petstoremobile.R;
|
||||
import com.example.petstoremobile.activities.MainActivity;
|
||||
import com.example.petstoremobile.api.auth.AuthApi;
|
||||
import com.example.petstoremobile.api.auth.TokenManager;
|
||||
import com.example.petstoremobile.dtos.ErrorResponse;
|
||||
import com.example.petstoremobile.dtos.UserDTO;
|
||||
import com.example.petstoremobile.services.ChatNotificationService;
|
||||
import com.example.petstoremobile.utils.ErrorUtils;
|
||||
import com.example.petstoremobile.utils.FileUtils;
|
||||
import com.example.petstoremobile.utils.GlideUtils;
|
||||
import com.example.petstoremobile.utils.ImagePickerHelper;
|
||||
import com.example.petstoremobile.utils.InputValidator;
|
||||
import com.google.gson.Gson;
|
||||
import com.example.petstoremobile.utils.UIUtils;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.InputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import javax.inject.Inject;
|
||||
@@ -59,13 +44,15 @@ import retrofit2.Call;
|
||||
import retrofit2.Callback;
|
||||
import retrofit2.Response;
|
||||
|
||||
/**
|
||||
* Fragment that displays and allows editing of the user's profile information.
|
||||
*/
|
||||
@AndroidEntryPoint
|
||||
public class ProfileFragment extends Fragment {
|
||||
|
||||
//initialize the view/controls
|
||||
private ImageView imgProfile;
|
||||
private TextView tvProfileName, tvProfileEmail, tvProfilePhone, tvProfileRole;
|
||||
private Uri photoUri;
|
||||
private UserDTO currentUser;
|
||||
private boolean hasImage = false;
|
||||
|
||||
@@ -73,71 +60,31 @@ public class ProfileFragment extends Fragment {
|
||||
@Inject TokenManager tokenManager;
|
||||
@Inject @Named("baseUrl") String baseUrl;
|
||||
|
||||
//Initialize the launchers for camera and gallery
|
||||
private ActivityResultLauncher<Intent> galleryLauncher;
|
||||
private ActivityResultLauncher<Uri> cameraLauncher;
|
||||
private ActivityResultLauncher<String> permissionLauncher;
|
||||
private ImagePickerHelper imagePickerHelper;
|
||||
|
||||
//Called when the fragment is created, sets up the launchers is set profile image
|
||||
/**
|
||||
* Initializes activity launchers and the ImagePickerHelper for camera and gallary.
|
||||
*/
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
// Launcher to open gallery to select profile image
|
||||
galleryLauncher = registerForActivityResult(
|
||||
//open gallery
|
||||
new ActivityResultContracts.StartActivityForResult(),
|
||||
result -> {
|
||||
//if the user selects an image and its not null
|
||||
if (result.getResultCode() == Activity.RESULT_OK
|
||||
&& result.getData() != null) {
|
||||
//get the selected image and set the image to the profile
|
||||
Uri selectedImage = result.getData().getData();
|
||||
uploadAvatar(selectedImage);
|
||||
}
|
||||
}
|
||||
);
|
||||
imagePickerHelper = new ImagePickerHelper(this, "profile_photo.jpg", new ImagePickerHelper.ImagePickerListener() {
|
||||
@Override
|
||||
public void onImagePicked(Uri uri) {
|
||||
uploadAvatar(uri);
|
||||
}
|
||||
|
||||
// Launcher for camera to open and capture profile image
|
||||
cameraLauncher = registerForActivityResult(
|
||||
//open camera
|
||||
new ActivityResultContracts.TakePicture(),
|
||||
success -> {
|
||||
//if a photo is taken set the image profile to it otherwise do nothing
|
||||
if (success) {
|
||||
uploadAvatar(photoUri);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// Launcher to request camera permission
|
||||
permissionLauncher = registerForActivityResult(
|
||||
//ask user for camera permission
|
||||
new ActivityResultContracts.RequestPermission(),
|
||||
granted -> {
|
||||
//if the permission is granted launch the camera
|
||||
if (granted) {
|
||||
launchCamera();
|
||||
}
|
||||
else {
|
||||
//if the permission is denied then tell the user to grant it
|
||||
new AlertDialog.Builder(requireContext())
|
||||
.setTitle("Permission Permission Required")
|
||||
.setMessage("Please grant camera permission to use this feature")
|
||||
.setPositiveButton("Open Settings", (dialog, which) ->{
|
||||
//open the settings page to grant the permission when they click open settings
|
||||
Intent intent = new Intent(android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
|
||||
intent.setData(Uri.fromParts("package", requireContext().getPackageName(), null));
|
||||
startActivity(intent);
|
||||
})
|
||||
//close the dialog when the user clicks cancel
|
||||
.setNegativeButton("Cancel", null)
|
||||
.show();
|
||||
}
|
||||
}
|
||||
);
|
||||
@Override
|
||||
public void onImageRemoved() {
|
||||
deleteAvatar();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Inflates the fragment layout and sets up listeners for profile.
|
||||
*/
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container,
|
||||
Bundle savedInstanceState) {
|
||||
@@ -160,39 +107,7 @@ public class ProfileFragment extends Fragment {
|
||||
//Set up listeners for the buttons
|
||||
//Change photo button
|
||||
btnChangePhoto.setOnClickListener(v -> {
|
||||
List<String> options = new ArrayList<>();
|
||||
options.add("Take Photo");
|
||||
options.add("Choose from Gallery");
|
||||
if (hasImage) {
|
||||
options.add("Remove Photo");
|
||||
}
|
||||
|
||||
//Show alert dialog to user to select from gallery or camera
|
||||
new AlertDialog.Builder(requireContext())
|
||||
.setTitle("Change Profile Photo")
|
||||
//set the options for the alert dialog
|
||||
.setItems(options.toArray(new String[0]), (dialog, which) -> {
|
||||
String selected = options.get(which);
|
||||
if (selected.equals("Take Photo")) {
|
||||
// Choose Camera
|
||||
//Checks if the user has granted the camera permission already
|
||||
if (ContextCompat.checkSelfPermission(requireContext(), Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED) {
|
||||
//if the permission is already granted then launch the camera
|
||||
launchCamera();
|
||||
} else {
|
||||
//otherwise request the permission
|
||||
permissionLauncher.launch(Manifest.permission.CAMERA);
|
||||
}
|
||||
} else if (selected.equals("Choose from Gallery")) {
|
||||
// Choose Gallery
|
||||
Intent intent = new Intent(Intent.ACTION_PICK,
|
||||
MediaStore.Images.Media.EXTERNAL_CONTENT_URI);
|
||||
galleryLauncher.launch(intent);
|
||||
} else if (selected.equals("Remove Photo")) {
|
||||
deleteAvatar();
|
||||
}
|
||||
})
|
||||
.show();
|
||||
imagePickerHelper.showImagePickerDialog("Change Profile Photo", hasImage);
|
||||
});
|
||||
|
||||
//Edit email button
|
||||
@@ -231,11 +146,10 @@ public class ProfileFragment extends Fragment {
|
||||
input.setText(tvProfilePhone.getText().toString());
|
||||
|
||||
//set input type to phone number
|
||||
input.setInputType(InputType.TYPE_CLASS_PHONE);
|
||||
input.setInputType(android.view.inputmethod.EditorInfo.TYPE_CLASS_PHONE);
|
||||
|
||||
//add canada phone number formatting to input (XXX) XXX-XXXX
|
||||
input.addTextChangedListener(new android.telephony.PhoneNumberFormattingTextWatcher("CA"));
|
||||
input.setFilters(new android.text.InputFilter[]{new android.text.InputFilter.LengthFilter(14)});
|
||||
//add canada phone number formatting to input
|
||||
UIUtils.formatPhoneInput(input);
|
||||
|
||||
|
||||
//Show alert dialog to user to enter new phone
|
||||
@@ -256,13 +170,13 @@ public class ProfileFragment extends Fragment {
|
||||
//Logout button
|
||||
btnLogout.setOnClickListener(v -> {
|
||||
// Stop notification service before logging out so notifications stop
|
||||
Intent serviceIntent = new Intent(requireContext(), ChatNotificationService.class);
|
||||
android.content.Intent serviceIntent = new android.content.Intent(requireContext(), ChatNotificationService.class);
|
||||
requireContext().stopService(serviceIntent);
|
||||
|
||||
tokenManager.clearLoginData(); // clear the token for next login
|
||||
//get the intent to the main activity and clear the back stack so the back button won't allow the user to go back to the previous screen
|
||||
Intent intent = new Intent(getActivity(), MainActivity.class);
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
android.content.Intent intent = new android.content.Intent(getActivity(), MainActivity.class);
|
||||
intent.addFlags(android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP | android.content.Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
//start the activity to go to login page and finish the current activity
|
||||
startActivity(intent);
|
||||
requireActivity().finish();
|
||||
@@ -271,17 +185,9 @@ public class ProfileFragment extends Fragment {
|
||||
return view;
|
||||
}
|
||||
|
||||
//Helper function create a file in the cache directory to store the photo in then launch the camera to capture the photo
|
||||
private void launchCamera() {
|
||||
//create a file in the cache directory to store the photo in
|
||||
File photoFile = new File(requireContext().getCacheDir(), "profile_photo.jpg");
|
||||
//get the uri for the file made
|
||||
photoUri = FileProvider.getUriForFile(requireContext(), requireContext().getPackageName() + ".fileprovider", photoFile);
|
||||
//launch the camera to capture the photo and save the photo to photoUri
|
||||
cameraLauncher.launch(photoUri);
|
||||
}
|
||||
|
||||
//Helper function to call the backend to get profile data and load it to the view
|
||||
/**
|
||||
* Fetches current user profile data from the API and then updates the UI.
|
||||
*/
|
||||
private void loadProfileData() {
|
||||
authApi.getMe().enqueue(new Callback<UserDTO>() {
|
||||
@Override
|
||||
@@ -300,44 +206,21 @@ public class ProfileFragment extends Fragment {
|
||||
String avatarUrl = baseUrl + AuthApi.AVATAR_FILE_PATH;
|
||||
String token = tokenManager.getToken();
|
||||
|
||||
if (token != null) {
|
||||
// Create GlideUrl with token to fetch the image
|
||||
GlideUrl glideUrl = new GlideUrl(avatarUrl, new LazyHeaders.Builder()
|
||||
.addHeader("Authorization", "Bearer " + token)
|
||||
.build());
|
||||
GlideUtils.loadImageWithToken(requireContext(), imgProfile, avatarUrl, token, R.drawable.placeholder, new GlideUtils.ImageLoadListener() {
|
||||
@Override
|
||||
public void onResourceReady() {
|
||||
hasImage = true;
|
||||
}
|
||||
|
||||
// Load image using Glide
|
||||
Glide.with(ProfileFragment.this)
|
||||
.load(glideUrl)
|
||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||
.skipMemoryCache(true)
|
||||
.placeholder(R.drawable.placeholder)
|
||||
.error(R.drawable.placeholder)
|
||||
.listener(new com.bumptech.glide.request.RequestListener<android.graphics.drawable.Drawable>() {
|
||||
@Override
|
||||
public boolean onLoadFailed(@androidx.annotation.Nullable com.bumptech.glide.load.engine.GlideException e, Object model, com.bumptech.glide.request.target.Target<android.graphics.drawable.Drawable> target, boolean isFirstResource) {
|
||||
hasImage = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onResourceReady(android.graphics.drawable.Drawable resource, Object model, com.bumptech.glide.request.target.Target<android.graphics.drawable.Drawable> target, com.bumptech.glide.load.DataSource dataSource, boolean isFirstResource) {
|
||||
hasImage = true;
|
||||
return false;
|
||||
}
|
||||
})
|
||||
.into(imgProfile);
|
||||
} else {
|
||||
// load placeholder image if token is null
|
||||
hasImage = false;
|
||||
Glide.with(ProfileFragment.this)
|
||||
.load(R.drawable.placeholder)
|
||||
.into(imgProfile);
|
||||
}
|
||||
@Override
|
||||
public void onLoadFailed() {
|
||||
hasImage = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
else {
|
||||
Log.e("onResponse: ", response.message());
|
||||
Toast.makeText(getContext(), "Failed to load profile: ", Toast.LENGTH_SHORT).show();
|
||||
ErrorUtils.showErrorMessage(getContext(), response, "Failed to load profile");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -349,10 +232,12 @@ public class ProfileFragment extends Fragment {
|
||||
});
|
||||
}
|
||||
|
||||
//Helper function to call the backend to upload a profile image
|
||||
/**
|
||||
* Uploads the selected or captured image as the user's new avatar.
|
||||
*/
|
||||
private void uploadAvatar(Uri uri) {
|
||||
try {
|
||||
File file = getFileFromUri(uri);
|
||||
File file = FileUtils.getFileFromUri(requireContext(), uri);
|
||||
if (file == null) return;
|
||||
|
||||
// Create RequestBody for file upload
|
||||
@@ -369,7 +254,7 @@ public class ProfileFragment extends Fragment {
|
||||
// Reload image after successful upload
|
||||
loadProfileData();
|
||||
} else {
|
||||
Toast.makeText(requireContext(), "Failed to upload avatar", Toast.LENGTH_SHORT).show();
|
||||
ErrorUtils.showErrorMessage(getContext(), response, "Failed to upload avatar");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -384,6 +269,9 @@ public class ProfileFragment extends Fragment {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a request to the API to delete the current user's avatar image.
|
||||
*/
|
||||
private void deleteAvatar() {
|
||||
authApi.deleteAvatar().enqueue(new Callback<Void>() {
|
||||
@Override
|
||||
@@ -393,7 +281,7 @@ public class ProfileFragment extends Fragment {
|
||||
hasImage = false;
|
||||
imgProfile.setImageResource(R.drawable.placeholder);
|
||||
} else {
|
||||
Toast.makeText(requireContext(), "Failed to remove avatar", Toast.LENGTH_SHORT).show();
|
||||
ErrorUtils.showErrorMessage(getContext(), response, "Failed to remove avatar");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -405,27 +293,9 @@ public class ProfileFragment extends Fragment {
|
||||
});
|
||||
}
|
||||
|
||||
// Helper function to create a temporary File object from a Uri for uploading the avatar
|
||||
private File getFileFromUri(Uri uri) {
|
||||
try {
|
||||
InputStream inputStream = requireContext().getContentResolver().openInputStream(uri);
|
||||
File tempFile = new File(requireContext().getCacheDir(), "upload_avatar.jpg");
|
||||
FileOutputStream outputStream = new FileOutputStream(tempFile);
|
||||
byte[] buffer = new byte[1024];
|
||||
int length;
|
||||
while ((length = inputStream.read(buffer)) > 0) {
|
||||
outputStream.write(buffer, 0, length);
|
||||
}
|
||||
outputStream.close();
|
||||
inputStream.close();
|
||||
return tempFile;
|
||||
} catch (Exception e) {
|
||||
Log.e("FILE_UTILS", "Error creating temp file", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
//Helper function to update a profile field in the backend
|
||||
/**
|
||||
* Updates a specific profile field (like email or phone) by sending a request to the API.
|
||||
*/
|
||||
private void updateProfileField(String fieldName, String value) {
|
||||
Map<String, String> updates = new HashMap<>();
|
||||
updates.put(fieldName, value);
|
||||
@@ -440,15 +310,7 @@ public class ProfileFragment extends Fragment {
|
||||
tvProfilePhone.setText(currentUser.getPhone());
|
||||
Toast.makeText(requireContext(), "Profile updated successfully", Toast.LENGTH_SHORT).show();
|
||||
} else {
|
||||
try {
|
||||
String errorJson = response.errorBody().string();
|
||||
ErrorResponse errorResponse = new Gson().fromJson(errorJson, ErrorResponse.class);
|
||||
String errorMessage = errorResponse.getMessage();
|
||||
Toast.makeText(requireContext(), errorMessage, Toast.LENGTH_LONG).show();
|
||||
} catch (Exception e) {
|
||||
Log.e("UPDATE_PROFILE", "Error parsing error body", e);
|
||||
Toast.makeText(requireContext(), "Failed to update profile", Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
ErrorUtils.showErrorMessage(getContext(), response, "Failed to update profile");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -459,4 +321,4 @@ public class ProfileFragment extends Fragment {
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,12 +48,18 @@ public class AdoptionFragment extends Fragment implements AdoptionAdapter.OnAdop
|
||||
private boolean isMonthMode = false;
|
||||
private final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd", Locale.getDefault());
|
||||
|
||||
/**
|
||||
* Initializes the fragment and its ViewModel.
|
||||
*/
|
||||
@Override
|
||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
viewModel = new ViewModelProvider(this).get(AdoptionViewModel.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up the fragment's UI components, including RecyclerView, Search, SwipeRefresh, and Calendar.
|
||||
*/
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
|
||||
Bundle savedInstanceState) {
|
||||
@@ -87,6 +93,9 @@ public class AdoptionFragment extends Fragment implements AdoptionAdapter.OnAdop
|
||||
return view;
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggles the calendar display between week and month modes.
|
||||
*/
|
||||
private void toggleCalendarMode() {
|
||||
isMonthMode = !isMonthMode;
|
||||
calendarView.state().edit()
|
||||
@@ -94,6 +103,9 @@ public class AdoptionFragment extends Fragment implements AdoptionAdapter.OnAdop
|
||||
.commit();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up the date selection listener for the calendar.
|
||||
*/
|
||||
private void setupCalendar() {
|
||||
calendarView.setOnDateChangedListener(new OnDateSelectedListener() {
|
||||
@Override
|
||||
@@ -113,6 +125,9 @@ public class AdoptionFragment extends Fragment implements AdoptionAdapter.OnAdop
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the calendar decorators to highlight days with adoptions.
|
||||
*/
|
||||
private void updateCalendarDecorators() {
|
||||
HashSet<CalendarDay> datesWithAdoptions = new HashSet<>();
|
||||
for (AdoptionDTO adoption : adoptionList) {
|
||||
@@ -133,6 +148,9 @@ public class AdoptionFragment extends Fragment implements AdoptionAdapter.OnAdop
|
||||
calendarView.addDecorator(new EventDecorator(Color.RED, datesWithAdoptions));
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the RecyclerView for displaying adoptions.
|
||||
*/
|
||||
private void setupRecyclerView(View view) {
|
||||
RecyclerView rv = view.findViewById(R.id.recyclerViewAdoptions);
|
||||
adapter = new AdoptionAdapter(filteredList, this);
|
||||
@@ -140,6 +158,9 @@ public class AdoptionFragment extends Fragment implements AdoptionAdapter.OnAdop
|
||||
rv.setAdapter(adapter);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up the search bar for filtering
|
||||
*/
|
||||
private void setupSearch(View view) {
|
||||
etSearch = view.findViewById(R.id.etSearchAdoption);
|
||||
etSearch.addTextChangedListener(new TextWatcher() {
|
||||
@@ -151,11 +172,17 @@ public class AdoptionFragment extends Fragment implements AdoptionAdapter.OnAdop
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up the SwipeRefreshLayout to reload adoption data.
|
||||
*/
|
||||
private void setupSwipeRefresh(View view) {
|
||||
swipeRefresh = view.findViewById(R.id.swipeRefreshAdoption);
|
||||
swipeRefresh.setOnRefreshListener(this::loadAdoptions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters the adoption list based on search query and selected calendar date.
|
||||
*/
|
||||
private void filter(String query) {
|
||||
filteredList.clear();
|
||||
String lowerQuery = query.toLowerCase();
|
||||
@@ -182,7 +209,9 @@ public class AdoptionFragment extends Fragment implements AdoptionAdapter.OnAdop
|
||||
adapter.notifyDataSetChanged();
|
||||
}
|
||||
|
||||
// Helper function to get a list of all adoptions from the backend
|
||||
/**
|
||||
* Fetches the adoption list from the server through the ViewModel.
|
||||
*/
|
||||
private void loadAdoptions() {
|
||||
//Load all adoptions from the backend using viewModel
|
||||
viewModel.getAllAdoptions(0, 500).observe(getViewLifecycleOwner(), resource -> {
|
||||
@@ -214,6 +243,9 @@ public class AdoptionFragment extends Fragment implements AdoptionAdapter.OnAdop
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigates to the adoption detail screen for a specific adoption or to create a new one.
|
||||
*/
|
||||
private void openDetail(int position) {
|
||||
Bundle args = new Bundle();
|
||||
|
||||
@@ -229,6 +261,9 @@ public class AdoptionFragment extends Fragment implements AdoptionAdapter.OnAdop
|
||||
NavHostFragment.findNavController(this).navigate(R.id.nav_adoption_detail, args);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles item click in the adoption list.
|
||||
*/
|
||||
@Override
|
||||
public void onAdoptionClick(int position) { openDetail(position); }
|
||||
}
|
||||
|
||||
@@ -71,6 +71,9 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter.
|
||||
private boolean isMonthMode = false;
|
||||
private final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd", Locale.getDefault());
|
||||
|
||||
/**
|
||||
* Initializes the fragment and its associated ViewModels.
|
||||
*/
|
||||
@Override
|
||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
@@ -79,6 +82,9 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter.
|
||||
serviceViewModel = new ViewModelProvider(this).get(ServiceViewModel.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up the fragment's UI, including RecyclerView, search, swipe-to-refresh, and calendar.
|
||||
*/
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
|
||||
Bundle savedInstanceState) {
|
||||
@@ -114,7 +120,9 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter.
|
||||
return view;
|
||||
}
|
||||
|
||||
// Toggle Calendar Mode from week to month and other way around
|
||||
/**
|
||||
* Toggles the calendar between week and month display modes.
|
||||
*/
|
||||
private void toggleCalendarMode() {
|
||||
isMonthMode = !isMonthMode;
|
||||
calendarView.state().edit()
|
||||
@@ -122,6 +130,9 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter.
|
||||
.commit();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up the date selection listener for the calendar.
|
||||
*/
|
||||
private void setupCalendar() {
|
||||
calendarView.setOnDateChangedListener(new OnDateSelectedListener() {
|
||||
@Override
|
||||
@@ -141,7 +152,9 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter.
|
||||
});
|
||||
}
|
||||
|
||||
//Set indicators for dates with appointments on the calendar
|
||||
/**
|
||||
* Updates calendar indicators to highlight dates that have scheduled appointments.
|
||||
*/
|
||||
private void updateCalendarDecorators() {
|
||||
HashSet<CalendarDay> datesWithAppointments = new HashSet<>();
|
||||
for (AppointmentDTO appointment : appointmentList) {
|
||||
@@ -163,6 +176,9 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter.
|
||||
calendarView.addDecorator(new EventDecorator(Color.RED, datesWithAppointments));
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures the search bar for filtering.
|
||||
*/
|
||||
private void setupSearch(View view) {
|
||||
etSearch = view.findViewById(R.id.etSearchAppointment);
|
||||
etSearch.addTextChangedListener(new TextWatcher() {
|
||||
@@ -181,6 +197,9 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter.
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters the appointment list based on the search query and selected calendar date.
|
||||
*/
|
||||
private void filterAppointments(String query) {
|
||||
filteredList.clear();
|
||||
String lowerQuery = query.toLowerCase();
|
||||
@@ -207,11 +226,17 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter.
|
||||
adapter.notifyDataSetChanged();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the SwipeRefreshLayout to allow manual data refreshing.
|
||||
*/
|
||||
private void setupSwipeRefresh(View view) {
|
||||
swipeRefreshLayout = view.findViewById(R.id.swipeRefreshAppointment);
|
||||
swipeRefreshLayout.setOnRefreshListener(this::loadAppointmentData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigates to the appointment detail screen for editing or creating an appointment.
|
||||
*/
|
||||
private void openAppointmentDetails(int position) {
|
||||
Bundle args = new Bundle();
|
||||
|
||||
@@ -230,20 +255,32 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter.
|
||||
|
||||
NavHostFragment.findNavController(this).navigate(R.id.nav_appointment_detail, args);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reloads data when an appointment is saved.
|
||||
*/
|
||||
public void onAppointmentSaved(int position, AppointmentDTO appointment) {
|
||||
loadAppointmentData();
|
||||
}
|
||||
|
||||
/**
|
||||
* Reloads data when an appointment is deleted.
|
||||
*/
|
||||
public void onAppointmentDeleted(int position) {
|
||||
loadAppointmentData();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles item click in the appointment list.
|
||||
*/
|
||||
@Override
|
||||
public void onAppointmentClick(int position) {
|
||||
openAppointmentDetails(position);
|
||||
}
|
||||
|
||||
// Helper function to get a list of all appointments from the backend
|
||||
/**
|
||||
* Fetches all appointment data from the server.
|
||||
*/
|
||||
private void loadAppointmentData() {
|
||||
//Load all appointments from the backend using viewModel
|
||||
appointmentViewModel.getAllAppointments(0, 500).observe(getViewLifecycleOwner(), resource -> {
|
||||
@@ -275,7 +312,9 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter.
|
||||
});
|
||||
}
|
||||
|
||||
// Load Pets
|
||||
/**
|
||||
* Fetches the full list of pets from the server.
|
||||
*/
|
||||
private void loadPets() {
|
||||
petViewModel.getAllPets(0, 100).observe(getViewLifecycleOwner(), resource -> {
|
||||
if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) {
|
||||
@@ -285,7 +324,9 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter.
|
||||
});
|
||||
}
|
||||
|
||||
// Load Services
|
||||
/**
|
||||
* Fetches the full list of services from the server.
|
||||
*/
|
||||
private void loadServices() {
|
||||
serviceViewModel.getAllServices(0, 100).observe(getViewLifecycleOwner(), resource -> {
|
||||
if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) {
|
||||
@@ -295,6 +336,9 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter.
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a pet's name based on its ID.
|
||||
*/
|
||||
private String getPetName(Long id) {
|
||||
for (PetDTO p : petList) {
|
||||
if (p.getPetId().equals(id)) return p.getPetName();
|
||||
@@ -303,6 +347,9 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter.
|
||||
return "";
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a service's name based on its ID.
|
||||
*/
|
||||
private String getServiceName(Long id) {
|
||||
for (ServiceDTO s : serviceList) {
|
||||
if (s.getServiceId().equals(id))return s.getServiceName();
|
||||
@@ -310,6 +357,9 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter.
|
||||
return "";
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the RecyclerView for displaying appointments.
|
||||
*/
|
||||
private void setupRecyclerView(View view) {
|
||||
RecyclerView recyclerView = view.findViewById(R.id.recyclerViewAppointments);
|
||||
adapter = new AppointmentAdapter(filteredList, this);
|
||||
|
||||
@@ -74,12 +74,18 @@ public class InventoryFragment extends Fragment implements InventoryAdapter.OnIn
|
||||
// Prevent spinner from firing on initial load
|
||||
private boolean spinnerReady = false;
|
||||
|
||||
/**
|
||||
* Initializes the fragment and its ViewModel.
|
||||
*/
|
||||
@Override
|
||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
viewModel = new ViewModelProvider(this).get(InventoryViewModel.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up the fragment's UI components, including the inventory list, search, and category filter.
|
||||
*/
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
|
||||
Bundle savedInstanceState) {
|
||||
@@ -114,7 +120,9 @@ public class InventoryFragment extends Fragment implements InventoryAdapter.OnIn
|
||||
return view;
|
||||
}
|
||||
|
||||
// Categories
|
||||
/**
|
||||
* Fetches all product categories to populate the filter spinner.
|
||||
*/
|
||||
private void loadCategories() {
|
||||
viewModel.getAllCategories(0, 100).observe(getViewLifecycleOwner(), resource -> {
|
||||
if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) {
|
||||
@@ -128,6 +136,9 @@ public class InventoryFragment extends Fragment implements InventoryAdapter.OnIn
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup the category filter spinner.
|
||||
*/
|
||||
private void setupCategorySpinner() {
|
||||
// First item is always "All Categories"
|
||||
List<String> categoryNames = new ArrayList<>();
|
||||
@@ -167,8 +178,9 @@ public class InventoryFragment extends Fragment implements InventoryAdapter.OnIn
|
||||
}
|
||||
}
|
||||
|
||||
// Search
|
||||
|
||||
/**
|
||||
* Sets up the search bar for filtering.
|
||||
*/
|
||||
private void setupSearch(View view) {
|
||||
etSearch = view.findViewById(R.id.etSearchInventory);
|
||||
etSearch.addTextChangedListener(new TextWatcher() {
|
||||
@@ -193,7 +205,9 @@ public class InventoryFragment extends Fragment implements InventoryAdapter.OnIn
|
||||
});
|
||||
}
|
||||
|
||||
// RecyclerView + infinite scroll
|
||||
/**
|
||||
* Initializes the RecyclerView with a layout manager, and adapter.
|
||||
*/
|
||||
private void setupRecyclerView(View view) {
|
||||
RecyclerView rv = view.findViewById(R.id.recyclerViewInventory);
|
||||
adapter = new InventoryAdapter(inventoryList, this);
|
||||
@@ -218,12 +232,17 @@ public class InventoryFragment extends Fragment implements InventoryAdapter.OnIn
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up the SwipeRefreshLayout to reload the first page of inventory items.
|
||||
*/
|
||||
private void setupSwipeRefresh(View view) {
|
||||
swipeRefreshLayout = view.findViewById(R.id.swipeRefreshInventory);
|
||||
swipeRefreshLayout.setOnRefreshListener(() -> loadInventory(true));
|
||||
}
|
||||
|
||||
// Helper function to get a list of all inventory items from the backend
|
||||
/**
|
||||
* Fetches a page of inventory items from the API.
|
||||
*/
|
||||
private void loadInventory(boolean reset) {
|
||||
if (isLoading)
|
||||
return;
|
||||
@@ -270,7 +289,9 @@ public class InventoryFragment extends Fragment implements InventoryAdapter.OnIn
|
||||
});
|
||||
}
|
||||
|
||||
// Combines search text and category into one query string for ?q=
|
||||
/**
|
||||
* Constructs a query string based on the current search text and selected category.
|
||||
*/
|
||||
private String buildQuery() {
|
||||
String q = null;
|
||||
if (!currentQuery.isEmpty() && selectedCategory != null) {
|
||||
@@ -284,7 +305,9 @@ public class InventoryFragment extends Fragment implements InventoryAdapter.OnIn
|
||||
return q;
|
||||
}
|
||||
|
||||
// Bulk delete
|
||||
/**
|
||||
* Displays a confirmation dialog before performing a bulk deletion of selected items.
|
||||
*/
|
||||
private void confirmBulkDelete() {
|
||||
List<Long> ids = adapter.getSelectedIds();
|
||||
if (ids.isEmpty())
|
||||
@@ -298,6 +321,9 @@ public class InventoryFragment extends Fragment implements InventoryAdapter.OnIn
|
||||
.show();
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes the bulk deletion of inventory items through the ViewModel.
|
||||
*/
|
||||
private void bulkDelete(List<Long> ids) {
|
||||
viewModel.bulkDeleteInventory(ids).observe(getViewLifecycleOwner(), resource -> {
|
||||
if (resource != null && resource.status != Resource.Status.LOADING) {
|
||||
@@ -313,6 +339,9 @@ public class InventoryFragment extends Fragment implements InventoryAdapter.OnIn
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Hides the bulk deletion UI bar.
|
||||
*/
|
||||
private void hideBulkDeleteBar() {
|
||||
if (btnBulkDelete != null)
|
||||
btnBulkDelete.setVisibility(View.GONE);
|
||||
@@ -320,7 +349,9 @@ public class InventoryFragment extends Fragment implements InventoryAdapter.OnIn
|
||||
tvSelectionCount.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
// Navigation
|
||||
/**
|
||||
* Navigates to the inventory detail screen for a specific item or to add a new one.
|
||||
*/
|
||||
private void openDetail(InventoryDTO inv) {
|
||||
Bundle args = new Bundle();
|
||||
|
||||
@@ -335,12 +366,16 @@ public class InventoryFragment extends Fragment implements InventoryAdapter.OnIn
|
||||
NavHostFragment.findNavController(this).navigate(R.id.nav_inventory_detail, args);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reloads inventory data when changes occur.
|
||||
*/
|
||||
public void onInventoryChanged() {
|
||||
loadInventory(true);
|
||||
}
|
||||
|
||||
// Adapter callbacks
|
||||
|
||||
/**
|
||||
* Handles item click in the inventory list.
|
||||
*/
|
||||
@Override
|
||||
public void onInventoryClick(int position) {
|
||||
if (position >= 0 && position < inventoryList.size()) {
|
||||
@@ -348,6 +383,9 @@ public class InventoryFragment extends Fragment implements InventoryAdapter.OnIn
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the bulk deletion UI visibility and count when items are selected or deselected.
|
||||
*/
|
||||
@Override
|
||||
public void onSelectionChanged(int selectedCount) {
|
||||
if (selectedCount > 0) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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); }
|
||||
}
|
||||
|
||||
@@ -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); }
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -14,7 +14,8 @@ import com.example.petstoremobile.R;
|
||||
import com.example.petstoremobile.adapters.BlackTextArrayAdapter;
|
||||
import com.example.petstoremobile.api.*;
|
||||
import com.example.petstoremobile.dtos.*;
|
||||
import com.example.petstoremobile.fragments.ListFragment;
|
||||
import com.example.petstoremobile.utils.ErrorUtils;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
import javax.inject.Inject;
|
||||
@@ -22,6 +23,9 @@ import javax.inject.Inject;
|
||||
import dagger.hilt.android.AndroidEntryPoint;
|
||||
import retrofit2.*;
|
||||
|
||||
/**
|
||||
* Fragment for displaying and editing adoption request details.
|
||||
*/
|
||||
@AndroidEntryPoint
|
||||
public class AdoptionDetailFragment extends Fragment {
|
||||
|
||||
@@ -60,6 +64,9 @@ public class AdoptionDetailFragment extends Fragment {
|
||||
return view;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes UI components from the layout.
|
||||
*/
|
||||
private void initViews(View v) {
|
||||
tvMode = v.findViewById(R.id.tvAdoptionMode);
|
||||
tvAdoptionId = v.findViewById(R.id.tvAdoptionId);
|
||||
@@ -72,11 +79,17 @@ public class AdoptionDetailFragment extends Fragment {
|
||||
btnBack = v.findViewById(R.id.btnAdoptionBack);
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures the spinner for adoption status.
|
||||
*/
|
||||
private void setupSpinners() {
|
||||
spinnerStatus.setAdapter(new BlackTextArrayAdapter<>(requireContext(),
|
||||
android.R.layout.simple_spinner_item, STATUSES));
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures the date picker dialog for the adoption date field.
|
||||
*/
|
||||
private void setupDatePicker() {
|
||||
etAdoptionDate.setOnClickListener(v -> {
|
||||
Calendar c = Calendar.getInstance();
|
||||
@@ -89,11 +102,17 @@ public class AdoptionDetailFragment extends Fragment {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches required data (pets and customers) from the backend.
|
||||
*/
|
||||
private void loadData() {
|
||||
loadPets();
|
||||
loadCustomers();
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the list of pets from the API.
|
||||
*/
|
||||
private void loadPets() {
|
||||
petApi.getAllPets(0, 200)
|
||||
.enqueue(new Callback<PageResponse<PetDTO>>() {
|
||||
@@ -110,6 +129,9 @@ public class AdoptionDetailFragment extends Fragment {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Populates the pet selection spinner.
|
||||
*/
|
||||
private void populatePetSpinner() {
|
||||
List<String> names = new ArrayList<>();
|
||||
names.add("-- Select Pet --");
|
||||
@@ -125,6 +147,9 @@ public class AdoptionDetailFragment extends Fragment {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the list of customers from the API.
|
||||
*/
|
||||
private void loadCustomers() {
|
||||
customerApi.getAllCustomers(0, 200)
|
||||
.enqueue(new Callback<PageResponse<CustomerDTO>>() {
|
||||
@@ -141,6 +166,9 @@ public class AdoptionDetailFragment extends Fragment {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Populates the customer selection spinner.
|
||||
*/
|
||||
private void populateCustomerSpinner() {
|
||||
List<String> names = new ArrayList<>();
|
||||
names.add("-- Select Customer --");
|
||||
@@ -157,6 +185,9 @@ public class AdoptionDetailFragment extends Fragment {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles arguments to determine if the fragment is in edit or add mode.
|
||||
*/
|
||||
private void handleArguments() {
|
||||
Bundle a = getArguments();
|
||||
if (a != null && a.containsKey("adoptionId")) {
|
||||
@@ -185,6 +216,9 @@ public class AdoptionDetailFragment extends Fragment {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates input and saves the adoption request to the backend.
|
||||
*/
|
||||
private void saveAdoption() {
|
||||
if (spinnerCustomer.getSelectedItemPosition() == 0) {
|
||||
Toast.makeText(getContext(), "Select a customer", Toast.LENGTH_SHORT).show(); return;
|
||||
@@ -219,6 +253,9 @@ public class AdoptionDetailFragment extends Fragment {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* callback for adoption save/update operations.
|
||||
*/
|
||||
private Callback<AdoptionDTO> simpleCallback(String msg) {
|
||||
return new Callback<>() {
|
||||
public void onResponse(Call<AdoptionDTO> c, Response<AdoptionDTO> r) {
|
||||
@@ -227,13 +264,7 @@ public class AdoptionDetailFragment extends Fragment {
|
||||
Toast.makeText(getContext(), msg, Toast.LENGTH_SHORT).show();
|
||||
navigateBack();
|
||||
} else {
|
||||
try {
|
||||
String err = r.errorBody().string();
|
||||
Log.e("ADOPTION_SAVE", "Error: " + err);
|
||||
Toast.makeText(getContext(), "Error " + r.code(), Toast.LENGTH_SHORT).show();
|
||||
} catch (Exception e) {
|
||||
Log.e("ADOPTION_SAVE", "Failed to read error");
|
||||
}
|
||||
ErrorUtils.showErrorMessage(getContext(), r, "Error " + r.code());
|
||||
}
|
||||
}
|
||||
public void onFailure(Call<AdoptionDTO> c, Throwable t) {
|
||||
@@ -243,15 +274,16 @@ public class AdoptionDetailFragment extends Fragment {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows a confirmation dialog before deleting an adoption request.
|
||||
*/
|
||||
private void confirmDelete() {
|
||||
new AlertDialog.Builder(requireContext())
|
||||
.setTitle("Delete Adoption?")
|
||||
.setPositiveButton("Yes", (d, w) ->
|
||||
adoptionApi.deleteAdoption(adoptionId)
|
||||
.enqueue(new Callback<Void>() {
|
||||
public void onResponse(Call<Void> c, Response<Void> r) {
|
||||
navigateBack();
|
||||
}
|
||||
public void onResponse(Call<Void> c, Response<Void> r) { navigateBack(); }
|
||||
public void onFailure(Call<Void> c, Throwable t) {
|
||||
Toast.makeText(getContext(), "Delete failed",
|
||||
Toast.LENGTH_SHORT).show();
|
||||
@@ -260,6 +292,9 @@ public class AdoptionDetailFragment extends Fragment {
|
||||
.setNegativeButton("No", null).show();
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigates back to the previous fragment.
|
||||
*/
|
||||
private void navigateBack() {
|
||||
NavHostFragment.findNavController(this).popBackStack();
|
||||
}
|
||||
|
||||
@@ -14,7 +14,8 @@ import com.example.petstoremobile.R;
|
||||
import com.example.petstoremobile.adapters.BlackTextArrayAdapter;
|
||||
import com.example.petstoremobile.api.*;
|
||||
import com.example.petstoremobile.dtos.*;
|
||||
import com.example.petstoremobile.fragments.ListFragment;
|
||||
import com.example.petstoremobile.utils.ErrorUtils;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
import javax.inject.Inject;
|
||||
@@ -22,6 +23,9 @@ import javax.inject.Inject;
|
||||
import dagger.hilt.android.AndroidEntryPoint;
|
||||
import retrofit2.*;
|
||||
|
||||
/**
|
||||
* Fragment for displaying and editing appointment details.
|
||||
*/
|
||||
@AndroidEntryPoint
|
||||
public class AppointmentDetailFragment extends Fragment {
|
||||
|
||||
@@ -69,6 +73,9 @@ public class AppointmentDetailFragment extends Fragment {
|
||||
return view;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes UI components from the layout.
|
||||
*/
|
||||
private void initViews(View v) {
|
||||
tvMode = v.findViewById(R.id.tvApptMode);
|
||||
tvAppointmentId = v.findViewById(R.id.tvAppointmentId);
|
||||
@@ -85,6 +92,9 @@ public class AppointmentDetailFragment extends Fragment {
|
||||
btnBack = v.findViewById(R.id.btnApptBack);
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures the adapters for spinners.
|
||||
*/
|
||||
private void setupSpinners() {
|
||||
spinnerStatus.setAdapter(new BlackTextArrayAdapter<>(requireContext(),
|
||||
android.R.layout.simple_spinner_item, STATUSES));
|
||||
@@ -98,6 +108,9 @@ public class AppointmentDetailFragment extends Fragment {
|
||||
android.R.layout.simple_spinner_item, new String[]{"00","15","30","45"}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures the date picker dialog for the appointment date field.
|
||||
*/
|
||||
private void setupDatePicker() {
|
||||
etAppointmentDate.setOnClickListener(v -> {
|
||||
Calendar c = Calendar.getInstance();
|
||||
@@ -111,6 +124,9 @@ public class AppointmentDetailFragment extends Fragment {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches all required data from the backend to populate the fragment.
|
||||
*/
|
||||
private void loadData() {
|
||||
loadPets();
|
||||
loadServices();
|
||||
@@ -119,6 +135,9 @@ public class AppointmentDetailFragment extends Fragment {
|
||||
loadAllAppointments();
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the list of pets from the API.
|
||||
*/
|
||||
private void loadPets() {
|
||||
petApi.getAllPets(0, 200)
|
||||
.enqueue(new Callback<PageResponse<PetDTO>>() {
|
||||
@@ -134,6 +153,9 @@ public class AppointmentDetailFragment extends Fragment {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Populates the pet selection spinner.
|
||||
*/
|
||||
private void populatePetSpinner() {
|
||||
List<String> names = new ArrayList<>();
|
||||
names.add("-- Select Pet --");
|
||||
@@ -149,6 +171,9 @@ public class AppointmentDetailFragment extends Fragment {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the list of services from the API.
|
||||
*/
|
||||
private void loadServices() {
|
||||
serviceApi.getAllServices(0, 200)
|
||||
.enqueue(new Callback<PageResponse<ServiceDTO>>() {
|
||||
@@ -164,6 +189,9 @@ public class AppointmentDetailFragment extends Fragment {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Populates the service selection spinner.
|
||||
*/
|
||||
private void populateServiceSpinner() {
|
||||
List<String> names = new ArrayList<>();
|
||||
names.add("-- Select Service --");
|
||||
@@ -179,6 +207,9 @@ public class AppointmentDetailFragment extends Fragment {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the list of customers from the API.
|
||||
*/
|
||||
private void loadCustomers() {
|
||||
customerApi.getAllCustomers(0, 200)
|
||||
.enqueue(new Callback<PageResponse<CustomerDTO>>() {
|
||||
@@ -194,6 +225,9 @@ public class AppointmentDetailFragment extends Fragment {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Populates the customer spinner.
|
||||
*/
|
||||
private void populateCustomerSpinner() {
|
||||
List<String> names = new ArrayList<>();
|
||||
names.add("-- Select Customer --");
|
||||
@@ -210,6 +244,9 @@ public class AppointmentDetailFragment extends Fragment {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the list of stores from the API.
|
||||
*/
|
||||
private void loadStores() {
|
||||
storeApi.getAllStores(0, 50)
|
||||
.enqueue(new Callback<PageResponse<StoreDTO>>() {
|
||||
@@ -225,6 +262,9 @@ public class AppointmentDetailFragment extends Fragment {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Populates the store spinner.
|
||||
*/
|
||||
private void populateStoreSpinner() {
|
||||
List<String> names = new ArrayList<>();
|
||||
names.add("-- Select Store --");
|
||||
@@ -240,6 +280,9 @@ public class AppointmentDetailFragment extends Fragment {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads all appointments from the API.
|
||||
*/
|
||||
private void loadAllAppointments() {
|
||||
appointmentApi.getAllAppointments(0, 500)
|
||||
.enqueue(new Callback<PageResponse<AppointmentDTO>>() {
|
||||
@@ -251,6 +294,9 @@ public class AppointmentDetailFragment extends Fragment {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles arguments to determine if the fragment is in edit or add mode.
|
||||
*/
|
||||
private void handleArguments() {
|
||||
Bundle a = getArguments();
|
||||
if (a != null && a.containsKey("appointmentId")) {
|
||||
@@ -292,6 +338,9 @@ public class AppointmentDetailFragment extends Fragment {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates input and saves the appointment to the backend.
|
||||
*/
|
||||
private void saveAppointment() {
|
||||
if (spinnerCustomer.getSelectedItemPosition() == 0) {
|
||||
Toast.makeText(getContext(), "Select a customer", Toast.LENGTH_SHORT).show(); return;
|
||||
@@ -370,6 +419,9 @@ public class AppointmentDetailFragment extends Fragment {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* callback for appointment save/update operations.
|
||||
*/
|
||||
private Callback<AppointmentDTO> simpleCallback(String msg) {
|
||||
return new Callback<>() {
|
||||
public void onResponse(Call<AppointmentDTO> c, Response<AppointmentDTO> r) {
|
||||
@@ -387,22 +439,12 @@ public class AppointmentDetailFragment extends Fragment {
|
||||
showErrorDialog("Invalid Date/Time",
|
||||
"Booked appointments must be scheduled in the future. " +
|
||||
"Please select a future date and time.");
|
||||
//------------------------------------------
|
||||
} else if (errorBody.toLowerCase().contains("not available") ||
|
||||
errorBody.toLowerCase().contains("time is not available")) {
|
||||
showNoAvailabilityDialog();
|
||||
} else if (r.code() == 404) {
|
||||
showErrorDialog("Not Found",
|
||||
"The selected pet, customer or service was not found.");
|
||||
} else if (r.code() == 403) {
|
||||
showErrorDialog("Access Denied",
|
||||
"You don't have permission to perform this action.");
|
||||
} else if (r.code() == 400) {
|
||||
showErrorDialog("Invalid Request", errorBody);
|
||||
} else {
|
||||
showErrorDialog("Error", "Something went wrong. Please try again.");
|
||||
ErrorUtils.showErrorMessage(getContext(), r, "Something went wrong. Please try again.");
|
||||
}
|
||||
//-----------------------------
|
||||
} catch (Exception e) {
|
||||
Log.e("APPT_SAVE", "Failed to read error body");
|
||||
showErrorDialog("Error", "Something went wrong. Please try again.");
|
||||
@@ -417,6 +459,9 @@ public class AppointmentDetailFragment extends Fragment {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows a specialized dialog when a time slot is not available.
|
||||
*/
|
||||
private void showNoAvailabilityDialog() {
|
||||
new AlertDialog.Builder(requireContext())
|
||||
.setTitle("No Availability")
|
||||
@@ -427,6 +472,9 @@ public class AppointmentDetailFragment extends Fragment {
|
||||
.show();
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows a generic error dialog with a title and message.
|
||||
*/
|
||||
private void showErrorDialog(String title, String message) {
|
||||
new AlertDialog.Builder(requireContext())
|
||||
.setTitle(title)
|
||||
@@ -434,6 +482,10 @@ public class AppointmentDetailFragment extends Fragment {
|
||||
.setPositiveButton("OK", null)
|
||||
.show();
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows a confirmation dialog and handles the deletion of an appointment.
|
||||
*/
|
||||
private void confirmDelete() {
|
||||
new AlertDialog.Builder(requireContext())
|
||||
.setTitle("Delete Appointment?")
|
||||
@@ -448,6 +500,9 @@ public class AppointmentDetailFragment extends Fragment {
|
||||
.setNegativeButton("No", null).show();
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigates back to the previous screen.
|
||||
*/
|
||||
private void navigateBack() {
|
||||
NavHostFragment.findNavController(this).popBackStack();
|
||||
}
|
||||
|
||||
@@ -26,8 +26,8 @@ import com.example.petstoremobile.dtos.InventoryDTO;
|
||||
import com.example.petstoremobile.dtos.InventoryRequest;
|
||||
import com.example.petstoremobile.dtos.PageResponse;
|
||||
import com.example.petstoremobile.dtos.ProductDTO;
|
||||
import com.example.petstoremobile.fragments.ListFragment;
|
||||
import com.example.petstoremobile.fragments.listfragments.InventoryFragment;
|
||||
import com.example.petstoremobile.utils.ErrorUtils;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
@@ -39,6 +39,9 @@ import retrofit2.Call;
|
||||
import retrofit2.Callback;
|
||||
import retrofit2.Response;
|
||||
|
||||
/**
|
||||
* Fragment for displaying and editing inventory item details.
|
||||
*/
|
||||
@AndroidEntryPoint
|
||||
public class InventoryDetailFragment extends Fragment {
|
||||
|
||||
@@ -65,6 +68,9 @@ public class InventoryDetailFragment extends Fragment {
|
||||
private final List<ProductDTO> productSuggestions = new ArrayList<>();
|
||||
private ArrayAdapter<String> dropdownAdapter;
|
||||
|
||||
/**
|
||||
* Sets the parent inventory fragment to notify of changes.
|
||||
*/
|
||||
public void setInventoryFragment(InventoryFragment fragment) {
|
||||
this.inventoryFragment = fragment;
|
||||
}
|
||||
@@ -85,6 +91,9 @@ public class InventoryDetailFragment extends Fragment {
|
||||
return view;
|
||||
}
|
||||
|
||||
/**
|
||||
* get the layout view and set adapter.
|
||||
*/
|
||||
private void initViews(View view) {
|
||||
tvMode = view.findViewById(R.id.tvInventoryMode);
|
||||
tvInventoryId = view.findViewById(R.id.tvInventoryId);
|
||||
@@ -102,7 +111,9 @@ public class InventoryDetailFragment extends Fragment {
|
||||
etProductSearch.setThreshold(1); // start showing after 1 character
|
||||
}
|
||||
|
||||
// Product search dropdown
|
||||
/**
|
||||
* setup the product search dropdown.
|
||||
*/
|
||||
private void setupProductSearch() {
|
||||
etProductSearch.addTextChangedListener(new TextWatcher() {
|
||||
@Override
|
||||
@@ -143,6 +154,9 @@ public class InventoryDetailFragment extends Fragment {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Searches for products matching the query from the backend.
|
||||
*/
|
||||
private void searchProducts(String query) {
|
||||
productApi.getAllProducts(query, 0, 20).enqueue(new Callback<PageResponse<ProductDTO>>() {
|
||||
@Override
|
||||
@@ -172,8 +186,9 @@ public class InventoryDetailFragment extends Fragment {
|
||||
});
|
||||
}
|
||||
|
||||
// Arguments (edit mode)
|
||||
|
||||
/**
|
||||
* arguments to set up edit or add mode.
|
||||
*/
|
||||
private void handleArguments() {
|
||||
Bundle args = getArguments();
|
||||
if (args != null && args.containsKey("inventoryId")) {
|
||||
@@ -214,7 +229,9 @@ public class InventoryDetailFragment extends Fragment {
|
||||
}
|
||||
}
|
||||
|
||||
// Save
|
||||
/**
|
||||
* Saves the current inventory item details to the backend.
|
||||
*/
|
||||
private void saveInventory() {
|
||||
if (selectedProduct == null) {
|
||||
etProductSearch.setError("Please select a product from the list");
|
||||
@@ -255,7 +272,7 @@ public class InventoryDetailFragment extends Fragment {
|
||||
Toast.makeText(getContext(), "Inventory updated", Toast.LENGTH_SHORT).show();
|
||||
notifyParentAndGoBack();
|
||||
} else {
|
||||
Toast.makeText(getContext(), "Update failed: " + response.code(), Toast.LENGTH_SHORT).show();
|
||||
ErrorUtils.showErrorMessage(getContext(), response, "Update failed");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -274,7 +291,7 @@ public class InventoryDetailFragment extends Fragment {
|
||||
Toast.makeText(getContext(), "Inventory created", Toast.LENGTH_SHORT).show();
|
||||
notifyParentAndGoBack();
|
||||
} else {
|
||||
Toast.makeText(getContext(), "Create failed: " + response.code(), Toast.LENGTH_SHORT).show();
|
||||
ErrorUtils.showErrorMessage(getContext(), response, "Create failed");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -287,7 +304,9 @@ public class InventoryDetailFragment extends Fragment {
|
||||
}
|
||||
}
|
||||
|
||||
// Delete
|
||||
/**
|
||||
* Shows a confirmation dialog before deleting an inventory item.
|
||||
*/
|
||||
private void confirmDelete() {
|
||||
new AlertDialog.Builder(requireContext())
|
||||
.setTitle("Delete inventory item?")
|
||||
@@ -297,6 +316,9 @@ public class InventoryDetailFragment extends Fragment {
|
||||
.show();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a request to the API to delete the inventory item.
|
||||
*/
|
||||
private void deleteInventory() {
|
||||
setButtonsEnabled(false);
|
||||
inventoryApi.deleteInventory(inventoryId).enqueue(new Callback<Void>() {
|
||||
@@ -307,7 +329,7 @@ public class InventoryDetailFragment extends Fragment {
|
||||
Toast.makeText(getContext(), "Inventory deleted", Toast.LENGTH_SHORT).show();
|
||||
notifyParentAndGoBack();
|
||||
} else {
|
||||
Toast.makeText(getContext(), "Delete failed: " + response.code(), Toast.LENGTH_SHORT).show();
|
||||
ErrorUtils.showErrorMessage(getContext(), response, "Delete failed");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -319,18 +341,25 @@ public class InventoryDetailFragment extends Fragment {
|
||||
});
|
||||
}
|
||||
|
||||
// Helpers
|
||||
|
||||
/**
|
||||
* Notifies the parent fragment of a change and navigates back.
|
||||
*/
|
||||
private void notifyParentAndGoBack() {
|
||||
if (inventoryFragment != null)
|
||||
inventoryFragment.onInventoryChanged();
|
||||
navigateBack();
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigates back to the previous fragment.
|
||||
*/
|
||||
private void navigateBack() {
|
||||
NavHostFragment.findNavController(this).popBackStack();
|
||||
}
|
||||
|
||||
/**
|
||||
* Enables or disables action buttons.
|
||||
*/
|
||||
private void setButtonsEnabled(boolean enabled) {
|
||||
btnSave.setEnabled(enabled);
|
||||
btnDelete.setEnabled(enabled);
|
||||
|
||||
@@ -3,9 +3,7 @@ package com.example.petstoremobile.fragments.listfragments.detailfragments;
|
||||
import android.os.Bundle;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.navigation.fragment.NavHostFragment;
|
||||
|
||||
@@ -13,7 +11,6 @@ import android.util.Log;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ArrayAdapter;
|
||||
import android.widget.Button;
|
||||
import android.widget.EditText;
|
||||
import android.widget.Spinner;
|
||||
@@ -24,9 +21,8 @@ import com.example.petstoremobile.R;
|
||||
import com.example.petstoremobile.adapters.BlackTextArrayAdapter;
|
||||
import com.example.petstoremobile.api.PetApi;
|
||||
import com.example.petstoremobile.dtos.PetDTO;
|
||||
import com.example.petstoremobile.fragments.ListFragment;
|
||||
import com.example.petstoremobile.fragments.listfragments.PetFragment;
|
||||
import com.example.petstoremobile.utils.ActivityLogger;
|
||||
import com.example.petstoremobile.utils.ErrorUtils;
|
||||
import com.example.petstoremobile.utils.InputValidator;
|
||||
|
||||
import javax.inject.Inject;
|
||||
@@ -36,6 +32,9 @@ import retrofit2.Call;
|
||||
import retrofit2.Callback;
|
||||
import retrofit2.Response;
|
||||
|
||||
/**
|
||||
* Fragment for displaying and editing pet details.
|
||||
*/
|
||||
@AndroidEntryPoint
|
||||
public class PetDetailFragment extends Fragment {
|
||||
|
||||
@@ -66,7 +65,9 @@ public class PetDetailFragment extends Fragment {
|
||||
return view;
|
||||
}
|
||||
|
||||
//Method to Update or Add a pet
|
||||
/**
|
||||
* Handles the saving of pet data (adding/updating).
|
||||
*/
|
||||
private void savePet() {
|
||||
// Validates all fields using InputValidator
|
||||
if (!InputValidator.isNotEmpty(etPetName, "Pet Name")) return;
|
||||
@@ -104,7 +105,7 @@ public class PetDetailFragment extends Fragment {
|
||||
Toast.makeText(getContext(), "Pet updated successfully!", Toast.LENGTH_SHORT).show();
|
||||
navigateBack();
|
||||
} else {
|
||||
Toast.makeText(getContext(), "Failed to update pet: " + response.code(), Toast.LENGTH_SHORT).show();
|
||||
ErrorUtils.showErrorMessage(getContext(), response, "Failed to update pet");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -112,7 +113,7 @@ public class PetDetailFragment extends Fragment {
|
||||
public void onFailure(Call<PetDTO> call, Throwable t) {
|
||||
ActivityLogger.logException(requireContext(), "PetDetailFragment.updatePet", new Exception(t));
|
||||
Log.e("PetDetailFragment", "Error updating pet", t);
|
||||
Toast.makeText(getContext(), "Error: " + t.getMessage(), Toast.LENGTH_SHORT).show();
|
||||
Toast.makeText(getContext(), "Network error: " + t.getMessage(), Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
@@ -125,7 +126,7 @@ public class PetDetailFragment extends Fragment {
|
||||
Toast.makeText(getContext(), "Pet added successfully!", Toast.LENGTH_SHORT).show();
|
||||
navigateBack();
|
||||
} else {
|
||||
Toast.makeText(getContext(), "Failed to add pet: " + response.code(), Toast.LENGTH_SHORT).show();
|
||||
ErrorUtils.showErrorMessage(getContext(), response, "Failed to add pet");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -133,13 +134,15 @@ public class PetDetailFragment extends Fragment {
|
||||
public void onFailure(Call<PetDTO> call, Throwable t) {
|
||||
ActivityLogger.logException(requireContext(), "PetDetailFragment.createPet", new Exception(t));
|
||||
Log.e("PetDetailFragment", "Error adding pet", t);
|
||||
Toast.makeText(getContext(), "Error: " + t.getMessage(), Toast.LENGTH_SHORT).show();
|
||||
Toast.makeText(getContext(), "Network error: " + t.getMessage(), Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
//Method to Delete a pet
|
||||
/**
|
||||
* Displays a confirmation dialog and handles the deletion of a pet.
|
||||
*/
|
||||
private void deletePet() {
|
||||
//Alert the user to confirm the delete
|
||||
new AlertDialog.Builder(requireContext())
|
||||
@@ -155,7 +158,7 @@ public class PetDetailFragment extends Fragment {
|
||||
Toast.makeText(getContext(), "Pet deleted successfully!", Toast.LENGTH_SHORT).show();
|
||||
navigateBack();
|
||||
} else {
|
||||
Toast.makeText(getContext(), "Failed to delete pet: " + response.code(), Toast.LENGTH_SHORT).show();
|
||||
ErrorUtils.showErrorMessage(getContext(), response, "Failed to delete pet");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -163,7 +166,7 @@ public class PetDetailFragment extends Fragment {
|
||||
public void onFailure(Call<Void> call, Throwable t) {
|
||||
ActivityLogger.logException(requireContext(), "PetDetailFragment.deletePet", new Exception(t));
|
||||
Log.e("PetDetailFragment", "Error deleting pet", t);
|
||||
Toast.makeText(getContext(), "Error: " + t.getMessage(), Toast.LENGTH_SHORT).show();
|
||||
Toast.makeText(getContext(), "Network error: " + t.getMessage(), Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
});
|
||||
})
|
||||
@@ -171,12 +174,16 @@ public class PetDetailFragment extends Fragment {
|
||||
.show();
|
||||
}
|
||||
|
||||
//Helper method to navigate back to the list
|
||||
/**
|
||||
* Navigates back to the previous screen.
|
||||
*/
|
||||
private void navigateBack() {
|
||||
NavHostFragment.findNavController(this).popBackStack();
|
||||
}
|
||||
|
||||
//helper function to check if pet is being edited or added and show the view accordingly
|
||||
/**
|
||||
* Handles arguments passed to the fragment to determine if it's in edit or add mode.
|
||||
*/
|
||||
private void handleArguments() {
|
||||
// Pet is being edited if the bundle contains a petId
|
||||
if (getArguments() != null && getArguments().containsKey("petId")) {
|
||||
@@ -208,7 +215,9 @@ public class PetDetailFragment extends Fragment {
|
||||
}
|
||||
}
|
||||
|
||||
//helper function to get controls from layout
|
||||
/**
|
||||
* Binds UI components from the layout.
|
||||
*/
|
||||
private void initViews(View view) {
|
||||
tvMode = view.findViewById(R.id.tvMode);
|
||||
tvPetId = view.findViewById(R.id.tvPetId);
|
||||
@@ -223,7 +232,9 @@ public class PetDetailFragment extends Fragment {
|
||||
btnBack = view.findViewById(R.id.btnBack);
|
||||
}
|
||||
|
||||
//helper function to set up the spinner menu for pet status
|
||||
/**
|
||||
* Initializes the spinner for pet status selection.
|
||||
*/
|
||||
private void setupSpinner() {
|
||||
BlackTextArrayAdapter<String> adapter = new BlackTextArrayAdapter<>(requireContext(),
|
||||
android.R.layout.simple_spinner_item,
|
||||
|
||||
@@ -1,38 +1,25 @@
|
||||
package com.example.petstoremobile.fragments.listfragments.detailfragments;
|
||||
|
||||
import android.Manifest;
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.provider.MediaStore;
|
||||
import android.util.Log;
|
||||
import android.view.*;
|
||||
import android.widget.*;
|
||||
import androidx.activity.result.ActivityResultLauncher;
|
||||
import androidx.activity.result.contract.ActivityResultContracts;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.core.content.FileProvider;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
import androidx.navigation.fragment.NavHostFragment;
|
||||
|
||||
import com.bumptech.glide.Glide;
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy;
|
||||
import com.bumptech.glide.load.model.GlideUrl;
|
||||
import com.bumptech.glide.load.model.LazyHeaders;
|
||||
import com.example.petstoremobile.R;
|
||||
import com.example.petstoremobile.adapters.BlackTextArrayAdapter;
|
||||
import com.example.petstoremobile.api.*;
|
||||
import com.example.petstoremobile.api.auth.TokenManager;
|
||||
import com.example.petstoremobile.dtos.*;
|
||||
import com.example.petstoremobile.viewmodels.ProductViewModel;
|
||||
import com.example.petstoremobile.utils.Resource;
|
||||
import com.example.petstoremobile.utils.ErrorUtils;
|
||||
import com.example.petstoremobile.utils.FileUtils;
|
||||
import com.example.petstoremobile.utils.GlideUtils;
|
||||
import com.example.petstoremobile.utils.ImagePickerHelper;
|
||||
|
||||
import java.io.File;
|
||||
import java.math.BigDecimal;
|
||||
@@ -46,6 +33,9 @@ import okhttp3.MediaType;
|
||||
import okhttp3.MultipartBody;
|
||||
import okhttp3.RequestBody;
|
||||
|
||||
/**
|
||||
* Fragment for displaying and editing product details, including image selection.
|
||||
*/
|
||||
@AndroidEntryPoint
|
||||
public class ProductDetailFragment extends Fragment {
|
||||
|
||||
@@ -65,52 +55,43 @@ public class ProductDetailFragment extends Fragment {
|
||||
private List<CategoryDTO> categoryList = new ArrayList<>();
|
||||
private Uri photoUri;
|
||||
private ProductViewModel viewModel;
|
||||
private ImagePickerHelper imagePickerHelper;
|
||||
|
||||
@Inject @Named("baseUrl") String baseUrl;
|
||||
@Inject TokenManager tokenManager;
|
||||
|
||||
private ActivityResultLauncher<Intent> galleryLauncher;
|
||||
private ActivityResultLauncher<Uri> cameraLauncher;
|
||||
private ActivityResultLauncher<String> permissionLauncher;
|
||||
|
||||
/**
|
||||
* Initializes activity launchers and the ImagePickerHelper.
|
||||
*/
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
viewModel = new ViewModelProvider(this).get(ProductViewModel.class);
|
||||
|
||||
galleryLauncher = registerForActivityResult(
|
||||
new ActivityResultContracts.StartActivityForResult(),
|
||||
result -> {
|
||||
if (result.getResultCode() == Activity.RESULT_OK && result.getData() != null) {
|
||||
Uri selectedImage = result.getData().getData();
|
||||
Glide.with(this).load(selectedImage).into(ivProductImage);
|
||||
photoUri = selectedImage;
|
||||
hasImage = true;
|
||||
isImageChanged = true;
|
||||
isImageRemoved = false;
|
||||
}
|
||||
}
|
||||
);
|
||||
cameraLauncher = registerForActivityResult(
|
||||
new ActivityResultContracts.TakePicture(),
|
||||
success -> {
|
||||
if (success) {
|
||||
Glide.with(this).load(photoUri).into(ivProductImage);
|
||||
hasImage = true;
|
||||
isImageChanged = true;
|
||||
isImageRemoved = false;
|
||||
}
|
||||
}
|
||||
);
|
||||
permissionLauncher = registerForActivityResult(
|
||||
new ActivityResultContracts.RequestPermission(),
|
||||
granted -> {
|
||||
if (granted) launchCamera();
|
||||
else Toast.makeText(getContext(), "Camera permission denied", Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
);
|
||||
imagePickerHelper = new ImagePickerHelper(this, "product_photo.jpg", new ImagePickerHelper.ImagePickerListener() {
|
||||
@Override
|
||||
public void onImagePicked(Uri uri) {
|
||||
photoUri = uri;
|
||||
Glide.with(ProductDetailFragment.this).load(uri).into(ivProductImage);
|
||||
hasImage = true;
|
||||
isImageChanged = true;
|
||||
isImageRemoved = false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onImageRemoved() {
|
||||
photoUri = null;
|
||||
hasImage = false;
|
||||
isImageChanged = false;
|
||||
isImageRemoved = true;
|
||||
ivProductImage.setImageResource(R.drawable.placeholder2);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Inflates the layout and initializes UI components and listeners.
|
||||
*/
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
|
||||
Bundle savedInstanceState) {
|
||||
@@ -122,10 +103,13 @@ public class ProductDetailFragment extends Fragment {
|
||||
btnBack.setOnClickListener(v -> navigateBack());
|
||||
btnSave.setOnClickListener(v -> saveProduct());
|
||||
btnDelete.setOnClickListener(v -> confirmDelete());
|
||||
ivProductImage.setOnClickListener(v -> showImagePickerDialog());
|
||||
ivProductImage.setOnClickListener(v -> imagePickerHelper.showImagePickerDialog("Select Product Image", hasImage));
|
||||
return view;
|
||||
}
|
||||
|
||||
/**
|
||||
* get the UI components from the layout.
|
||||
*/
|
||||
private void initViews(View v) {
|
||||
tvMode = v.findViewById(R.id.tvProductMode);
|
||||
tvProductId = v.findViewById(R.id.tvProductId);
|
||||
@@ -139,63 +123,21 @@ public class ProductDetailFragment extends Fragment {
|
||||
ivProductImage = v.findViewById(R.id.ivProductImage);
|
||||
}
|
||||
|
||||
// Helper function to show the image picker dialog
|
||||
private void showImagePickerDialog() {
|
||||
List<String> options = new ArrayList<>();
|
||||
options.add("Take Photo");
|
||||
options.add("Choose from Gallery");
|
||||
if (hasImage) {
|
||||
options.add("Remove Photo");
|
||||
}
|
||||
|
||||
new AlertDialog.Builder(requireContext())
|
||||
.setTitle("Select Product Image")
|
||||
.setItems(options.toArray(new String[0]), (dialog, which) -> {
|
||||
String selectedOption = options.get(which);
|
||||
if (selectedOption.equals("Take Photo")) {
|
||||
if (ContextCompat.checkSelfPermission(requireContext(), Manifest.permission.CAMERA)
|
||||
== PackageManager.PERMISSION_GRANTED) {
|
||||
launchCamera();
|
||||
} else {
|
||||
permissionLauncher.launch(Manifest.permission.CAMERA);
|
||||
}
|
||||
} else if (selectedOption.equals("Choose from Gallery")) {
|
||||
Intent intent = new Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI);
|
||||
galleryLauncher.launch(intent);
|
||||
} else if (selectedOption.equals("Remove Photo")) {
|
||||
removePhoto();
|
||||
}
|
||||
})
|
||||
.show();
|
||||
}
|
||||
|
||||
// Helper function to remove the photo locally
|
||||
private void removePhoto() {
|
||||
photoUri = null;
|
||||
hasImage = false;
|
||||
isImageChanged = false;
|
||||
isImageRemoved = true;
|
||||
Glide.with(this).load(R.drawable.placeholder2).into(ivProductImage);
|
||||
}
|
||||
|
||||
// Helper function to launch the camera
|
||||
private void launchCamera() {
|
||||
File photoFile = new File(requireContext().getCacheDir(), "product_photo.jpg");
|
||||
photoUri = FileProvider.getUriForFile(requireContext(), requireContext().getPackageName() + ".fileprovider", photoFile);
|
||||
cameraLauncher.launch(photoUri);
|
||||
}
|
||||
|
||||
// Helper function to load categories from the backend for the spinner
|
||||
/**
|
||||
* Fetches all product categories for the selection spinner.
|
||||
*/
|
||||
private void loadCategories() {
|
||||
viewModel.getAllCategories(0, 100).observe(getViewLifecycleOwner(), resource -> {
|
||||
if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) {
|
||||
if (resource != null && resource.status == com.example.petstoremobile.utils.Resource.Status.SUCCESS && resource.data != null) {
|
||||
categoryList = resource.data.getContent();
|
||||
populateCategorySpinner();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Helper function to populate the category spinner
|
||||
/**
|
||||
* Fills the spinner with category names.
|
||||
*/
|
||||
private void populateCategorySpinner() {
|
||||
List<String> names = new ArrayList<>();
|
||||
names.add("-- Select Category --");
|
||||
@@ -211,6 +153,9 @@ public class ProductDetailFragment extends Fragment {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the fragment was opened with existing product data for editing.
|
||||
*/
|
||||
private void handleArguments() {
|
||||
Bundle a = getArguments();
|
||||
if (a != null && a.containsKey("prodId")) {
|
||||
@@ -235,34 +180,34 @@ public class ProductDetailFragment extends Fragment {
|
||||
}
|
||||
}
|
||||
|
||||
//load the product image from the backend
|
||||
/**
|
||||
* Loads the product image from the backend.
|
||||
*/
|
||||
private void loadProductImage() {
|
||||
String imageUrl = baseUrl + String.format(Locale.US, ProductApi.PRODUCT_IMAGE_PATH, prodId);
|
||||
String token = tokenManager.getToken();
|
||||
|
||||
Object loadTarget = imageUrl;
|
||||
if (token != null) {
|
||||
loadTarget = new GlideUrl(imageUrl, new LazyHeaders.Builder()
|
||||
.addHeader("Authorization", "Bearer " + token)
|
||||
.build());
|
||||
}
|
||||
GlideUtils.loadImageWithToken(requireContext(), ivProductImage, imageUrl, token, R.drawable.placeholder2, new GlideUtils.ImageLoadListener() {
|
||||
@Override
|
||||
public void onResourceReady() {
|
||||
hasImage = true;
|
||||
}
|
||||
|
||||
Glide.with(this)
|
||||
.load(loadTarget)
|
||||
.skipMemoryCache(true)
|
||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||
.placeholder(R.drawable.placeholder2)
|
||||
.error(R.drawable.placeholder2)
|
||||
.into(ivProductImage);
|
||||
@Override
|
||||
public void onLoadFailed() {
|
||||
hasImage = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Function to check any changes to the image and perform the appropriate action
|
||||
// updating/adding photo, removing photo or no change
|
||||
/**
|
||||
* Performs image related actions (upload/delete) after product details are saved.
|
||||
*/
|
||||
private void performPendingImageActions(String successMsg) {
|
||||
if (isImageRemoved) {
|
||||
viewModel.deleteProductImage(prodId).observe(getViewLifecycleOwner(), resource -> {
|
||||
if (resource != null && resource.status != Resource.Status.LOADING) {
|
||||
if (resource.status == Resource.Status.SUCCESS) {
|
||||
if (resource != null && resource.status != com.example.petstoremobile.utils.Resource.Status.LOADING) {
|
||||
if (resource.status == com.example.petstoremobile.utils.Resource.Status.SUCCESS) {
|
||||
Toast.makeText(getContext(), successMsg, Toast.LENGTH_SHORT).show();
|
||||
} else {
|
||||
Toast.makeText(getContext(), successMsg + " (but image removal failed)", Toast.LENGTH_SHORT).show();
|
||||
@@ -278,8 +223,9 @@ public class ProductDetailFragment extends Fragment {
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to upload the product image by calling the backend
|
||||
// and then navigate back to the previous screen
|
||||
/**
|
||||
* Uploads the selected image file to the server.
|
||||
*/
|
||||
private void uploadProductImageAndNavigate(Uri uri, String successMsg) {
|
||||
File file = FileUtils.getFileFromUri(requireContext(), uri);
|
||||
if (file == null) {
|
||||
@@ -292,8 +238,8 @@ public class ProductDetailFragment extends Fragment {
|
||||
MultipartBody.Part body = MultipartBody.Part.createFormData("image", file.getName(), requestFile);
|
||||
|
||||
viewModel.uploadProductImage(prodId, body).observe(getViewLifecycleOwner(), resource -> {
|
||||
if (resource != null && resource.status != Resource.Status.LOADING) {
|
||||
if (resource.status == Resource.Status.SUCCESS) {
|
||||
if (resource != null && resource.status != com.example.petstoremobile.utils.Resource.Status.LOADING) {
|
||||
if (resource.status == com.example.petstoremobile.utils.Resource.Status.SUCCESS) {
|
||||
Toast.makeText(getContext(), successMsg, Toast.LENGTH_SHORT).show();
|
||||
} else {
|
||||
Toast.makeText(getContext(), successMsg + " (but image upload failed)", Toast.LENGTH_SHORT).show();
|
||||
@@ -303,6 +249,9 @@ public class ProductDetailFragment extends Fragment {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates input fields and saves product information to the backend.
|
||||
*/
|
||||
private void saveProduct() {
|
||||
String name = etProductName.getText().toString().trim();
|
||||
String desc = etProductDesc.getText().toString().trim();
|
||||
@@ -330,8 +279,8 @@ public class ProductDetailFragment extends Fragment {
|
||||
|
||||
if (isEditing) {
|
||||
viewModel.updateProduct(prodId, dto).observe(getViewLifecycleOwner(), resource -> {
|
||||
if (resource != null && resource.status != Resource.Status.LOADING) {
|
||||
if (resource.status == Resource.Status.SUCCESS) {
|
||||
if (resource != null && resource.status != com.example.petstoremobile.utils.Resource.Status.LOADING) {
|
||||
if (resource.status == com.example.petstoremobile.utils.Resource.Status.SUCCESS) {
|
||||
performPendingImageActions("Updated");
|
||||
} else {
|
||||
Toast.makeText(getContext(), "Error: " + resource.message, Toast.LENGTH_SHORT).show();
|
||||
@@ -340,8 +289,8 @@ public class ProductDetailFragment extends Fragment {
|
||||
});
|
||||
} else {
|
||||
viewModel.createProduct(dto).observe(getViewLifecycleOwner(), resource -> {
|
||||
if (resource != null && resource.status != Resource.Status.LOADING) {
|
||||
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
|
||||
if (resource != null && resource.status != com.example.petstoremobile.utils.Resource.Status.LOADING) {
|
||||
if (resource.status == com.example.petstoremobile.utils.Resource.Status.SUCCESS && resource.data != null) {
|
||||
prodId = resource.data.getProdId();
|
||||
performPendingImageActions("Saved");
|
||||
} else {
|
||||
@@ -352,21 +301,26 @@ public class ProductDetailFragment extends Fragment {
|
||||
}
|
||||
}
|
||||
|
||||
// Function to delete the product from the server
|
||||
/**
|
||||
* Displays a confirmation dialog before deleting the product.
|
||||
*/
|
||||
private void confirmDelete() {
|
||||
new AlertDialog.Builder(requireContext())
|
||||
new androidx.appcompat.app.AlertDialog.Builder(requireContext())
|
||||
.setTitle("Delete Product?")
|
||||
.setPositiveButton("Yes", (d, w) ->
|
||||
viewModel.deleteProduct(prodId).observe(getViewLifecycleOwner(), resource -> {
|
||||
if (resource != null && resource.status == Resource.Status.SUCCESS) {
|
||||
if (resource != null && resource.status == com.example.petstoremobile.utils.Resource.Status.SUCCESS) {
|
||||
navigateBack();
|
||||
} else if (resource != null && resource.status == Resource.Status.ERROR) {
|
||||
} else if (resource != null && resource.status == com.example.petstoremobile.utils.Resource.Status.ERROR) {
|
||||
Toast.makeText(getContext(), "Delete failed: " + resource.message, Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}))
|
||||
.setNegativeButton("No", null).show();
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigates back to the previous fragment.
|
||||
*/
|
||||
private void navigateBack() {
|
||||
NavHostFragment.findNavController(this).popBackStack();
|
||||
}
|
||||
|
||||
@@ -13,7 +13,8 @@ import com.example.petstoremobile.R;
|
||||
import com.example.petstoremobile.adapters.BlackTextArrayAdapter;
|
||||
import com.example.petstoremobile.api.*;
|
||||
import com.example.petstoremobile.dtos.*;
|
||||
import com.example.petstoremobile.fragments.ListFragment;
|
||||
import com.example.petstoremobile.utils.ErrorUtils;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.*;
|
||||
|
||||
@@ -22,6 +23,9 @@ import javax.inject.Inject;
|
||||
import dagger.hilt.android.AndroidEntryPoint;
|
||||
import retrofit2.*;
|
||||
|
||||
/**
|
||||
* Fragment for displaying and editing the relationship between products and suppliers,
|
||||
*/
|
||||
@AndroidEntryPoint
|
||||
public class ProductSupplierDetailFragment extends Fragment {
|
||||
|
||||
@@ -57,6 +61,9 @@ public class ProductSupplierDetailFragment extends Fragment {
|
||||
return view;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes UI components from the layout.
|
||||
*/
|
||||
private void initViews(View v) {
|
||||
tvMode = v.findViewById(R.id.tvPSMode);
|
||||
spinnerProduct = v.findViewById(R.id.spinnerPSProduct);
|
||||
@@ -67,11 +74,17 @@ public class ProductSupplierDetailFragment extends Fragment {
|
||||
btnBack = v.findViewById(R.id.btnPSBack);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches products and suppliers to populate the spinners.
|
||||
*/
|
||||
private void loadData() {
|
||||
loadProducts();
|
||||
loadSuppliers();
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the list of products from the API.
|
||||
*/
|
||||
private void loadProducts() {
|
||||
productApi.getAllProducts(null, 0, 200)
|
||||
.enqueue(new Callback<PageResponse<ProductDTO>>() {
|
||||
@@ -88,6 +101,9 @@ public class ProductSupplierDetailFragment extends Fragment {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Populates the product spinner.
|
||||
*/
|
||||
private void populateProductSpinner() {
|
||||
List<String> names = new ArrayList<>();
|
||||
names.add("-- Select Product --");
|
||||
@@ -103,6 +119,9 @@ public class ProductSupplierDetailFragment extends Fragment {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the list of suppliers from the API.
|
||||
*/
|
||||
private void loadSuppliers() {
|
||||
supplierApi.getAllSuppliers(0, 200)
|
||||
.enqueue(new Callback<PageResponse<SupplierDTO>>() {
|
||||
@@ -119,6 +138,9 @@ public class ProductSupplierDetailFragment extends Fragment {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Populates the supplier spinner.
|
||||
*/
|
||||
private void populateSupplierSpinner() {
|
||||
List<String> names = new ArrayList<>();
|
||||
names.add("-- Select Supplier --");
|
||||
@@ -134,6 +156,9 @@ public class ProductSupplierDetailFragment extends Fragment {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles arguments to determine if the fragment is in edit or add mode.
|
||||
*/
|
||||
private void handleArguments() {
|
||||
Bundle a = getArguments();
|
||||
if (a != null && a.containsKey("productId")) {
|
||||
@@ -151,6 +176,9 @@ public class ProductSupplierDetailFragment extends Fragment {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates input and saves the product-supplier to the backend.
|
||||
*/
|
||||
private void save() {
|
||||
if (spinnerProduct.getSelectedItemPosition() == 0) {
|
||||
Toast.makeText(getContext(), "Select a product", Toast.LENGTH_SHORT).show(); return;
|
||||
@@ -183,6 +211,9 @@ public class ProductSupplierDetailFragment extends Fragment {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* callback for product-supplier save/update operations.
|
||||
*/
|
||||
private Callback<ProductSupplierDTO> simpleCallback(String msg) {
|
||||
return new Callback<>() {
|
||||
public void onResponse(Call<ProductSupplierDTO> c, Response<ProductSupplierDTO> r) {
|
||||
@@ -190,13 +221,7 @@ public class ProductSupplierDetailFragment extends Fragment {
|
||||
Toast.makeText(getContext(), msg, Toast.LENGTH_SHORT).show();
|
||||
navigateBack();
|
||||
} else {
|
||||
try {
|
||||
String err = r.errorBody().string();
|
||||
Log.e("PS_SAVE", "Error: " + err);
|
||||
Toast.makeText(getContext(), "Error " + r.code(), Toast.LENGTH_SHORT).show();
|
||||
} catch (Exception e) {
|
||||
Log.e("PS_SAVE", "Failed to read error");
|
||||
}
|
||||
ErrorUtils.showErrorMessage(getContext(), r, "Error " + r.code());
|
||||
}
|
||||
}
|
||||
public void onFailure(Call<ProductSupplierDTO> c, Throwable t) {
|
||||
@@ -206,6 +231,9 @@ public class ProductSupplierDetailFragment extends Fragment {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows a confirmation dialog before deleting a product-supplier relationship.
|
||||
*/
|
||||
private void confirmDelete() {
|
||||
new AlertDialog.Builder(requireContext())
|
||||
.setTitle("Delete?")
|
||||
@@ -223,6 +251,9 @@ public class ProductSupplierDetailFragment extends Fragment {
|
||||
.setNegativeButton("No", null).show();
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigates back to the previous screen.
|
||||
*/
|
||||
private void navigateBack() {
|
||||
NavHostFragment.findNavController(this).popBackStack();
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -20,6 +20,7 @@ import com.example.petstoremobile.api.ServiceApi;
|
||||
import com.example.petstoremobile.dtos.ServiceDTO;
|
||||
import com.example.petstoremobile.fragments.listfragments.ServiceFragment;
|
||||
import com.example.petstoremobile.utils.ActivityLogger;
|
||||
import com.example.petstoremobile.utils.ErrorUtils;
|
||||
import com.example.petstoremobile.utils.InputValidator;
|
||||
|
||||
import javax.inject.Inject;
|
||||
@@ -29,6 +30,9 @@ import retrofit2.Call;
|
||||
import retrofit2.Callback;
|
||||
import retrofit2.Response;
|
||||
|
||||
/**
|
||||
* Fragment for displaying and editing service details.
|
||||
*/
|
||||
@AndroidEntryPoint
|
||||
public class ServiceDetailFragment extends Fragment {
|
||||
|
||||
@@ -41,7 +45,9 @@ public class ServiceDetailFragment extends Fragment {
|
||||
|
||||
@Inject ServiceApi serviceApi;
|
||||
|
||||
//set the service fragment to the parent so we refer back to service view when save or delete is done
|
||||
/**
|
||||
* Sets the parent service fragment to notify of changes.
|
||||
*/
|
||||
public void setServiceFragment(ServiceFragment fragment) {
|
||||
this.serviceFragment = fragment;
|
||||
}
|
||||
@@ -63,7 +69,9 @@ public class ServiceDetailFragment extends Fragment {
|
||||
return view;
|
||||
}
|
||||
|
||||
//Method to Update or Add a service
|
||||
/**
|
||||
* Handles the saving of service data (adding or updating).
|
||||
*/
|
||||
private void saveService() {
|
||||
// Validates all fields using InputValidator
|
||||
if (!InputValidator.isNotEmpty(etServiceName, "Service Name")) return;
|
||||
@@ -96,7 +104,7 @@ public class ServiceDetailFragment extends Fragment {
|
||||
Toast.makeText(getContext(), "Service updated successfully!", Toast.LENGTH_SHORT).show();
|
||||
navigateBack();
|
||||
} else {
|
||||
Toast.makeText(getContext(), "Failed to update service: " + response.code(), Toast.LENGTH_SHORT).show();
|
||||
ErrorUtils.showErrorMessage(getContext(), response, "Failed to update service");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,7 +112,7 @@ public class ServiceDetailFragment extends Fragment {
|
||||
public void onFailure(Call<ServiceDTO> call, Throwable t) {
|
||||
ActivityLogger.logException(requireContext(), "ServiceDetailFragment.updateService", new Exception(t));
|
||||
Log.e("ServiceDetailFragment", "Error updating service", t);
|
||||
Toast.makeText(getContext(), "Error: " + t.getMessage(), Toast.LENGTH_SHORT).show();
|
||||
Toast.makeText(getContext(), "Network error: " + t.getMessage(), Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
@@ -117,7 +125,7 @@ public class ServiceDetailFragment extends Fragment {
|
||||
Toast.makeText(getContext(), "Service added successfully!", Toast.LENGTH_SHORT).show();
|
||||
navigateBack();
|
||||
} else {
|
||||
Toast.makeText(getContext(), "Failed to add service: " + response.code(), Toast.LENGTH_SHORT).show();
|
||||
ErrorUtils.showErrorMessage(getContext(), response, "Failed to add service");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -125,13 +133,15 @@ public class ServiceDetailFragment extends Fragment {
|
||||
public void onFailure(Call<ServiceDTO> call, Throwable t) {
|
||||
ActivityLogger.logException(requireContext(), "ServiceDetailFragment.createService", new Exception(t));
|
||||
Log.e("ServiceDetailFragment", "Error adding service", t);
|
||||
Toast.makeText(getContext(), "Error: " + t.getMessage(), Toast.LENGTH_SHORT).show();
|
||||
Toast.makeText(getContext(), "Network error: " + t.getMessage(), Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
//Method to Delete a service
|
||||
/**
|
||||
* Displays a confirmation dialog and handles the deletion of a service.
|
||||
*/
|
||||
private void deleteService() {
|
||||
//Alert the user to confirm the delete
|
||||
new AlertDialog.Builder(requireContext())
|
||||
@@ -146,7 +156,7 @@ public class ServiceDetailFragment extends Fragment {
|
||||
Toast.makeText(getContext(), "Service deleted successfully!", Toast.LENGTH_SHORT).show();
|
||||
navigateBack();
|
||||
} else {
|
||||
Toast.makeText(getContext(), "Failed to delete service: " + response.code(), Toast.LENGTH_SHORT).show();
|
||||
ErrorUtils.showErrorMessage(getContext(), response, "Failed to delete service");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -154,7 +164,7 @@ public class ServiceDetailFragment extends Fragment {
|
||||
public void onFailure(Call<Void> call, Throwable t) {
|
||||
ActivityLogger.logException(requireContext(), "ServiceDetailFragment.deleteService", new Exception(t));
|
||||
Log.e("ServiceDetailFragment", "Error deleting service", t);
|
||||
Toast.makeText(getContext(), "Error: " + t.getMessage(), Toast.LENGTH_SHORT).show();
|
||||
Toast.makeText(getContext(), "Network error: " + t.getMessage(), Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
});
|
||||
})
|
||||
@@ -162,12 +172,16 @@ public class ServiceDetailFragment extends Fragment {
|
||||
.show();
|
||||
}
|
||||
|
||||
//Helper method to navigate back to the list
|
||||
/**
|
||||
* Navigates back to the previous screen.
|
||||
*/
|
||||
private void navigateBack() {
|
||||
NavHostFragment.findNavController(this).popBackStack();
|
||||
}
|
||||
|
||||
//helper function to check if service is being edited or added and show the view accordingly
|
||||
/**
|
||||
* Handles arguments passed to the fragment to determine if it's in edit or add mode.
|
||||
*/
|
||||
private void handleArguments() {
|
||||
// Service is being edited if the bundle contains a serviceId
|
||||
if (getArguments() != null && getArguments().containsKey("serviceId")) {
|
||||
@@ -192,7 +206,9 @@ public class ServiceDetailFragment extends Fragment {
|
||||
}
|
||||
}
|
||||
|
||||
//helper function to get controls from layout
|
||||
/**
|
||||
* Set UI components from the layout.
|
||||
*/
|
||||
private void initViews(View view) {
|
||||
tvMode = view.findViewById(R.id.tvMode);
|
||||
tvServiceId = view.findViewById(R.id.tvServiceId);
|
||||
|
||||
@@ -19,7 +19,9 @@ import com.example.petstoremobile.R;
|
||||
import com.example.petstoremobile.api.SupplierApi;
|
||||
import com.example.petstoremobile.dtos.SupplierDTO;
|
||||
import com.example.petstoremobile.utils.ActivityLogger;
|
||||
import com.example.petstoremobile.utils.ErrorUtils;
|
||||
import com.example.petstoremobile.utils.InputValidator;
|
||||
import com.example.petstoremobile.utils.UIUtils;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
@@ -28,6 +30,9 @@ import retrofit2.Call;
|
||||
import retrofit2.Callback;
|
||||
import retrofit2.Response;
|
||||
|
||||
/**
|
||||
* Fragment for displaying and editing supplier details.
|
||||
*/
|
||||
@AndroidEntryPoint
|
||||
public class SupplierDetailFragment extends Fragment {
|
||||
|
||||
@@ -56,7 +61,9 @@ public class SupplierDetailFragment extends Fragment {
|
||||
return view;
|
||||
}
|
||||
|
||||
//Method to Update or Add a supplier
|
||||
/**
|
||||
* Handles the saving of supplier data (adding or updating).
|
||||
*/
|
||||
private void saveSupplier() {
|
||||
// Validates all fields using InputValidator
|
||||
if (!InputValidator.isNotEmpty(etSupCompany, "Company Name")) return;
|
||||
@@ -92,7 +99,7 @@ public class SupplierDetailFragment extends Fragment {
|
||||
Toast.makeText(getContext(), "Supplier updated successfully!", Toast.LENGTH_SHORT).show();
|
||||
navigateBack();
|
||||
} else {
|
||||
Toast.makeText(getContext(), "Failed to update supplier: " + response.code(), Toast.LENGTH_SHORT).show();
|
||||
ErrorUtils.showErrorMessage(getContext(), response, "Failed to update supplier");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,7 +107,7 @@ public class SupplierDetailFragment extends Fragment {
|
||||
public void onFailure(Call<SupplierDTO> call, Throwable t) {
|
||||
ActivityLogger.logException(requireContext(), "SupplierDetailFragment.updateSupplier", new Exception(t));
|
||||
Log.e("SupplierDetailFragment", "Error updating supplier", t);
|
||||
Toast.makeText(getContext(), "Error: " + t.getMessage(), Toast.LENGTH_SHORT).show();
|
||||
Toast.makeText(getContext(), "Network error: " + t.getMessage(), Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
@@ -113,7 +120,7 @@ public class SupplierDetailFragment extends Fragment {
|
||||
Toast.makeText(getContext(), "Supplier added successfully!", Toast.LENGTH_SHORT).show();
|
||||
navigateBack();
|
||||
} else {
|
||||
Toast.makeText(getContext(), "Failed to add supplier: " + response.code(), Toast.LENGTH_SHORT).show();
|
||||
ErrorUtils.showErrorMessage(getContext(), response, "Failed to add supplier");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -121,13 +128,15 @@ public class SupplierDetailFragment extends Fragment {
|
||||
public void onFailure(Call<SupplierDTO> call, Throwable t) {
|
||||
ActivityLogger.logException(requireContext(), "SupplierDetailFragment.createSupplier", new Exception(t));
|
||||
Log.e("SupplierDetailFragment", "Error adding supplier", t);
|
||||
Toast.makeText(getContext(), "Error: " + t.getMessage(), Toast.LENGTH_SHORT).show();
|
||||
Toast.makeText(getContext(), "Network error: " + t.getMessage(), Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
//Method to Delete a supplier
|
||||
/**
|
||||
* Displays a confirmation dialog and handles the deletion of a supplier.
|
||||
*/
|
||||
private void deleteSupplier() {
|
||||
//Alert the user to confirm the delete
|
||||
new AlertDialog.Builder(requireContext())
|
||||
@@ -142,7 +151,7 @@ public class SupplierDetailFragment extends Fragment {
|
||||
Toast.makeText(getContext(), "Supplier deleted successfully!", Toast.LENGTH_SHORT).show();
|
||||
navigateBack();
|
||||
} else {
|
||||
Toast.makeText(getContext(), "Failed to delete supplier: " + response.code(), Toast.LENGTH_SHORT).show();
|
||||
ErrorUtils.showErrorMessage(getContext(), response, "Failed to delete supplier");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -150,7 +159,7 @@ public class SupplierDetailFragment extends Fragment {
|
||||
public void onFailure(Call<Void> call, Throwable t) {
|
||||
ActivityLogger.logException(requireContext(), "SupplierDetailFragment.deleteSupplier", new Exception(t));
|
||||
Log.e("SupplierDetailFragment", "Error deleting supplier", t);
|
||||
Toast.makeText(getContext(), "Error: " + t.getMessage(), Toast.LENGTH_SHORT).show();
|
||||
Toast.makeText(getContext(), "Network error: " + t.getMessage(), Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
});
|
||||
})
|
||||
@@ -158,12 +167,16 @@ public class SupplierDetailFragment extends Fragment {
|
||||
.show();
|
||||
}
|
||||
|
||||
//Helper method to navigate back to the list
|
||||
/**
|
||||
* Navigates back to the previous screen.
|
||||
*/
|
||||
private void navigateBack() {
|
||||
NavHostFragment.findNavController(this).popBackStack();
|
||||
}
|
||||
|
||||
//helper function to check if supplier is being edited or added and show the view accordingly
|
||||
/**
|
||||
* Handles arguments passed to the fragment to determine if it's in edit or add mode.
|
||||
*/
|
||||
private void handleArguments() {
|
||||
// Supplier is being edited if the bundle contains a supId
|
||||
if (getArguments() != null && getArguments().containsKey("supId")) {
|
||||
@@ -189,7 +202,9 @@ public class SupplierDetailFragment extends Fragment {
|
||||
}
|
||||
}
|
||||
|
||||
//helper function to get controls from layout
|
||||
/**
|
||||
* Initializes the UI components and sets up formatting for phone input.
|
||||
*/
|
||||
private void initViews(View view) {
|
||||
tvMode = view.findViewById(R.id.tvMode);
|
||||
tvSupId = view.findViewById(R.id.tvSupId);
|
||||
@@ -200,8 +215,7 @@ public class SupplierDetailFragment extends Fragment {
|
||||
etSupPhone = view.findViewById(R.id.etSupPhone);
|
||||
|
||||
// Add phone number formatting (CA) and limit length to 14 characters
|
||||
etSupPhone.addTextChangedListener(new android.telephony.PhoneNumberFormattingTextWatcher("CA"));
|
||||
etSupPhone.setFilters(new android.text.InputFilter[]{new android.text.InputFilter.LengthFilter(14)});
|
||||
UIUtils.formatPhoneInput(etSupPhone);
|
||||
|
||||
btnSaveSupplier = view.findViewById(R.id.btnSaveSupplier);
|
||||
btnDeleteSupplier = view.findViewById(R.id.btnDeleteSupplier);
|
||||
|
||||
@@ -1,22 +1,11 @@
|
||||
package com.example.petstoremobile.fragments.listfragments.listprofilefragments;
|
||||
|
||||
import android.Manifest;
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.graphics.Color;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
|
||||
import androidx.activity.result.ActivityResultLauncher;
|
||||
import androidx.activity.result.contract.ActivityResultContracts;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.core.content.FileProvider;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.navigation.fragment.NavHostFragment;
|
||||
|
||||
import android.provider.MediaStore;
|
||||
import android.util.Log;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
@@ -26,21 +15,14 @@ import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.bumptech.glide.Glide;
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy;
|
||||
import com.bumptech.glide.load.model.GlideUrl;
|
||||
import com.bumptech.glide.load.model.LazyHeaders;
|
||||
import com.example.petstoremobile.R;
|
||||
import com.example.petstoremobile.api.PetApi;
|
||||
import com.example.petstoremobile.api.auth.TokenManager;
|
||||
import com.example.petstoremobile.fragments.ListFragment;
|
||||
import com.example.petstoremobile.fragments.listfragments.detailfragments.PetDetailFragment;
|
||||
import com.example.petstoremobile.utils.FileUtils;
|
||||
import com.example.petstoremobile.utils.GlideUtils;
|
||||
import com.example.petstoremobile.utils.ImagePickerHelper;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.InputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
import javax.inject.Inject;
|
||||
@@ -60,7 +42,6 @@ public class PetProfileFragment extends Fragment {
|
||||
private TextView tvPetName, tvPetSpecies, tvPetBreed, tvPetAge, tvPetPrice;
|
||||
private Button btnBack, btnEditPet, btnChangePhoto;
|
||||
private ImageView imgPet;
|
||||
private Uri photoUri;
|
||||
private int petId;
|
||||
private boolean hasImage = false;
|
||||
|
||||
@@ -68,59 +49,32 @@ public class PetProfileFragment extends Fragment {
|
||||
@Inject @Named("baseUrl") String baseUrl;
|
||||
@Inject TokenManager tokenManager;
|
||||
|
||||
// launchers for camera and gallery
|
||||
private ActivityResultLauncher<Intent> galleryLauncher;
|
||||
private ActivityResultLauncher<Uri> cameraLauncher;
|
||||
private ActivityResultLauncher<String> permissionLauncher;
|
||||
private ImagePickerHelper imagePickerHelper;
|
||||
|
||||
|
||||
/**
|
||||
* Initializes activity launchers for gallery, camera, and permissions.
|
||||
*/
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
// Launcher to open gallery to select image
|
||||
galleryLauncher = registerForActivityResult(
|
||||
new ActivityResultContracts.StartActivityForResult(),
|
||||
result -> {
|
||||
if (result.getResultCode() == Activity.RESULT_OK && result.getData() != null) {
|
||||
Uri selectedImage = result.getData().getData();
|
||||
uploadPetImage(selectedImage);
|
||||
}
|
||||
}
|
||||
);
|
||||
imagePickerHelper = new ImagePickerHelper(this, "pet_photo.jpg", new ImagePickerHelper.ImagePickerListener() {
|
||||
@Override
|
||||
public void onImagePicked(Uri uri) {
|
||||
uploadPetImage(uri);
|
||||
}
|
||||
|
||||
// Launcher for camera to open and capture image
|
||||
cameraLauncher = registerForActivityResult(
|
||||
new ActivityResultContracts.TakePicture(),
|
||||
success -> {
|
||||
if (success) {
|
||||
uploadPetImage(photoUri);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// Launcher to request camera permission
|
||||
permissionLauncher = registerForActivityResult(
|
||||
new ActivityResultContracts.RequestPermission(),
|
||||
granted -> {
|
||||
if (granted) {
|
||||
launchCamera();
|
||||
} else {
|
||||
new AlertDialog.Builder(requireContext())
|
||||
.setTitle("Permission Required")
|
||||
.setMessage("Please grant camera permission to use this feature")
|
||||
.setPositiveButton("Open Settings", (dialog, which) -> {
|
||||
Intent intent = new Intent(android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
|
||||
intent.setData(Uri.fromParts("package", requireContext().getPackageName(), null));
|
||||
startActivity(intent);
|
||||
})
|
||||
.setNegativeButton("Cancel", null)
|
||||
.show();
|
||||
}
|
||||
}
|
||||
);
|
||||
@Override
|
||||
public void onImageRemoved() {
|
||||
deletePetImage();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Inflates the layout, initializes views, and sets up click listeners.
|
||||
*/
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container,
|
||||
Bundle savedInstanceState) {
|
||||
@@ -164,78 +118,38 @@ public class PetProfileFragment extends Fragment {
|
||||
|
||||
//Make change photo button ask user to select a new photo
|
||||
btnChangePhoto.setOnClickListener(v -> {
|
||||
List<String> options = new ArrayList<>();
|
||||
options.add("Take Photo");
|
||||
options.add("Choose from Gallery");
|
||||
if (hasImage) {
|
||||
options.add("Remove Photo");
|
||||
}
|
||||
|
||||
new AlertDialog.Builder(requireContext())
|
||||
.setTitle("Change Pet Photo")
|
||||
.setItems(options.toArray(new String[0]), (dialog, which) -> {
|
||||
String selected = options.get(which);
|
||||
if (selected.equals("Take Photo")) {
|
||||
// Choose Camera
|
||||
//Checks if the user has granted the camera permission already
|
||||
if (ContextCompat.checkSelfPermission(requireContext(), Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED) {
|
||||
//if the permission is already granted then launch the camera
|
||||
launchCamera();
|
||||
} else {
|
||||
//otherwise request the permission
|
||||
permissionLauncher.launch(Manifest.permission.CAMERA);
|
||||
}
|
||||
} else if (selected.equals("Choose from Gallery")) {
|
||||
Intent intent = new Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI);
|
||||
galleryLauncher.launch(intent);
|
||||
} else if (selected.equals("Remove Photo")) {
|
||||
deletePetImage();
|
||||
}
|
||||
})
|
||||
.show();
|
||||
imagePickerHelper.showImagePickerDialog("Change Pet Photo", hasImage);
|
||||
});
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
// Helper function to load pet image from backend
|
||||
/**
|
||||
* Fetches and displays the pet's image from the server.
|
||||
*/
|
||||
private void loadPetImage(int petId) {
|
||||
String imageUrl = baseUrl + String.format(Locale.US, PetApi.PET_IMAGE_PATH, petId);
|
||||
String token = tokenManager.getToken();
|
||||
|
||||
Object loadTarget = imageUrl;
|
||||
if (token != null) {
|
||||
loadTarget = new GlideUrl(imageUrl, new LazyHeaders.Builder()
|
||||
.addHeader("Authorization", "Bearer " + token)
|
||||
.build());
|
||||
}
|
||||
GlideUtils.loadImageWithToken(requireContext(), imgPet, imageUrl, token, R.drawable.placeholder, new GlideUtils.ImageLoadListener() {
|
||||
@Override
|
||||
public void onResourceReady() {
|
||||
hasImage = true;
|
||||
}
|
||||
|
||||
Glide.with(this)
|
||||
.load(loadTarget)
|
||||
.skipMemoryCache(true)
|
||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||
.placeholder(R.drawable.placeholder)
|
||||
.error(R.drawable.placeholder)
|
||||
.listener(new com.bumptech.glide.request.RequestListener<android.graphics.drawable.Drawable>() {
|
||||
@Override
|
||||
public boolean onLoadFailed(@androidx.annotation.Nullable com.bumptech.glide.load.engine.GlideException e, Object model, com.bumptech.glide.request.target.Target<android.graphics.drawable.Drawable> target, boolean isFirstResource) {
|
||||
hasImage = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onResourceReady(android.graphics.drawable.Drawable resource, Object model, com.bumptech.glide.request.target.Target<android.graphics.drawable.Drawable> target, com.bumptech.glide.load.DataSource dataSource, boolean isFirstResource) {
|
||||
hasImage = true;
|
||||
return false;
|
||||
}
|
||||
})
|
||||
.into(imgPet);
|
||||
@Override
|
||||
public void onLoadFailed() {
|
||||
hasImage = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Helper function to upload pet image to backend
|
||||
/**
|
||||
* Uploads a selected or captured image a pet photo through the API.
|
||||
*/
|
||||
private void uploadPetImage(Uri uri) {
|
||||
try {
|
||||
File file = getFileFromUri(uri);
|
||||
File file = FileUtils.getFileFromUri(requireContext(), uri);
|
||||
if (file == null) return;
|
||||
|
||||
// Create RequestBody for file upload
|
||||
@@ -266,6 +180,9 @@ public class PetProfileFragment extends Fragment {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a request to the API to remove the current pet photo.
|
||||
*/
|
||||
private void deletePetImage() {
|
||||
petApi.deletePetImage((long) petId).enqueue(new Callback<Void>() {
|
||||
@Override
|
||||
@@ -286,30 +203,4 @@ public class PetProfileFragment extends Fragment {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Helper function to create a temporary File object from a Uri for uploading
|
||||
private File getFileFromUri(Uri uri) {
|
||||
try {
|
||||
InputStream inputStream = requireContext().getContentResolver().openInputStream(uri);
|
||||
File tempFile = new File(requireContext().getCacheDir(), "upload_pet_image.jpg");
|
||||
FileOutputStream outputStream = new FileOutputStream(tempFile);
|
||||
byte[] buffer = new byte[1024];
|
||||
int length;
|
||||
while ((length = inputStream.read(buffer)) > 0) {
|
||||
outputStream.write(buffer, 0, length);
|
||||
}
|
||||
outputStream.close();
|
||||
inputStream.close();
|
||||
return tempFile;
|
||||
} catch (Exception e) {
|
||||
Log.e("FILE_UTILS", "Error creating temp file", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private void launchCamera() {
|
||||
File photoFile = new File(requireContext().getCacheDir(), "pet_photo.jpg");
|
||||
photoUri = FileProvider.getUriForFile(requireContext(), requireContext().getPackageName() + ".fileprovider", photoFile);
|
||||
cameraLauncher.launch(photoUri);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
package com.example.petstoremobile.utils;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.widget.ImageView;
|
||||
import androidx.annotation.Nullable;
|
||||
import com.bumptech.glide.Glide;
|
||||
import com.bumptech.glide.load.DataSource;
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy;
|
||||
import com.bumptech.glide.load.engine.GlideException;
|
||||
import com.bumptech.glide.load.model.GlideUrl;
|
||||
import com.bumptech.glide.load.model.LazyHeaders;
|
||||
import com.bumptech.glide.request.RequestListener;
|
||||
import com.bumptech.glide.request.target.Target;
|
||||
import com.example.petstoremobile.R;
|
||||
|
||||
/**
|
||||
* Utility class for loading images using Glide with authentication tokens.
|
||||
*/
|
||||
public class GlideUtils {
|
||||
|
||||
/**
|
||||
* interface to check the status of the image load.
|
||||
*/
|
||||
public interface ImageLoadListener {
|
||||
void onResourceReady();
|
||||
void onLoadFailed();
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads an image from a URL into an ImageView with token.
|
||||
*/
|
||||
public static void loadImageWithToken(Context context, ImageView imageView, String url, String token, int placeholder) {
|
||||
loadImageWithToken(context, imageView, url, token, placeholder, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads an image from a URL into an ImageView with token and listener.
|
||||
*/
|
||||
public static void loadImageWithToken(Context context, ImageView imageView, String url, String token, int placeholder, ImageLoadListener listener) {
|
||||
if (url == null) {
|
||||
imageView.setImageResource(placeholder);
|
||||
if (listener != null) listener.onLoadFailed();
|
||||
return;
|
||||
}
|
||||
|
||||
Object loadTarget = url;
|
||||
if (token != null && url.startsWith("http")) {
|
||||
loadTarget = new GlideUrl(url, new LazyHeaders.Builder()
|
||||
.addHeader("Authorization", "Bearer " + token)
|
||||
.build());
|
||||
}
|
||||
|
||||
Glide.with(context)
|
||||
.load(loadTarget)
|
||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||
.skipMemoryCache(true)
|
||||
.placeholder(placeholder)
|
||||
.error(placeholder)
|
||||
.listener(new RequestListener<Drawable>() {
|
||||
@Override
|
||||
public boolean onLoadFailed(@Nullable GlideException e, Object model, Target<Drawable> target, boolean isFirstResource) {
|
||||
if (listener != null) listener.onLoadFailed();
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onResourceReady(Drawable resource, Object model, Target<Drawable> target, DataSource dataSource, boolean isFirstResource) {
|
||||
if (listener != null) listener.onResourceReady();
|
||||
return false;
|
||||
}
|
||||
})
|
||||
.into(imageView);
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads an image from a URL into an ImageView with token and applies circle cropping for image.
|
||||
*/
|
||||
public static void loadImageWithTokenCircle(Context context, ImageView imageView, String url, String token, int placeholder) {
|
||||
loadImageWithTokenCircle(context, imageView, url, token, placeholder, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads an image from a URL into an ImageView with token, circle cropping, and listener.
|
||||
*/
|
||||
public static void loadImageWithTokenCircle(Context context, ImageView imageView, String url, String token, int placeholder, ImageLoadListener listener) {
|
||||
if (url == null) {
|
||||
imageView.setImageResource(placeholder);
|
||||
if (listener != null) listener.onLoadFailed();
|
||||
return;
|
||||
}
|
||||
|
||||
Object loadTarget = url;
|
||||
if (token != null && url.startsWith("http")) {
|
||||
loadTarget = new GlideUrl(url, new LazyHeaders.Builder()
|
||||
.addHeader("Authorization", "Bearer " + token)
|
||||
.build());
|
||||
}
|
||||
|
||||
Glide.with(context)
|
||||
.load(loadTarget)
|
||||
.circleCrop()
|
||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||
.skipMemoryCache(true)
|
||||
.placeholder(placeholder)
|
||||
.error(placeholder)
|
||||
.listener(new RequestListener<Drawable>() {
|
||||
@Override
|
||||
public boolean onLoadFailed(@Nullable GlideException e, Object model, Target<Drawable> target, boolean isFirstResource) {
|
||||
if (listener != null) listener.onLoadFailed();
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onResourceReady(Drawable resource, Object model, Target<Drawable> target, DataSource dataSource, boolean isFirstResource) {
|
||||
if (listener != null) listener.onResourceReady();
|
||||
return false;
|
||||
}
|
||||
})
|
||||
.into(imageView);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
package com.example.petstoremobile.utils;
|
||||
|
||||
import android.Manifest;
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.net.Uri;
|
||||
import android.provider.MediaStore;
|
||||
import androidx.activity.result.ActivityResultLauncher;
|
||||
import androidx.activity.result.contract.ActivityResultContracts;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.core.content.FileProvider;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Helper class to handle image picking from camera or gallery.
|
||||
*/
|
||||
public class ImagePickerHelper {
|
||||
|
||||
/**
|
||||
* Listener interface to handle the results of image picking.
|
||||
*/
|
||||
public interface ImagePickerListener {
|
||||
/**
|
||||
* Called when an image has been successfully selected or captured.
|
||||
*/
|
||||
void onImagePicked(Uri uri);
|
||||
/**
|
||||
* Called when the user chooses to remove the existing image.
|
||||
*/
|
||||
void onImageRemoved();
|
||||
}
|
||||
|
||||
private final Fragment fragment;
|
||||
private final ImagePickerListener listener;
|
||||
private final ActivityResultLauncher<Intent> galleryLauncher;
|
||||
private final ActivityResultLauncher<Uri> cameraLauncher;
|
||||
private final ActivityResultLauncher<String> permissionLauncher;
|
||||
private Uri photoUri;
|
||||
private final String tempFileName;
|
||||
|
||||
/**
|
||||
* Constructor for ImagePickerHelper.
|
||||
* Registers activity launchers for gallery, camera, and permissions.
|
||||
*/
|
||||
public ImagePickerHelper(Fragment fragment, String tempFileName, ImagePickerListener listener) {
|
||||
this.fragment = fragment;
|
||||
this.tempFileName = tempFileName;
|
||||
this.listener = listener;
|
||||
|
||||
// Launcher to open gallery to select image
|
||||
galleryLauncher = fragment.registerForActivityResult(
|
||||
new ActivityResultContracts.StartActivityForResult(),
|
||||
result -> {
|
||||
if (result.getResultCode() == Activity.RESULT_OK && result.getData() != null) {
|
||||
Uri selectedImage = result.getData().getData();
|
||||
if (selectedImage != null) {
|
||||
listener.onImagePicked(selectedImage);
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// Launcher for camera to open and capture image
|
||||
cameraLauncher = fragment.registerForActivityResult(
|
||||
new ActivityResultContracts.TakePicture(),
|
||||
success -> {
|
||||
if (success && photoUri != null) {
|
||||
listener.onImagePicked(photoUri);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// Launcher to request camera permission
|
||||
permissionLauncher = fragment.registerForActivityResult(
|
||||
new ActivityResultContracts.RequestPermission(),
|
||||
granted -> {
|
||||
if (granted) {
|
||||
launchCamera();
|
||||
} else {
|
||||
showPermissionDeniedDialog();
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows a dialog to choose between camera, gallery, and optionally remove photo.
|
||||
*/
|
||||
public void showImagePickerDialog(String title, boolean hasImage) {
|
||||
List<String> options = new ArrayList<>();
|
||||
options.add("Take Photo");
|
||||
options.add("Choose from Gallery");
|
||||
if (hasImage) {
|
||||
options.add("Remove Photo");
|
||||
}
|
||||
|
||||
new AlertDialog.Builder(fragment.requireContext())
|
||||
.setTitle(title)
|
||||
.setItems(options.toArray(new String[0]), (dialog, which) -> {
|
||||
String selected = options.get(which);
|
||||
if (selected.equals("Take Photo")) {
|
||||
checkCameraPermission();
|
||||
} else if (selected.equals("Choose from Gallery")) {
|
||||
launchGallery();
|
||||
} else if (selected.equals("Remove Photo")) {
|
||||
listener.onImageRemoved();
|
||||
}
|
||||
})
|
||||
.show();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if camera permission is granted and launches camera or requests permission.
|
||||
*/
|
||||
private void checkCameraPermission() {
|
||||
if (ContextCompat.checkSelfPermission(fragment.requireContext(), Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED) {
|
||||
launchCamera();
|
||||
} else {
|
||||
permissionLauncher.launch(Manifest.permission.CAMERA);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares a temporary file and launches the camera app.
|
||||
*/
|
||||
private void launchCamera() {
|
||||
File photoFile = new File(fragment.requireContext().getCacheDir(), tempFileName);
|
||||
photoUri = FileProvider.getUriForFile(fragment.requireContext(), fragment.requireContext().getPackageName() + ".fileprovider", photoFile);
|
||||
cameraLauncher.launch(photoUri);
|
||||
}
|
||||
|
||||
/**
|
||||
* Launches the gallery app to select an existing image.
|
||||
*/
|
||||
private void launchGallery() {
|
||||
Intent intent = new Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI);
|
||||
galleryLauncher.launch(intent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows a dialog explaining why camera permission is needed when denied.
|
||||
*/
|
||||
private void showPermissionDeniedDialog() {
|
||||
new AlertDialog.Builder(fragment.requireContext())
|
||||
.setTitle("Permission Required")
|
||||
.setMessage("Please grant camera permission to use this feature")
|
||||
.setPositiveButton("Open Settings", (dialog, which) -> {
|
||||
Intent intent = new Intent(android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
|
||||
intent.setData(Uri.fromParts("package", fragment.requireContext().getPackageName(), null));
|
||||
fragment.startActivity(intent);
|
||||
})
|
||||
.setNegativeButton("Cancel", null)
|
||||
.show();
|
||||
}
|
||||
}
|
||||
@@ -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)});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user