changed detailed fragment to fill data from the backend

This commit is contained in:
Alex
2026-04-06 03:12:42 -06:00
parent d62113c0f5
commit 23d765c6b5
46 changed files with 1181 additions and 760 deletions

View File

@@ -58,41 +58,45 @@ android {
}
dependencies {
// Core AndroidX & UI
implementation(libs.appcompat)
implementation(libs.material)
implementation(libs.activity)
implementation(libs.constraintlayout)
implementation(libs.swiperefreshlayout)
implementation(libs.viewpager2)
// Hilt Dependency Injection
implementation(libs.hilt.android)
annotationProcessor(libs.hilt.compiler)
// Navigation Component
implementation(libs.navigation.fragment)
implementation(libs.navigation.ui)
implementation("com.squareup.retrofit2:retrofit:2.9.0")
implementation("com.squareup.retrofit2:converter-gson:2.9.0")
implementation("com.squareup.okhttp3:logging-interceptor:4.12.0")
implementation("com.squareup.okhttp3:okhttp:4.12.0")
// Networking
implementation(libs.retrofit)
implementation(libs.retrofit.gson)
implementation(libs.okhttp)
implementation(libs.okhttp.logging)
implementation("com.google.android.material:material:1.11.0")
implementation("androidx.viewpager2:viewpager2:1.1.0")
// CameraX
implementation(libs.camera.core)
implementation(libs.camera.camera2)
implementation(libs.camera.lifecycle)
implementation(libs.camera.view)
implementation("androidx.camera:camera-core:1.4.0")
implementation("androidx.camera:camera-camera2:1.4.0")
implementation("androidx.camera:camera-lifecycle:1.4.0")
implementation("androidx.camera:camera-view:1.4.0")
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0")
implementation(libs.swiperefreshlayout)
// Image Loading
implementation(libs.glide)
annotationProcessor(libs.glide.compiler)
// Other Third-party Libraries
implementation("com.github.NaikSoftware:StompProtocolAndroid:1.6.6")
implementation("io.reactivex.rxjava2:rxjava:2.2.21")
implementation("io.reactivex.rxjava2:rxandroid:2.1.1")
implementation("com.github.bumptech.glide:glide:4.16.0")
annotationProcessor("com.github.bumptech.glide:compiler:4.16.0")
implementation("com.github.prolificinteractive:material-calendarview:2.0.1")
// Testing
testImplementation(libs.junit)
androidTestImplementation(libs.ext.junit)
androidTestImplementation(libs.espresso.core)

View File

@@ -3,9 +3,14 @@ package com.example.petstoremobile.adapters;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import com.bumptech.glide.Glide;
import com.bumptech.glide.load.engine.DiskCacheStrategy;
import com.bumptech.glide.load.model.GlideUrl;
import com.bumptech.glide.load.model.LazyHeaders;
import com.example.petstoremobile.R;
import com.example.petstoremobile.models.Message;
import java.util.List;
@@ -17,6 +22,7 @@ public class MessageAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder
private final List<Message> messages;
private Long currentUserId;
private String token;
public MessageAdapter(List<Message> messages, Long currentUserId) {
this.messages = messages;
@@ -28,6 +34,10 @@ public class MessageAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder
notifyDataSetChanged();
}
public void setToken(String token) {
this.token = token;
}
@Override
public int getItemViewType(int position) {
Message m = messages.get(position);
@@ -52,27 +62,70 @@ public class MessageAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder
@Override
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
Message m = messages.get(position);
if (holder instanceof SentHolder) ((SentHolder) holder).bind(m);
if (holder instanceof ReceivedHolder) ((ReceivedHolder) holder).bind(m);
if (holder instanceof SentHolder) ((SentHolder) holder).bind(m, token);
if (holder instanceof ReceivedHolder) ((ReceivedHolder) holder).bind(m, token);
}
@Override public int getItemCount() { return messages.size(); }
static class SentHolder extends RecyclerView.ViewHolder {
TextView tvMessage;
TextView tvMessage, tvAttachmentName;
ImageView ivAttachment;
SentHolder(View v) {
super(v);
tvMessage = v.findViewById(R.id.tvMessageContent); // updated
tvMessage = v.findViewById(R.id.tvMessageContent);
tvAttachmentName = v.findViewById(R.id.tvAttachmentName);
ivAttachment = v.findViewById(R.id.ivAttachment);
}
void bind(Message m, String token) {
tvMessage.setText(m.getContent());
displayAttachment(m, ivAttachment, tvAttachmentName, token);
}
void bind(Message m) { tvMessage.setText(m.getContent()); }
}
static class ReceivedHolder extends RecyclerView.ViewHolder {
TextView tvMessage;
TextView tvMessage, tvAttachmentName;
ImageView ivAttachment;
ReceivedHolder(View v) {
super(v);
tvMessage = v.findViewById(R.id.tvMessageContent); // updated
tvMessage = v.findViewById(R.id.tvMessageContent);
tvAttachmentName = v.findViewById(R.id.tvAttachmentName);
ivAttachment = v.findViewById(R.id.ivAttachment);
}
void bind(Message m, String token) {
tvMessage.setText(m.getContent());
displayAttachment(m, ivAttachment, tvAttachmentName, token);
}
}
// helper function to display the attachment to the chat bubble
private static void displayAttachment(Message m, ImageView iv, TextView tvName, String token) {
if (m.getAttachmentUrl() != null) {
if (m.getAttachmentType() != null && m.getAttachmentType().startsWith("image/")) {
iv.setVisibility(View.VISIBLE);
tvName.setVisibility(View.GONE);
Object loadTarget = m.getAttachmentUrl();
if (token != null && m.getAttachmentUrl().startsWith("http")) {
loadTarget = new GlideUrl(m.getAttachmentUrl(), new LazyHeaders.Builder()
.addHeader("Authorization", "Bearer " + token)
.build());
}
Glide.with(iv.getContext())
.load(loadTarget)
.diskCacheStrategy(DiskCacheStrategy.ALL)
.placeholder(R.drawable.placeholder)
.error(R.drawable.placeholder)
.into(iv);
} else {
iv.setVisibility(View.GONE);
tvName.setVisibility(View.VISIBLE);
tvName.setText(m.getAttachmentName() != null ? m.getAttachmentName() : "Attachment");
}
} else {
iv.setVisibility(View.GONE);
tvName.setVisibility(View.GONE);
}
void bind(Message m) { tvMessage.setText(m.getContent()); }
}
}

View File

@@ -3,10 +3,14 @@ package com.example.petstoremobile.api;
import com.example.petstoremobile.dtos.MessageDTO;
import com.example.petstoremobile.dtos.SendMessageRequest;
import java.util.List;
import okhttp3.MultipartBody;
import okhttp3.RequestBody;
import retrofit2.Call;
import retrofit2.http.Body;
import retrofit2.http.GET;
import retrofit2.http.Multipart;
import retrofit2.http.POST;
import retrofit2.http.Part;
import retrofit2.http.Path;
//api calls to get and send messages
@@ -17,4 +21,12 @@ public interface MessageApi {
@POST("api/v1/chat/conversations/{id}/messages")
Call<MessageDTO> sendMessage(@Path("id") Long conversationId, @Body SendMessageRequest request);
@Multipart
@POST("api/v1/chat/conversations/{id}/messages/attachment")
Call<MessageDTO> sendMessageWithAttachment(
@Path("id") Long conversationId,
@Part("content") RequestBody content,
@Part MultipartBody.Part file
);
}

View File

@@ -12,6 +12,11 @@ public interface ProductSupplierApi {
@Query("page") int page,
@Query("size") int size);
@GET("api/v1/product-suppliers/{productId}/{supplierId}")
Call<ProductSupplierDTO> getProductSupplierById(
@Path("productId") Long productId,
@Path("supplierId") Long supplierId);
@POST("api/v1/product-suppliers")
Call<ProductSupplierDTO> createProductSupplier(@Body ProductSupplierDTO dto);

View File

@@ -14,33 +14,35 @@ import androidx.activity.result.contract.ActivityResultContracts;
import androidx.annotation.NonNull;
import androidx.core.view.GravityCompat;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.ViewModelProvider;
import androidx.recyclerview.widget.LinearLayoutManager;
import com.bumptech.glide.Glide;
import com.example.petstoremobile.R;
import com.example.petstoremobile.adapters.ChatAdapter;
import com.example.petstoremobile.adapters.MessageAdapter;
import com.example.petstoremobile.api.auth.TokenManager;
import com.example.petstoremobile.api.ChatApi;
import com.example.petstoremobile.api.CustomerApi;
import com.example.petstoremobile.api.MessageApi;
import com.example.petstoremobile.databinding.FragmentChatBinding;
import com.example.petstoremobile.dtos.ConversationDTO;
import com.example.petstoremobile.dtos.CustomerDTO;
import com.example.petstoremobile.dtos.MessageDTO;
import com.example.petstoremobile.dtos.SendMessageRequest;
import com.example.petstoremobile.models.Chat;
import com.example.petstoremobile.models.Message;
import com.example.petstoremobile.services.ChatNotificationService;
import com.example.petstoremobile.utils.RetrofitUtils;
import com.example.petstoremobile.utils.FileUtils;
import com.example.petstoremobile.utils.Resource;
import com.example.petstoremobile.viewmodels.ChatViewModel;
import com.example.petstoremobile.websocket.StompChatManager;
import java.io.File;
import java.util.*;
import java.util.stream.Collectors;
import javax.inject.Inject;
import javax.inject.Named;
import dagger.hilt.android.AndroidEntryPoint;
import okhttp3.MediaType;
import okhttp3.MultipartBody;
import okhttp3.RequestBody;
@AndroidEntryPoint
public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickListener, StompChatManager.MessageListener,
@@ -49,6 +51,7 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis
private static final String TAG = "ChatFragment";
private FragmentChatBinding binding;
private ChatViewModel viewModel;
// Adapters
private ChatAdapter chatAdapter;
@@ -60,10 +63,6 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis
private final Map<Long, String> customerNames = new HashMap<>();
private Uri pendingAttachmentUri;
// APIs
@Inject ChatApi chatApi;
@Inject CustomerApi customerApi;
@Inject MessageApi messageApi;
@Inject TokenManager tokenManager;
@Inject @Named("baseUrl") String baseUrl;
@@ -79,6 +78,7 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
viewModel = new ViewModelProvider(this).get(ChatViewModel.class);
attachmentLauncher = registerForActivityResult(
new ActivityResultContracts.StartActivityForResult(),
result -> {
@@ -183,54 +183,53 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis
}
/**
* Fetches a list of customers from the API to display customer names for the chat list.
* Fetches a list of customers from the ViewModel to display customer names for the chat list.
*/
private void loadCustomers() {
customerApi.getAllCustomers(0, 100).enqueue(RetrofitUtils.createSilentCallback(TAG, result -> {
for (CustomerDTO c : result.getContent()) {
customerNames.put(c.getCustomerId(), c.getFullName());
viewModel.getAllCustomers(0, 100).observe(getViewLifecycleOwner(), resource -> {
if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) {
resource.data.getContent().forEach(c -> customerNames.put(c.getCustomerId(), c.getFullName()));
loadConversations();
}
loadConversations();
}));
});
}
/**
* Retrieves all conversations for the current user and populates the chat drawer.
* Retrieves all conversations for the current user through the ViewModel and populates the chat drawer.
*/
private void loadConversations() {
chatApi.getAllConversations().enqueue(RetrofitUtils.createSilentCallback(TAG, result -> {
chatList.clear();
List<Chat> loaded = result.stream()
.map(dto -> {
String name = customerNames.getOrDefault(
dto.getCustomerId(), "Customer #" + dto.getCustomerId());
return new Chat(String.valueOf(dto.getId()),
name, dto.getLastMessage(),
dto.getCustomerId(), dto.getStaffId());
})
.collect(Collectors.toList());
chatList.addAll(loaded);
chatAdapter.notifyDataSetChanged();
if (activeConversationId != null) {
setConversationActive(true);
// Update title to customer name of active conversation
for (Chat chat : chatList) {
if (chat.getChatId().equals(String.valueOf(activeConversationId))) {
binding.tvChatTitle.setText(chat.getCustomerName());
break;
viewModel.getAllConversations().observe(getViewLifecycleOwner(), resource -> {
if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) {
chatList.clear();
for (ConversationDTO dto : resource.data) {
String name = customerNames.getOrDefault(
dto.getCustomerId(), "Customer #" + dto.getCustomerId());
chatList.add(new Chat(String.valueOf(dto.getId()),
name, dto.getLastMessage(),
dto.getCustomerId(), dto.getStaffId()));
}
chatAdapter.notifyDataSetChanged();
if (activeConversationId != null) {
setConversationActive(true);
// Update title to customer name of active conversation
for (Chat chat : chatList) {
if (chat.getChatId().equals(String.valueOf(activeConversationId))) {
binding.tvChatTitle.setText(chat.getCustomerName());
break;
}
}
if (stompChatManager != null) {
stompChatManager.subscribeToConversation(activeConversationId);
}
loadMessageHistory(activeConversationId);
} else {
messageList.clear();
messageAdapter.notifyDataSetChanged();
setConversationActive(false);
}
if (stompChatManager != null) {
stompChatManager.subscribeToConversation(activeConversationId);
}
loadMessageHistory(activeConversationId);
} else {
messageList.clear();
messageAdapter.notifyDataSetChanged();
setConversationActive(false);
}
}));
});
}
/**
@@ -251,21 +250,23 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis
}
/**
* Fetches the full message history for a specific conversation from the API.
* Fetches the full message history for a specific conversation from the ViewModel.
*/
private void loadMessageHistory(Long conversationId) {
messageApi.getMessages(conversationId).enqueue(RetrofitUtils.createSilentCallback(TAG, result -> {
messageList.clear();
for (MessageDTO dto : result) {
messageList.add(dtoToModel(dto));
viewModel.getMessages(conversationId).observe(getViewLifecycleOwner(), resource -> {
if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) {
messageList.clear();
for (MessageDTO dto : resource.data) {
messageList.add(dtoToModel(dto));
}
messageAdapter.notifyDataSetChanged();
scrollToBottom();
}
messageAdapter.notifyDataSetChanged();
scrollToBottom();
}));
});
}
/**
* Sends a plain text message to the currently active conversation.
* Sends a plain text message to the currently active conversation through the ViewModel.
*/
private void sendMessage() {
//check if a chat is selected
@@ -278,14 +279,15 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis
//clear text field after sending
binding.etMessage.setText("");
//calls api to send the message
messageApi.sendMessage(activeConversationId, new SendMessageRequest(text))
.enqueue(RetrofitUtils.createSilentCallback(TAG, result -> {
messageList.add(dtoToModel(result));
messageAdapter.notifyItemInserted(messageList.size() - 1);
scrollToBottom();
loadConversations();
}));
//calls viewmodel to send the message
viewModel.sendMessage(activeConversationId, new SendMessageRequest(text)).observe(getViewLifecycleOwner(), resource -> {
if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) {
messageList.add(dtoToModel(resource.data));
messageAdapter.notifyItemInserted(messageList.size() - 1);
scrollToBottom();
loadConversations();
}
});
}
/**
@@ -351,13 +353,34 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis
}
/**
* Handles sending a message that includes a file attachment.
* Handles sending a message that includes a file attachment via the ViewModel.
*/
private void sendWithAttachment(Uri uri) {
if (activeConversationId == null) return;
String text = binding.etMessage.getText().toString().trim();
binding.etMessage.setText("");
removeAttachment();
//TODO: send the message with attachment when backend is done
Log.d(TAG, "Send with attachment happening");
try {
File file = FileUtils.getFileFromUri(requireContext(), uri);
if (file == null) return;
String mimeType = requireContext().getContentResolver().getType(uri);
RequestBody requestFile = RequestBody.create(file, MediaType.parse(mimeType != null ? mimeType : "application/octet-stream"));
MultipartBody.Part filePart = MultipartBody.Part.createFormData("file", file.getName(), requestFile);
RequestBody contentPart = RequestBody.create(text, MediaType.parse("text/plain"));
viewModel.sendMessageWithAttachment(activeConversationId, contentPart, filePart).observe(getViewLifecycleOwner(), resource -> {
if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) {
messageList.add(dtoToModel(resource.data));
messageAdapter.notifyItemInserted(messageList.size() - 1);
scrollToBottom();
loadConversations();
}
});
} catch (Exception e) {
Log.e(TAG, "Error sending message with attachment", e);
}
}
/**
@@ -376,8 +399,10 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis
//else add the message to the active chat if it's not from the current user
messageList.add(dtoToModel(dto));
messageAdapter.notifyItemInserted(messageList.size() - 1);
scrollToBottom();
requireActivity().runOnUiThread(() -> {
messageAdapter.notifyItemInserted(messageList.size() - 1);
scrollToBottom();
});
}
/**
@@ -385,41 +410,43 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis
*/
@Override
public void onConversationUpdated(ConversationDTO dto) {
boolean updated = false;
String name = customerNames.getOrDefault(
dto.getCustomerId(), "Customer #" + dto.getCustomerId());
requireActivity().runOnUiThread(() -> {
boolean updated = false;
String name = customerNames.getOrDefault(
dto.getCustomerId(), "Customer #" + dto.getCustomerId());
for (int i = 0; i < chatList.size(); i++) {
Chat existing = chatList.get(i);
if (existing.getChatId().equals(String.valueOf(dto.getId()))) {
chatList.set(i, new Chat(
for (int i = 0; i < chatList.size(); i++) {
Chat existing = chatList.get(i);
if (existing.getChatId().equals(String.valueOf(dto.getId()))) {
chatList.set(i, new Chat(
String.valueOf(dto.getId()),
name,
dto.getLastMessage(),
dto.getCustomerId(),
dto.getStaffId()
));
chatAdapter.notifyItemChanged(i);
updated = true;
break;
}
}
if (!updated) {
chatList.add(0, new Chat(
String.valueOf(dto.getId()),
name,
dto.getLastMessage(),
dto.getCustomerId(),
dto.getStaffId()
));
chatAdapter.notifyItemChanged(i);
updated = true;
break;
chatAdapter.notifyItemInserted(0);
}
}
if (!updated) {
chatList.add(0, new Chat(
String.valueOf(dto.getId()),
name,
dto.getLastMessage(),
dto.getCustomerId(),
dto.getStaffId()
));
chatAdapter.notifyItemInserted(0);
}
if (activeConversationId != null && activeConversationId.equals(dto.getId())) {
setConversationActive(true);
binding.tvChatTitle.setText(name);
}
if (activeConversationId != null && activeConversationId.equals(dto.getId())) {
setConversationActive(true);
binding.tvChatTitle.setText(name);
}
});
}
/**
@@ -430,10 +457,12 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis
if (!isAdded()) {
return;
}
loadConversations();
if (activeConversationId != null) {
loadMessageHistory(activeConversationId);
}
requireActivity().runOnUiThread(() -> {
loadConversations();
if (activeConversationId != null) {
loadMessageHistory(activeConversationId);
}
});
}
/**
@@ -444,7 +473,7 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis
if (!isAdded()) {
return;
}
loadConversations();
requireActivity().runOnUiThread(this::loadConversations);
}
/**
@@ -455,10 +484,12 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis
if (!isAdded()) {
return;
}
loadConversations();
if (activeConversationId != null) {
loadMessageHistory(activeConversationId);
}
requireActivity().runOnUiThread(() -> {
loadConversations();
if (activeConversationId != null) {
loadMessageHistory(activeConversationId);
}
});
}
/**
@@ -495,21 +526,23 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis
if (conversationId == null) {
return;
}
for (int i = 0; i < chatList.size(); i++) {
Chat existing = chatList.get(i);
if (existing.getChatId().equals(String.valueOf(conversationId))) {
Chat updated = new Chat(
existing.getChatId(),
existing.getCustomerName(),
lastMessage,
existing.getCustomerId(),
existing.getStaffId()
);
chatList.set(i, updated);
chatAdapter.notifyItemChanged(i);
return;
requireActivity().runOnUiThread(() -> {
for (int i = 0; i < chatList.size(); i++) {
Chat existing = chatList.get(i);
if (existing.getChatId().equals(String.valueOf(conversationId))) {
Chat updated = new Chat(
existing.getChatId(),
existing.getCustomerName(),
lastMessage,
existing.getCustomerId(),
existing.getStaffId()
);
chatList.set(i, updated);
chatAdapter.notifyItemChanged(i);
return;
}
}
}
});
}
/**

View File

@@ -4,8 +4,10 @@ import android.net.Uri;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.ViewModelProvider;
import android.util.Log;
import android.view.LayoutInflater;
@@ -25,8 +27,9 @@ import com.example.petstoremobile.utils.FileUtils;
import com.example.petstoremobile.utils.GlideUtils;
import com.example.petstoremobile.utils.ImagePickerHelper;
import com.example.petstoremobile.utils.InputValidator;
import com.example.petstoremobile.utils.RetrofitUtils;
import com.example.petstoremobile.utils.Resource;
import com.example.petstoremobile.utils.UIUtils;
import com.example.petstoremobile.viewmodels.AuthViewModel;
import java.io.File;
import java.util.HashMap;
@@ -48,9 +51,9 @@ public class ProfileFragment extends Fragment {
private FragmentProfileBinding binding;
private UserDTO currentUser;
private AuthViewModel viewModel;
private boolean hasImage = false;
@Inject AuthApi authApi;
@Inject TokenManager tokenManager;
@Inject @Named("baseUrl") String baseUrl;
@@ -62,6 +65,7 @@ public class ProfileFragment extends Fragment {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
viewModel = new ViewModelProvider(this).get(AuthViewModel.class);
imagePickerHelper = new ImagePickerHelper(this, "profile_photo.jpg", new ImagePickerHelper.ImagePickerListener() {
@Override
@@ -178,31 +182,36 @@ public class ProfileFragment extends Fragment {
* Fetches current user profile data from the API and then updates the UI.
*/
private void loadProfileData() {
authApi.getMe().enqueue(RetrofitUtils.createCallback(requireContext(), "PROFILE", null, result -> {
currentUser = result;
viewModel.getMe().observe(getViewLifecycleOwner(), resource -> {
if (resource == null) return;
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
currentUser = resource.data;
//set the user data to the view
binding.tvProfileName.setText(currentUser.getFullName());
binding.tvProfileEmail.setText(currentUser.getEmail());
binding.tvProfilePhone.setText(currentUser.getPhone());
binding.tvProfileRole.setText(currentUser.getRole());
//set the user data to the view
binding.tvProfileName.setText(currentUser.getFullName());
binding.tvProfileEmail.setText(currentUser.getEmail());
binding.tvProfilePhone.setText(currentUser.getPhone());
binding.tvProfileRole.setText(currentUser.getRole());
// get the avatar endpoint to load profile image and the token for authorization
String avatarUrl = baseUrl + AuthApi.AVATAR_FILE_PATH;
String token = tokenManager.getToken();
// get the avatar endpoint to load profile image and the token for authorization
String avatarUrl = baseUrl + AuthApi.AVATAR_FILE_PATH;
String token = tokenManager.getToken();
GlideUtils.loadImageWithToken(requireContext(), binding.imgProfile, avatarUrl, token, R.drawable.placeholder, new GlideUtils.ImageLoadListener() {
@Override
public void onResourceReady() {
hasImage = true;
}
GlideUtils.loadImageWithToken(requireContext(), binding.imgProfile, avatarUrl, token, R.drawable.placeholder, new GlideUtils.ImageLoadListener() {
@Override
public void onResourceReady() {
hasImage = true;
}
@Override
public void onLoadFailed() {
hasImage = false;
}
});
}));
@Override
public void onLoadFailed() {
hasImage = false;
}
});
} else if (resource.status == Resource.Status.ERROR) {
Toast.makeText(getContext(), "Failed to load profile: " + resource.message, Toast.LENGTH_SHORT).show();
}
});
}
/**
@@ -218,11 +227,17 @@ public class ProfileFragment extends Fragment {
MultipartBody.Part body = MultipartBody.Part.createFormData("avatar", file.getName(), requestFile);
//Call the backend to upload the avatar
authApi.uploadAvatar(body).enqueue(RetrofitUtils.createCallback(requireContext(), "UPLOAD_AVATAR", "Avatar updated successfully", result -> {
currentUser = result;
// Reload image after successful upload
loadProfileData();
}));
viewModel.uploadAvatar(body).observe(getViewLifecycleOwner(), resource -> {
if (resource == null) return;
if (resource.status == Resource.Status.SUCCESS) {
currentUser = resource.data;
Toast.makeText(getContext(), "Avatar updated successfully", Toast.LENGTH_SHORT).show();
// Reload image after successful upload
loadProfileData();
} else if (resource.status == Resource.Status.ERROR) {
Toast.makeText(getContext(), "Upload failed: " + resource.message, Toast.LENGTH_SHORT).show();
}
});
} catch (Exception e) {
Log.e("UPLOAD_AVATAR", "Error: " + e.getMessage());
}
@@ -232,10 +247,16 @@ public class ProfileFragment extends Fragment {
* Sends a request to the API to delete the current user's avatar image.
*/
private void deleteAvatar() {
authApi.deleteAvatar().enqueue(RetrofitUtils.createCallback(requireContext(), "DELETE_AVATAR", "Avatar removed successfully", result -> {
hasImage = false;
binding.imgProfile.setImageResource(R.drawable.placeholder);
}));
viewModel.deleteAvatar().observe(getViewLifecycleOwner(), resource -> {
if (resource == null) return;
if (resource.status == Resource.Status.SUCCESS) {
hasImage = false;
binding.imgProfile.setImageResource(R.drawable.placeholder);
Toast.makeText(getContext(), "Avatar removed successfully", Toast.LENGTH_SHORT).show();
} else if (resource.status == Resource.Status.ERROR) {
Toast.makeText(getContext(), "Removal failed: " + resource.message, Toast.LENGTH_SHORT).show();
}
});
}
/**
@@ -245,11 +266,17 @@ public class ProfileFragment extends Fragment {
Map<String, String> updates = new HashMap<>();
updates.put(fieldName, value);
authApi.updateMe(updates).enqueue(RetrofitUtils.createCallback(requireContext(), "UPDATE_PROFILE", "Profile updated successfully", result -> {
currentUser = result;
// Update the view with the new data from backend
binding.tvProfileEmail.setText(currentUser.getEmail());
binding.tvProfilePhone.setText(currentUser.getPhone());
}));
viewModel.updateMe(updates).observe(getViewLifecycleOwner(), resource -> {
if (resource == null) return;
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
currentUser = resource.data;
Toast.makeText(getContext(), "Profile updated successfully", Toast.LENGTH_SHORT).show();
// Update the view with the new data from backend
binding.tvProfileEmail.setText(currentUser.getEmail());
binding.tvProfilePhone.setText(currentUser.getPhone());
} else if (resource.status == Resource.Status.ERROR) {
Toast.makeText(getContext(), "Update failed: " + resource.message, Toast.LENGTH_SHORT).show();
}
});
}
}

View File

@@ -239,10 +239,6 @@ public class AdoptionFragment extends Fragment implements AdoptionAdapter.OnAdop
if (position != -1) {
AdoptionDTO a = filteredList.get(position);
args.putLong("adoptionId", a.getAdoptionId());
args.putLong("petId", a.getPetId() != null ? a.getPetId() : -1);
args.putLong("customerId", a.getCustomerId() != null ? a.getCustomerId() : -1);
args.putString("adoptionDate", a.getAdoptionDate());
args.putString("adoptionStatus", a.getAdoptionStatus());
}
NavHostFragment.findNavController(this).navigate(R.id.nav_adoption_detail, args);

View File

@@ -22,13 +22,8 @@ import com.example.petstoremobile.R;
import com.example.petstoremobile.adapters.AppointmentAdapter;
import com.example.petstoremobile.databinding.FragmentAppointmentBinding;
import com.example.petstoremobile.dtos.AppointmentDTO;
import com.example.petstoremobile.dtos.ServiceDTO;
import com.example.petstoremobile.dtos.PetDTO;
import com.example.petstoremobile.fragments.ListFragment;
import com.example.petstoremobile.viewmodels.AppointmentViewModel;
import com.example.petstoremobile.viewmodels.PetViewModel;
import com.example.petstoremobile.viewmodels.ServiceViewModel;
import com.example.petstoremobile.utils.Resource;
import com.example.petstoremobile.utils.EventDecorator;
import com.prolificinteractive.materialcalendarview.CalendarDay;
import com.prolificinteractive.materialcalendarview.CalendarMode;
@@ -50,13 +45,9 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter.
private FragmentAppointmentBinding binding;
private List<AppointmentDTO> appointmentList = new ArrayList<>();
private List<AppointmentDTO> filteredList = new ArrayList<>();
private List<PetDTO> petList = new ArrayList<>();
private List<ServiceDTO> serviceList = new ArrayList<>();
private AppointmentAdapter adapter;
private AppointmentViewModel appointmentViewModel;
private PetViewModel petViewModel;
private ServiceViewModel serviceViewModel;
private CalendarDay selectedCalendarDay;
private boolean isMonthMode = false;
@@ -69,8 +60,6 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter.
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
appointmentViewModel = new ViewModelProvider(this).get(AppointmentViewModel.class);
petViewModel = new ViewModelProvider(this).get(PetViewModel.class);
serviceViewModel = new ViewModelProvider(this).get(ServiceViewModel.class);
}
/**
@@ -86,8 +75,6 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter.
setupSwipeRefresh();
setupCalendar();
loadAppointmentData();
loadPets();
loadServices();
binding.fabAddAppointment.setOnClickListener(v -> openAppointmentDetails(-1));
@@ -226,37 +213,13 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter.
*/
private void openAppointmentDetails(int position) {
Bundle args = new Bundle();
if (position != -1) {
AppointmentDTO a = filteredList.get(position);
args.putLong("appointmentId", a.getAppointmentId());
args.putString("appointmentDate", a.getAppointmentDate());
args.putString("appointmentTime", a.getAppointmentTime());
args.putString("appointmentStatus", a.getAppointmentStatus());
// IDs for pre-selecting spinners
if (a.getPetID() != null) args.putLong("petId", a.getPetID());
if (a.getServiceId() != null) args.putLong("serviceId", a.getServiceId());
if (a.getCustomerId() != null) args.putLong("customerId", a.getCustomerId());
if (a.getStoreId() != null) args.putLong("storeId", a.getStoreId());
}
NavHostFragment.findNavController(this).navigate(R.id.nav_appointment_detail, args);
}
/**
* Reloads data when an appointment is saved.
*/
public void onAppointmentSaved(int position, AppointmentDTO appointment) {
loadAppointmentData();
}
/**
* Reloads data when an appointment is deleted.
*/
public void onAppointmentDeleted(int position) {
loadAppointmentData();
}
/**
* Handles item click in the appointment list.
*/
@@ -299,30 +262,6 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter.
});
}
/**
* Fetches the full list of pets from the server.
*/
private void loadPets() {
petViewModel.getAllPets(0, 100).observe(getViewLifecycleOwner(), resource -> {
if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) {
petList.clear();
petList.addAll(resource.data.getContent());
}
});
}
/**
* Fetches the full list of services from the server.
*/
private void loadServices() {
serviceViewModel.getAllServices(0, 100).observe(getViewLifecycleOwner(), resource -> {
if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) {
serviceList.clear();
serviceList.addAll(resource.data.getContent());
}
});
}
/**
* Initializes the RecyclerView for displaying appointments.
*/

View File

@@ -341,10 +341,6 @@ public class InventoryFragment extends Fragment implements InventoryAdapter.OnIn
if (inv != null) {
args.putLong("inventoryId", inv.getInventoryId());
args.putLong("prodId", inv.getProdId() != null ? inv.getProdId() : -1);
args.putString("productName", inv.getProductName());
args.putString("categoryName", inv.getCategoryName());
args.putInt("quantity", inv.getQuantity() != null ? inv.getQuantity() : 0);
}
NavHostFragment.findNavController(this).navigate(R.id.nav_inventory_detail, args);

View File

@@ -166,18 +166,7 @@ public class PetFragment extends Fragment implements PetAdapter.OnPetClickListen
private void openPetProfile(int position) {
Bundle args = new Bundle();
PetDTO pet = filteredList.get(position);
args.putInt("petId", pet.getPetId().intValue());
args.putString("petName", pet.getPetName());
args.putString("petSpecies", pet.getPetSpecies());
args.putString("petBreed", pet.getPetBreed());
args.putInt("petAge", pet.getPetAge());
args.putString("petStatus", pet.getPetStatus());
try {
args.putDouble("petPrice", Double.parseDouble(pet.getPetPrice()));
} catch (Exception e) {
args.putDouble("petPrice", 0.0);
}
args.putLong("petId", pet.getPetId());
NavHostFragment.findNavController(this).navigate(R.id.nav_pet_profile, args);
}

View File

@@ -170,10 +170,6 @@ public class ProductFragment extends Fragment implements ProductAdapter.OnProduc
if (position != -1) {
ProductDTO p = filteredList.get(position);
args.putLong("prodId", p.getProdId());
args.putString("prodName", p.getProdName());
args.putString("prodDesc", p.getProdDesc() != null ? p.getProdDesc() : "");
args.putString("prodPrice", p.getProdPrice() != null ? p.getProdPrice().toString() : "");
args.putLong("categoryId", p.getCategoryId() != null ? p.getCategoryId() : -1);
}
NavHostFragment.findNavController(this).navigate(R.id.nav_product_detail, args);
}

View File

@@ -166,9 +166,6 @@ public class ProductSupplierFragment extends Fragment
ProductSupplierDTO ps = filteredList.get(position);
args.putLong("productId", ps.getProductId());
args.putLong("supplierId", ps.getSupplierId());
args.putString("productName", ps.getProductName());
args.putString("supplierName", ps.getSupplierName());
args.putString("cost", ps.getCost() != null ? ps.getCost().toString() : "");
}
NavHostFragment.findNavController(this).navigate(R.id.nav_product_supplier_detail, args);
}

View File

@@ -166,9 +166,6 @@ public class PurchaseOrderFragment extends Fragment
Bundle args = new Bundle();
PurchaseOrderDTO po = filteredList.get(position);
args.putLong("purchaseOrderId", po.getPurchaseOrderId());
args.putString("supplierName", po.getSupplierName());
args.putString("orderDate", po.getOrderDate());
args.putString("status", po.getStatus());
NavHostFragment.findNavController(this).navigate(R.id.nav_purchase_order_detail, args);
}

View File

@@ -128,16 +128,11 @@ public class ServiceFragment extends Fragment implements ServiceAdapter.OnServic
private void openServiceDetails(int position) {
//Make a bundle to pass data to the detail fragment
Bundle args = new Bundle();
args.putInt("position", position);
//if editing a service, add the service data to the bundle
//if editing a service, add the service id to the bundle
if (position != -1) {
ServiceDTO service = filteredList.get(position);
args.putInt("serviceId", service.getServiceId().intValue());
args.putString("serviceName", service.getServiceName());
args.putString("serviceDesc", service.getServiceDesc());
args.putInt("serviceDuration", service.getServiceDuration());
args.putDouble("servicePrice", service.getServicePrice());
args.putLong("serviceId", service.getServiceId());
}
NavHostFragment.findNavController(this).navigate(R.id.nav_service_detail, args);

View File

@@ -129,17 +129,11 @@ public class SupplierFragment extends Fragment implements SupplierAdapter.OnSupp
private void openSupplierDetails(int position) {
//Make a bundle to pass data to the detail fragment
Bundle args = new Bundle();
args.putInt("position", position);
//if editing a supplier, add the supplier data to the bundle
//if editing a supplier, add the supplier id to the bundle
if (position != -1) {
SupplierDTO supplier = filteredList.get(position);
args.putInt("supId", supplier.getSupId().intValue());
args.putString("supCompany", supplier.getSupCompany());
args.putString("supContactFirstName", supplier.getSupContactFirstName());
args.putString("supContactLastName", supplier.getSupContactLastName());
args.putString("supEmail", supplier.getSupEmail());
args.putString("supPhone", supplier.getSupPhone());
args.putLong("supId", supplier.getSupId());
}
NavHostFragment.findNavController(this).navigate(R.id.nav_supplier_detail, args);

View File

@@ -5,6 +5,7 @@ import android.os.Bundle;
import android.view.*;
import android.widget.*;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.ViewModelProvider;
import androidx.navigation.fragment.NavHostFragment;
@@ -56,15 +57,20 @@ public class AdoptionDetailFragment extends Fragment {
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
binding = FragmentAdoptionDetailBinding.inflate(inflater, container, false);
return binding.getRoot();
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
setupSpinners();
setupDatePicker();
loadData();
loadSpinnersData();
handleArguments();
binding.btnAdoptionBack.setOnClickListener(v -> navigateBack());
binding.btnSaveAdoption.setOnClickListener(v -> saveAdoption());
binding.btnDeleteAdoption.setOnClickListener(v -> confirmDelete());
return binding.getRoot();
}
@Override
@@ -96,9 +102,9 @@ public class AdoptionDetailFragment extends Fragment {
}
/**
* Fetches required data (pets and customers) from the backend.
* Fetches required data for spinners from the backend.
*/
private void loadData() {
private void loadSpinnersData() {
loadPets();
loadCustomers();
}
@@ -110,13 +116,20 @@ public class AdoptionDetailFragment extends Fragment {
petViewModel.getAllPets(0, 200).observe(getViewLifecycleOwner(), resource -> {
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
petList = resource.data.getContent();
SpinnerUtils.populateSpinner(requireContext(), binding.spinnerAdoptionPet, petList,
PetDTO::getPetName, "-- Select Pet --",
preselectedPetId, PetDTO::getPetId);
refreshPetSpinner();
}
});
}
/**
* Populates the pet selection spinner with data.
*/
private void refreshPetSpinner() {
SpinnerUtils.populateSpinner(requireContext(), binding.spinnerAdoptionPet, petList,
PetDTO::getPetName, "-- Select Pet --",
preselectedPetId, PetDTO::getPetId);
}
/**
* Loads the list of customers from the API.
*/
@@ -124,14 +137,21 @@ public class AdoptionDetailFragment extends Fragment {
customerViewModel.getAllCustomers(0, 200).observe(getViewLifecycleOwner(), resource -> {
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
customerList = resource.data.getContent();
SpinnerUtils.populateSpinner(requireContext(), binding.spinnerAdoptionCustomer, customerList,
item -> item.getFirstName() + " " + item.getLastName(),
"-- Select Customer --",
preselectedCustomerId, CustomerDTO::getCustomerId);
refreshCustomerSpinner();
}
});
}
/**
* Populates the customer selection spinner with data.
*/
private void refreshCustomerSpinner() {
SpinnerUtils.populateSpinner(requireContext(), binding.spinnerAdoptionCustomer, customerList,
item -> item.getFirstName() + " " + item.getLastName(),
"-- Select Customer --",
preselectedCustomerId, CustomerDTO::getCustomerId);
}
/**
* Handles arguments to determine if the fragment is in edit or add mode.
*/
@@ -139,18 +159,12 @@ public class AdoptionDetailFragment extends Fragment {
Bundle a = getArguments();
if (a != null && a.containsKey("adoptionId")) {
isEditing = true;
adoptionId = a.getLong("adoptionId");
preselectedPetId = a.getLong("petId", -1);
preselectedCustomerId = a.getLong("customerId", -1);
adoptionId = a.getLong("adoptionId");
binding.tvAdoptionMode.setText("Edit Adoption");
binding.tvAdoptionId.setText("ID: " + adoptionId);
binding.tvAdoptionId.setVisibility(View.VISIBLE);
binding.etAdoptionDate.setText(a.getString("adoptionDate"));
binding.btnDeleteAdoption.setVisibility(View.VISIBLE);
// Pre-fill status
SpinnerUtils.setSelectionByValue(binding.spinnerAdoptionStatus, a.getString("adoptionStatus", "Pending"));
loadAdoptionData();
} else {
binding.tvAdoptionMode.setText("Add Adoption");
binding.btnDeleteAdoption.setVisibility(View.GONE);
@@ -158,6 +172,27 @@ public class AdoptionDetailFragment extends Fragment {
}
}
/**
* Fetches specific adoption details from the backend using the ID.
*/
private void loadAdoptionData() {
adoptionViewModel.getAdoptionById(adoptionId).observe(getViewLifecycleOwner(), resource -> {
if (resource == null) return;
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
AdoptionDTO a = resource.data;
preselectedPetId = a.getPetId() != null ? a.getPetId() : -1;
preselectedCustomerId = a.getCustomerId() != null ? a.getCustomerId() : -1;
binding.etAdoptionDate.setText(a.getAdoptionDate());
SpinnerUtils.setSelectionByValue(binding.spinnerAdoptionStatus, a.getAdoptionStatus());
refreshPetSpinner();
refreshCustomerSpinner();
} else if (resource.status == Resource.Status.ERROR) {
Toast.makeText(getContext(), "Failed to load adoption: " + resource.message, Toast.LENGTH_SHORT).show();
}
});
}
/**
* Validates input and saves the adoption request to the backend.
*/

View File

@@ -6,6 +6,7 @@ import android.util.Log;
import android.view.*;
import android.widget.*;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.ViewModelProvider;
import androidx.navigation.fragment.NavHostFragment;
@@ -68,15 +69,20 @@ public class AppointmentDetailFragment extends Fragment {
@Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
binding = FragmentAppointmentDetailBinding.inflate(inflater, container, false);
return binding.getRoot();
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
setupSpinners();
setupDatePicker();
loadData();
loadSpinnersData();
handleArguments();
binding.btnApptBack.setOnClickListener(v -> navigateBack());
binding.btnSaveAppointment.setOnClickListener(v -> saveAppointment());
binding.btnDeleteAppointment.setOnClickListener(v -> confirmDelete());
return binding.getRoot();
}
@Override
@@ -115,9 +121,9 @@ public class AppointmentDetailFragment extends Fragment {
}
/**
* Fetches all required data from the backend to populate the fragment.
* Fetches all required data for spinners from the backend.
*/
private void loadData() {
private void loadSpinnersData() {
loadPets();
loadServices();
loadCustomers();
@@ -131,13 +137,20 @@ public class AppointmentDetailFragment extends Fragment {
petViewModel.getAllPets(0, 200).observe(getViewLifecycleOwner(), resource -> {
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
petList = resource.data.getContent();
SpinnerUtils.populateSpinner(requireContext(), binding.spinnerPet, petList,
PetDTO::getPetName, "-- Select Pet --",
preselectedPetId, PetDTO::getPetId);
refreshPetSpinner();
}
});
}
/**
* Populates the pet selection spinner.
*/
private void refreshPetSpinner() {
SpinnerUtils.populateSpinner(requireContext(), binding.spinnerPet, petList,
PetDTO::getPetName, "-- Select Pet --",
preselectedPetId, PetDTO::getPetId);
}
/**
* Loads the list of services from the API.
*/
@@ -145,13 +158,20 @@ public class AppointmentDetailFragment extends Fragment {
serviceViewModel.getAllServices(0, 200).observe(getViewLifecycleOwner(), resource -> {
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
serviceList = resource.data.getContent();
SpinnerUtils.populateSpinner(requireContext(), binding.spinnerService, serviceList,
ServiceDTO::getServiceName, "-- Select Service --",
preselectedServiceId, ServiceDTO::getServiceId);
refreshServiceSpinner();
}
});
}
/**
* Populates the service selection spinner.
*/
private void refreshServiceSpinner() {
SpinnerUtils.populateSpinner(requireContext(), binding.spinnerService, serviceList,
ServiceDTO::getServiceName, "-- Select Service --",
preselectedServiceId, ServiceDTO::getServiceId);
}
/**
* Loads the list of customers from the API.
*/
@@ -159,14 +179,21 @@ public class AppointmentDetailFragment extends Fragment {
customerViewModel.getAllCustomers(0, 200).observe(getViewLifecycleOwner(), resource -> {
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
customerList = resource.data.getContent();
SpinnerUtils.populateSpinner(requireContext(), binding.spinnerCustomer, customerList,
item -> item.getFirstName() + " " + item.getLastName(),
"-- Select Customer --",
preselectedCustomerId, CustomerDTO::getCustomerId);
refreshCustomerSpinner();
}
});
}
/**
* Populates the customer selection spinner.
*/
private void refreshCustomerSpinner() {
SpinnerUtils.populateSpinner(requireContext(), binding.spinnerCustomer, customerList,
item -> item.getFirstName() + " " + item.getLastName(),
"-- Select Customer --",
preselectedCustomerId, CustomerDTO::getCustomerId);
}
/**
* Loads the list of stores from the API.
*/
@@ -174,13 +201,20 @@ public class AppointmentDetailFragment extends Fragment {
storeViewModel.getAllStores(0, 50).observe(getViewLifecycleOwner(), resource -> {
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
storeList = resource.data.getContent();
SpinnerUtils.populateSpinner(requireContext(), binding.spinnerStore, storeList,
StoreDTO::getStoreName, "-- Select Store --",
preselectedStoreId, StoreDTO::getStoreId);
refreshStoreSpinner();
}
});
}
/**
* Populates the store selection spinner.
*/
private void refreshStoreSpinner() {
SpinnerUtils.populateSpinner(requireContext(), binding.spinnerStore, storeList,
StoreDTO::getStoreName, "-- Select Store --",
preselectedStoreId, StoreDTO::getStoreId);
}
/**
* Handles arguments to determine if the fragment is in edit or add mode.
*/
@@ -188,34 +222,12 @@ public class AppointmentDetailFragment extends Fragment {
Bundle a = getArguments();
if (a != null && a.containsKey("appointmentId")) {
isEditing = true;
appointmentId = a.getLong("appointmentId");
preselectedPetId = a.getLong("petId", -1);
preselectedServiceId= a.getLong("serviceId", -1);
preselectedCustomerId = a.getLong("customerId", -1);
preselectedStoreId = a.getLong("storeId", -1);
appointmentId = a.getLong("appointmentId");
binding.tvApptMode.setText("Edit Appointment");
binding.tvAppointmentId.setText("ID: " + appointmentId);
binding.tvAppointmentId.setVisibility(View.VISIBLE);
binding.etAppointmentDate.setText(a.getString("appointmentDate"));
binding.btnDeleteAppointment.setVisibility(View.VISIBLE);
// Pre-fill time spinners
String time = a.getString("appointmentTime", "09:00");
if (time.length() > 5) time = time.substring(0, 5);
String[] parts = time.split(":");
if (parts.length == 2) {
int hour = Integer.parseInt(parts[0]);
int min = Integer.parseInt(parts[1]);
for (int i = 0; i < HOURS.length; i++)
if (HOURS[i] == hour) { binding.spinnerHour.setSelection(i); break; }
for (int i = 0; i < MINUTES.length; i++)
if (MINUTES[i] == min) { binding.spinnerMinute.setSelection(i); break; }
}
// Pre-fill status
SpinnerUtils.setSelectionByValue(binding.spinnerAppointmentStatus, a.getString("appointmentStatus", "Booked"));
loadAppointmentData();
} else {
binding.tvApptMode.setText("Add Appointment");
binding.btnDeleteAppointment.setVisibility(View.GONE);
@@ -223,6 +235,48 @@ public class AppointmentDetailFragment extends Fragment {
}
}
/**
* Fetches specific appointment details from the backend using the ID.
*/
private void loadAppointmentData() {
appointmentViewModel.getAppointmentById(appointmentId).observe(getViewLifecycleOwner(), resource -> {
if (resource == null) return;
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
AppointmentDTO a = resource.data;
preselectedPetId = (a.getPetID() != null) ? a.getPetID() : -1;
preselectedServiceId = (a.getServiceId() != null) ? a.getServiceId() : -1;
preselectedCustomerId = (a.getCustomerId() != null) ? a.getCustomerId() : -1;
preselectedStoreId = (a.getStoreId() != null) ? a.getStoreId() : -1;
binding.etAppointmentDate.setText(a.getAppointmentDate());
// Pre-fill time spinners
String time = a.getAppointmentTime() != null ? a.getAppointmentTime() : "09:00";
if (time.length() > 5) time = time.substring(0, 5);
String[] parts = time.split(":");
if (parts.length == 2) {
try {
int hour = Integer.parseInt(parts[0]);
int min = Integer.parseInt(parts[1]);
for (int i = 0; i < HOURS.length; i++)
if (HOURS[i] == hour) { binding.spinnerHour.setSelection(i); break; }
for (int i = 0; i < MINUTES.length; i++)
if (MINUTES[i] == min) { binding.spinnerMinute.setSelection(i); break; }
} catch (NumberFormatException ignored) {}
}
SpinnerUtils.setSelectionByValue(binding.spinnerAppointmentStatus, a.getAppointmentStatus());
refreshPetSpinner();
refreshServiceSpinner();
refreshCustomerSpinner();
refreshStoreSpinner();
} else if (resource.status == Resource.Status.ERROR) {
Toast.makeText(getContext(), "Failed to load appointment: " + resource.message, Toast.LENGTH_SHORT).show();
}
});
}
/**
* Validates input and saves the appointment to the backend.
*/
@@ -307,6 +361,9 @@ public class AppointmentDetailFragment extends Fragment {
}
}
/**
* Handles errors that occur during the saving process.
*/
private void handleSaveError(String errorMessage) {
if (errorMessage != null) {
Log.e("APPT_SAVE", "Error: " + errorMessage);

View File

@@ -12,6 +12,7 @@ import android.widget.ArrayAdapter;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.ViewModelProvider;
@@ -19,6 +20,7 @@ import androidx.navigation.fragment.NavHostFragment;
import com.example.petstoremobile.adapters.BlackTextArrayAdapter;
import com.example.petstoremobile.databinding.FragmentInventoryDetailBinding;
import com.example.petstoremobile.dtos.InventoryDTO;
import com.example.petstoremobile.dtos.InventoryRequest;
import com.example.petstoremobile.dtos.ProductDTO;
import com.example.petstoremobile.utils.InputValidator;
@@ -56,6 +58,9 @@ public class InventoryDetailFragment extends Fragment {
private final List<ProductDTO> productSuggestions = new ArrayList<>();
private ArrayAdapter<String> dropdownAdapter;
/**
* Initializes the view models.
*/
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
@@ -63,10 +68,22 @@ public class InventoryDetailFragment extends Fragment {
productViewModel = new ViewModelProvider(this).get(ProductViewModel.class);
}
/**
* Inflates the layout.
*/
@Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
binding = FragmentInventoryDetailBinding.inflate(inflater, container, false);
return binding.getRoot();
}
/**
* Sets up UI components after the view is created.
*/
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
setupProductSearch();
handleArguments();
@@ -80,27 +97,26 @@ public class InventoryDetailFragment extends Fragment {
android.R.layout.simple_dropdown_item_1line, new ArrayList<>());
binding.etProductSearch.setAdapter(dropdownAdapter);
binding.etProductSearch.setThreshold(1); // start showing after 1 character
return binding.getRoot();
}
@Override
public void onDestroyView() {
super.onDestroyView();
if (searchRunnable != null) {
searchHandler.removeCallbacks(searchRunnable);
}
binding = null;
}
/**
* setup the product search dropdown.
* Sets up the product search dropdown.
*/
private void setupProductSearch() {
binding.etProductSearch.addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int i, int i1, int i2) {
@Override public void beforeTextChanged(CharSequence s, int i, int i1, int i2) {
}
@Override
public void afterTextChanged(Editable s) {
@Override public void afterTextChanged(Editable s) {
}
@Override
@@ -137,6 +153,7 @@ public class InventoryDetailFragment extends Fragment {
* Searches for products matching the query from the backend.
*/
private void searchProducts(String query) {
if (getView() == null) return;
productViewModel.getAllProducts(query, 0, 20).observe(getViewLifecycleOwner(), resource -> {
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
productSuggestions.clear();
@@ -157,7 +174,7 @@ public class InventoryDetailFragment extends Fragment {
}
/**
* arguments to set up edit or add mode.
* Handles fragment arguments to determine if we are in edit or add mode.
*/
private void handleArguments() {
Bundle args = getArguments();
@@ -168,27 +185,10 @@ public class InventoryDetailFragment extends Fragment {
binding.tvInventoryMode.setText("Edit Inventory");
binding.tvInventoryId.setText("Inventory ID: " + inventoryId);
binding.tvInventoryId.setVisibility(View.VISIBLE);
// Pre-fill search box with existing product name
String productName = args.getString("productName", "");
long prodId = args.getLong("prodId", -1);
binding.etProductSearch.setText(productName);
// Show existing product info
if (prodId != -1) {
binding.tvProductInfo.setText(
"ID: " + prodId
+ "" + args.getString("categoryName", ""));
binding.tvProductInfo.setVisibility(View.VISIBLE);
// Build a minimal ProductDTO so selectedProduct is not null on save
selectedProduct = new ProductDTO(productName, null, null, null);
selectedProduct.setProdId(prodId);
}
binding.etQuantity.setText(String.valueOf(args.getInt("quantity", 0)));
binding.btnDeleteInventory.setVisibility(View.VISIBLE);
binding.btnSaveInventory.setText("Save");
loadInventoryData();
} else {
isEditing = false;
binding.tvInventoryMode.setText("Add Inventory");
@@ -200,7 +200,35 @@ public class InventoryDetailFragment extends Fragment {
}
/**
* Saves the current inventory item details to the backend.
* Loads existing inventory data from the backend.
*/
private void loadInventoryData() {
inventoryViewModel.getInventoryById(inventoryId).observe(getViewLifecycleOwner(), resource -> {
if (resource == null) return;
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
InventoryDTO inv = resource.data;
binding.etProductSearch.setText(inv.getProductName());
binding.etQuantity.setText(String.valueOf(inv.getQuantity()));
if (inv.getProdId() != null) {
binding.tvProductInfo.setText(
"ID: " + inv.getProdId()
+ "" + inv.getCategoryName());
binding.tvProductInfo.setVisibility(View.VISIBLE);
selectedProduct = new ProductDTO();
selectedProduct.setProdId(inv.getProdId());
selectedProduct.setProdName(inv.getProductName());
selectedProduct.setCategoryName(inv.getCategoryName());
}
} else if (resource.status == Resource.Status.ERROR) {
Toast.makeText(getContext(), "Failed to load inventory: " + resource.message, Toast.LENGTH_SHORT).show();
}
});
}
/**
* Validates input and saves the current inventory item details to the backend.
*/
private void saveInventory() {
if (selectedProduct == null) {

View File

@@ -3,6 +3,7 @@ package com.example.petstoremobile.fragments.listfragments.detailfragments;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.ViewModelProvider;
import androidx.navigation.fragment.NavHostFragment;
@@ -31,7 +32,7 @@ import dagger.hilt.android.AndroidEntryPoint;
public class PetDetailFragment extends Fragment {
private FragmentPetDetailBinding binding;
private int petId;
private long petId;
private boolean isEditing = false;
private PetViewModel viewModel;
@@ -46,6 +47,12 @@ public class PetDetailFragment extends Fragment {
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
binding = FragmentPetDetailBinding.inflate(inflater, container, false);
return binding.getRoot();
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
setupSpinner();
handleArguments();
@@ -54,8 +61,6 @@ public class PetDetailFragment extends Fragment {
binding.btnBack.setOnClickListener(v -> navigateBack());
binding.btnSavePet.setOnClickListener(v -> savePet());
binding.btnDeletePet.setOnClickListener(v -> deletePet());
return binding.getRoot();
}
@Override
@@ -95,10 +100,10 @@ public class PetDetailFragment extends Fragment {
//check if the pet is being edited or added
if (isEditing) {
// Update existing pet
petDTO.setPetId((long) petId);
viewModel.updatePet((long) petId, petDTO).observe(getViewLifecycleOwner(), resource -> {
petDTO.setPetId(petId);
viewModel.updatePet(petId, petDTO).observe(getViewLifecycleOwner(), resource -> {
if (resource.status == Resource.Status.SUCCESS) {
ActivityLogger.logChange(requireContext(), "Pet", "UPDATED", petId);
ActivityLogger.logChange(requireContext(), "Pet", "UPDATED", (int) petId);
Toast.makeText(getContext(), "Pet updated successfully!", Toast.LENGTH_SHORT).show();
navigateToPetList();
} else if (resource.status == Resource.Status.ERROR) {
@@ -124,9 +129,9 @@ public class PetDetailFragment extends Fragment {
*/
private void deletePet() {
DialogUtils.showDeleteConfirmDialog(requireContext(), "Pet", () ->
viewModel.deletePet((long) petId).observe(getViewLifecycleOwner(), resource -> {
viewModel.deletePet(petId).observe(getViewLifecycleOwner(), resource -> {
if (resource.status == Resource.Status.SUCCESS) {
ActivityLogger.logChange(requireContext(), "Pet", "DELETED", petId);
ActivityLogger.logChange(requireContext(), "Pet", "DELETED", (int) petId);
Toast.makeText(getContext(), "Pet deleted successfully!", Toast.LENGTH_SHORT).show();
navigateToPetList();
} else if (resource.status == Resource.Status.ERROR) {
@@ -157,16 +162,12 @@ public class PetDetailFragment extends Fragment {
if (getArguments() != null && getArguments().containsKey("petId")) {
// Get pet data from arguments and populate fields
isEditing = true;
petId = getArguments().getInt("petId");
petId = getArguments().getLong("petId");
binding.tvMode.setText("Edit Pet");
binding.tvPetId.setText("ID: " + petId);
binding.etPetName.setText(getArguments().getString("petName"));
binding.etPetSpecies.setText(getArguments().getString("petSpecies"));
binding.etPetBreed.setText(getArguments().getString("petBreed"));
binding.etPetAge.setText(String.valueOf(getArguments().getInt("petAge")));
binding.etPetPrice.setText(String.valueOf(getArguments().getDouble("petPrice")));
SpinnerUtils.setSelectionByValue(binding.spinnerPetStatus, getArguments().getString("petStatus"));
binding.tvPetId.setVisibility(View.VISIBLE);
binding.btnDeletePet.setVisibility(View.VISIBLE);
loadPetData();
} else {
// Pet is being added
// Set default values for add a new pet
@@ -178,6 +179,26 @@ public class PetDetailFragment extends Fragment {
}
}
/**
* Fetches specific pet details from the backend using the ID.
*/
private void loadPetData() {
viewModel.getPetById(petId).observe(getViewLifecycleOwner(), resource -> {
if (resource == null) return;
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
PetDTO p = resource.data;
binding.etPetName.setText(p.getPetName());
binding.etPetSpecies.setText(p.getPetSpecies());
binding.etPetBreed.setText(p.getPetBreed());
binding.etPetAge.setText(String.valueOf(p.getPetAge()));
binding.etPetPrice.setText(p.getPetPrice());
SpinnerUtils.setSelectionByValue(binding.spinnerPetStatus, p.getPetStatus());
} else if (resource.status == Resource.Status.ERROR) {
Toast.makeText(getContext(), "Failed to load pet: " + resource.message, Toast.LENGTH_SHORT).show();
}
});
}
/**
* Initializes the spinner for pet status selection.
*/

View File

@@ -5,6 +5,7 @@ import android.os.Bundle;
import android.view.*;
import android.widget.*;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.ViewModelProvider;
import androidx.navigation.fragment.NavHostFragment;
@@ -21,6 +22,7 @@ import com.example.petstoremobile.utils.FileUtils;
import com.example.petstoremobile.utils.GlideUtils;
import com.example.petstoremobile.utils.ImagePickerHelper;
import com.example.petstoremobile.utils.InputValidator;
import com.example.petstoremobile.utils.Resource;
import com.example.petstoremobile.utils.SpinnerUtils;
import java.io.File;
@@ -89,12 +91,21 @@ public class ProductDetailFragment extends Fragment {
}
/**
* Inflates the layout and initializes UI components and listeners.
* Inflates the layout.
*/
@Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
binding = FragmentProductDetailBinding.inflate(inflater, container, false);
return binding.getRoot();
}
/**
* Sets up UI components and listeners after the view is created.
*/
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
loadCategories();
handleArguments();
@@ -103,7 +114,6 @@ public class ProductDetailFragment extends Fragment {
binding.btnSaveProduct.setOnClickListener(v -> saveProduct());
binding.btnDeleteProduct.setOnClickListener(v -> confirmDelete());
binding.ivProductImage.setOnClickListener(v -> imagePickerHelper.showImagePickerDialog("Select Product Image", hasImage));
return binding.getRoot();
}
@Override
@@ -117,7 +127,7 @@ public class ProductDetailFragment extends Fragment {
*/
private void loadCategories() {
viewModel.getAllCategories(0, 100).observe(getViewLifecycleOwner(), resource -> {
if (resource != null && resource.status == com.example.petstoremobile.utils.Resource.Status.SUCCESS && resource.data != null) {
if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) {
categoryList = resource.data.getContent();
SpinnerUtils.populateSpinner(requireContext(), binding.spinnerProductCategory, categoryList,
CategoryDTO::getCategoryName, "-- Select Category --",
@@ -134,16 +144,11 @@ public class ProductDetailFragment extends Fragment {
if (a != null && a.containsKey("prodId")) {
isEditing = true;
prodId = a.getLong("prodId");
preselectedCategoryId = a.getLong("categoryId", -1);
hasImage = true;
binding.tvProductMode.setText("Edit Product");
binding.tvProductId.setText("ID: " + prodId);
binding.tvProductId.setVisibility(View.VISIBLE);
binding.etProductName.setText(a.getString("prodName"));
binding.etProductDesc.setText(a.getString("prodDesc"));
binding.etProductPrice.setText(a.getString("prodPrice"));
binding.btnDeleteProduct.setVisibility(View.VISIBLE);
loadProductData();
loadProductImage();
} else {
binding.tvProductMode.setText("Add Product");
@@ -153,6 +158,31 @@ public class ProductDetailFragment extends Fragment {
}
}
/**
* Loads the product data from the backend.
*/
private void loadProductData() {
viewModel.getProductById(prodId).observe(getViewLifecycleOwner(), resource -> {
if (resource == null) return;
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
ProductDTO p = resource.data;
binding.etProductName.setText(p.getProdName());
binding.etProductDesc.setText(p.getProdDesc());
binding.etProductPrice.setText(p.getProdPrice() != null ? p.getProdPrice().toString() : "");
preselectedCategoryId = p.getCategoryId() != null ? p.getCategoryId() : -1;
// Refresh spinner selection once data is loaded
if (!categoryList.isEmpty()) {
SpinnerUtils.populateSpinner(requireContext(), binding.spinnerProductCategory, categoryList,
CategoryDTO::getCategoryName, "-- Select Category --",
preselectedCategoryId, CategoryDTO::getCategoryId);
}
} else if (resource.status == Resource.Status.ERROR) {
Toast.makeText(getContext(), "Failed to load product: " + resource.message, Toast.LENGTH_SHORT).show();
}
});
}
/**
* Loads the product image from the backend.
*/
@@ -179,8 +209,8 @@ public class ProductDetailFragment extends Fragment {
private void performPendingImageActions(String successMsg) {
if (isImageRemoved) {
viewModel.deleteProductImage(prodId).observe(getViewLifecycleOwner(), resource -> {
if (resource != null && resource.status != com.example.petstoremobile.utils.Resource.Status.LOADING) {
if (resource.status == com.example.petstoremobile.utils.Resource.Status.SUCCESS) {
if (resource != null && resource.status != Resource.Status.LOADING) {
if (resource.status == Resource.Status.SUCCESS) {
Toast.makeText(getContext(), successMsg, Toast.LENGTH_SHORT).show();
} else {
Toast.makeText(getContext(), successMsg + " (but image removal failed)", Toast.LENGTH_SHORT).show();
@@ -211,8 +241,8 @@ public class ProductDetailFragment extends Fragment {
MultipartBody.Part body = MultipartBody.Part.createFormData("image", file.getName(), requestFile);
viewModel.uploadProductImage(prodId, body).observe(getViewLifecycleOwner(), resource -> {
if (resource != null && resource.status != com.example.petstoremobile.utils.Resource.Status.LOADING) {
if (resource.status == com.example.petstoremobile.utils.Resource.Status.SUCCESS) {
if (resource != null && resource.status != Resource.Status.LOADING) {
if (resource.status == Resource.Status.SUCCESS) {
Toast.makeText(getContext(), successMsg, Toast.LENGTH_SHORT).show();
} else {
Toast.makeText(getContext(), successMsg + " (but image upload failed)", Toast.LENGTH_SHORT).show();
@@ -246,8 +276,8 @@ public class ProductDetailFragment extends Fragment {
if (isEditing) {
viewModel.updateProduct(prodId, dto).observe(getViewLifecycleOwner(), resource -> {
if (resource != null && resource.status != com.example.petstoremobile.utils.Resource.Status.LOADING) {
if (resource.status == com.example.petstoremobile.utils.Resource.Status.SUCCESS) {
if (resource != null && resource.status != Resource.Status.LOADING) {
if (resource.status == Resource.Status.SUCCESS) {
performPendingImageActions("Updated");
} else {
Toast.makeText(getContext(), "Error: " + resource.message, Toast.LENGTH_SHORT).show();
@@ -256,8 +286,8 @@ public class ProductDetailFragment extends Fragment {
});
} else {
viewModel.createProduct(dto).observe(getViewLifecycleOwner(), resource -> {
if (resource != null && resource.status != com.example.petstoremobile.utils.Resource.Status.LOADING) {
if (resource.status == com.example.petstoremobile.utils.Resource.Status.SUCCESS && resource.data != null) {
if (resource != null && resource.status != Resource.Status.LOADING) {
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
prodId = resource.data.getProdId();
performPendingImageActions("Saved");
} else {
@@ -274,9 +304,9 @@ public class ProductDetailFragment extends Fragment {
private void confirmDelete() {
DialogUtils.showDeleteConfirmDialog(requireContext(), "Product", () ->
viewModel.deleteProduct(prodId).observe(getViewLifecycleOwner(), resource -> {
if (resource != null && resource.status == com.example.petstoremobile.utils.Resource.Status.SUCCESS) {
if (resource != null && resource.status == Resource.Status.SUCCESS) {
navigateBack();
} else if (resource != null && resource.status == com.example.petstoremobile.utils.Resource.Status.ERROR) {
} else if (resource != null && resource.status == Resource.Status.ERROR) {
Toast.makeText(getContext(), "Delete failed: " + resource.message, Toast.LENGTH_SHORT).show();
}
}));

View File

@@ -4,6 +4,7 @@ import android.os.Bundle;
import android.view.*;
import android.widget.*;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.ViewModelProvider;
import androidx.navigation.fragment.NavHostFragment;
@@ -56,13 +57,18 @@ public class ProductSupplierDetailFragment extends Fragment {
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
binding = FragmentProductSupplierDetailBinding.inflate(inflater, container, false);
loadData();
return binding.getRoot();
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
loadSpinnersData();
handleArguments();
binding.btnPSBack.setOnClickListener(v -> navigateBack());
binding.btnSavePS.setOnClickListener(v -> save());
binding.btnDeletePS.setOnClickListener(v -> confirmDelete());
return binding.getRoot();
}
@Override
@@ -74,7 +80,7 @@ public class ProductSupplierDetailFragment extends Fragment {
/**
* Fetches products and suppliers to populate the spinners.
*/
private void loadData() {
private void loadSpinnersData() {
loadProducts();
loadSuppliers();
}
@@ -86,13 +92,17 @@ public class ProductSupplierDetailFragment extends Fragment {
productViewModel.getAllProducts(null, 0, 200).observe(getViewLifecycleOwner(), resource -> {
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
productList = resource.data.getContent();
SpinnerUtils.populateSpinner(requireContext(), binding.spinnerPSProduct, productList,
ProductDTO::getProdName, "-- Select Product --",
preselectedProductId, ProductDTO::getProdId);
refreshProductSpinner();
}
});
}
private void refreshProductSpinner() {
SpinnerUtils.populateSpinner(requireContext(), binding.spinnerPSProduct, productList,
ProductDTO::getProdName, "-- Select Product --",
preselectedProductId, ProductDTO::getProdId);
}
/**
* Loads the list of suppliers from the API.
*/
@@ -100,33 +110,39 @@ public class ProductSupplierDetailFragment extends Fragment {
supplierViewModel.getAllSuppliers(0, 200).observe(getViewLifecycleOwner(), resource -> {
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
supplierList = resource.data.getContent();
SpinnerUtils.populateSpinner(requireContext(), binding.spinnerPSSupplier, supplierList,
SupplierDTO::getSupCompany, "-- Select Supplier --",
preselectedSupplierId, SupplierDTO::getSupId);
refreshSupplierSpinner();
}
});
}
private void refreshSupplierSpinner() {
SpinnerUtils.populateSpinner(requireContext(), binding.spinnerPSSupplier, supplierList,
SupplierDTO::getSupCompany, "-- Select Supplier --",
preselectedSupplierId, SupplierDTO::getSupId);
}
/**
* Handles arguments to determine if the fragment is in edit or add mode.
*/
private void handleArguments() {
Bundle a = getArguments();
if (a != null && a.containsKey("productId")) {
if (a != null && a.containsKey("productId") && a.containsKey("supplierId")) {
isEditing = true;
editProductId = a.getLong("productId");
editSupplierId = a.getLong("supplierId");
preselectedProductId = editProductId;
editProductId = a.getLong("productId");
editSupplierId = a.getLong("supplierId");
preselectedProductId = editProductId;
preselectedSupplierId = editSupplierId;
binding.etPSCost.setText(a.getString("cost"));
binding.tvPSMode.setText("Edit Product Supplier");
binding.btnDeletePS.setVisibility(View.VISIBLE);
} else {
binding.tvPSMode.setText("Add Product Supplier");
binding.btnDeletePS.setVisibility(View.GONE);
}
}
/**
* Validates input and saves the product-supplier to the backend.
*/

View File

@@ -3,11 +3,18 @@ package com.example.petstoremobile.fragments.listfragments.detailfragments;
import android.graphics.Color;
import android.os.Bundle;
import android.view.*;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.ViewModelProvider;
import androidx.navigation.fragment.NavHostFragment;
import com.example.petstoremobile.databinding.FragmentPurchaseOrderDetailBinding;
import com.example.petstoremobile.dtos.PurchaseOrderDTO;
import com.example.petstoremobile.utils.Resource;
import com.example.petstoremobile.viewmodels.PurchaseOrderViewModel;
import dagger.hilt.android.AndroidEntryPoint;
@@ -18,40 +25,72 @@ import dagger.hilt.android.AndroidEntryPoint;
public class PurchaseOrderDetailFragment extends Fragment {
private FragmentPurchaseOrderDetailBinding binding;
private PurchaseOrderViewModel viewModel;
private long purchaseOrderId;
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
viewModel = new ViewModelProvider(this).get(PurchaseOrderViewModel.class);
}
/**
* Inflates the layout, initializes views, and populates order data from arguments.
* Inflates the layout.
*/
@Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
binding = FragmentPurchaseOrderDetailBinding.inflate(inflater, container, false);
return binding.getRoot();
}
Bundle a = getArguments();
if (a != null) {
binding.tvPODetailId.setText("PO #" + a.getLong("purchaseOrderId"));
binding.tvPODetailSupplier.setText(a.getString("supplierName"));
binding.tvPODetailDate.setText(a.getString("orderDate"));
/**
* Initializes views and populates order data from backend after the view is created.
*/
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
String status = a.getString("status", "");
binding.tvPODetailStatus.setText(status);
switch (status) {
case "Completed":
binding.tvPODetailStatus.setTextColor(Color.parseColor("#4CAF50")); break;
case "Pending":
binding.tvPODetailStatus.setTextColor(Color.parseColor("#FF9800")); break;
case "Cancelled":
binding.tvPODetailStatus.setTextColor(Color.parseColor("#F44336")); break;
default:
binding.tvPODetailStatus.setTextColor(Color.parseColor("#9E9E9E")); break;
}
}
handleArguments();
binding.btnPOBack.setOnClickListener(v -> {
NavHostFragment.findNavController(this).popBackStack();
});
}
return binding.getRoot();
private void handleArguments() {
Bundle a = getArguments();
if (a != null && a.containsKey("purchaseOrderId")) {
purchaseOrderId = a.getLong("purchaseOrderId");
loadPurchaseOrderData();
}
}
private void loadPurchaseOrderData() {
viewModel.getPurchaseOrderById(purchaseOrderId).observe(getViewLifecycleOwner(), resource -> {
if (resource == null) return;
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
PurchaseOrderDTO po = resource.data;
binding.tvPODetailId.setText("PO #" + po.getPurchaseOrderId());
binding.tvPODetailSupplier.setText(po.getSupplierName());
binding.tvPODetailDate.setText(po.getOrderDate());
String status = po.getStatus() != null ? po.getStatus() : "";
binding.tvPODetailStatus.setText(status);
switch (status) {
case "Completed":
binding.tvPODetailStatus.setTextColor(Color.parseColor("#4CAF50")); break;
case "Pending":
binding.tvPODetailStatus.setTextColor(Color.parseColor("#FF9800")); break;
case "Cancelled":
binding.tvPODetailStatus.setTextColor(Color.parseColor("#F44336")); break;
default:
binding.tvPODetailStatus.setTextColor(Color.parseColor("#9E9E9E")); break;
}
} else if (resource.status == Resource.Status.ERROR) {
Toast.makeText(getContext(), "Failed to load order: " + resource.message, Toast.LENGTH_SHORT).show();
}
});
}
@Override

View File

@@ -2,6 +2,7 @@ package com.example.petstoremobile.fragments.listfragments.detailfragments;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.navigation.fragment.NavHostFragment;
@@ -38,15 +39,18 @@ public class RefundDetailFragment extends Fragment {
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
binding = FragmentRefundDetailBinding.inflate(inflater, container, false);
return binding.getRoot();
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
setupSpinner();
handleArguments();
binding.btnRefundBack.setOnClickListener(v -> goBack());
binding.btnLoadSale.setOnClickListener(v -> loadSaleDetails());
binding.btnProcessRefund.setOnClickListener(v -> processRefund());
return binding.getRoot();
}
@Override

View File

@@ -3,6 +3,7 @@ package com.example.petstoremobile.fragments.listfragments.detailfragments;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.ViewModelProvider;
import androidx.navigation.fragment.NavHostFragment;
@@ -30,7 +31,7 @@ import dagger.hilt.android.AndroidEntryPoint;
public class ServiceDetailFragment extends Fragment {
private FragmentServiceDetailBinding binding;
private int serviceId;
private long serviceId;
private boolean isEditing = false;
private ServiceViewModel viewModel;
@@ -45,6 +46,12 @@ public class ServiceDetailFragment extends Fragment {
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
binding = FragmentServiceDetailBinding.inflate(inflater, container, false);
return binding.getRoot();
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
//get controls from layout and display the view depending on the mode
handleArguments();
@@ -53,8 +60,6 @@ public class ServiceDetailFragment extends Fragment {
binding.btnBack.setOnClickListener(v -> navigateBack());
binding.btnSaveService.setOnClickListener(v -> saveService());
binding.btnDeleteService.setOnClickListener(v -> deleteService());
return binding.getRoot();
}
@Override
@@ -89,10 +94,10 @@ public class ServiceDetailFragment extends Fragment {
//check if the service is being edited or added
if (isEditing) {
// Update existing service
serviceDTO.setServiceId((long) serviceId);
viewModel.updateService((long) serviceId, serviceDTO).observe(getViewLifecycleOwner(), resource -> {
serviceDTO.setServiceId(serviceId);
viewModel.updateService(serviceId, serviceDTO).observe(getViewLifecycleOwner(), resource -> {
if (resource.status == Resource.Status.SUCCESS) {
ActivityLogger.logChange(requireContext(), "Service", "UPDATED", serviceId);
ActivityLogger.logChange(requireContext(), "Service", "UPDATED", (int) serviceId);
Toast.makeText(getContext(), "Service updated successfully!", Toast.LENGTH_SHORT).show();
navigateBack();
} else if (resource.status == Resource.Status.ERROR) {
@@ -117,9 +122,9 @@ public class ServiceDetailFragment extends Fragment {
*/
private void deleteService() {
DialogUtils.showDeleteConfirmDialog(requireContext(), "Service", () ->
viewModel.deleteService((long) serviceId).observe(getViewLifecycleOwner(), resource -> {
viewModel.deleteService(serviceId).observe(getViewLifecycleOwner(), resource -> {
if (resource.status == Resource.Status.SUCCESS) {
ActivityLogger.logChange(requireContext(), "Service", "DELETED", serviceId);
ActivityLogger.logChange(requireContext(), "Service", "DELETED", (int) serviceId);
Toast.makeText(getContext(), "Service deleted successfully!", Toast.LENGTH_SHORT).show();
navigateBack();
} else if (resource.status == Resource.Status.ERROR) {
@@ -143,14 +148,11 @@ public class ServiceDetailFragment extends Fragment {
if (getArguments() != null && getArguments().containsKey("serviceId")) {
// Get service data from arguments and populate fields
isEditing = true;
serviceId = getArguments().getInt("serviceId");
serviceId = getArguments().getLong("serviceId");
binding.tvMode.setText("Edit Service");
binding.tvServiceId.setText("ID: " + serviceId);
binding.etServiceName.setText(getArguments().getString("serviceName"));
binding.etServiceDesc.setText(getArguments().getString("serviceDesc"));
binding.etServiceDuration.setText(String.valueOf(getArguments().getInt("serviceDuration")));
binding.etServicePrice.setText(String.valueOf(getArguments().getDouble("servicePrice")));
binding.btnDeleteService.setVisibility(View.VISIBLE);
loadServiceData();
} else {
// Service is being added
// Set default values for add a new service
@@ -161,4 +163,22 @@ public class ServiceDetailFragment extends Fragment {
binding.btnSaveService.setText("Add");
}
}
/**
* Fetches specific service details from the backend using the ID.
*/
private void loadServiceData() {
viewModel.getServiceById(serviceId).observe(getViewLifecycleOwner(), resource -> {
if (resource == null) return;
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
ServiceDTO s = resource.data;
binding.etServiceName.setText(s.getServiceName());
binding.etServiceDesc.setText(s.getServiceDesc());
binding.etServiceDuration.setText(String.valueOf(s.getServiceDuration()));
binding.etServicePrice.setText(String.valueOf(s.getServicePrice()));
} else if (resource.status == Resource.Status.ERROR) {
Toast.makeText(getContext(), "Failed to load service: " + resource.message, Toast.LENGTH_SHORT).show();
}
});
}
}

View File

@@ -3,6 +3,7 @@ package com.example.petstoremobile.fragments.listfragments.detailfragments;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.ViewModelProvider;
import androidx.navigation.fragment.NavHostFragment;
@@ -30,7 +31,7 @@ import dagger.hilt.android.AndroidEntryPoint;
public class SupplierDetailFragment extends Fragment {
private FragmentSupplierDetailBinding binding;
private int supId;
private long supId;
private boolean isEditing = false;
private SupplierViewModel viewModel;
@@ -45,6 +46,12 @@ public class SupplierDetailFragment extends Fragment {
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
binding = FragmentSupplierDetailBinding.inflate(inflater, container, false);
return binding.getRoot();
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
// Add phone number formatting (CA) and limit length to 14 characters
UIUtils.formatPhoneInput(binding.etSupPhone);
@@ -55,8 +62,6 @@ public class SupplierDetailFragment extends Fragment {
binding.btnBack.setOnClickListener(v -> navigateBack());
binding.btnSaveSupplier.setOnClickListener(v -> saveSupplier());
binding.btnDeleteSupplier.setOnClickListener(v -> deleteSupplier());
return binding.getRoot();
}
@Override
@@ -94,10 +99,10 @@ public class SupplierDetailFragment extends Fragment {
//check if the supplier is being edited or added
if (isEditing) {
// Update existing supplier
supplierDTO.setSupId((long) supId);
viewModel.updateSupplier((long) supId, supplierDTO).observe(getViewLifecycleOwner(), resource -> {
supplierDTO.setSupId(supId);
viewModel.updateSupplier(supId, supplierDTO).observe(getViewLifecycleOwner(), resource -> {
if (resource.status == Resource.Status.SUCCESS) {
ActivityLogger.logChange(requireContext(), "Supplier", "UPDATED", supId);
ActivityLogger.logChange(requireContext(), "Supplier", "UPDATED", (int) supId);
Toast.makeText(getContext(), "Supplier updated successfully!", Toast.LENGTH_SHORT).show();
navigateBack();
} else if (resource.status == Resource.Status.ERROR) {
@@ -123,9 +128,9 @@ public class SupplierDetailFragment extends Fragment {
*/
private void deleteSupplier() {
DialogUtils.showDeleteConfirmDialog(requireContext(), "Supplier", () ->
viewModel.deleteSupplier((long) supId).observe(getViewLifecycleOwner(), resource -> {
viewModel.deleteSupplier(supId).observe(getViewLifecycleOwner(), resource -> {
if (resource.status == Resource.Status.SUCCESS) {
ActivityLogger.logChange(requireContext(), "Supplier", "DELETED", supId);
ActivityLogger.logChange(requireContext(), "Supplier", "DELETED", (int) supId);
Toast.makeText(getContext(), "Supplier deleted successfully!", Toast.LENGTH_SHORT).show();
navigateBack();
} else if (resource.status == Resource.Status.ERROR) {
@@ -149,15 +154,12 @@ public class SupplierDetailFragment extends Fragment {
if (getArguments() != null && getArguments().containsKey("supId")) {
// Get supplier data from arguments and populate fields
isEditing = true;
supId = getArguments().getInt("supId");
supId = getArguments().getLong("supId");
binding.tvMode.setText("Edit Supplier");
binding.tvSupId.setText("ID: " + supId);
binding.etSupCompany.setText(getArguments().getString("supCompany"));
binding.etSupContactFirstName.setText(getArguments().getString("supContactFirstName"));
binding.etSupContactLastName.setText(getArguments().getString("supContactLastName"));
binding.etSupEmail.setText(getArguments().getString("supEmail"));
binding.etSupPhone.setText(getArguments().getString("supPhone"));
binding.tvSupId.setVisibility(View.VISIBLE);
binding.btnDeleteSupplier.setVisibility(View.VISIBLE);
loadSupplierData();
} else {
// Supplier is being added
// Set default values for add a new supplier
@@ -168,4 +170,23 @@ public class SupplierDetailFragment extends Fragment {
binding.btnSaveSupplier.setText("Add");
}
}
/**
* Fetches specific supplier details from the backend using the ID.
*/
private void loadSupplierData() {
viewModel.getSupplierById(supId).observe(getViewLifecycleOwner(), resource -> {
if (resource == null) return;
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
SupplierDTO s = resource.data;
binding.etSupCompany.setText(s.getSupCompany());
binding.etSupContactFirstName.setText(s.getSupContactFirstName());
binding.etSupContactLastName.setText(s.getSupContactLastName());
binding.etSupEmail.setText(s.getSupEmail());
binding.etSupPhone.setText(s.getSupPhone());
} else if (resource.status == Resource.Status.ERROR) {
Toast.makeText(getContext(), "Failed to load supplier: " + resource.message, Toast.LENGTH_SHORT).show();
}
});
}
}

View File

@@ -3,24 +3,27 @@ package com.example.petstoremobile.fragments.listfragments.listprofilefragments;
import android.net.Uri;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.ViewModelProvider;
import androidx.navigation.fragment.NavHostFragment;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast;
import com.example.petstoremobile.R;
import com.example.petstoremobile.api.PetApi;
import com.example.petstoremobile.api.auth.TokenManager;
import com.example.petstoremobile.databinding.FragmentPetProfileBinding;
import com.example.petstoremobile.dtos.PetDTO;
import com.example.petstoremobile.utils.FileUtils;
import com.example.petstoremobile.utils.GlideUtils;
import com.example.petstoremobile.utils.ImagePickerHelper;
import com.example.petstoremobile.utils.RetrofitUtils;
import com.example.petstoremobile.utils.Resource;
import com.example.petstoremobile.viewmodels.PetViewModel;
import java.io.File;
import java.util.Locale;
@@ -36,16 +39,14 @@ import okhttp3.RequestBody;
@AndroidEntryPoint
public class PetProfileFragment extends Fragment {
private TextView tvPetName, tvPetSpecies, tvPetBreed, tvPetAge, tvPetPrice;
private Button btnBack, btnEditPet, btnChangePhoto;
private ImageView imgPet;
private int petId;
private FragmentPetProfileBinding binding;
private long petId;
private boolean hasImage = false;
@Inject PetApi petApi;
@Inject @Named("baseUrl") String baseUrl;
@Inject TokenManager tokenManager;
private PetViewModel viewModel;
private ImagePickerHelper imagePickerHelper;
@@ -55,6 +56,7 @@ public class PetProfileFragment extends Fragment {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
viewModel = new ViewModelProvider(this).get(PetViewModel.class);
imagePickerHelper = new ImagePickerHelper(this, "pet_photo.jpg", new ImagePickerHelper.ImagePickerListener() {
@Override
@@ -70,65 +72,77 @@ public class PetProfileFragment extends Fragment {
}
/**
* Inflates the layout, initializes views, and sets up click listeners.
* Inflates the layout using view binding, initializes views, and sets up click listeners.
*/
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_pet_profile, container, false);
// Initialize views
tvPetName = view.findViewById(R.id.tvPetName);
tvPetSpecies = view.findViewById(R.id.tvPetSpecies);
tvPetBreed = view.findViewById(R.id.tvPetBreed);
tvPetAge = view.findViewById(R.id.tvPetAge);
tvPetPrice = view.findViewById(R.id.tvPetPrice);
btnBack = view.findViewById(R.id.btnBack);
btnEditPet = view.findViewById(R.id.btnEditPet);
btnChangePhoto = view.findViewById(R.id.btnChangePhoto);
imgPet = view.findViewById(R.id.imgPet);
binding = FragmentPetProfileBinding.inflate(inflater, container, false);
// Set pet details to display
if (getArguments() != null) {
petId = getArguments().getInt("petId");
tvPetName.setText(getArguments().getString("petName"));
tvPetSpecies.setText(getArguments().getString("petSpecies"));
tvPetBreed.setText(getArguments().getString("petBreed"));
tvPetAge.setText(String.format(Locale.getDefault(), "%d yr(s)", getArguments().getInt("petAge")));
tvPetPrice.setText(String.format(Locale.getDefault(), "$%.2f", getArguments().getDouble("petPrice")));
// Load pet image from backend
loadPetImage(petId);
petId = getArguments().getLong("petId");
loadPetData();
loadPetImage((int) petId);
}
//set button click listeners
btnBack.setOnClickListener(v -> {
binding.btnBack.setOnClickListener(v -> {
NavHostFragment.findNavController(this).popBackStack();
});
//Make the edit button go to the pet detail view
btnEditPet.setOnClickListener(v -> {
if (getArguments() == null) return;
NavHostFragment.findNavController(this).navigate(R.id.nav_pet_detail, getArguments());
binding.btnEditPet.setOnClickListener(v -> {
Bundle args = new Bundle();
args.putLong("petId", petId);
NavHostFragment.findNavController(this).navigate(R.id.nav_pet_detail, args);
});
//Make change photo button ask user to select a new photo
btnChangePhoto.setOnClickListener(v -> {
binding.btnChangePhoto.setOnClickListener(v -> {
imagePickerHelper.showImagePickerDialog("Change Pet Photo", hasImage);
});
return view;
return binding.getRoot();
}
@Override
public void onDestroyView() {
super.onDestroyView();
binding = null;
}
/**
* Fetches and displays the pet's image from the server.
* Fetches current pet data from the backend and updates the UI.
*/
private void loadPetData() {
viewModel.getPetById(petId).observe(getViewLifecycleOwner(), resource -> {
if (resource == null) return;
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
PetDTO pet = resource.data;
binding.tvPetName.setText(pet.getPetName());
binding.tvPetSpecies.setText(pet.getPetSpecies());
binding.tvPetBreed.setText(pet.getPetBreed());
binding.tvPetAge.setText(String.format(Locale.getDefault(), "%d yr(s)", pet.getPetAge()));
try {
binding.tvPetPrice.setText(String.format(Locale.getDefault(), "$%.2f", Double.parseDouble(pet.getPetPrice())));
} catch (Exception e) {
binding.tvPetPrice.setText("$0.00");
}
} else if (resource.status == Resource.Status.ERROR) {
Toast.makeText(getContext(), "Failed to load pet data: " + resource.message, Toast.LENGTH_SHORT).show();
}
});
}
/**
* Fetches and displays the pet\'s image from the server.
*/
private void loadPetImage(int petId) {
String imageUrl = baseUrl + String.format(Locale.US, PetApi.PET_IMAGE_PATH, petId);
String token = tokenManager.getToken();
GlideUtils.loadImageWithToken(requireContext(), imgPet, imageUrl, token, R.drawable.placeholder, new GlideUtils.ImageLoadListener() {
GlideUtils.loadImageWithToken(requireContext(), binding.imgPet, imageUrl, token, R.drawable.placeholder, new GlideUtils.ImageLoadListener() {
@Override
public void onResourceReady() {
hasImage = true;
@@ -142,7 +156,7 @@ public class PetProfileFragment extends Fragment {
}
/**
* Uploads a selected or captured image a pet photo through the API.
* Uploads a selected or captured image a pet photo through the ViewModel.
*/
private void uploadPetImage(Uri uri) {
try {
@@ -153,30 +167,36 @@ public class PetProfileFragment extends Fragment {
RequestBody requestFile = RequestBody.create(file, MediaType.parse(requireContext().getContentResolver().getType(uri)));
MultipartBody.Part body = MultipartBody.Part.createFormData("image", file.getName(), requestFile);
// Call the backend to upload the image
petApi.uploadPetImage((long) petId, body).enqueue(RetrofitUtils.createCallback(
requireContext(),
"UPLOAD_PET_IMAGE",
"Pet photo updated successfully",
result -> loadPetImage(petId)
));
// Use ViewModel to upload image
viewModel.uploadPetImage(petId, body).observe(getViewLifecycleOwner(), resource -> {
if (resource != null && resource.status != Resource.Status.LOADING) {
if (resource.status == Resource.Status.SUCCESS) {
Toast.makeText(getContext(), "Pet photo updated successfully", Toast.LENGTH_SHORT).show();
loadPetImage((int) petId);
} else {
Toast.makeText(getContext(), "Upload failed: " + resource.message, Toast.LENGTH_SHORT).show();
}
}
});
} catch (Exception e) {
Log.e("UPLOAD_PET_IMAGE", "Error: " + e.getMessage());
}
}
/**
* Sends a request to the API to remove the current pet photo.
* Sends a request to the ViewModel to remove the current pet photo.
*/
private void deletePetImage() {
petApi.deletePetImage((long) petId).enqueue(RetrofitUtils.createCallback(
requireContext(),
"DELETE_PET_IMAGE",
"Pet photo removed",
result -> {
viewModel.deletePetImage(petId).observe(getViewLifecycleOwner(), resource -> {
if (resource != null && resource.status != Resource.Status.LOADING) {
if (resource.status == Resource.Status.SUCCESS) {
Toast.makeText(getContext(), "Pet photo removed", Toast.LENGTH_SHORT).show();
hasImage = false;
imgPet.setImageResource(R.drawable.placeholder);
binding.imgPet.setImageResource(R.drawable.placeholder);
} else {
Toast.makeText(getContext(), "Delete failed: " + resource.message, Toast.LENGTH_SHORT).show();
}
));
}
});
}
}

View File

@@ -1,24 +1,22 @@
package com.example.petstoremobile.repositories;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import com.example.petstoremobile.api.AdoptionApi;
import com.example.petstoremobile.dtos.AdoptionDTO;
import com.example.petstoremobile.dtos.PageResponse;
import com.example.petstoremobile.utils.Resource;
import com.example.petstoremobile.utils.RetrofitUtils;
import javax.inject.Inject;
import javax.inject.Singleton;
@Singleton
public class AdoptionRepository {
private static final String TAG = "AdoptionRepository";
public class AdoptionRepository extends BaseRepository {
private final AdoptionApi adoptionApi;
@Inject
public AdoptionRepository(AdoptionApi adoptionApi) {
super("AdoptionRepository");
this.adoptionApi = adoptionApi;
}
@@ -26,44 +24,34 @@ public class AdoptionRepository {
* Retrieves a paginated list of all adoptions from the API.
*/
public LiveData<Resource<PageResponse<AdoptionDTO>>> getAllAdoptions(int page, int size) {
MutableLiveData<Resource<PageResponse<AdoptionDTO>>> data = new MutableLiveData<>();
RetrofitUtils.enqueue(adoptionApi.getAllAdoptions(page, size), data, TAG);
return data;
return executeCall(adoptionApi.getAllAdoptions(page, size));
}
/**
* Retrieves a specific adoption record by its ID from the API.
*/
public LiveData<Resource<AdoptionDTO>> getAdoptionById(Long id) {
MutableLiveData<Resource<AdoptionDTO>> data = new MutableLiveData<>();
RetrofitUtils.enqueue(adoptionApi.getAdoptionById(id), data, TAG);
return data;
return executeCall(adoptionApi.getAdoptionById(id));
}
/**
* Sends a request to the API to create a new adoption record.
*/
public LiveData<Resource<AdoptionDTO>> createAdoption(AdoptionDTO adoption) {
MutableLiveData<Resource<AdoptionDTO>> data = new MutableLiveData<>();
RetrofitUtils.enqueue(adoptionApi.createAdoption(adoption), data, TAG);
return data;
return executeCall(adoptionApi.createAdoption(adoption));
}
/**
* Sends a request to the API to update an existing adoption record by ID.
*/
public LiveData<Resource<AdoptionDTO>> updateAdoption(Long id, AdoptionDTO adoption) {
MutableLiveData<Resource<AdoptionDTO>> data = new MutableLiveData<>();
RetrofitUtils.enqueue(adoptionApi.updateAdoption(id, adoption), data, TAG);
return data;
return executeCall(adoptionApi.updateAdoption(id, adoption));
}
/**
* Sends a request to the API to delete a specific adoption record.
*/
public LiveData<Resource<Void>> deleteAdoption(Long id) {
MutableLiveData<Resource<Void>> data = new MutableLiveData<>();
RetrofitUtils.enqueue(adoptionApi.deleteAdoption(id), data, TAG);
return data;
return executeCall(adoptionApi.deleteAdoption(id));
}
}

View File

@@ -1,24 +1,22 @@
package com.example.petstoremobile.repositories;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import com.example.petstoremobile.api.AppointmentApi;
import com.example.petstoremobile.dtos.AppointmentDTO;
import com.example.petstoremobile.dtos.PageResponse;
import com.example.petstoremobile.utils.Resource;
import com.example.petstoremobile.utils.RetrofitUtils;
import javax.inject.Inject;
import javax.inject.Singleton;
@Singleton
public class AppointmentRepository {
private static final String TAG = "AppointmentRepository";
public class AppointmentRepository extends BaseRepository {
private final AppointmentApi appointmentApi;
@Inject
public AppointmentRepository(AppointmentApi appointmentApi) {
super("AppointmentRepository");
this.appointmentApi = appointmentApi;
}
@@ -26,44 +24,34 @@ public class AppointmentRepository {
* Retrieves a paginated list of all appointments from the API.
*/
public LiveData<Resource<PageResponse<AppointmentDTO>>> getAllAppointments(int page, int size) {
MutableLiveData<Resource<PageResponse<AppointmentDTO>>> data = new MutableLiveData<>();
RetrofitUtils.enqueue(appointmentApi.getAllAppointments(page, size), data, TAG);
return data;
return executeCall(appointmentApi.getAllAppointments(page, size));
}
/**
* Retrieves a specific appointment by its ID from the API.
*/
public LiveData<Resource<AppointmentDTO>> getAppointmentById(Long id) {
MutableLiveData<Resource<AppointmentDTO>> data = new MutableLiveData<>();
RetrofitUtils.enqueue(appointmentApi.getAppointmentById(id), data, TAG);
return data;
return executeCall(appointmentApi.getAppointmentById(id));
}
/**
* Sends a request to the API to create a new appointment record.
*/
public LiveData<Resource<AppointmentDTO>> createAppointment(AppointmentDTO appointment) {
MutableLiveData<Resource<AppointmentDTO>> data = new MutableLiveData<>();
RetrofitUtils.enqueue(appointmentApi.createAppointment(appointment), data, TAG);
return data;
return executeCall(appointmentApi.createAppointment(appointment));
}
/**
* Sends a request to the API to update an existing appointment record by ID.
*/
public LiveData<Resource<AppointmentDTO>> updateAppointment(Long id, AppointmentDTO appointment) {
MutableLiveData<Resource<AppointmentDTO>> data = new MutableLiveData<>();
RetrofitUtils.enqueue(appointmentApi.updateAppointment(id, appointment), data, TAG);
return data;
return executeCall(appointmentApi.updateAppointment(id, appointment));
}
/**
* Sends a request to the API to delete a specific appointment record.
*/
public LiveData<Resource<Void>> deleteAppointment(Long id) {
MutableLiveData<Resource<Void>> data = new MutableLiveData<>();
RetrofitUtils.enqueue(appointmentApi.deleteAppointment(id), data, TAG);
return data;
return executeCall(appointmentApi.deleteAppointment(id));
}
}

View File

@@ -1,7 +1,5 @@
package com.example.petstoremobile.repositories;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
@@ -12,7 +10,6 @@ import com.example.petstoremobile.dtos.AuthDTO;
import com.example.petstoremobile.dtos.UserDTO;
import com.example.petstoremobile.utils.ErrorUtils;
import com.example.petstoremobile.utils.Resource;
import com.example.petstoremobile.utils.RetrofitUtils;
import java.util.Map;
@@ -25,13 +22,13 @@ import retrofit2.Callback;
import retrofit2.Response;
@Singleton
public class AuthRepository {
private static final String TAG = "AuthRepository";
public class AuthRepository extends BaseRepository {
private final AuthApi authApi;
private final TokenManager tokenManager;
@Inject
public AuthRepository(AuthApi authApi, TokenManager tokenManager) {
super("AuthRepository");
this.authApi = authApi;
this.tokenManager = tokenManager;
}
@@ -62,7 +59,7 @@ public class AuthRepository {
@Override
public void onFailure(@NonNull Call<AuthDTO.LoginResponse> call, @NonNull Throwable t) {
data.setValue(Resource.error("Network error: " + t.getMessage(), null));
data.setValue(Resource.error(ErrorUtils.getFailureMessage(t), null));
}
});
@@ -73,36 +70,28 @@ public class AuthRepository {
* Retrieves the current user's profile information from the API.
*/
public LiveData<Resource<UserDTO>> getMe() {
MutableLiveData<Resource<UserDTO>> data = new MutableLiveData<>();
RetrofitUtils.enqueue(authApi.getMe(), data, TAG);
return data;
return executeCall(authApi.getMe());
}
/**
* Updates the current user's profile details.
*/
public LiveData<Resource<UserDTO>> updateMe(Map<String, String> updates) {
MutableLiveData<Resource<UserDTO>> data = new MutableLiveData<>();
RetrofitUtils.enqueue(authApi.updateMe(updates), data, TAG);
return data;
return executeCall(authApi.updateMe(updates));
}
/**
* Uploads a multipart image to be used as the current user's avatar.
*/
public LiveData<Resource<UserDTO>> uploadAvatar(MultipartBody.Part avatar) {
MutableLiveData<Resource<UserDTO>> data = new MutableLiveData<>();
RetrofitUtils.enqueue(authApi.uploadAvatar(avatar), data, TAG);
return data;
return executeCall(authApi.uploadAvatar(avatar));
}
/**
* Sends a request to the API to remove the current user's avatar.
*/
public LiveData<Resource<Void>> deleteAvatar() {
MutableLiveData<Resource<Void>> data = new MutableLiveData<>();
RetrofitUtils.enqueue(authApi.deleteAvatar(), data, TAG);
return data;
return executeCall(authApi.deleteAvatar());
}
/**

View File

@@ -0,0 +1,29 @@
package com.example.petstoremobile.repositories;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import com.example.petstoremobile.utils.Resource;
import com.example.petstoremobile.utils.RetrofitUtils;
import retrofit2.Call;
/**
* Base class for all repositories to provide common functionality for API calls.
*/
public abstract class BaseRepository {
protected final String TAG;
protected BaseRepository(String tag) {
this.TAG = tag;
}
/**
* Executes a Retrofit call and returns a LiveData containing the Resource.
*/
protected <T> LiveData<Resource<T>> executeCall(Call<T> call) {
MutableLiveData<Resource<T>> data = new MutableLiveData<>();
RetrofitUtils.enqueue(call, data, TAG);
return data;
}
}

View File

@@ -1,24 +1,22 @@
package com.example.petstoremobile.repositories;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import com.example.petstoremobile.api.CategoryApi;
import com.example.petstoremobile.dtos.CategoryDTO;
import com.example.petstoremobile.dtos.PageResponse;
import com.example.petstoremobile.utils.Resource;
import com.example.petstoremobile.utils.RetrofitUtils;
import javax.inject.Inject;
import javax.inject.Singleton;
@Singleton
public class CategoryRepository {
private static final String TAG = "CategoryRepository";
public class CategoryRepository extends BaseRepository {
private final CategoryApi categoryApi;
@Inject
public CategoryRepository(CategoryApi categoryApi) {
super("CategoryRepository");
this.categoryApi = categoryApi;
}
@@ -26,8 +24,6 @@ public class CategoryRepository {
* Retrieves a paginated list of all product categories from the API.
*/
public LiveData<Resource<PageResponse<CategoryDTO>>> getAllCategories(int page, int size) {
MutableLiveData<Resource<PageResponse<CategoryDTO>>> data = new MutableLiveData<>();
RetrofitUtils.enqueue(categoryApi.getAllCategories(page, size), data, TAG);
return data;
return executeCall(categoryApi.getAllCategories(page, size));
}
}

View File

@@ -0,0 +1,74 @@
package com.example.petstoremobile.repositories;
import androidx.lifecycle.LiveData;
import com.example.petstoremobile.api.ChatApi;
import com.example.petstoremobile.api.CustomerApi;
import com.example.petstoremobile.api.MessageApi;
import com.example.petstoremobile.dtos.ConversationDTO;
import com.example.petstoremobile.dtos.CustomerDTO;
import com.example.petstoremobile.dtos.MessageDTO;
import com.example.petstoremobile.dtos.PageResponse;
import com.example.petstoremobile.dtos.SendMessageRequest;
import com.example.petstoremobile.utils.Resource;
import java.util.List;
import javax.inject.Inject;
import javax.inject.Singleton;
import okhttp3.MultipartBody;
import okhttp3.RequestBody;
/**
* Repository for handling chat-related data operations.
*/
@Singleton
public class ChatRepository extends BaseRepository {
private final ChatApi chatApi;
private final MessageApi messageApi;
private final CustomerApi customerApi;
@Inject
public ChatRepository(ChatApi chatApi, MessageApi messageApi, CustomerApi customerApi) {
super("ChatRepository");
this.chatApi = chatApi;
this.messageApi = messageApi;
this.customerApi = customerApi;
}
/**
* Retrieves all chat conversations for the current user.
*/
public LiveData<Resource<List<ConversationDTO>>> getAllConversations() {
return executeCall(chatApi.getAllConversations());
}
/**
* Retrieves the message history for a specific conversation.
*/
public LiveData<Resource<List<MessageDTO>>> getMessages(Long conversationId) {
return executeCall(messageApi.getMessages(conversationId));
}
/**
* Sends a plain text message to a conversation.
*/
public LiveData<Resource<MessageDTO>> sendMessage(Long conversationId, SendMessageRequest request) {
return executeCall(messageApi.sendMessage(conversationId, request));
}
/**
* Sends a message with a file attachment to a conversation.
*/
public LiveData<Resource<MessageDTO>> sendMessageWithAttachment(Long conversationId, RequestBody content, MultipartBody.Part file) {
return executeCall(messageApi.sendMessageWithAttachment(conversationId, content, file));
}
/**
* Fetches a paginated list of customers.
*/
public LiveData<Resource<PageResponse<CustomerDTO>>> getAllCustomers(int page, int size) {
return executeCall(customerApi.getAllCustomers(page, size));
}
}

View File

@@ -1,24 +1,22 @@
package com.example.petstoremobile.repositories;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import com.example.petstoremobile.api.CustomerApi;
import com.example.petstoremobile.dtos.CustomerDTO;
import com.example.petstoremobile.dtos.PageResponse;
import com.example.petstoremobile.utils.Resource;
import com.example.petstoremobile.utils.RetrofitUtils;
import javax.inject.Inject;
import javax.inject.Singleton;
@Singleton
public class CustomerRepository {
private static final String TAG = "CustomerRepository";
public class CustomerRepository extends BaseRepository {
private final CustomerApi customerApi;
@Inject
public CustomerRepository(CustomerApi customerApi) {
super("CustomerRepository");
this.customerApi = customerApi;
}
@@ -26,17 +24,13 @@ public class CustomerRepository {
* Retrieves a paginated list of all customers from the API.
*/
public LiveData<Resource<PageResponse<CustomerDTO>>> getAllCustomers(int page, int size) {
MutableLiveData<Resource<PageResponse<CustomerDTO>>> data = new MutableLiveData<>();
RetrofitUtils.enqueue(customerApi.getAllCustomers(page, size), data, TAG);
return data;
return executeCall(customerApi.getAllCustomers(page, size));
}
/**
* Retrieves a specific customer by their ID.
*/
public LiveData<Resource<CustomerDTO>> getCustomerById(Long id) {
MutableLiveData<Resource<CustomerDTO>> data = new MutableLiveData<>();
RetrofitUtils.enqueue(customerApi.getCustomerById(id), data, TAG);
return data;
return executeCall(customerApi.getCustomerById(id));
}
}

View File

@@ -1,7 +1,6 @@
package com.example.petstoremobile.repositories;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import com.example.petstoremobile.api.InventoryApi;
import com.example.petstoremobile.dtos.BulkDeleteRequest;
@@ -9,20 +8,17 @@ import com.example.petstoremobile.dtos.InventoryDTO;
import com.example.petstoremobile.dtos.InventoryRequest;
import com.example.petstoremobile.dtos.PageResponse;
import com.example.petstoremobile.utils.Resource;
import com.example.petstoremobile.utils.RetrofitUtils;
import java.util.List;
import javax.inject.Inject;
import javax.inject.Singleton;
@Singleton
public class InventoryRepository {
private static final String TAG = "InventoryRepository";
public class InventoryRepository extends BaseRepository {
private final InventoryApi inventoryApi;
@Inject
public InventoryRepository(InventoryApi inventoryApi) {
super("InventoryRepository");
this.inventoryApi = inventoryApi;
}
@@ -30,47 +26,35 @@ public class InventoryRepository {
* Retrieves a paginated list of inventory items from the API with optional search and sort.
*/
public LiveData<Resource<PageResponse<InventoryDTO>>> getAllInventory(String query, int page, int size, String sort) {
MutableLiveData<Resource<PageResponse<InventoryDTO>>> data = new MutableLiveData<>();
RetrofitUtils.enqueue(inventoryApi.getAllInventory(query, page, size, sort), data, TAG);
return data;
return executeCall(inventoryApi.getAllInventory(query, page, size, sort));
}
/**
* Retrieves a specific inventory item by its ID from the API.
*/
public LiveData<Resource<InventoryDTO>> getInventoryById(Long id) {
MutableLiveData<Resource<InventoryDTO>> data = new MutableLiveData<>();
RetrofitUtils.enqueue(inventoryApi.getInventoryById(id), data, TAG);
return data;
return executeCall(inventoryApi.getInventoryById(id));
}
/**
* Sends a request to the API to create a new inventory record.
*/
public LiveData<Resource<InventoryDTO>> createInventory(InventoryRequest request) {
MutableLiveData<Resource<InventoryDTO>> data = new MutableLiveData<>();
RetrofitUtils.enqueue(inventoryApi.createInventory(request), data, TAG);
return data;
return executeCall(inventoryApi.createInventory(request));
}
public LiveData<Resource<InventoryDTO>> updateInventory(Long id, InventoryRequest request) {
MutableLiveData<Resource<InventoryDTO>> data = new MutableLiveData<>();
RetrofitUtils.enqueue(inventoryApi.updateInventory(id, request), data, TAG);
return data;
return executeCall(inventoryApi.updateInventory(id, request));
}
/**
* Sends a request to the API to delete a specific inventory record.
*/
public LiveData<Resource<Void>> deleteInventory(Long id) {
MutableLiveData<Resource<Void>> data = new MutableLiveData<>();
RetrofitUtils.enqueue(inventoryApi.deleteInventory(id), data, TAG);
return data;
return executeCall(inventoryApi.deleteInventory(id));
}
public LiveData<Resource<Void>> bulkDeleteInventory(BulkDeleteRequest request) {
MutableLiveData<Resource<Void>> data = new MutableLiveData<>();
RetrofitUtils.enqueue(inventoryApi.bulkDeleteInventory(request), data, TAG);
return data;
return executeCall(inventoryApi.bulkDeleteInventory(request));
}
}

View File

@@ -1,13 +1,11 @@
package com.example.petstoremobile.repositories;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import com.example.petstoremobile.api.PetApi;
import com.example.petstoremobile.dtos.PageResponse;
import com.example.petstoremobile.dtos.PetDTO;
import com.example.petstoremobile.utils.Resource;
import com.example.petstoremobile.utils.RetrofitUtils;
import javax.inject.Inject;
import javax.inject.Singleton;
@@ -15,12 +13,12 @@ import javax.inject.Singleton;
import okhttp3.MultipartBody;
@Singleton
public class PetRepository {
private static final String TAG = "PetRepository";
public class PetRepository extends BaseRepository {
private final PetApi petApi;
@Inject
public PetRepository(PetApi petApi) {
super("PetRepository");
this.petApi = petApi;
}
@@ -28,62 +26,48 @@ public class PetRepository {
* Retrieves a paginated list of all pets from the API.
*/
public LiveData<Resource<PageResponse<PetDTO>>> getAllPets(int page, int size) {
MutableLiveData<Resource<PageResponse<PetDTO>>> data = new MutableLiveData<>();
RetrofitUtils.enqueue(petApi.getAllPets(page, size), data, TAG);
return data;
return executeCall(petApi.getAllPets(page, size));
}
/**
* Retrieves a specific pet by its ID from the API.
*/
public LiveData<Resource<PetDTO>> getPetById(Long id) {
MutableLiveData<Resource<PetDTO>> data = new MutableLiveData<>();
RetrofitUtils.enqueue(petApi.getPetById(id), data, TAG);
return data;
return executeCall(petApi.getPetById(id));
}
/**
* Sends a request to the API to create a new pet record.
*/
public LiveData<Resource<PetDTO>> createPet(PetDTO pet) {
MutableLiveData<Resource<PetDTO>> data = new MutableLiveData<>();
RetrofitUtils.enqueue(petApi.createPet(pet), data, TAG);
return data;
return executeCall(petApi.createPet(pet));
}
/**
* Sends a request to the API to update an existing pet record.
*/
public LiveData<Resource<PetDTO>> updatePet(Long id, PetDTO pet) {
MutableLiveData<Resource<PetDTO>> data = new MutableLiveData<>();
RetrofitUtils.enqueue(petApi.updatePet(id, pet), data, TAG);
return data;
return executeCall(petApi.updatePet(id, pet));
}
/**
* Sends a request to the API to delete a specific pet record.
*/
public LiveData<Resource<Void>> deletePet(Long id) {
MutableLiveData<Resource<Void>> data = new MutableLiveData<>();
RetrofitUtils.enqueue(petApi.deletePet(id), data, TAG);
return data;
return executeCall(petApi.deletePet(id));
}
/**
* Uploads an image file for a specific pet via the API.
*/
public LiveData<Resource<Void>> uploadPetImage(Long id, MultipartBody.Part image) {
MutableLiveData<Resource<Void>> data = new MutableLiveData<>();
RetrofitUtils.enqueue(petApi.uploadPetImage(id, image), data, TAG);
return data;
return executeCall(petApi.uploadPetImage(id, image));
}
/**
* Sends a request to the API to delete the image of a specific pet.
*/
public LiveData<Resource<Void>> deletePetImage(Long id) {
MutableLiveData<Resource<Void>> data = new MutableLiveData<>();
RetrofitUtils.enqueue(petApi.deletePetImage(id), data, TAG);
return data;
return executeCall(petApi.deletePetImage(id));
}
}

View File

@@ -1,13 +1,11 @@
package com.example.petstoremobile.repositories;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import com.example.petstoremobile.api.ProductApi;
import com.example.petstoremobile.dtos.PageResponse;
import com.example.petstoremobile.dtos.ProductDTO;
import com.example.petstoremobile.utils.Resource;
import com.example.petstoremobile.utils.RetrofitUtils;
import javax.inject.Inject;
import javax.inject.Singleton;
@@ -15,12 +13,12 @@ import javax.inject.Singleton;
import okhttp3.MultipartBody;
@Singleton
public class ProductRepository {
private static final String TAG = "ProductRepository";
public class ProductRepository extends BaseRepository {
private final ProductApi productApi;
@Inject
public ProductRepository(ProductApi productApi) {
super("ProductRepository");
this.productApi = productApi;
}
@@ -28,62 +26,48 @@ public class ProductRepository {
* Retrieves a paginated list of products from the API, filtered by an optional query.
*/
public LiveData<Resource<PageResponse<ProductDTO>>> getAllProducts(String query, int page, int size) {
MutableLiveData<Resource<PageResponse<ProductDTO>>> data = new MutableLiveData<>();
RetrofitUtils.enqueue(productApi.getAllProducts(query, page, size), data, TAG);
return data;
return executeCall(productApi.getAllProducts(query, page, size));
}
/**
* Retrieves a specific product by its ID from the API.
*/
public LiveData<Resource<ProductDTO>> getProductById(Long id) {
MutableLiveData<Resource<ProductDTO>> data = new MutableLiveData<>();
RetrofitUtils.enqueue(productApi.getProductById(id), data, TAG);
return data;
return executeCall(productApi.getProductById(id));
}
/**
* Sends a request to the API to create a new product.
*/
public LiveData<Resource<ProductDTO>> createProduct(ProductDTO product) {
MutableLiveData<Resource<ProductDTO>> data = new MutableLiveData<>();
RetrofitUtils.enqueue(productApi.createProduct(product), data, TAG);
return data;
return executeCall(productApi.createProduct(product));
}
/**
* Sends a request to the API to update an existing product by ID.
*/
public LiveData<Resource<ProductDTO>> updateProduct(Long id, ProductDTO product) {
MutableLiveData<Resource<ProductDTO>> data = new MutableLiveData<>();
RetrofitUtils.enqueue(productApi.updateProduct(id, product), data, TAG);
return data;
return executeCall(productApi.updateProduct(id, product));
}
/**
* Sends a request to the API to delete a specific product.
*/
public LiveData<Resource<Void>> deleteProduct(Long id) {
MutableLiveData<Resource<Void>> data = new MutableLiveData<>();
RetrofitUtils.enqueue(productApi.deleteProduct(id), data, TAG);
return data;
return executeCall(productApi.deleteProduct(id));
}
/**
* Uploads an image file for a specific product via the API.
*/
public LiveData<Resource<Void>> uploadProductImage(Long id, MultipartBody.Part image) {
MutableLiveData<Resource<Void>> data = new MutableLiveData<>();
RetrofitUtils.enqueue(productApi.uploadProductImage(id, image), data, TAG);
return data;
return executeCall(productApi.uploadProductImage(id, image));
}
/**
* Sends a request to the API to delete the image of a specific product.
*/
public LiveData<Resource<Void>> deleteProductImage(Long id) {
MutableLiveData<Resource<Void>> data = new MutableLiveData<>();
RetrofitUtils.enqueue(productApi.deleteProductImage(id), data, TAG);
return data;
return executeCall(productApi.deleteProductImage(id));
}
}

View File

@@ -1,24 +1,22 @@
package com.example.petstoremobile.repositories;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import com.example.petstoremobile.api.ProductSupplierApi;
import com.example.petstoremobile.dtos.PageResponse;
import com.example.petstoremobile.dtos.ProductSupplierDTO;
import com.example.petstoremobile.utils.Resource;
import com.example.petstoremobile.utils.RetrofitUtils;
import javax.inject.Inject;
import javax.inject.Singleton;
@Singleton
public class ProductSupplierRepository {
private static final String TAG = "ProductSupplierRepository";
public class ProductSupplierRepository extends BaseRepository {
private final ProductSupplierApi api;
@Inject
public ProductSupplierRepository(ProductSupplierApi api) {
super("ProductSupplierRepository");
this.api = api;
}
@@ -26,35 +24,34 @@ public class ProductSupplierRepository {
* Retrieves a paginated list of all product-supplier relationships from the API.
*/
public LiveData<Resource<PageResponse<ProductSupplierDTO>>> getAllProductSuppliers(int page, int size) {
MutableLiveData<Resource<PageResponse<ProductSupplierDTO>>> data = new MutableLiveData<>();
RetrofitUtils.enqueue(api.getAllProductSuppliers(page, size), data, TAG);
return data;
return executeCall(api.getAllProductSuppliers(page, size));
}
/**
* Retrieves a single product-supplier relationship by product and supplier IDs.
*/
public LiveData<Resource<ProductSupplierDTO>> getProductSupplierById(Long productId, Long supplierId) {
return executeCall(api.getProductSupplierById(productId, supplierId));
}
/**
* Sends a request to the API to create a new product-supplier relationship.
*/
public LiveData<Resource<ProductSupplierDTO>> createProductSupplier(ProductSupplierDTO dto) {
MutableLiveData<Resource<ProductSupplierDTO>> data = new MutableLiveData<>();
RetrofitUtils.enqueue(api.createProductSupplier(dto), data, TAG);
return data;
return executeCall(api.createProductSupplier(dto));
}
/**
* Sends a request to the API to update an existing product-supplier relationship.
*/
public LiveData<Resource<ProductSupplierDTO>> updateProductSupplier(Long productId, Long supplierId, ProductSupplierDTO dto) {
MutableLiveData<Resource<ProductSupplierDTO>> data = new MutableLiveData<>();
RetrofitUtils.enqueue(api.updateProductSupplier(productId, supplierId, dto), data, TAG);
return data;
return executeCall(api.updateProductSupplier(productId, supplierId, dto));
}
/**
* Sends a request to the API to delete a specific product-supplier relationship.
*/
public LiveData<Resource<Void>> deleteProductSupplier(Long productId, Long supplierId) {
MutableLiveData<Resource<Void>> data = new MutableLiveData<>();
RetrofitUtils.enqueue(api.deleteProductSupplier(productId, supplierId), data, TAG);
return data;
return executeCall(api.deleteProductSupplier(productId, supplierId));
}
}

View File

@@ -1,24 +1,22 @@
package com.example.petstoremobile.repositories;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import com.example.petstoremobile.api.PurchaseOrderApi;
import com.example.petstoremobile.dtos.PageResponse;
import com.example.petstoremobile.dtos.PurchaseOrderDTO;
import com.example.petstoremobile.utils.Resource;
import com.example.petstoremobile.utils.RetrofitUtils;
import javax.inject.Inject;
import javax.inject.Singleton;
@Singleton
public class PurchaseOrderRepository {
private static final String TAG = "PurchaseOrderRepo";
public class PurchaseOrderRepository extends BaseRepository {
private final PurchaseOrderApi api;
@Inject
public PurchaseOrderRepository(PurchaseOrderApi api) {
super("PurchaseOrderRepo");
this.api = api;
}
@@ -26,17 +24,13 @@ public class PurchaseOrderRepository {
* Retrieves a paginated list of all purchase orders from the API.
*/
public LiveData<Resource<PageResponse<PurchaseOrderDTO>>> getAllPurchaseOrders(int page, int size) {
MutableLiveData<Resource<PageResponse<PurchaseOrderDTO>>> data = new MutableLiveData<>();
RetrofitUtils.enqueue(api.getAllPurchaseOrders(page, size), data, TAG);
return data;
return executeCall(api.getAllPurchaseOrders(page, size));
}
/**
* Retrieves a specific purchase order by its ID from the API.
*/
public LiveData<Resource<PurchaseOrderDTO>> getPurchaseOrderById(Long id) {
MutableLiveData<Resource<PurchaseOrderDTO>> data = new MutableLiveData<>();
RetrofitUtils.enqueue(api.getPurchaseOrderById(id), data, TAG);
return data;
return executeCall(api.getPurchaseOrderById(id));
}
}

View File

@@ -1,24 +1,22 @@
package com.example.petstoremobile.repositories;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import com.example.petstoremobile.api.ServiceApi;
import com.example.petstoremobile.dtos.PageResponse;
import com.example.petstoremobile.dtos.ServiceDTO;
import com.example.petstoremobile.utils.Resource;
import com.example.petstoremobile.utils.RetrofitUtils;
import javax.inject.Inject;
import javax.inject.Singleton;
@Singleton
public class ServiceRepository {
private static final String TAG = "ServiceRepository";
public class ServiceRepository extends BaseRepository {
private final ServiceApi serviceApi;
@Inject
public ServiceRepository(ServiceApi serviceApi) {
super("ServiceRepository");
this.serviceApi = serviceApi;
}
@@ -26,44 +24,34 @@ public class ServiceRepository {
* Retrieves a paginated list of all services from the API.
*/
public LiveData<Resource<PageResponse<ServiceDTO>>> getAllServices(int page, int size) {
MutableLiveData<Resource<PageResponse<ServiceDTO>>> data = new MutableLiveData<>();
RetrofitUtils.enqueue(serviceApi.getAllServices(page, size), data, TAG);
return data;
return executeCall(serviceApi.getAllServices(page, size));
}
/**
* Retrieves a specific service by its ID from the API.
*/
public LiveData<Resource<ServiceDTO>> getServiceById(Long id) {
MutableLiveData<Resource<ServiceDTO>> data = new MutableLiveData<>();
RetrofitUtils.enqueue(serviceApi.getServiceById(id), data, TAG);
return data;
return executeCall(serviceApi.getServiceById(id));
}
/**
* Sends a request to the API to create a new service.
*/
public LiveData<Resource<ServiceDTO>> createService(ServiceDTO service) {
MutableLiveData<Resource<ServiceDTO>> data = new MutableLiveData<>();
RetrofitUtils.enqueue(serviceApi.createService(service), data, TAG);
return data;
return executeCall(serviceApi.createService(service));
}
/**
* Sends a request to the API to update an existing service by ID.
*/
public LiveData<Resource<ServiceDTO>> updateService(Long id, ServiceDTO service) {
MutableLiveData<Resource<ServiceDTO>> data = new MutableLiveData<>();
RetrofitUtils.enqueue(serviceApi.updateService(id, service), data, TAG);
return data;
return executeCall(serviceApi.updateService(id, service));
}
/**
* Sends a request to the API to delete a specific service.
*/
public LiveData<Resource<Void>> deleteService(Long id) {
MutableLiveData<Resource<Void>> data = new MutableLiveData<>();
RetrofitUtils.enqueue(serviceApi.deleteService(id), data, TAG);
return data;
return executeCall(serviceApi.deleteService(id));
}
}

View File

@@ -1,24 +1,22 @@
package com.example.petstoremobile.repositories;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import com.example.petstoremobile.api.StoreApi;
import com.example.petstoremobile.dtos.PageResponse;
import com.example.petstoremobile.dtos.StoreDTO;
import com.example.petstoremobile.utils.Resource;
import com.example.petstoremobile.utils.RetrofitUtils;
import javax.inject.Inject;
import javax.inject.Singleton;
@Singleton
public class StoreRepository {
private static final String TAG = "StoreRepository";
public class StoreRepository extends BaseRepository {
private final StoreApi storeApi;
@Inject
public StoreRepository(StoreApi storeApi) {
super("StoreRepository");
this.storeApi = storeApi;
}
@@ -26,8 +24,6 @@ public class StoreRepository {
* Retrieves a paginated list of all stores from the API.
*/
public LiveData<Resource<PageResponse<StoreDTO>>> getAllStores(int page, int size) {
MutableLiveData<Resource<PageResponse<StoreDTO>>> data = new MutableLiveData<>();
RetrofitUtils.enqueue(storeApi.getAllStores(page, size), data, TAG);
return data;
return executeCall(storeApi.getAllStores(page, size));
}
}

View File

@@ -1,24 +1,22 @@
package com.example.petstoremobile.repositories;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import com.example.petstoremobile.api.SupplierApi;
import com.example.petstoremobile.dtos.PageResponse;
import com.example.petstoremobile.dtos.SupplierDTO;
import com.example.petstoremobile.utils.Resource;
import com.example.petstoremobile.utils.RetrofitUtils;
import javax.inject.Inject;
import javax.inject.Singleton;
@Singleton
public class SupplierRepository {
private static final String TAG = "SupplierRepository";
public class SupplierRepository extends BaseRepository {
private final SupplierApi supplierApi;
@Inject
public SupplierRepository(SupplierApi supplierApi) {
super("SupplierRepository");
this.supplierApi = supplierApi;
}
@@ -26,44 +24,34 @@ public class SupplierRepository {
* Retrieves a paginated list of all suppliers from the API.
*/
public LiveData<Resource<PageResponse<SupplierDTO>>> getAllSuppliers(int page, int size) {
MutableLiveData<Resource<PageResponse<SupplierDTO>>> data = new MutableLiveData<>();
RetrofitUtils.enqueue(supplierApi.getAllSuppliers(page, size), data, TAG);
return data;
return executeCall(supplierApi.getAllSuppliers(page, size));
}
/**
* Retrieves a specific supplier by its ID from the API.
*/
public LiveData<Resource<SupplierDTO>> getSupplierById(Long id) {
MutableLiveData<Resource<SupplierDTO>> data = new MutableLiveData<>();
RetrofitUtils.enqueue(supplierApi.getSupplierById(id), data, TAG);
return data;
return executeCall(supplierApi.getSupplierById(id));
}
/**
* Sends a request to the API to create a new supplier record.
*/
public LiveData<Resource<SupplierDTO>> createSupplier(SupplierDTO supplier) {
MutableLiveData<Resource<SupplierDTO>> data = new MutableLiveData<>();
RetrofitUtils.enqueue(supplierApi.createSupplier(supplier), data, TAG);
return data;
return executeCall(supplierApi.createSupplier(supplier));
}
/**
* Sends a request to the API to update an existing supplier record by ID.
*/
public LiveData<Resource<SupplierDTO>> updateSupplier(Long id, SupplierDTO supplier) {
MutableLiveData<Resource<SupplierDTO>> data = new MutableLiveData<>();
RetrofitUtils.enqueue(supplierApi.updateSupplier(id, supplier), data, TAG);
return data;
return executeCall(supplierApi.updateSupplier(id, supplier));
}
/**
* Sends a request to the API to delete a specific supplier record.
*/
public LiveData<Resource<Void>> deleteSupplier(Long id) {
MutableLiveData<Resource<Void>> data = new MutableLiveData<>();
RetrofitUtils.enqueue(supplierApi.deleteSupplier(id), data, TAG);
return data;
return executeCall(supplierApi.deleteSupplier(id));
}
}

View File

@@ -5,12 +5,19 @@ import android.util.Log;
import android.widget.Toast;
import com.example.petstoremobile.dtos.ErrorResponse;
import com.google.gson.Gson;
import java.io.IOException;
import java.net.ConnectException;
import java.net.SocketTimeoutException;
import java.net.UnknownHostException;
import retrofit2.Response;
/**
* Utility class for handling API error responses.
*/
public class ErrorUtils {
private static final String TAG = "ErrorUtils";
private static final Gson gson = new Gson();
/**
* Shows an error message to toast based on the response.
*/
@@ -19,20 +26,46 @@ public class ErrorUtils {
}
/**
* Extracts the error message from the response body.
* Extracts a user-friendly error message from the response body or status code.
*/
public static String getErrorMessage(Response<?> response, String defaultMessage) {
if (response == null) return defaultMessage;
try {
if (response != null && response.errorBody() != null) {
if (response.errorBody() != null) {
String errorJson = response.errorBody().string();
ErrorResponse errorResponse = new Gson().fromJson(errorJson, ErrorResponse.class);
ErrorResponse errorResponse = gson.fromJson(errorJson, ErrorResponse.class);
if (errorResponse != null && errorResponse.getMessage() != null) {
return errorResponse.getMessage();
}
}
} catch (Exception e) {
Log.e("ErrorUtils", "Error parsing error body", e);
Log.e(TAG, "Error parsing error body", e);
}
// Handle specific status codes if no message was provided by the API
switch (response.code()) {
case 401: return "Unauthorized. Please login again.";
case 403: return "Access denied.";
case 404: return "Resource not found.";
case 500: return "Internal server error. Please try again later.";
case 503: return "Service unavailable. The server might be down.";
default: return defaultMessage + " (Code: " + response.code() + ")";
}
}
/**
* Converts a Throwable (from onFailure) into a user-friendly network error message.
*/
public static String getFailureMessage(Throwable t) {
if (t instanceof UnknownHostException || t instanceof ConnectException) {
return "No internet connection. Please check your settings.";
} else if (t instanceof SocketTimeoutException) {
return "The connection timed out. Please try again.";
} else if (t instanceof IOException) {
return "Network error occurred. Please try again.";
} else {
return "An unexpected error occurred: " + t.getLocalizedMessage();
}
return defaultMessage;
}
}

View File

@@ -25,7 +25,7 @@ public class RetrofitUtils {
}
/**
* call and updates the provided MutableLiveData with Resource states.
* Enqueues a Retrofit call and updates the provided MutableLiveData with Resource states.
*/
public static <T> void enqueue(@NonNull Call<T> call, @NonNull MutableLiveData<Resource<T>> data, String tag) {
data.setValue(Resource.loading(null));
@@ -43,8 +43,8 @@ public class RetrofitUtils {
@Override
public void onFailure(@NonNull Call<T> call, @NonNull Throwable t) {
String errorMsg = "Network Error: " + t.getMessage();
Log.e(tag, errorMsg);
String errorMsg = ErrorUtils.getFailureMessage(t);
Log.e(tag, "Network Error: " + t.getMessage(), t);
data.setValue(Resource.error(errorMsg, null));
}
});
@@ -52,6 +52,7 @@ public class RetrofitUtils {
/**
* Creates a callback for Retrofit calls that handles errors and logging.
* @deprecated Use {@link #enqueue(Call, MutableLiveData, String)} for LiveData-based architecture.
*/
@Deprecated
public static <T> Callback<T> createCallback(Context context, String tag, String successMsg, SuccessCallback<T> successCallback) {
@@ -73,8 +74,9 @@ public class RetrofitUtils {
@Override
public void onFailure(@NonNull Call<T> call, @NonNull Throwable t) {
Log.e(tag, "Network Error: " + t.getMessage());
Toast.makeText(context, "Network error. Please try again.", Toast.LENGTH_SHORT).show();
String errorMsg = ErrorUtils.getFailureMessage(t);
Log.e(tag, "Network Error: " + t.getMessage(), t);
Toast.makeText(context, errorMsg, Toast.LENGTH_SHORT).show();
}
};
}
@@ -96,7 +98,7 @@ public class RetrofitUtils {
@Override
public void onFailure(@NonNull Call<T> call, @NonNull Throwable t) {
Log.e(tag, "Network Error: " + t.getMessage());
Log.e(tag, "Network Error: " + t.getMessage(), t);
}
};
}

View File

@@ -0,0 +1,68 @@
package com.example.petstoremobile.viewmodels;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.ViewModel;
import com.example.petstoremobile.dtos.ConversationDTO;
import com.example.petstoremobile.dtos.CustomerDTO;
import com.example.petstoremobile.dtos.MessageDTO;
import com.example.petstoremobile.dtos.PageResponse;
import com.example.petstoremobile.dtos.SendMessageRequest;
import com.example.petstoremobile.repositories.ChatRepository;
import com.example.petstoremobile.utils.Resource;
import java.util.List;
import javax.inject.Inject;
import dagger.hilt.android.lifecycle.HiltViewModel;
import okhttp3.MultipartBody;
import okhttp3.RequestBody;
/**
* ViewModel for managing chat-related UI state and data operations.
*/
@HiltViewModel
public class ChatViewModel extends ViewModel {
private final ChatRepository repository;
@Inject
public ChatViewModel(ChatRepository repository) {
this.repository = repository;
}
/**
* Retrieves all chat conversations for the current user.
*/
public LiveData<Resource<List<ConversationDTO>>> getAllConversations() {
return repository.getAllConversations();
}
/**
* Retrieves the message history for a specific conversation.
*/
public LiveData<Resource<List<MessageDTO>>> getMessages(Long conversationId) {
return repository.getMessages(conversationId);
}
/**
* Sends a plain text message to a conversation.
*/
public LiveData<Resource<MessageDTO>> sendMessage(Long conversationId, SendMessageRequest request) {
return repository.sendMessage(conversationId, request);
}
/**
* Sends a message with a file attachment to a conversation.
*/
public LiveData<Resource<MessageDTO>> sendMessageWithAttachment(Long conversationId, RequestBody content, MultipartBody.Part file) {
return repository.sendMessageWithAttachment(conversationId, content, file);
}
/**
* Fetches a paginated list of customers.
*/
public LiveData<Resource<PageResponse<CustomerDTO>>> getAllCustomers(int page, int size) {
return repository.getAllCustomers(page, size);
}
}

View File

@@ -5,11 +5,16 @@ junitVersion = "1.3.0"
espressoCore = "3.7.0"
appcompat = "1.7.1"
material = "1.13.0"
activity = "1.12.4"
activity = "1.13.0"
constraintlayout = "2.2.1"
swiperefreshlayout = "1.2.0"
hilt = "2.51.1"
navigation = "2.8.8"
retrofit = "2.11.0"
okhttp = "4.12.0"
glide = "4.16.0"
viewpager2 = "1.1.0"
camera = "1.4.1"
[libraries]
junit = { group = "junit", name = "junit", version.ref = "junit" }
@@ -25,6 +30,23 @@ hilt-compiler = { group = "com.google.dagger", name = "hilt-compiler", version.r
navigation-fragment = { group = "androidx.navigation", name = "navigation-fragment", version.ref = "navigation" }
navigation-ui = { group = "androidx.navigation", name = "navigation-ui", version.ref = "navigation" }
# Networking
retrofit = { group = "com.squareup.retrofit2", name = "retrofit", version.ref = "retrofit" }
retrofit-gson = { group = "com.squareup.retrofit2", name = "converter-gson", version.ref = "retrofit" }
okhttp = { group = "com.squareup.okhttp3", name = "okhttp", version.ref = "okhttp" }
okhttp-logging = { group = "com.squareup.okhttp3", name = "logging-interceptor", version.ref = "okhttp" }
# UI Components
viewpager2 = { group = "androidx.viewpager2", name = "viewpager2", version.ref = "viewpager2" }
glide = { group = "com.github.bumptech.glide", name = "glide", version.ref = "glide" }
glide-compiler = { group = "com.github.bumptech.glide", name = "compiler", version.ref = "glide" }
# CameraX
camera-core = { group = "androidx.camera", name = "camera-core", version.ref = "camera" }
camera-camera2 = { group = "androidx.camera", name = "camera-camera2", version.ref = "camera" }
camera-lifecycle = { group = "androidx.camera", name = "camera-lifecycle", version.ref = "camera" }
camera-view = { group = "androidx.camera", name = "camera-view", version.ref = "camera" }
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
hilt = { id = "com.google.dagger.hilt.android", version.ref = "hilt" }

View File

@@ -1,8 +1,7 @@
#Sun Mar 01 14:36:37 MST 2026
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionSha256Sum=a17ddd85a26b6a7f5ddb71ff8b05fc5104c0202c6e64782429790c933686c806
distributionUrl=https\://services.gradle.org/distributions/gradle-9.1.0-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME