Attachments to Chat #162

Merged
RecentRunner merged 20 commits from AttachmentsToChat into main 2026-04-09 22:34:59 -06:00
117 changed files with 8282 additions and 7138 deletions

View File

@@ -22,9 +22,15 @@ public class MessageAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder
private static final int TYPE_SENT = 1; private static final int TYPE_SENT = 1;
private static final int TYPE_RECEIVED = 2; private static final int TYPE_RECEIVED = 2;
public interface OnAttachmentClickListener {
void onAttachmentClick(Message message);
}
private final List<Message> messages; private final List<Message> messages;
private Long currentUserId; private Long currentUserId;
private String token; private String token;
private String baseUrl;
private OnAttachmentClickListener attachmentClickListener;
public MessageAdapter(List<Message> messages, Long currentUserId) { public MessageAdapter(List<Message> messages, Long currentUserId) {
this.messages = messages; this.messages = messages;
@@ -40,6 +46,14 @@ public class MessageAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder
this.token = token; this.token = token;
} }
public void setBaseUrl(String baseUrl) {
this.baseUrl = baseUrl;
}
public void setOnAttachmentClickListener(OnAttachmentClickListener listener) {
this.attachmentClickListener = listener;
}
@Override @Override
public int getItemViewType(int position) { public int getItemViewType(int position) {
Message m = messages.get(position); Message m = messages.get(position);
@@ -64,8 +78,8 @@ public class MessageAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder
@Override @Override
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) { public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
Message m = messages.get(position); Message m = messages.get(position);
if (holder instanceof SentHolder) ((SentHolder) holder).bind(m, token); if (holder instanceof SentHolder) ((SentHolder) holder).bind(m, token, baseUrl, attachmentClickListener);
if (holder instanceof ReceivedHolder) ((ReceivedHolder) holder).bind(m, token); if (holder instanceof ReceivedHolder) ((ReceivedHolder) holder).bind(m, token, baseUrl, attachmentClickListener);
} }
@Override public int getItemCount() { return messages.size(); } @Override public int getItemCount() { return messages.size(); }
@@ -76,9 +90,23 @@ public class MessageAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder
super(binding.getRoot()); super(binding.getRoot());
this.binding = binding; this.binding = binding;
} }
void bind(Message m, String token) { void bind(Message m, String token, String baseUrl, OnAttachmentClickListener listener) {
binding.tvMessageContent.setText(m.getContent()); // Check for Text
displayAttachment(m, binding.ivAttachment, binding.tvAttachmentName, token); if (m.getContent() != null && !m.getContent().isEmpty()) {
binding.tvMessageContent.setVisibility(View.VISIBLE);
binding.tvMessageContent.setText(m.getContent());
} else {
binding.tvMessageContent.setVisibility(View.GONE);
}
// Check for Attachment
displayAttachment(m, binding.ivAttachment, binding.tvAttachmentName, token, baseUrl);
View.OnClickListener click = v -> {
if (listener != null) listener.onAttachmentClick(m);
};
binding.ivAttachment.setOnClickListener(click);
binding.tvAttachmentName.setOnClickListener(click);
} }
} }
@@ -88,22 +116,52 @@ public class MessageAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder
super(binding.getRoot()); super(binding.getRoot());
this.binding = binding; this.binding = binding;
} }
void bind(Message m, String token) { void bind(Message m, String token, String baseUrl, OnAttachmentClickListener listener) {
binding.tvMessageContent.setText(m.getContent()); // Check for Text
displayAttachment(m, binding.ivAttachment, binding.tvAttachmentName, token); if (m.getContent() != null && !m.getContent().isEmpty()) {
binding.tvMessageContent.setVisibility(View.VISIBLE);
binding.tvMessageContent.setText(m.getContent());
} else {
binding.tvMessageContent.setVisibility(View.GONE);
}
// Check for Attachment
displayAttachment(m, binding.ivAttachment, binding.tvAttachmentName, token, baseUrl);
View.OnClickListener click = v -> {
if (listener != null) listener.onAttachmentClick(m);
};
binding.ivAttachment.setOnClickListener(click);
binding.tvAttachmentName.setOnClickListener(click);
} }
} }
// helper function to display the attachment to the chat bubble // helper function to display the attachment to the chat bubble
private static void displayAttachment(Message m, ImageView iv, TextView tvName, String token) { private static void displayAttachment(Message m, ImageView iv, TextView tvName, String token, String baseUrl) {
if (m.getAttachmentUrl() != null) { // Check if there's an attachment by looking at name or mime type
if (m.getAttachmentType() != null && m.getAttachmentType().startsWith("image/")) { if (m.getAttachmentName() != null || m.getAttachmentMimeType() != null) {
// Construct the download URL using the message ID
String url;
if (baseUrl != null) {
String cleanBase = baseUrl.endsWith("/") ? baseUrl.substring(0, baseUrl.length() - 1) : baseUrl;
url = cleanBase + "/api/v1/chat/messages/" + m.getId() + "/attachment";
} else {
url = m.getAttachmentUrl(); // Fallback
}
if (url == null) {
iv.setVisibility(View.GONE);
tvName.setVisibility(View.GONE);
return;
}
if (m.getAttachmentMimeType() != null && m.getAttachmentMimeType().startsWith("image/")) {
iv.setVisibility(View.VISIBLE); iv.setVisibility(View.VISIBLE);
tvName.setVisibility(View.GONE); tvName.setVisibility(View.GONE);
Object loadTarget = m.getAttachmentUrl(); Object loadTarget = url;
if (token != null && m.getAttachmentUrl().startsWith("http")) { if (token != null) {
loadTarget = new GlideUrl(m.getAttachmentUrl(), new LazyHeaders.Builder() loadTarget = new GlideUrl(url, new LazyHeaders.Builder()
.addHeader("Authorization", "Bearer " + token) .addHeader("Authorization", "Bearer " + token)
.build()); .build());
} }

View File

@@ -1,6 +1,7 @@
package com.example.petstoremobile.api; package com.example.petstoremobile.api;
import com.example.petstoremobile.dtos.CustomerDTO; import com.example.petstoremobile.dtos.CustomerDTO;
import com.example.petstoremobile.dtos.DropdownDTO;
import com.example.petstoremobile.dtos.PageResponse; import com.example.petstoremobile.dtos.PageResponse;
import java.util.List; import java.util.List;
@@ -18,4 +19,7 @@ public interface CustomerApi {
@GET("api/v1/customers/{customerId}") @GET("api/v1/customers/{customerId}")
Call<CustomerDTO> getCustomerById(@Path("customerId") Long customerId); Call<CustomerDTO> getCustomerById(@Path("customerId") Long customerId);
@GET("api/v1/dropdowns/customers")
Call<List<DropdownDTO>> getCustomerDropdowns();
} }

View File

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

View File

@@ -1,9 +1,12 @@
package com.example.petstoremobile.api; package com.example.petstoremobile.api;
import com.example.petstoremobile.dtos.BulkDeleteRequest; import com.example.petstoremobile.dtos.BulkDeleteRequest;
import com.example.petstoremobile.dtos.DropdownDTO;
import com.example.petstoremobile.dtos.PageResponse; import com.example.petstoremobile.dtos.PageResponse;
import com.example.petstoremobile.dtos.PetDTO; import com.example.petstoremobile.dtos.PetDTO;
import java.util.List;
import okhttp3.MultipartBody; import okhttp3.MultipartBody;
import retrofit2.Call; import retrofit2.Call;
import retrofit2.http.Body; import retrofit2.http.Body;
@@ -31,9 +34,16 @@ public interface PetApi {
@Query("status") String status, @Query("status") String status,
@Query("species") String species, @Query("species") String species,
@Query("storeId") Long storeId, @Query("storeId") Long storeId,
@Query("customerId") Long customerId,
@Query("sort") String sort @Query("sort") String sort
); );
@GET("api/v1/dropdowns/customers/{customerId}/pets")
Call<List<DropdownDTO>> getCustomerPets(@Path("customerId") Long customerId);
@GET("api/v1/dropdowns/adoption-pets")
Call<List<DropdownDTO>> getAdoptionPets();
// Get pet by id // Get pet by id
@GET("api/v1/pets/{id}") @GET("api/v1/pets/{id}")
Call<PetDTO> getPetById(@Path("id") Long id); Call<PetDTO> getPetById(@Path("id") Long id);

View File

@@ -1,10 +1,14 @@
package com.example.petstoremobile.api; package com.example.petstoremobile.api;
import com.example.petstoremobile.dtos.DropdownDTO;
import com.example.petstoremobile.dtos.PageResponse; import com.example.petstoremobile.dtos.PageResponse;
import com.example.petstoremobile.dtos.StoreDTO; import com.example.petstoremobile.dtos.StoreDTO;
import java.util.List;
import retrofit2.Call; import retrofit2.Call;
import retrofit2.http.GET; import retrofit2.http.GET;
import retrofit2.http.Path;
import retrofit2.http.Query; import retrofit2.http.Query;
public interface StoreApi { public interface StoreApi {
@@ -13,4 +17,10 @@ public interface StoreApi {
Call<PageResponse<StoreDTO>> getAllStores( Call<PageResponse<StoreDTO>> getAllStores(
@Query("page") int page, @Query("page") int page,
@Query("size") int size); @Query("size") int size);
@GET("api/v1/dropdowns/stores")
Call<List<DropdownDTO>> getStoreDropdowns();
@GET("api/v1/dropdowns/stores/{storeId}/employees")
Call<List<DropdownDTO>> getStoreEmployees(@Path("storeId") Long storeId);
} }

View File

@@ -55,7 +55,7 @@ public class NetworkModule {
@Singleton @Singleton
public static OkHttpClient provideOkHttpClient(TokenManager tokenManager) { public static OkHttpClient provideOkHttpClient(TokenManager tokenManager) {
HttpLoggingInterceptor interceptor = new HttpLoggingInterceptor(); HttpLoggingInterceptor interceptor = new HttpLoggingInterceptor();
interceptor.setLevel(HttpLoggingInterceptor.Level.BODY); interceptor.setLevel(HttpLoggingInterceptor.Level.HEADERS);
return new OkHttpClient.Builder() return new OkHttpClient.Builder()
.addInterceptor(interceptor) .addInterceptor(interceptor)
@@ -191,4 +191,4 @@ public class NetworkModule {
public static RefundApi provideRefundApi(Retrofit retrofit) { public static RefundApi provideRefundApi(Retrofit retrofit) {
return retrofit.create(RefundApi.class); return retrofit.create(RefundApi.class);
} }
} }

View File

@@ -12,6 +12,14 @@ public class ConversationDTO {
public ConversationDTO() {} public ConversationDTO() {}
public ConversationDTO(Long id, Long customerId, Long staffId, String lastMessage, String status) {
this.id = id;
this.customerId = customerId;
this.staffId = staffId;
this.lastMessage = lastMessage;
this.status = status;
}
public Long getId() { public Long getId() {
return id; return id;
} }

View File

@@ -0,0 +1,29 @@
package com.example.petstoremobile.dtos;
public class DropdownDTO {
private Long id;
private String label;
public DropdownDTO() {}
public DropdownDTO(Long id, String label) {
this.id = id;
this.label = label;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getLabel() {
return label;
}
public void setLabel(String label) {
this.label = label;
}
}

View File

@@ -28,8 +28,11 @@ public class MessageDTO {
@SerializedName("attachmentName") @SerializedName("attachmentName")
private String attachmentName; private String attachmentName;
@SerializedName("attachmentType") @SerializedName("attachmentMimeType")
private String attachmentType; private String attachmentMimeType;
@SerializedName("attachmentSizeBytes")
private Long attachmentSizeBytes;
public MessageDTO() {} public MessageDTO() {}
@@ -57,6 +60,9 @@ public class MessageDTO {
public String getAttachmentName() { return attachmentName; } public String getAttachmentName() { return attachmentName; }
public void setAttachmentName(String attachmentName) { this.attachmentName = attachmentName; } public void setAttachmentName(String attachmentName) { this.attachmentName = attachmentName; }
public String getAttachmentType() { return attachmentType; } public String getAttachmentMimeType() { return attachmentMimeType; }
public void setAttachmentType(String attachmentType) { this.attachmentType = attachmentType; } public void setAttachmentMimeType(String attachmentMimeType) { this.attachmentMimeType = attachmentMimeType; }
public Long getAttachmentSizeBytes() { return attachmentSizeBytes; }
public void setAttachmentSizeBytes(Long attachmentSizeBytes) { this.attachmentSizeBytes = attachmentSizeBytes; }
} }

View File

@@ -1,15 +1,27 @@
package com.example.petstoremobile.fragments; package com.example.petstoremobile.fragments;
import android.app.Activity; import android.app.Activity;
import android.app.Dialog;
import android.content.ContentValues;
import android.content.Intent; import android.content.Intent;
import android.database.Cursor;
import android.net.Uri; import android.net.Uri;
import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import android.provider.OpenableColumns; import android.os.Environment;
import android.provider.MediaStore;
import android.util.Log; import android.util.Log;
import android.view.*; import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.inputmethod.EditorInfo; import android.view.inputmethod.EditorInfo;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
import android.widget.Toast; import android.widget.Toast;
import androidx.activity.result.ActivityResultLauncher; import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts; import androidx.activity.result.contract.ActivityResultContracts;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
@@ -17,6 +29,7 @@ import androidx.core.view.GravityCompat;
import androidx.fragment.app.Fragment; import androidx.fragment.app.Fragment;
import androidx.lifecycle.ViewModelProvider; import androidx.lifecycle.ViewModelProvider;
import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.LinearLayoutManager;
import com.bumptech.glide.Glide; import com.bumptech.glide.Glide;
import com.example.petstoremobile.R; import com.example.petstoremobile.R;
import com.example.petstoremobile.adapters.ChatAdapter; import com.example.petstoremobile.adapters.ChatAdapter;
@@ -25,20 +38,32 @@ import com.example.petstoremobile.api.auth.TokenManager;
import com.example.petstoremobile.databinding.FragmentChatBinding; import com.example.petstoremobile.databinding.FragmentChatBinding;
import com.example.petstoremobile.dtos.ConversationDTO; import com.example.petstoremobile.dtos.ConversationDTO;
import com.example.petstoremobile.dtos.MessageDTO; import com.example.petstoremobile.dtos.MessageDTO;
import com.example.petstoremobile.dtos.SendMessageRequest;
import com.example.petstoremobile.models.Chat; import com.example.petstoremobile.models.Chat;
import com.example.petstoremobile.models.Message; import com.example.petstoremobile.models.Message;
import com.example.petstoremobile.services.ChatNotificationService; import com.example.petstoremobile.services.ChatNotificationService;
import com.example.petstoremobile.utils.DialogUtils;
import com.example.petstoremobile.utils.FileUtils;
import com.example.petstoremobile.utils.GlideUtils;
import com.example.petstoremobile.utils.Resource; import com.example.petstoremobile.utils.Resource;
import com.example.petstoremobile.viewmodels.ChatViewModel; import com.example.petstoremobile.utils.UIUtils;
import com.example.petstoremobile.viewmodels.ChatListViewModel;
import com.example.petstoremobile.websocket.StompChatManager; import com.example.petstoremobile.websocket.StompChatManager;
import java.util.*; import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.List;
import javax.inject.Inject; import javax.inject.Inject;
import javax.inject.Named; import javax.inject.Named;
import dagger.hilt.android.AndroidEntryPoint; import dagger.hilt.android.AndroidEntryPoint;
import okhttp3.MediaType;
import okhttp3.MultipartBody;
import okhttp3.RequestBody;
import okhttp3.ResponseBody;
@AndroidEntryPoint @AndroidEntryPoint
public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickListener, StompChatManager.MessageListener, public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickListener, StompChatManager.MessageListener,
@@ -47,59 +72,46 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis
private static final String TAG = "ChatFragment"; private static final String TAG = "ChatFragment";
private FragmentChatBinding binding; private FragmentChatBinding binding;
private ChatViewModel viewModel; private ChatListViewModel viewModel;
// Adapters private ChatAdapter activeChatAdapter;
private ChatAdapter chatAdapter; private ChatAdapter closedChatAdapter;
private MessageAdapter messageAdapter; private MessageAdapter messageAdapter;
// Data private final List<Chat> activeChatList = new ArrayList<>();
private final List<Chat> chatList = new ArrayList<>(); private final List<Chat> closedChatList = new ArrayList<>();
private final List<Message> messageList = new ArrayList<>(); private final List<Message> messageList = new ArrayList<>();
private final Map<Long, String> customerNames = new HashMap<>();
private Uri pendingAttachmentUri; private Uri pendingAttachmentUri;
@Inject TokenManager tokenManager; @Inject TokenManager tokenManager;
@Inject @Named("baseUrl") String baseUrl; @Inject @Named("baseUrl") String baseUrl;
// chat
private Long currentUserId;
private Long activeConversationId; private Long activeConversationId;
private StompChatManager stompChatManager; private StompChatManager stompChatManager;
private ActivityResultLauncher<Intent> attachmentLauncher; private ActivityResultLauncher<Intent> attachmentLauncher;
/**
* Initializes the attachment launcher to handle file selection from the gallery.
*/
@Override @Override
public void onCreate(Bundle savedInstanceState) { public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
viewModel = new ViewModelProvider(this).get(ChatViewModel.class); viewModel = new ViewModelProvider(requireActivity()).get(ChatListViewModel.class);
attachmentLauncher = registerForActivityResult( attachmentLauncher = registerForActivityResult(
new ActivityResultContracts.StartActivityForResult(), new ActivityResultContracts.StartActivityForResult(),
result -> { result -> {
if (result.getResultCode() == Activity.RESULT_OK && result.getData() != null) { if (result.getResultCode() == Activity.RESULT_OK && result.getData() != null) {
Uri uri = result.getData().getData(); Uri uri = result.getData().getData();
if (uri != null) { if (uri != null) showAttachmentPreview(uri);
showAttachmentPreview(uri);
}
} }
} }
); );
} }
/**
* Inflates the layout, initializes UI components, and sets up click listeners for messaging.
*/
@Override @Override
public View onCreateView(@NonNull LayoutInflater inflater, public View onCreateView(@NonNull LayoutInflater inflater,
ViewGroup container, Bundle savedInstanceState) { ViewGroup container, Bundle savedInstanceState) {
binding = FragmentChatBinding.inflate(inflater, container, false); binding = FragmentChatBinding.inflate(inflater, container, false);
binding.btnHamburger.setOnClickListener(v -> binding.chatDrawerLayout.openDrawer(GravityCompat.START)); binding.btnHamburger.setOnClickListener(v -> binding.chatDrawerLayout.openDrawer(GravityCompat.START));
// Set editor action listener for message field to send when enter is pressed
binding.etMessage.setOnEditorActionListener((v, actionId, event) -> { binding.etMessage.setOnEditorActionListener((v, actionId, event) -> {
if (actionId == EditorInfo.IME_ACTION_SEND || actionId == EditorInfo.IME_NULL) { if (actionId == EditorInfo.IME_ACTION_SEND || actionId == EditorInfo.IME_NULL) {
binding.btnSend.performClick(); binding.btnSend.performClick();
@@ -108,63 +120,208 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis
return false; return false;
}); });
//When the send button is clicked check if there is an attachment and send using the correct helper function
binding.btnSend.setOnClickListener(v -> { binding.btnSend.setOnClickListener(v -> {
if (pendingAttachmentUri != null) { if (pendingAttachmentUri != null) sendWithAttachment(pendingAttachmentUri);
sendWithAttachment(pendingAttachmentUri); else sendMessage();
} else {
sendMessage();
}
}); });
//When the attachment button is clicked open the file picker
binding.btnAttach.setOnClickListener(v -> selectAttachment()); binding.btnAttach.setOnClickListener(v -> selectAttachment());
binding.btnRemoveAttachment.setOnClickListener(v -> removeAttachment()); binding.btnRemoveAttachment.setOnClickListener(v -> removeAttachment());
setupDrawerToggles();
setupRecyclerViews(); setupRecyclerViews();
observeViewModel();
loadInitialData(); loadInitialData();
return binding.getRoot(); return binding.getRoot();
} }
/** private void setupDrawerToggles() {
* Configures the RecyclerViews for the conversation list and the message history. binding.headerActiveChats.setOnClickListener(v -> {
*/ if (binding.rvActiveChats.getVisibility() == View.VISIBLE) {
private void setupRecyclerViews() { binding.rvActiveChats.setVisibility(View.GONE);
// Set up Drawer menu to select conversation binding.ivActiveChevron.setImageResource(android.R.drawable.arrow_down_float);
chatAdapter = new ChatAdapter(chatList, this); } else {
binding.rvChatList.setLayoutManager(new LinearLayoutManager(getContext())); binding.rvActiveChats.setVisibility(View.VISIBLE);
binding.rvChatList.setAdapter(chatAdapter); binding.ivActiveChevron.setImageResource(android.R.drawable.arrow_up_float);
}
});
binding.headerClosedChats.setOnClickListener(v -> {
if (binding.rvClosedChats.getVisibility() == View.VISIBLE) {
binding.rvClosedChats.setVisibility(View.GONE);
binding.ivClosedChevron.setImageResource(android.R.drawable.arrow_down_float);
} else {
binding.rvClosedChats.setVisibility(View.VISIBLE);
binding.ivClosedChevron.setImageResource(android.R.drawable.arrow_up_float);
}
});
}
private void setupRecyclerViews() {
activeChatAdapter = new ChatAdapter(activeChatList, this);
binding.rvActiveChats.setLayoutManager(new LinearLayoutManager(getContext()));
binding.rvActiveChats.setAdapter(activeChatAdapter);
closedChatAdapter = new ChatAdapter(closedChatList, this);
binding.rvClosedChats.setLayoutManager(new LinearLayoutManager(getContext()));
binding.rvClosedChats.setAdapter(closedChatAdapter);
// set up RecyclerView for selected chat to show messages
messageAdapter = new MessageAdapter(messageList, null); messageAdapter = new MessageAdapter(messageList, null);
messageAdapter.setBaseUrl(baseUrl);
messageAdapter.setOnAttachmentClickListener(message -> {
if (message.getAttachmentMimeType() != null && message.getAttachmentMimeType().startsWith("image/")) {
showFullScreenImage(message);
} else {
downloadFile(message);
}
});
LinearLayoutManager lm = new LinearLayoutManager(getContext()); LinearLayoutManager lm = new LinearLayoutManager(getContext());
lm.setStackFromEnd(true); lm.setStackFromEnd(true);
binding.rvMessages.setLayoutManager(lm); binding.rvMessages.setLayoutManager(lm);
binding.rvMessages.setAdapter(messageAdapter); binding.rvMessages.setAdapter(messageAdapter);
setConversationActive(false); setConversationActive(false, null);
}
private void showFullScreenImage(Message message) {
if (baseUrl == null || message.getId() == null) return;
Dialog dialog = new Dialog(requireContext(), android.R.style.Theme_Black_NoTitleBar_Fullscreen);
dialog.setContentView(R.layout.dialog_full_screen_image);
ImageView imageView = dialog.findViewById(R.id.ivFullScreen);
ImageButton closeButton = dialog.findViewById(R.id.btnClose);
ImageButton downloadButton = dialog.findViewById(R.id.btnDownload);
String cleanBase = baseUrl.endsWith("/") ? baseUrl.substring(0, baseUrl.length() - 1) : baseUrl;
String downloadUrl = cleanBase + "/api/v1/chat/messages/" + message.getId() + "/attachment";
GlideUtils.loadImageWithToken(requireContext(), imageView, downloadUrl, tokenManager.getToken(), R.drawable.placeholder);
closeButton.setOnClickListener(v -> dialog.dismiss());
downloadButton.setOnClickListener(v -> downloadFile(message));
imageView.setOnClickListener(v -> dialog.dismiss());
dialog.show();
}
private void downloadFile(Message message) {
if (message.getId() == null) return;
DialogUtils.showConfirmDialog(requireContext(), "Download Attachment",
"Do you want to download \"" + message.getAttachmentName() + "\"?", () -> {
Toast.makeText(requireContext(), "Downloading " + message.getAttachmentName() + "...", Toast.LENGTH_SHORT).show();
viewModel.downloadAttachment(message.getId()).observe(getViewLifecycleOwner(), resource -> {
if (resource == null) return;
setLoading(resource.status == Resource.Status.LOADING);
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
saveFileToDownloads(resource.data, message.getAttachmentName(), message.getAttachmentMimeType());
} else if (resource.status == Resource.Status.ERROR) {
Toast.makeText(requireContext(), "Download failed: " + resource.message, Toast.LENGTH_SHORT).show();
}
});
});
}
private void saveFileToDownloads(ResponseBody body, String fileName, String mimeType) {
try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
ContentValues values = new ContentValues();
values.put(MediaStore.Downloads.DISPLAY_NAME, fileName);
values.put(MediaStore.Downloads.MIME_TYPE, mimeType);
values.put(MediaStore.Downloads.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS);
Uri uri = requireContext().getContentResolver().insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, values);
if (uri != null) {
try (OutputStream outputStream = requireContext().getContentResolver().openOutputStream(uri);
InputStream inputStream = body.byteStream()) {
byte[] buffer = new byte[4096];
int bytesRead;
while ((bytesRead = inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, bytesRead);
}
Toast.makeText(requireContext(), "File saved to Downloads", Toast.LENGTH_SHORT).show();
}
}
} else {
File downloadsDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS);
File file = new File(downloadsDir, fileName);
try (OutputStream outputStream = new FileOutputStream(file);
InputStream inputStream = body.byteStream()) {
byte[] buffer = new byte[4096];
int bytesRead;
while ((bytesRead = inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, bytesRead);
}
Toast.makeText(requireContext(), "File saved to Downloads: " + file.getAbsolutePath(), Toast.LENGTH_LONG).show();
}
}
body.close();
} catch (Exception e) {
Log.e(TAG, "Error saving file", e);
Toast.makeText(requireContext(), "Error saving file", Toast.LENGTH_SHORT).show();
}
}
private void observeViewModel() {
viewModel.getActiveChats().observe(getViewLifecycleOwner(), list -> {
activeChatList.clear();
activeChatList.addAll(list);
activeChatAdapter.notifyDataSetChanged();
updateTitleAndStateIfActive(list);
});
viewModel.getClosedChats().observe(getViewLifecycleOwner(), list -> {
closedChatList.clear();
closedChatList.addAll(list);
closedChatAdapter.notifyDataSetChanged();
updateTitleAndStateIfActive(list);
});
viewModel.getMessageList().observe(getViewLifecycleOwner(), list -> {
messageList.clear();
messageList.addAll(list);
messageAdapter.notifyDataSetChanged();
scrollToBottom();
});
viewModel.getIsLoading().observe(getViewLifecycleOwner(), this::setLoading);
}
private void setLoading(boolean loading) {
if (binding != null && binding.progressBar != null) {
binding.progressBar.setVisibility(loading ? View.VISIBLE : View.GONE);
}
}
private void updateTitleAndStateIfActive(List<Chat> list) {
if (activeConversationId != null) {
for (Chat chat : list) {
if (chat.getChatId().equals(String.valueOf(activeConversationId))) {
binding.tvChatTitle.setText(chat.getCustomerName());
setConversationActive(true, chat.getStatus());
break;
}
}
}
} }
/**
* Loads authentication tokens and user info, then initializes the Stomp WebSocket connection.
*/
private void loadInitialData() { private void loadInitialData() {
String token = tokenManager.getToken(); String token = tokenManager.getToken();
currentUserId = tokenManager.getUserId(); Long currentUserId = tokenManager.getUserId();
String role = tokenManager.getRole(); String role = tokenManager.getRole();
messageAdapter.setCurrentUserId(currentUserId); messageAdapter.setCurrentUserId(currentUserId);
messageAdapter.setToken(token); messageAdapter.setToken(token);
// if token exist then connect to websocket
if (token != null) { if (token != null) {
stompChatManager = new StompChatManager(token, role, baseUrl); stompChatManager = new StompChatManager(token, role, baseUrl);
stompChatManager.setMessageListener(this); stompChatManager.setMessageListener(this);
stompChatManager.setConversationListener(this); stompChatManager.setConversationListener(this);
stompChatManager.setConnectionListener(this); stompChatManager.setConnectionListener(this);
stompChatManager.connect(); stompChatManager.connect();
} else {
Log.e(TAG, "No token found");
} }
if (getArguments() != null && getArguments().containsKey("conversation_id")) { if (getArguments() != null && getArguments().containsKey("conversation_id")) {
@@ -172,141 +329,60 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis
} else if (getActivity() != null && getActivity().getIntent().hasExtra("conversation_id")) { } else if (getActivity() != null && getActivity().getIntent().hasExtra("conversation_id")) {
activeConversationId = getActivity().getIntent().getLongExtra("conversation_id", -1); activeConversationId = getActivity().getIntent().getLongExtra("conversation_id", -1);
getActivity().getIntent().removeExtra("conversation_id"); getActivity().getIntent().removeExtra("conversation_id");
getActivity().getIntent().removeExtra("navigate_to"); } else {
activeConversationId = viewModel.getLastActiveConversationId();
} }
loadCustomers(); viewModel.loadCustomers();
if (activeConversationId != null) {
if (stompChatManager != null) stompChatManager.subscribeToConversation(activeConversationId);
viewModel.loadMessageHistory(activeConversationId);
} else {
setConversationActive(false, null);
}
} }
/**
* Fetches a list of customers from the ViewModel to display customer names for the chat list.
*/
private void loadCustomers() {
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();
}
});
}
/**
* Retrieves all conversations for the current user through the ViewModel and populates the chat drawer.
*/
private void loadConversations() {
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);
}
}
});
}
/**
* Handles selection of a chat from the drawer, updating the UI and subscribing to the WebSocket.
*/
@Override @Override
public void onChatClick(Chat chat) { public void onChatClick(Chat chat) {
activeConversationId = Long.parseLong(chat.getChatId()); activeConversationId = Long.parseLong(chat.getChatId());
setConversationActive(true); viewModel.setLastActiveConversationId(activeConversationId);
setConversationActive(true, chat.getStatus());
binding.tvChatTitle.setText(chat.getCustomerName()); binding.tvChatTitle.setText(chat.getCustomerName());
binding.chatDrawerLayout.closeDrawer(GravityCompat.START); binding.chatDrawerLayout.closeDrawer(GravityCompat.START);
if (stompChatManager != null) { if (stompChatManager != null) stompChatManager.subscribeToConversation(activeConversationId);
stompChatManager.subscribeToConversation(activeConversationId); viewModel.loadMessageHistory(activeConversationId);
}
loadMessageHistory(activeConversationId);
} }
/**
* Fetches the full message history for a specific conversation from the ViewModel.
*/
private void loadMessageHistory(Long conversationId) {
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();
}
});
}
/**
* Sends a plain text message to the currently active conversation through the ViewModel.
*/
private void sendMessage() { private void sendMessage() {
//check if a chat is selected
if (activeConversationId == null) return; if (activeConversationId == null) return;
//get the message from text field
String text = binding.etMessage.getText().toString().trim(); String text = binding.etMessage.getText().toString().trim();
if (text.isEmpty()) return; if (text.isEmpty()) return;
//clear text field after sending
binding.etMessage.setText(""); binding.etMessage.setText("");
viewModel.sendMessage(activeConversationId, text).observe(getViewLifecycleOwner(), resource -> {
//calls viewmodel to send the message if (resource == null) return;
viewModel.sendMessage(activeConversationId, new SendMessageRequest(text)).observe(getViewLifecycleOwner(), resource -> { setLoading(resource.status == Resource.Status.LOADING);
if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) { if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
messageList.add(dtoToModel(resource.data)); viewModel.addMessageLocally(resource.data);
messageAdapter.notifyItemInserted(messageList.size() - 1); viewModel.loadConversations();
scrollToBottom();
loadConversations();
} }
}); });
} }
/**
* Launches a file picker intent to select an attachment for the message.
*/
private void selectAttachment() { private void selectAttachment() {
Intent intent = new Intent(Intent.ACTION_GET_CONTENT); Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
intent.setType("*/*"); intent.setType("*/*");
attachmentLauncher.launch(intent); attachmentLauncher.launch(intent);
} }
/**
* Displays a preview of the selected attachment in the UI.
*/
private void showAttachmentPreview(Uri uri) { private void showAttachmentPreview(Uri uri) {
pendingAttachmentUri = uri; pendingAttachmentUri = uri;
binding.layoutAttachmentPreview.setVisibility(View.VISIBLE); binding.layoutAttachmentPreview.setVisibility(View.VISIBLE);
String mimeType = requireContext().getContentResolver().getType(uri); String mimeType = requireContext().getContentResolver().getType(uri);
String fileName = getFileName(uri); binding.tvPreviewName.setText(FileUtils.getFileName(requireContext(), uri));
binding.tvPreviewName.setText(fileName);
// If the file is an image, display a thumbnail of the image as well
if (mimeType != null && mimeType.startsWith("image/")) { if (mimeType != null && mimeType.startsWith("image/")) {
binding.ivPreview.setVisibility(View.VISIBLE); binding.ivPreview.setVisibility(View.VISIBLE);
Glide.with(this).load(uri).into(binding.ivPreview); Glide.with(this).load(uri).into(binding.ivPreview);
@@ -315,183 +391,94 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis
} }
} }
/**
* Clears the current attachment selection and hides the preview UI.
*/
private void removeAttachment() { private void removeAttachment() {
pendingAttachmentUri = null; pendingAttachmentUri = null;
binding.layoutAttachmentPreview.setVisibility(View.GONE); binding.layoutAttachmentPreview.setVisibility(View.GONE);
} }
/**
* Show the display name of the file from its Uri.
*/
private String getFileName(Uri uri) {
String result = null;
if (uri.getScheme().equals("content")) {
try (Cursor cursor = requireContext().getContentResolver().query(uri, null, null, null, null)) {
if (cursor != null && cursor.moveToFirst()) {
int index = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME);
if (index != -1) {
result = cursor.getString(index);
}
}
}
}
if (result == null) {
result = uri.getPath();
int cut = result.lastIndexOf('/');
if (cut != -1) {
result = result.substring(cut + 1);
}
}
return result;
}
/**
* Handles sending a message that includes a file attachment via the ViewModel.
*/
private void sendWithAttachment(Uri uri) { private void sendWithAttachment(Uri uri) {
if (activeConversationId == null) return; if (activeConversationId == null) return;
File file = FileUtils.getFileFromUri(requireContext(), uri);
if (file == null) {
Toast.makeText(requireContext(), "Failed to prepare file", Toast.LENGTH_SHORT).show();
return;
}
String text = binding.etMessage.getText().toString().trim(); String text = binding.etMessage.getText().toString().trim();
MultipartBody.Part contentPart = text.isEmpty()
? null
: MultipartBody.Part.createFormData("content", text);
String mimeType = requireContext().getContentResolver().getType(uri);
if (mimeType == null) mimeType = "application/octet-stream";
RequestBody filePartBody = RequestBody.create(file, MediaType.parse(mimeType));
MultipartBody.Part filePart = MultipartBody.Part.createFormData("file", file.getName(), filePartBody);
binding.etMessage.setText(""); binding.etMessage.setText("");
removeAttachment(); removeAttachment();
if (!text.isEmpty()) { viewModel.sendMessageWithAttachment(activeConversationId, contentPart, filePart).observe(getViewLifecycleOwner(), resource -> {
binding.etMessage.setText(text); if (resource == null) return;
} setLoading(resource.status == Resource.Status.LOADING);
Toast.makeText(requireContext(), "File attachments are not supported", Toast.LENGTH_SHORT).show(); if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
} viewModel.addMessageLocally(resource.data);
viewModel.loadConversations();
/** } else if (resource.status == Resource.Status.ERROR) {
* Callback triggered when a new message is received via the WebSocket. Toast.makeText(requireContext(), "Failed to send attachment: " + resource.message, Toast.LENGTH_SHORT).show();
*/ }
@Override });
public void onMessageReceived(MessageDTO dto) { }
//if there is no active selected conversation or the message received is for another chat, then just update the preview of last message
if (activeConversationId == null || !activeConversationId.equals(dto.getConversationId())) { @Override
updateConversationPreview(dto.getConversationId(), dto.getContent()); public void onMessageReceived(MessageDTO dto) {
return; requireActivity().runOnUiThread(() -> {
} if (activeConversationId != null && activeConversationId.equals(dto.getConversationId())) {
updateConversationPreview(dto.getConversationId(), dto.getContent()); if (!tokenManager.getUserId().equals(dto.getSenderId())) {
viewModel.addMessageLocally(dto);
if (currentUserId != null && currentUserId.equals(dto.getSenderId())) return; }
}
//else add the message to the active chat if it's not from the current user viewModel.updateConversationLocally(new ConversationDTO(dto.getConversationId(), 0L, 0L, dto.getContent(), ""));
messageList.add(dtoToModel(dto));
requireActivity().runOnUiThread(() -> {
messageAdapter.notifyItemInserted(messageList.size() - 1);
scrollToBottom();
}); });
} }
/**
* Callback triggered when a conversation is created or updated via the WebSocket.
*/
@Override @Override
public void onConversationUpdated(ConversationDTO dto) { public void onConversationUpdated(ConversationDTO dto) {
requireActivity().runOnUiThread(() -> { requireActivity().runOnUiThread(() -> {
boolean updated = false; viewModel.updateConversationLocally(dto);
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(
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.notifyItemInserted(0);
}
if (activeConversationId != null && activeConversationId.equals(dto.getId())) { if (activeConversationId != null && activeConversationId.equals(dto.getId())) {
setConversationActive(true); setConversationActive(true, dto.getStatus());
binding.tvChatTitle.setText(name); binding.tvChatTitle.setText(viewModel.getCustomerName(dto.getCustomerId()));
} }
}); });
} }
/**
* Callback triggered when the WebSocket connection is successfully opened.
*/
@Override @Override
public void onSocketOpened() { public void onSocketOpened() {
if (!isAdded()) { if (!isAdded()) return;
return;
}
requireActivity().runOnUiThread(() -> { requireActivity().runOnUiThread(() -> {
loadConversations(); viewModel.loadConversations();
if (activeConversationId != null) { if (activeConversationId != null) viewModel.loadMessageHistory(activeConversationId);
loadMessageHistory(activeConversationId);
}
}); });
} }
/**
* Callback triggered when the WebSocket connection is closed.
*/
@Override @Override
public void onSocketClosed() { public void onSocketClosed() {
if (!isAdded()) { if (!isAdded()) return;
return; requireActivity().runOnUiThread(viewModel::loadConversations);
}
requireActivity().runOnUiThread(this::loadConversations);
} }
/**
* Callback triggered when a WebSocket connection error occurs.
*/
@Override @Override
public void onSocketError() { public void onSocketError() {
if (!isAdded()) { if (!isAdded()) return;
return;
}
requireActivity().runOnUiThread(() -> { requireActivity().runOnUiThread(() -> {
loadConversations(); viewModel.loadConversations();
if (activeConversationId != null) { if (activeConversationId != null) viewModel.loadMessageHistory(activeConversationId);
loadMessageHistory(activeConversationId);
}
}); });
} }
/**
* Converts a MessageDTO into a Message object.
*/
private Message dtoToModel(MessageDTO dto) {
Message m = new Message();
m.setId(dto.getId());
m.setConversationId(dto.getConversationId());
m.setSenderId(dto.getSenderId());
m.setContent(dto.getContent());
m.setTimestamp(dto.getTimestamp());
m.setIsRead(dto.getIsRead());
m.setAttachmentUrl(dto.getAttachmentUrl());
m.setAttachmentName(dto.getAttachmentName());
m.setAttachmentType(dto.getAttachmentType());
return m;
}
/**
* Scrolls the message history RecyclerView to the most recent message.
*/
private void scrollToBottom() { private void scrollToBottom() {
if (!messageList.isEmpty()) { if (!messageList.isEmpty()) {
binding.rvMessages.post(() -> binding.rvMessages.post(() ->
@@ -499,60 +486,26 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis
} }
} }
/** private void setConversationActive(boolean active, String status) {
* Updates the preview snippet of the last message for a specific conversation in the drawer. boolean isClosed = "CLOSED".equalsIgnoreCase(status);
*/ UIUtils.setViewsEnabled(active && !isClosed, binding.btnSend, binding.etMessage, binding.btnAttach);
private void updateConversationPreview(Long conversationId, String lastMessage) {
if (conversationId == null) {
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;
}
}
});
}
/**
* Toggles the UI state based on whether a conversation is currently selected.
*/
private void setConversationActive(boolean active) {
binding.btnSend.setEnabled(active);
binding.etMessage.setEnabled(active);
binding.btnAttach.setEnabled(active);
if (!active) { if (!active) {
activeConversationId = null; activeConversationId = null;
ChatNotificationService.activeConversationIdInUi = null; ChatNotificationService.activeConversationIdInUi = null;
removeAttachment(); removeAttachment();
if (binding != null && binding.tvChatTitle != null) binding.tvChatTitle.setText("Customer Chat"); if (binding != null && binding.tvChatTitle != null) binding.tvChatTitle.setText("Customer Chat");
if (stompChatManager != null) { if (stompChatManager != null) stompChatManager.clearConversationSubscription();
stompChatManager.clearConversationSubscription();
}
messageList.clear(); messageList.clear();
messageAdapter.notifyDataSetChanged(); messageAdapter.notifyDataSetChanged();
binding.etMessage.setText(""); binding.etMessage.setText("");
binding.etMessage.setHint("Select a chat to start messaging"); binding.etMessage.setHint("Select a chat to start messaging");
} else { } else {
binding.etMessage.setHint("Type a message..."); binding.etMessage.setHint(isClosed ? "This chat is closed" : "Type a message...");
ChatNotificationService.activeConversationIdInUi = activeConversationId; ChatNotificationService.activeConversationIdInUi = activeConversationId;
} }
} }
/**
* Disconnects the WebSocket manager when the fragment view is destroyed.
*/
@Override @Override
public void onDestroyView() { public void onDestroyView() {
super.onDestroyView(); super.onDestroyView();

View File

@@ -172,6 +172,12 @@ public class ProfileFragment extends Fragment {
return binding.getRoot(); return binding.getRoot();
} }
private void setLoading(boolean loading) {
if (binding != null && binding.progressBar != null) {
binding.progressBar.setVisibility(loading ? View.VISIBLE : View.GONE);
}
}
@Override @Override
public void onDestroyView() { public void onDestroyView() {
super.onDestroyView(); super.onDestroyView();
@@ -184,6 +190,7 @@ public class ProfileFragment extends Fragment {
private void loadProfileData() { private void loadProfileData() {
viewModel.getMe().observe(getViewLifecycleOwner(), resource -> { viewModel.getMe().observe(getViewLifecycleOwner(), resource -> {
if (resource == null) return; if (resource == null) return;
setLoading(resource.status == Resource.Status.LOADING);
if (resource.status == Resource.Status.SUCCESS && resource.data != null) { if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
currentUser = resource.data; currentUser = resource.data;
@@ -229,6 +236,7 @@ public class ProfileFragment extends Fragment {
//Call the backend to upload the avatar //Call the backend to upload the avatar
viewModel.uploadAvatar(body).observe(getViewLifecycleOwner(), resource -> { viewModel.uploadAvatar(body).observe(getViewLifecycleOwner(), resource -> {
if (resource == null) return; if (resource == null) return;
setLoading(resource.status == Resource.Status.LOADING);
if (resource.status == Resource.Status.SUCCESS) { if (resource.status == Resource.Status.SUCCESS) {
Toast.makeText(getContext(), "Avatar updated successfully", Toast.LENGTH_SHORT).show(); Toast.makeText(getContext(), "Avatar updated successfully", Toast.LENGTH_SHORT).show();
loadProfileData(); loadProfileData();
@@ -247,6 +255,7 @@ public class ProfileFragment extends Fragment {
private void deleteAvatar() { private void deleteAvatar() {
viewModel.deleteAvatar().observe(getViewLifecycleOwner(), resource -> { viewModel.deleteAvatar().observe(getViewLifecycleOwner(), resource -> {
if (resource == null) return; if (resource == null) return;
setLoading(resource.status == Resource.Status.LOADING);
if (resource.status == Resource.Status.SUCCESS) { if (resource.status == Resource.Status.SUCCESS) {
hasImage = false; hasImage = false;
binding.imgProfile.setImageResource(R.drawable.placeholder); binding.imgProfile.setImageResource(R.drawable.placeholder);
@@ -266,6 +275,7 @@ public class ProfileFragment extends Fragment {
viewModel.updateMe(updates).observe(getViewLifecycleOwner(), resource -> { viewModel.updateMe(updates).observe(getViewLifecycleOwner(), resource -> {
if (resource == null) return; if (resource == null) return;
setLoading(resource.status == Resource.Status.LOADING);
if (resource.status == Resource.Status.SUCCESS && resource.data != null) { if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
currentUser = resource.data; currentUser = resource.data;
Toast.makeText(getContext(), "Profile updated successfully", Toast.LENGTH_SHORT).show(); Toast.makeText(getContext(), "Profile updated successfully", Toast.LENGTH_SHORT).show();

View File

@@ -24,9 +24,8 @@ import com.example.petstoremobile.utils.BulkDeleteHandler;
import com.example.petstoremobile.utils.Resource; import com.example.petstoremobile.utils.Resource;
import com.example.petstoremobile.utils.SpinnerUtils; import com.example.petstoremobile.utils.SpinnerUtils;
import com.example.petstoremobile.utils.UIUtils; import com.example.petstoremobile.utils.UIUtils;
import com.example.petstoremobile.viewmodels.AdoptionViewModel; import com.example.petstoremobile.viewmodels.AdoptionListViewModel;
import com.example.petstoremobile.utils.EventDecorator; import com.example.petstoremobile.utils.EventDecorator;
import com.example.petstoremobile.viewmodels.StoreViewModel;
import com.prolificinteractive.materialcalendarview.CalendarDay; import com.prolificinteractive.materialcalendarview.CalendarDay;
import com.prolificinteractive.materialcalendarview.CalendarMode; import com.prolificinteractive.materialcalendarview.CalendarMode;
@@ -46,28 +45,19 @@ public class AdoptionFragment extends Fragment implements AdoptionAdapter.OnAdop
private FragmentAdoptionBinding binding; private FragmentAdoptionBinding binding;
private List<AdoptionDTO> adoptionList = new ArrayList<>(); private List<AdoptionDTO> adoptionList = new ArrayList<>();
private List<StoreDTO> storeList = new ArrayList<>();
private AdoptionAdapter adapter; private AdoptionAdapter adapter;
private AdoptionViewModel adoptionViewModel; private AdoptionListViewModel viewModel;
private StoreViewModel storeViewModel;
private BulkDeleteHandler bulkDeleteHandler; private BulkDeleteHandler bulkDeleteHandler;
private CalendarDay selectedCalendarDay; private CalendarDay selectedCalendarDay;
private boolean isMonthMode = false; private boolean isMonthMode = false;
private final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()); private final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd", Locale.getDefault());
/**
* Initializes the fragment and its ViewModels.
*/
@Override @Override
public void onCreate(@Nullable Bundle savedInstanceState) { public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
adoptionViewModel = new ViewModelProvider(this).get(AdoptionViewModel.class); viewModel = new ViewModelProvider(this).get(AdoptionListViewModel.class);
storeViewModel = new ViewModelProvider(this).get(StoreViewModel.class);
} }
/**
* Sets up the fragment's UI components, including RecyclerView, Search, SwipeRefresh, and Calendar.
*/
@Override @Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) { Bundle savedInstanceState) {
@@ -81,6 +71,7 @@ public class AdoptionFragment extends Fragment implements AdoptionAdapter.OnAdop
setupCalendar(); setupCalendar();
setupFilterToggle(); setupFilterToggle();
setupBulkDelete(); setupBulkDelete();
observeViewModel();
binding.fabAddAdoption.setOnClickListener(v -> openDetail(-1)); binding.fabAddAdoption.setOnClickListener(v -> openDetail(-1));
@@ -91,6 +82,24 @@ public class AdoptionFragment extends Fragment implements AdoptionAdapter.OnAdop
return binding.getRoot(); return binding.getRoot();
} }
private void observeViewModel() {
viewModel.getAdoptions().observe(getViewLifecycleOwner(), list -> {
adoptionList.clear();
adoptionList.addAll(list);
updateCalendarDecorators();
adapter.notifyDataSetChanged();
});
viewModel.getStores().observe(getViewLifecycleOwner(), list -> {
SpinnerUtils.populateWhiteSpinner(requireContext(), binding.spinnerStoreAdoption, list,
StoreDTO::getStoreName, "All Stores", -1L, StoreDTO::getStoreId);
});
viewModel.getIsLoading().observe(getViewLifecycleOwner(), loading -> {
binding.swipeRefreshAdoption.setRefreshing(loading);
});
}
private void setupBulkDelete() { private void setupBulkDelete() {
bulkDeleteHandler = new BulkDeleteHandler( bulkDeleteHandler = new BulkDeleteHandler(
this, this,
@@ -99,27 +108,18 @@ public class AdoptionFragment extends Fragment implements AdoptionAdapter.OnAdop
binding.btnBulkDelete, binding.btnBulkDelete,
adapter, adapter,
"adoption", "adoption",
adoptionViewModel::bulkDeleteAdoptions, viewModel::bulkDeleteAdoptions,
this::loadAdoptions this::loadAdoptions
); );
} }
@Override
public void onDestroyView() {
super.onDestroyView();
binding = null;
}
@Override @Override
public void onResume() { public void onResume() {
super.onResume(); super.onResume();
loadAdoptions(); loadAdoptions();
loadStoreData(); viewModel.loadStores();
} }
/**
* Toggles the calendar display between week and month modes.
*/
private void toggleCalendarMode() { private void toggleCalendarMode() {
isMonthMode = !isMonthMode; isMonthMode = !isMonthMode;
binding.calendarViewAdoption.state().edit() binding.calendarViewAdoption.state().edit()
@@ -127,35 +127,11 @@ public class AdoptionFragment extends Fragment implements AdoptionAdapter.OnAdop
.commit(); .commit();
} }
/**
* Sets up the filter toggle button to show/hide the filter layout.
*/
private void setupFilterToggle() { private void setupFilterToggle() {
UIUtils.setupFilterToggle(binding.btnToggleFilterAdoption, binding.layoutFilterAdoption, UIUtils.setupFilterToggle(binding.btnToggleFilterAdoption, binding.layoutFilterAdoption,
binding.etSearchAdoption, binding.spinnerStatusAdoption, binding.spinnerStoreAdoption); binding.etSearchAdoption, binding.spinnerStatusAdoption, binding.spinnerStoreAdoption);
binding.btnToggleFilterAdoption.setOnClickListener(v -> {
boolean isVisible = binding.layoutFilterAdoption.getVisibility() == View.VISIBLE;
binding.layoutFilterAdoption.setVisibility(isVisible ? View.GONE : View.VISIBLE);
binding.btnToggleFilterAdoption.setImageResource(isVisible ?
android.R.drawable.ic_menu_search :
android.R.drawable.ic_menu_close_clear_cancel);
if (isVisible) {
binding.etSearchAdoption.setText("");
binding.spinnerStatusAdoption.setSelection(0);
binding.spinnerStoreAdoption.setSelection(0);
selectedCalendarDay = null;
binding.calendarViewAdoption.clearSelection();
loadAdoptions();
}
});
} }
/**
* Sets up the date selection listener for the calendar.
*/
private void setupCalendar() { private void setupCalendar() {
binding.calendarViewAdoption.setOnDateChangedListener((widget, date, selected) -> { binding.calendarViewAdoption.setOnDateChangedListener((widget, date, selected) -> {
if (selected) { if (selected) {
@@ -172,9 +148,6 @@ public class AdoptionFragment extends Fragment implements AdoptionAdapter.OnAdop
}); });
} }
/**
* Updates the calendar decorators to highlight days with adoptions.
*/
private void updateCalendarDecorators() { private void updateCalendarDecorators() {
HashSet<CalendarDay> datesWithAdoptions = new HashSet<>(); HashSet<CalendarDay> datesWithAdoptions = new HashSet<>();
for (AdoptionDTO adoption : adoptionList) { for (AdoptionDTO adoption : adoptionList) {
@@ -195,67 +168,37 @@ public class AdoptionFragment extends Fragment implements AdoptionAdapter.OnAdop
binding.calendarViewAdoption.addDecorator(new EventDecorator(Color.RED, datesWithAdoptions)); binding.calendarViewAdoption.addDecorator(new EventDecorator(Color.RED, datesWithAdoptions));
} }
/**
* Initializes the RecyclerView for displaying adoptions.
*/
private void setupRecyclerView() { private void setupRecyclerView() {
adapter = new AdoptionAdapter(adoptionList, this); adapter = new AdoptionAdapter(adoptionList, this);
binding.recyclerViewAdoptions.setLayoutManager(new LinearLayoutManager(getContext())); binding.recyclerViewAdoptions.setLayoutManager(new LinearLayoutManager(getContext()));
binding.recyclerViewAdoptions.setAdapter(adapter); binding.recyclerViewAdoptions.setAdapter(adapter);
} }
/**
* Sets up the search bar for filtering
*/
private void setupSearch() { private void setupSearch() {
UIUtils.attachSearch(binding.etSearchAdoption, this::loadAdoptions); UIUtils.attachSearch(binding.etSearchAdoption, this::loadAdoptions);
} }
/**
* Configures the status filter spinner.
*/
private void setupStatusFilter() { private void setupStatusFilter() {
String[] statuses = {"All Statuses", "Completed", "Pending", "Cancelled"}; String[] statuses = {"All Statuses", "Completed", "Pending", "Cancelled"};
SpinnerUtils.setupStringFilterSpinner(requireContext(), binding.spinnerStatusAdoption, statuses, this::loadAdoptions); SpinnerUtils.setupStringFilterSpinner(requireContext(), binding.spinnerStatusAdoption, statuses, this::loadAdoptions);
} }
/**
* Configures the store filter spinner.
*/
private void setupStoreFilter() { private void setupStoreFilter() {
SpinnerUtils.setupFilterSpinner(binding.spinnerStoreAdoption, this::loadAdoptions); SpinnerUtils.setupFilterSpinner(binding.spinnerStoreAdoption, this::loadAdoptions);
} }
/**
* Fetches store data to populate the store filter.
*/
private void loadStoreData() {
storeViewModel.getAllStores(0, 100).observe(getViewLifecycleOwner(), resource -> {
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
storeList = resource.data.getContent();
SpinnerUtils.populateWhiteSpinner(requireContext(), binding.spinnerStoreAdoption, storeList,
StoreDTO::getStoreName, "All Stores", -1L, StoreDTO::getStoreId);
}
});
}
/**
* Sets up the SwipeRefreshLayout to reload adoption data.
*/
private void setupSwipeRefresh() { private void setupSwipeRefresh() {
binding.swipeRefreshAdoption.setOnRefreshListener(this::loadAdoptions); binding.swipeRefreshAdoption.setOnRefreshListener(this::loadAdoptions);
} }
/**
* Fetches the adoption list from the server through the ViewModel.
*/
private void loadAdoptions() { private void loadAdoptions() {
String query = binding.etSearchAdoption.getText().toString().trim(); String query = binding.etSearchAdoption.getText().toString().trim();
String status = binding.spinnerStatusAdoption.getSelectedItem() != null ? binding.spinnerStatusAdoption.getSelectedItem().toString() : "All Statuses"; String status = binding.spinnerStatusAdoption.getSelectedItem() != null ? binding.spinnerStatusAdoption.getSelectedItem().toString() : "All Statuses";
Long storeId = null; Long storeId = null;
if (binding.spinnerStoreAdoption.getSelectedItemPosition() > 0 && !storeList.isEmpty()) { List<StoreDTO> stores = viewModel.getStores().getValue();
storeId = storeList.get(binding.spinnerStoreAdoption.getSelectedItemPosition() - 1).getStoreId(); if (binding.spinnerStoreAdoption.getSelectedItemPosition() > 0 && stores != null && !stores.isEmpty()) {
storeId = stores.get(binding.spinnerStoreAdoption.getSelectedItemPosition() - 1).getStoreId();
} }
String selectedDateString = null; String selectedDateString = null;
@@ -267,52 +210,18 @@ public class AdoptionFragment extends Fragment implements AdoptionAdapter.OnAdop
if (status.equals("All Statuses")) status = null; if (status.equals("All Statuses")) status = null;
else status = status.toUpperCase(); else status = status.toUpperCase();
adoptionViewModel.getAllAdoptions(0, 500, query, status, storeId, selectedDateString, null).observe(getViewLifecycleOwner(), resource -> { viewModel.loadAdoptions(true, query, status, storeId, selectedDateString, null);
if (resource == null) return;
// Check the status to see if the resource is loaded and display the data
switch (resource.status) {
case LOADING:
// Show loading indicator
binding.swipeRefreshAdoption.setRefreshing(true);
break;
case SUCCESS:
// Hide loading indicator and display data
binding.swipeRefreshAdoption.setRefreshing(false);
if (resource.data != null) {
adoptionList.clear();
adoptionList.addAll(resource.data.getContent());
updateCalendarDecorators();
adapter.notifyDataSetChanged();
}
break;
case ERROR:
// Hide loading indicator and toast error message
binding.swipeRefreshAdoption.setRefreshing(false);
Toast.makeText(getContext(), "Failed to load adoptions: " + resource.message, Toast.LENGTH_SHORT).show();
Log.e("AdoptionFragment", "Error loading adoptions: " + resource.message);
break;
}
});
} }
/**
* Navigates to the adoption detail screen for a specific adoption or to create a new one.
*/
private void openDetail(int position) { private void openDetail(int position) {
Bundle args = new Bundle(); Bundle args = new Bundle();
if (position != -1) { if (position != -1) {
AdoptionDTO a = adoptionList.get(position); AdoptionDTO a = adoptionList.get(position);
args.putLong("adoptionId", a.getAdoptionId()); args.putLong("adoptionId", a.getAdoptionId());
} }
NavHostFragment.findNavController(this).navigate(R.id.nav_adoption_detail, args); NavHostFragment.findNavController(this).navigate(R.id.nav_adoption_detail, args);
} }
/**
* Handles item click in the adoption list.
*/
@Override @Override
public void onAdoptionClick(int position) { openDetail(position); } public void onAdoptionClick(int position) { openDetail(position); }
@@ -322,4 +231,10 @@ public class AdoptionFragment extends Fragment implements AdoptionAdapter.OnAdop
bulkDeleteHandler.onSelectionChanged(selectedCount); bulkDeleteHandler.onSelectionChanged(selectedCount);
} }
} }
@Override
public void onDestroyView() {
super.onDestroyView();
binding = null;
}
} }

View File

@@ -2,16 +2,14 @@ package com.example.petstoremobile.fragments.listfragments;
import android.graphics.Color; import android.graphics.Color;
import android.os.Bundle; import android.os.Bundle;
import android.util.Log;
import android.view.*; import android.view.*;
import android.widget.*; import android.widget.*;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment; import androidx.fragment.app.Fragment;
import androidx.lifecycle.ViewModelProvider; import androidx.lifecycle.ViewModelProvider;
import com.example.petstoremobile.databinding.FragmentAnalyticsBinding; import com.example.petstoremobile.databinding.FragmentAnalyticsBinding;
import com.example.petstoremobile.dtos.SaleDTO;
import com.example.petstoremobile.utils.UIUtils; import com.example.petstoremobile.utils.UIUtils;
import com.example.petstoremobile.viewmodels.SaleViewModel; import com.example.petstoremobile.viewmodels.AnalyticsViewModel;
import dagger.hilt.android.AndroidEntryPoint; import dagger.hilt.android.AndroidEntryPoint;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.math.RoundingMode; import java.math.RoundingMode;
@@ -21,228 +19,130 @@ import java.util.*;
public class AnalyticsFragment extends Fragment { public class AnalyticsFragment extends Fragment {
private FragmentAnalyticsBinding binding; private FragmentAnalyticsBinding binding;
private SaleViewModel saleViewModel; private AnalyticsViewModel viewModel;
@Override @Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) { Bundle savedInstanceState) {
binding = FragmentAnalyticsBinding.inflate(inflater, container, false); binding = FragmentAnalyticsBinding.inflate(inflater, container, false);
saleViewModel = new ViewModelProvider(this).get(SaleViewModel.class); viewModel = new ViewModelProvider(this).get(AnalyticsViewModel.class);
loadAnalytics(); observeViewModel();
viewModel.loadAnalytics();
binding.btnRefreshAnalytics.setOnClickListener(v -> loadAnalytics()); binding.btnRefreshAnalytics.setOnClickListener(v -> viewModel.loadAnalytics());
UIUtils.setupHamburgerMenu(binding.btnHamburgerAnalytics, this); UIUtils.setupHamburgerMenu(binding.btnHamburgerAnalytics, this);
return binding.getRoot(); return binding.getRoot();
} }
private void observeViewModel() {
viewModel.getAnalyticsData().observe(getViewLifecycleOwner(), this::computeAndDisplay);
viewModel.getIsLoading().observe(getViewLifecycleOwner(), loading -> {
binding.progressBar.setVisibility(loading ? View.VISIBLE : View.GONE);
if (loading) {
binding.tvTotalRevenue.setText("Loading...");
binding.tvTotalTransactions.setText("...");
binding.tvAvgTransaction.setText("...");
binding.tvTotalItems.setText("...");
}
});
viewModel.getErrorMessage().observe(getViewLifecycleOwner(), error -> {
if (error != null) showError(error);
});
}
@Override @Override
public void onDestroyView() { public void onDestroyView() {
super.onDestroyView(); super.onDestroyView();
binding = null; binding = null;
} }
private void loadAnalytics() { private void computeAndDisplay(AnalyticsViewModel.AnalyticsData data) {
// Clear all sections if (data == null) return;
// Summary
binding.tvTotalRevenue.setText("$" + data.totalRevenue.setScale(2, RoundingMode.HALF_UP));
binding.tvTotalTransactions.setText(String.valueOf(data.totalTransactions));
binding.tvAvgTransaction.setText("$" + data.avgTransaction);
binding.tvTotalItems.setText(String.valueOf(data.totalItems));
// Top Revenue Products
binding.llTopRevenue.removeAllViews(); binding.llTopRevenue.removeAllViews();
binding.llTopQuantity.removeAllViews(); if (data.topRevenueProducts != null && !data.topRevenueProducts.isEmpty()) {
binding.llPaymentMethods.removeAllViews(); BigDecimal maxRevenue = data.topRevenueProducts.get(0).getValue();
binding.llEmployeePerformance.removeAllViews(); if (maxRevenue.compareTo(BigDecimal.ZERO) == 0) maxRevenue = BigDecimal.ONE;
binding.llDailyRevenue.removeAllViews(); for (Map.Entry<String, BigDecimal> e : data.topRevenueProducts) {
addBarRow(binding.llTopRevenue, e.getKey(), "$" + e.getValue().setScale(2, RoundingMode.HALF_UP),
// Show loading e.getValue().floatValue() / maxRevenue.floatValue(), "#ff6b35");
binding.tvTotalRevenue.setText("Loading...");
binding.tvTotalTransactions.setText("...");
binding.tvAvgTransaction.setText("...");
binding.tvTotalItems.setText("...");
saleViewModel.getAllSales(0, 1000, null, null, null, "saleDate,desc")
.observe(getViewLifecycleOwner(), resource -> {
if (resource != null) {
switch (resource.status) {
case SUCCESS:
if (resource.data != null) {
computeAndDisplay(resource.data.getContent());
}
break;
case ERROR:
Log.e("Analytics", resource.message != null ? resource.message : "Error loading sales");
showError("Failed to load sales data");
break;
case LOADING:
// Already showing loading state in UI
break;
}
}
});
}
private void computeAndDisplay(List<SaleDTO> sales) {
// Filter out refunds for most metrics
List<SaleDTO> regularSales = new ArrayList<>();
for (SaleDTO s : sales) {
if (!Boolean.TRUE.equals(s.getIsRefund()))
regularSales.add(s);
}
// ── Summary ──────────────────────────────────────────
BigDecimal totalRevenue = BigDecimal.ZERO;
int totalItems = 0;
for (SaleDTO s : regularSales) {
if (s.getTotalAmount() != null)
totalRevenue = totalRevenue.add(s.getTotalAmount());
if (s.getItems() != null) {
for (SaleDTO.SaleItemDTO item : s.getItems()) {
if (item.getQuantity() != null)
totalItems += Math.abs(item.getQuantity());
}
} }
} } else {
int totalTx = regularSales.size();
BigDecimal avgTx = totalTx > 0
? totalRevenue.divide(BigDecimal.valueOf(totalTx), 2, RoundingMode.HALF_UP)
: BigDecimal.ZERO;
binding.tvTotalRevenue.setText("$" + totalRevenue.setScale(2, RoundingMode.HALF_UP));
binding.tvTotalTransactions.setText(String.valueOf(totalTx));
binding.tvAvgTransaction.setText("$" + avgTx);
binding.tvTotalItems.setText(String.valueOf(totalItems));
// ── Top Products by Revenue ───────────────────────────
Map<String, BigDecimal> revenueByProduct = new LinkedHashMap<>();
Map<String, Integer> quantityByProduct = new LinkedHashMap<>();
for (SaleDTO s : regularSales) {
if (s.getItems() != null) {
for (SaleDTO.SaleItemDTO item : s.getItems()) {
String name = item.getProductName() != null ? item.getProductName() : "Unknown";
int qty = item.getQuantity() != null ? Math.abs(item.getQuantity()) : 0;
BigDecimal lineTotal = item.getUnitPrice() != null
? item.getUnitPrice().multiply(BigDecimal.valueOf(qty))
: BigDecimal.ZERO;
revenueByProduct.merge(name, lineTotal, BigDecimal::add);
quantityByProduct.merge(name, qty, Integer::sum);
}
}
}
// Sort by revenue desc, take top 5
List<Map.Entry<String, BigDecimal>> topRevenue = new ArrayList<>(revenueByProduct.entrySet());
topRevenue.sort((a, b) -> b.getValue().compareTo(a.getValue()));
BigDecimal maxRevenue = topRevenue.isEmpty() ? BigDecimal.ONE : topRevenue.get(0).getValue();
binding.llTopRevenue.removeAllViews();
for (int i = 0; i < Math.min(5, topRevenue.size()); i++) {
Map.Entry<String, BigDecimal> e = topRevenue.get(i);
addBarRow(binding.llTopRevenue, e.getKey(), "$" + e.getValue().setScale(2, RoundingMode.HALF_UP),
e.getValue().floatValue() / maxRevenue.floatValue(), "#ff6b35");
}
if (topRevenue.isEmpty())
addEmptyRow(binding.llTopRevenue, "No data"); addEmptyRow(binding.llTopRevenue, "No data");
}
// Sort by quantity desc, take top 5 // Top Quantity Products
List<Map.Entry<String, Integer>> topQuantity = new ArrayList<>(quantityByProduct.entrySet());
topQuantity.sort((a, b) -> b.getValue() - a.getValue());
int maxQty = topQuantity.isEmpty() ? 1 : topQuantity.get(0).getValue();
binding.llTopQuantity.removeAllViews(); binding.llTopQuantity.removeAllViews();
for (int i = 0; i < Math.min(5, topQuantity.size()); i++) { if (data.topQuantityProducts != null && !data.topQuantityProducts.isEmpty()) {
Map.Entry<String, Integer> e = topQuantity.get(i); int maxQty = data.topQuantityProducts.get(0).getValue();
addBarRow(binding.llTopQuantity, e.getKey(), e.getValue() + " units", if (maxQty == 0) maxQty = 1;
(float) e.getValue() / maxQty, "#4ecdc4"); for (Map.Entry<String, Integer> e : data.topQuantityProducts) {
} addBarRow(binding.llTopQuantity, e.getKey(), e.getValue() + " units",
if (topQuantity.isEmpty()) (float) e.getValue() / maxQty, "#4ecdc4");
addEmptyRow(binding.llTopQuantity, "No data");
// ── Payment Methods ───────────────────────────────────
Map<String, Integer> paymentCount = new LinkedHashMap<>();
for (SaleDTO s : regularSales) {
String method = s.getPaymentMethod() != null ? s.getPaymentMethod() : "Unknown";
paymentCount.merge(method, 1, Integer::sum);
}
int maxPayment = paymentCount.values().stream().max(Integer::compare).orElse(1);
String[] paymentColors = { "#1a759f", "#ff9f1c", "#577590", "#90be6d" };
int ci = 0;
binding.llPaymentMethods.removeAllViews();
for (Map.Entry<String, Integer> e : paymentCount.entrySet()) {
addBarRow(binding.llPaymentMethods, e.getKey(),
e.getValue() + " transactions",
(float) e.getValue() / maxPayment,
paymentColors[ci % paymentColors.length]);
ci++;
}
if (paymentCount.isEmpty())
addEmptyRow(binding.llPaymentMethods, "No data");
// ── Employee Performance ──────────────────────────────
Map<String, BigDecimal> employeeRevenue = new LinkedHashMap<>();
for (SaleDTO s : regularSales) {
String emp = s.getEmployeeName() != null ? s.getEmployeeName() : "Unknown";
if (s.getTotalAmount() != null)
employeeRevenue.merge(emp, s.getTotalAmount(), BigDecimal::add);
}
List<Map.Entry<String, BigDecimal>> empList = new ArrayList<>(employeeRevenue.entrySet());
empList.sort((a, b) -> b.getValue().compareTo(a.getValue()));
BigDecimal maxEmp = empList.isEmpty() ? BigDecimal.ONE : empList.get(0).getValue();
binding.llEmployeePerformance.removeAllViews();
for (Map.Entry<String, BigDecimal> e : empList) {
addBarRow(binding.llEmployeePerformance, e.getKey(),
"$" + e.getValue().setScale(2, RoundingMode.HALF_UP),
e.getValue().floatValue() / maxEmp.floatValue(),
"#1a759f");
}
if (empList.isEmpty())
addEmptyRow(binding.llEmployeePerformance, "No data");
// ── Daily Revenue (last 7 days) ───────────────────────
Map<String, BigDecimal> dailyRevenue = new TreeMap<>();
// Initialize last 7 days
Calendar cal = Calendar.getInstance();
for (int i = 6; i >= 0; i--) {
Calendar day = Calendar.getInstance();
day.add(Calendar.DAY_OF_YEAR, -i);
String key = String.format("%04d-%02d-%02d",
day.get(Calendar.YEAR),
day.get(Calendar.MONTH) + 1,
day.get(Calendar.DAY_OF_MONTH));
dailyRevenue.put(key, BigDecimal.ZERO);
}
for (SaleDTO s : regularSales) {
if (s.getSaleDate() != null && s.getTotalAmount() != null) {
String date = s.getSaleDate().length() >= 10
? s.getSaleDate().substring(0, 10)
: s.getSaleDate();
if (dailyRevenue.containsKey(date)) {
dailyRevenue.merge(date, s.getTotalAmount(), BigDecimal::add);
}
} }
} else {
addEmptyRow(binding.llTopQuantity, "No data");
} }
BigDecimal maxDaily = dailyRevenue.values().stream() // Payment Methods
.max(BigDecimal::compareTo).orElse(BigDecimal.ONE); binding.llPaymentMethods.removeAllViews();
if (maxDaily.compareTo(BigDecimal.ZERO) == 0) if (data.paymentMethodStats != null && !data.paymentMethodStats.isEmpty()) {
maxDaily = BigDecimal.ONE; int maxPayment = data.paymentMethodStats.stream().mapToInt(Map.Entry::getValue).max().orElse(1);
String[] paymentColors = { "#1a759f", "#ff9f1c", "#577590", "#90be6d" };
int ci = 0;
for (Map.Entry<String, Integer> e : data.paymentMethodStats) {
addBarRow(binding.llPaymentMethods, e.getKey(),
e.getValue() + " transactions",
(float) e.getValue() / maxPayment,
paymentColors[ci % paymentColors.length]);
ci++;
}
} else {
addEmptyRow(binding.llPaymentMethods, "No data");
}
// Employee Performance
binding.llEmployeePerformance.removeAllViews();
if (data.employeePerformance != null && !data.employeePerformance.isEmpty()) {
BigDecimal maxEmp = data.employeePerformance.get(data.employeePerformance.size() - 1).getValue();
if (maxEmp.compareTo(BigDecimal.ZERO) == 0) maxEmp = BigDecimal.ONE;
maxEmp = data.employeePerformance.get(0).getValue();
if (maxEmp.compareTo(BigDecimal.ZERO) == 0) maxEmp = BigDecimal.ONE;
for (Map.Entry<String, BigDecimal> e : data.employeePerformance) {
addBarRow(binding.llEmployeePerformance, e.getKey(),
"$" + e.getValue().setScale(2, RoundingMode.HALF_UP),
e.getValue().floatValue() / maxEmp.floatValue(),
"#1a759f");
}
} else {
addEmptyRow(binding.llEmployeePerformance, "No data");
}
// Daily Revenue
binding.llDailyRevenue.removeAllViews(); binding.llDailyRevenue.removeAllViews();
for (Map.Entry<String, BigDecimal> e : dailyRevenue.entrySet()) { if (data.dailyRevenue != null && !data.dailyRevenue.isEmpty()) {
// Show just MM-DD BigDecimal maxDaily = data.dailyRevenue.stream().map(Map.Entry::getValue).max(BigDecimal::compareTo).orElse(BigDecimal.ONE);
String label = e.getKey().length() >= 10 if (maxDaily.compareTo(BigDecimal.ZERO) == 0) maxDaily = BigDecimal.ONE;
? e.getKey().substring(5) for (Map.Entry<String, BigDecimal> e : data.dailyRevenue) {
: e.getKey(); String label = e.getKey().length() >= 10 ? e.getKey().substring(5) : e.getKey();
addBarRow(binding.llDailyRevenue, label, addBarRow(binding.llDailyRevenue, label,
"$" + e.getValue().setScale(2, RoundingMode.HALF_UP), "$" + e.getValue().setScale(2, RoundingMode.HALF_UP),
e.getValue().floatValue() / maxDaily.floatValue(), e.getValue().floatValue() / maxDaily.floatValue(),
"#ff6b35"); "#ff6b35");
}
} }
} }

View File

@@ -14,7 +14,6 @@ import android.util.Log;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.Toast;
import com.example.petstoremobile.R; import com.example.petstoremobile.R;
import com.example.petstoremobile.adapters.AppointmentAdapter; import com.example.petstoremobile.adapters.AppointmentAdapter;
@@ -25,10 +24,9 @@ import com.example.petstoremobile.utils.BulkDeleteHandler;
import com.example.petstoremobile.utils.Resource; import com.example.petstoremobile.utils.Resource;
import com.example.petstoremobile.utils.SpinnerUtils; import com.example.petstoremobile.utils.SpinnerUtils;
import com.example.petstoremobile.utils.UIUtils; import com.example.petstoremobile.utils.UIUtils;
import com.example.petstoremobile.viewmodels.AppointmentViewModel; import com.example.petstoremobile.viewmodels.AppointmentListViewModel;
import com.example.petstoremobile.utils.EventDecorator; import com.example.petstoremobile.utils.EventDecorator;
import com.example.petstoremobile.viewmodels.AuthViewModel; import com.example.petstoremobile.viewmodels.AuthViewModel;
import com.example.petstoremobile.viewmodels.StoreViewModel;
import com.prolificinteractive.materialcalendarview.CalendarDay; import com.prolificinteractive.materialcalendarview.CalendarDay;
import com.prolificinteractive.materialcalendarview.CalendarMode; import com.prolificinteractive.materialcalendarview.CalendarMode;
@@ -48,11 +46,9 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter.
private FragmentAppointmentBinding binding; private FragmentAppointmentBinding binding;
private List<AppointmentDTO> appointmentList = new ArrayList<>(); private List<AppointmentDTO> appointmentList = new ArrayList<>();
private List<StoreDTO> storeList = new ArrayList<>();
private AppointmentAdapter adapter; private AppointmentAdapter adapter;
private AppointmentViewModel appointmentViewModel; private AppointmentListViewModel viewModel;
private StoreViewModel storeViewModel;
private AuthViewModel authViewModel; private AuthViewModel authViewModel;
private BulkDeleteHandler bulkDeleteHandler; private BulkDeleteHandler bulkDeleteHandler;
@@ -61,20 +57,13 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter.
private Long currentUserId = null; private Long currentUserId = null;
private final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()); private final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd", Locale.getDefault());
/**
* Initializes the fragment and its associated ViewModels.
*/
@Override @Override
public void onCreate(@Nullable Bundle savedInstanceState) { public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
appointmentViewModel = new ViewModelProvider(this).get(AppointmentViewModel.class); viewModel = new ViewModelProvider(this).get(AppointmentListViewModel.class);
storeViewModel = new ViewModelProvider(this).get(StoreViewModel.class);
authViewModel = new ViewModelProvider(this).get(AuthViewModel.class); authViewModel = new ViewModelProvider(this).get(AuthViewModel.class);
} }
/**
* Sets up the fragment's UI, including RecyclerView, search, swipe-to-refresh, and calendar.
*/
@Override @Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) { Bundle savedInstanceState) {
@@ -89,6 +78,7 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter.
setupFilterToggle(); setupFilterToggle();
setupMyAppointmentFilter(); setupMyAppointmentFilter();
setupBulkDelete(); setupBulkDelete();
observeViewModel();
binding.fabAddAppointment.setOnClickListener(v -> openAppointmentDetails(-1)); binding.fabAddAppointment.setOnClickListener(v -> openAppointmentDetails(-1));
@@ -101,6 +91,24 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter.
return binding.getRoot(); return binding.getRoot();
} }
private void observeViewModel() {
viewModel.getAppointments().observe(getViewLifecycleOwner(), list -> {
appointmentList.clear();
appointmentList.addAll(list);
updateCalendarDecorators();
adapter.notifyDataSetChanged();
});
viewModel.getStores().observe(getViewLifecycleOwner(), list -> {
SpinnerUtils.populateWhiteSpinner(requireContext(), binding.spinnerStore, list,
StoreDTO::getStoreName, "All Stores", -1L, StoreDTO::getStoreId);
});
viewModel.getIsLoading().observe(getViewLifecycleOwner(), loading -> {
binding.swipeRefreshAppointment.setRefreshing(loading);
});
}
private void setupBulkDelete() { private void setupBulkDelete() {
bulkDeleteHandler = new BulkDeleteHandler( bulkDeleteHandler = new BulkDeleteHandler(
this, this,
@@ -109,27 +117,18 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter.
binding.btnBulkDelete, binding.btnBulkDelete,
adapter, adapter,
"appointment", "appointment",
appointmentViewModel::bulkDeleteAppointments, viewModel::bulkDeleteAppointments,
this::loadAppointmentData this::loadAppointmentData
); );
} }
@Override
public void onDestroyView() {
super.onDestroyView();
binding = null;
}
@Override @Override
public void onResume() { public void onResume() {
super.onResume(); super.onResume();
loadAppointmentData(); loadAppointmentData();
loadStoreData(); viewModel.loadStores();
} }
/**
* Toggles the calendar between week and month display modes.
*/
private void toggleCalendarMode() { private void toggleCalendarMode() {
isMonthMode = !isMonthMode; isMonthMode = !isMonthMode;
binding.calendarView.state().edit() binding.calendarView.state().edit()
@@ -137,18 +136,12 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter.
.commit(); .commit();
} }
/**
* Sets up the "My Appointments" filter button.
*/
private void setupMyAppointmentFilter() { private void setupMyAppointmentFilter() {
binding.btnMyAppointments.setOnClickListener(v -> { binding.btnMyAppointments.setOnClickListener(v -> {
loadAppointmentData(); loadAppointmentData();
}); });
} }
/**
* Fetches current user info to get the employeeId.
*/
private void loadCurrentUserInfo() { private void loadCurrentUserInfo() {
authViewModel.getMe().observe(getViewLifecycleOwner(), resource -> { authViewModel.getMe().observe(getViewLifecycleOwner(), resource -> {
if (resource.status == Resource.Status.SUCCESS && resource.data != null) { if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
@@ -157,37 +150,11 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter.
}); });
} }
/**
* Sets up the filter toggle button to show/hide the filter layout.
*/
private void setupFilterToggle() { private void setupFilterToggle() {
UIUtils.setupFilterToggle(binding.btnToggleFilter, binding.layoutFilter, binding.etSearchAppointment, UIUtils.setupFilterToggle(binding.btnToggleFilter, binding.layoutFilter, binding.etSearchAppointment,
binding.spinnerStatus, binding.spinnerStore); binding.spinnerStatus, binding.spinnerStore);
// Add additional reset logic for elements specific to this fragment
binding.btnToggleFilter.setOnClickListener(v -> {
boolean isVisible = binding.layoutFilter.getVisibility() == View.VISIBLE;
binding.layoutFilter.setVisibility(isVisible ? View.GONE : View.VISIBLE);
binding.btnToggleFilter.setImageResource(isVisible ?
android.R.drawable.ic_menu_search :
android.R.drawable.ic_menu_close_clear_cancel);
if (isVisible) {
binding.etSearchAppointment.setText("");
binding.spinnerStatus.setSelection(0);
binding.spinnerStore.setSelection(0);
binding.btnMyAppointments.setChecked(false);
selectedCalendarDay = null;
binding.calendarView.clearSelection();
loadAppointmentData();
}
});
} }
/**
* Sets up the date selection listener for the calendar.
*/
private void setupCalendar() { private void setupCalendar() {
binding.calendarView.setOnDateChangedListener((widget, date, selected) -> { binding.calendarView.setOnDateChangedListener((widget, date, selected) -> {
if (selected) { if (selected) {
@@ -204,17 +171,11 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter.
}); });
} }
/**
* Updates calendar indicators to highlight dates that have scheduled appointments.
*/
private void updateCalendarDecorators() { private void updateCalendarDecorators() {
HashSet<CalendarDay> datesWithAppointments = new HashSet<>(); HashSet<CalendarDay> datesWithAppointments = new HashSet<>();
SimpleDateFormat displayFormat = new SimpleDateFormat("yyyy-MM-dd", Locale.getDefault());
for (AppointmentDTO appointment : appointmentList) { for (AppointmentDTO appointment : appointmentList) {
try { try {
//Get the appointment date Date date = dateFormat.parse(appointment.getAppointmentDate());
Date date = displayFormat.parse(appointment.getAppointmentDate());
//if the date is not null, add it to the hashset
if (date != null) { if (date != null) {
Calendar cal = Calendar.getInstance(); Calendar cal = Calendar.getInstance();
cal.setTime(date); cal.setTime(date);
@@ -224,56 +185,27 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter.
Log.e("AppointmentFragment", "Error parsing date: " + appointment.getAppointmentDate()); Log.e("AppointmentFragment", "Error parsing date: " + appointment.getAppointmentDate());
} }
} }
//update the indicators to the calendar
binding.calendarView.removeDecorators(); binding.calendarView.removeDecorators();
binding.calendarView.addDecorator(new EventDecorator(Color.RED, datesWithAppointments)); binding.calendarView.addDecorator(new EventDecorator(Color.RED, datesWithAppointments));
} }
/**
* Configures the search bar for filtering.
*/
private void setupSearch() { private void setupSearch() {
UIUtils.attachSearch(binding.etSearchAppointment, this::loadAppointmentData); UIUtils.attachSearch(binding.etSearchAppointment, this::loadAppointmentData);
} }
/**
* Configures the status filter spinner.
*/
private void setupStatusFilter() { private void setupStatusFilter() {
String[] statuses = {"All Statuses", "Booked", "Completed", "Cancelled", "Missed"}; String[] statuses = {"All Statuses", "Booked", "Completed", "Cancelled", "Missed"};
SpinnerUtils.setupStringFilterSpinner(requireContext(), binding.spinnerStatus, statuses, this::loadAppointmentData); SpinnerUtils.setupStringFilterSpinner(requireContext(), binding.spinnerStatus, statuses, this::loadAppointmentData);
} }
/**
* Configures the store filter spinner.
*/
private void setupStoreFilter() { private void setupStoreFilter() {
SpinnerUtils.setupFilterSpinner(binding.spinnerStore, this::loadAppointmentData); SpinnerUtils.setupFilterSpinner(binding.spinnerStore, this::loadAppointmentData);
} }
/**
* Fetches store data to populate the store filter.
*/
private void loadStoreData() {
storeViewModel.getAllStores(0, 100).observe(getViewLifecycleOwner(), resource -> {
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
storeList = resource.data.getContent();
SpinnerUtils.populateWhiteSpinner(requireContext(), binding.spinnerStore, storeList,
StoreDTO::getStoreName, "All Stores", -1L, StoreDTO::getStoreId);
}
});
}
/**
* Initializes the SwipeRefreshLayout to allow manual data refreshing.
*/
private void setupSwipeRefresh() { private void setupSwipeRefresh() {
binding.swipeRefreshAppointment.setOnRefreshListener(this::loadAppointmentData); binding.swipeRefreshAppointment.setOnRefreshListener(this::loadAppointmentData);
} }
/**
* Navigates to the appointment detail screen for editing or creating an appointment.
*/
private void openAppointmentDetails(int position) { private void openAppointmentDetails(int position) {
Bundle args = new Bundle(); Bundle args = new Bundle();
if (position != -1) { if (position != -1) {
@@ -283,9 +215,6 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter.
NavHostFragment.findNavController(this).navigate(R.id.nav_appointment_detail, args); NavHostFragment.findNavController(this).navigate(R.id.nav_appointment_detail, args);
} }
/**
* Handles item click in the appointment list.
*/
@Override @Override
public void onAppointmentClick(int position) { public void onAppointmentClick(int position) {
openAppointmentDetails(position); openAppointmentDetails(position);
@@ -298,16 +227,14 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter.
} }
} }
/**
* Fetches appointment data from the server with all active filters.
*/
private void loadAppointmentData() { private void loadAppointmentData() {
String query = binding.etSearchAppointment.getText().toString().trim(); String query = binding.etSearchAppointment.getText().toString().trim();
String status = binding.spinnerStatus.getSelectedItem() != null ? binding.spinnerStatus.getSelectedItem().toString() : "All Statuses"; String status = binding.spinnerStatus.getSelectedItem() != null ? binding.spinnerStatus.getSelectedItem().toString() : "All Statuses";
Long storeId = null; Long storeId = null;
if (binding.spinnerStore.getSelectedItemPosition() > 0 && !storeList.isEmpty()) { List<StoreDTO> stores = viewModel.getStores().getValue();
storeId = storeList.get(binding.spinnerStore.getSelectedItemPosition() - 1).getStoreId(); if (binding.spinnerStore.getSelectedItemPosition() > 0 && stores != null && !stores.isEmpty()) {
storeId = stores.get(binding.spinnerStore.getSelectedItemPosition() - 1).getStoreId();
} }
String selectedDateString = null; String selectedDateString = null;
@@ -324,41 +251,18 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter.
if (status.equals("All Statuses")) status = null; if (status.equals("All Statuses")) status = null;
else status = status.toUpperCase(); else status = status.toUpperCase();
appointmentViewModel.getAllAppointments(0, 500, query, status, storeId, selectedDateString, employeeId).observe(getViewLifecycleOwner(), resource -> { viewModel.loadAppointments(query, status, storeId, selectedDateString, employeeId);
if (resource == null) return;
// Check the status to see if the resource is loaded and display the data
switch (resource.status) {
case LOADING:
// Show loading indicator
binding.swipeRefreshAppointment.setRefreshing(true);
break;
case SUCCESS:
// Hide loading indicator and display data
binding.swipeRefreshAppointment.setRefreshing(false);
if (resource.data != null) {
appointmentList.clear();
appointmentList.addAll(resource.data.getContent());
updateCalendarDecorators();
adapter.notifyDataSetChanged();
}
break;
case ERROR:
// Hide loading indicator and toast error message
binding.swipeRefreshAppointment.setRefreshing(false);
Toast.makeText(getContext(), "Failed to load appointments: " + resource.message, Toast.LENGTH_SHORT).show();
Log.e("AppointmentFragment", "Error loading appointments: " + resource.message);
break;
}
});
} }
/**
* Initializes the RecyclerView for displaying appointments.
*/
private void setupRecyclerView() { private void setupRecyclerView() {
adapter = new AppointmentAdapter(appointmentList, this); adapter = new AppointmentAdapter(appointmentList, this);
binding.recyclerViewAppointments.setLayoutManager(new LinearLayoutManager(getContext())); binding.recyclerViewAppointments.setLayoutManager(new LinearLayoutManager(getContext()));
binding.recyclerViewAppointments.setAdapter(adapter); binding.recyclerViewAppointments.setAdapter(adapter);
} }
@Override
public void onDestroyView() {
super.onDestroyView();
binding = null;
}
} }

View File

@@ -1,7 +1,6 @@
package com.example.petstoremobile.fragments.listfragments; package com.example.petstoremobile.fragments.listfragments;
import android.os.Bundle; import android.os.Bundle;
import android.util.Log;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
@@ -22,8 +21,7 @@ import com.example.petstoremobile.dtos.InventoryDTO;
import com.example.petstoremobile.dtos.StoreDTO; import com.example.petstoremobile.dtos.StoreDTO;
import com.example.petstoremobile.utils.BulkDeleteHandler; import com.example.petstoremobile.utils.BulkDeleteHandler;
import com.example.petstoremobile.utils.UIUtils; import com.example.petstoremobile.utils.UIUtils;
import com.example.petstoremobile.viewmodels.InventoryViewModel; import com.example.petstoremobile.viewmodels.InventoryListViewModel;
import com.example.petstoremobile.utils.Resource;
import com.example.petstoremobile.utils.SpinnerUtils; import com.example.petstoremobile.utils.SpinnerUtils;
import java.util.ArrayList; import java.util.ArrayList;
@@ -34,33 +32,18 @@ import dagger.hilt.android.AndroidEntryPoint;
@AndroidEntryPoint @AndroidEntryPoint
public class InventoryFragment extends Fragment implements InventoryAdapter.OnInventoryClickListener { public class InventoryFragment extends Fragment implements InventoryAdapter.OnInventoryClickListener {
private static final String TAG = "InventoryFragment";
private static final int PAGE_SIZE = 20;
private FragmentInventoryBinding binding; private FragmentInventoryBinding binding;
private final List<InventoryDTO> inventoryList = new ArrayList<>(); private final List<InventoryDTO> inventoryList = new ArrayList<>();
private List<StoreDTO> storeList = new ArrayList<>();
private InventoryAdapter adapter; private InventoryAdapter adapter;
private InventoryViewModel viewModel; private InventoryListViewModel viewModel;
private BulkDeleteHandler bulkDeleteHandler; private BulkDeleteHandler bulkDeleteHandler;
// Pagination
private int currentPage = 0;
private boolean isLastPage = false;
private boolean isLoading = false;
/**
* Initializes the fragment and its ViewModel.
*/
@Override @Override
public void onCreate(@Nullable Bundle savedInstanceState) { public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
viewModel = new ViewModelProvider(this).get(InventoryViewModel.class); viewModel = new ViewModelProvider(this).get(InventoryListViewModel.class);
} }
/**
* Sets up the fragment's UI components, including the inventory list and search.
*/
@Override @Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) { Bundle savedInstanceState) {
@@ -72,8 +55,9 @@ public class InventoryFragment extends Fragment implements InventoryAdapter.OnIn
setupSwipeRefresh(); setupSwipeRefresh();
setupFilterToggle(); setupFilterToggle();
setupBulkDelete(); setupBulkDelete();
observeViewModel();
loadInventory(true); loadInventory(true);
loadStoreData();
binding.fabAddInventory.setOnClickListener(v -> openDetail(null)); binding.fabAddInventory.setOnClickListener(v -> openDetail(null));
@@ -82,6 +66,23 @@ public class InventoryFragment extends Fragment implements InventoryAdapter.OnIn
return binding.getRoot(); return binding.getRoot();
} }
private void observeViewModel() {
viewModel.getInventory().observe(getViewLifecycleOwner(), list -> {
inventoryList.clear();
inventoryList.addAll(list);
adapter.notifyDataSetChanged();
});
viewModel.getStores().observe(getViewLifecycleOwner(), list -> {
SpinnerUtils.populateWhiteSpinner(requireContext(), binding.spinnerStore, list,
StoreDTO::getStoreName, "All Stores", -1L, StoreDTO::getStoreId);
});
viewModel.getIsLoading().observe(getViewLifecycleOwner(), loading -> {
binding.swipeRefreshInventory.setRefreshing(loading);
});
}
private void setupBulkDelete() { private void setupBulkDelete() {
bulkDeleteHandler = new BulkDeleteHandler( bulkDeleteHandler = new BulkDeleteHandler(
this, this,
@@ -95,49 +96,30 @@ public class InventoryFragment extends Fragment implements InventoryAdapter.OnIn
); );
} }
@Override
public void onResume() {
super.onResume();
viewModel.loadStores();
}
@Override @Override
public void onDestroyView() { public void onDestroyView() {
super.onDestroyView(); super.onDestroyView();
binding = null; binding = null;
} }
/**
* Sets up the filter toggle button to show/hide the filter layout.
*/
private void setupFilterToggle() { private void setupFilterToggle() {
UIUtils.setupFilterToggle(binding.btnToggleFilter, binding.layoutFilter, binding.etSearchInventory, binding.spinnerStore); UIUtils.setupFilterToggle(binding.btnToggleFilter, binding.layoutFilter, binding.etSearchInventory, binding.spinnerStore);
} }
/**
* Sets up the search bar for filtering.
*/
private void setupSearch() { private void setupSearch() {
UIUtils.attachSearch(binding.etSearchInventory, () -> loadInventory(true)); UIUtils.attachSearch(binding.etSearchInventory, () -> loadInventory(true));
} }
/**
* Configures the store filter spinner.
*/
private void setupStoreFilter() { private void setupStoreFilter() {
SpinnerUtils.setupFilterSpinner(binding.spinnerStore, () -> loadInventory(true)); SpinnerUtils.setupFilterSpinner(binding.spinnerStore, () -> loadInventory(true));
} }
/**
* Fetches store data to populate the store filter.
*/
private void loadStoreData() {
viewModel.getAllStores(0, 100).observe(getViewLifecycleOwner(), resource -> {
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
storeList = resource.data.getContent();
SpinnerUtils.populateWhiteSpinner(requireContext(), binding.spinnerStore, storeList,
StoreDTO::getStoreName, "All Stores", -1L, StoreDTO::getStoreId);
}
});
}
/**
* Initializes the RecyclerView with a layout manager, and adapter.
*/
private void setupRecyclerView() { private void setupRecyclerView() {
adapter = new InventoryAdapter(inventoryList, this); adapter = new InventoryAdapter(inventoryList, this);
binding.recyclerViewInventory.setLayoutManager(new LinearLayoutManager(getContext())); binding.recyclerViewInventory.setLayoutManager(new LinearLayoutManager(getContext()));
@@ -146,105 +128,45 @@ public class InventoryFragment extends Fragment implements InventoryAdapter.OnIn
binding.recyclerViewInventory.addOnScrollListener(new RecyclerView.OnScrollListener() { binding.recyclerViewInventory.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override @Override
public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) { public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
if (dy <= 0) if (dy <= 0) return;
return;
LinearLayoutManager lm = (LinearLayoutManager) binding.recyclerViewInventory.getLayoutManager(); LinearLayoutManager lm = (LinearLayoutManager) binding.recyclerViewInventory.getLayoutManager();
if (lm == null) if (lm == null) return;
return;
int visible = lm.getChildCount(); int visible = lm.getChildCount();
int total = lm.getItemCount(); int total = lm.getItemCount();
int firstVis = lm.findFirstVisibleItemPosition(); int firstVis = lm.findFirstVisibleItemPosition();
if (!isLoading && !isLastPage && (visible + firstVis) >= total - 3) { Boolean isLoading = viewModel.getIsLoading().getValue();
if ((isLoading == null || !isLoading) && !viewModel.isLastPage() && (visible + firstVis) >= total - 3) {
loadInventory(false); loadInventory(false);
} }
} }
}); });
} }
/**
* Sets up the SwipeRefreshLayout to reload the first page of inventory items.
*/
private void setupSwipeRefresh() { private void setupSwipeRefresh() {
binding.swipeRefreshInventory.setOnRefreshListener(() -> loadInventory(true)); binding.swipeRefreshInventory.setOnRefreshListener(() -> loadInventory(true));
} }
/**
* Fetches a page of inventory items from the API.
*/
private void loadInventory(boolean reset) { private void loadInventory(boolean reset) {
if (isLoading) return;
if (reset) {
currentPage = 0;
isLastPage = false;
}
// Search text from input
String query = binding.etSearchInventory != null ? binding.etSearchInventory.getText().toString().trim() : ""; String query = binding.etSearchInventory != null ? binding.etSearchInventory.getText().toString().trim() : "";
if (query.isEmpty()) query = null; if (query.isEmpty()) query = null;
Long storeId = null; Long storeId = null;
if (binding.spinnerStore.getSelectedItemPosition() > 0 && !storeList.isEmpty()) { List<StoreDTO> stores = viewModel.getStores().getValue();
storeId = storeList.get(binding.spinnerStore.getSelectedItemPosition() - 1).getStoreId(); if (binding.spinnerStore.getSelectedItemPosition() > 0 && stores != null && !stores.isEmpty()) {
storeId = stores.get(binding.spinnerStore.getSelectedItemPosition() - 1).getStoreId();
} }
//Load all inventory items from the backend using viewModel viewModel.loadInventory(reset, query, storeId);
viewModel.getAllInventory(query, storeId, currentPage, PAGE_SIZE, "product.prodName").observe(getViewLifecycleOwner(), resource -> {
if (resource == null) return;
// Check the status to see if the resource is loaded and display the data
switch (resource.status) {
case LOADING:
// Show loading indicator
isLoading = true;
binding.swipeRefreshInventory.setRefreshing(true);
break;
case SUCCESS:
// Hide loading indicator and display data
isLoading = false;
binding.swipeRefreshInventory.setRefreshing(false);
if (resource.data != null) {
if (reset) inventoryList.clear();
inventoryList.addAll(resource.data.getContent());
adapter.notifyDataSetChanged();
isLastPage = resource.data.isLast();
if (!isLastPage) currentPage++;
}
break;
case ERROR:
// Hide loading indicator and toast error message
isLoading = false;
binding.swipeRefreshInventory.setRefreshing(false);
Log.e(TAG, "Error: " + resource.message);
Toast.makeText(getContext(), "Failed to load inventory: " + resource.message, Toast.LENGTH_SHORT).show();
break;
}
});
} }
/**
* Navigates to the inventory detail screen for a specific item or to add a new one.
*/
private void openDetail(InventoryDTO inv) { private void openDetail(InventoryDTO inv) {
Bundle args = new Bundle(); Bundle args = new Bundle();
if (inv != null) { if (inv != null) {
args.putLong("inventoryId", inv.getInventoryId()); args.putLong("inventoryId", inv.getInventoryId());
} }
NavHostFragment.findNavController(this).navigate(R.id.nav_inventory_detail, args); NavHostFragment.findNavController(this).navigate(R.id.nav_inventory_detail, args);
} }
/**
* Reloads inventory data when changes occur.
*/
public void onInventoryChanged() {
loadInventory(true);
}
/**
* Handles item click in the inventory list.
*/
@Override @Override
public void onInventoryClick(int position) { public void onInventoryClick(int position) {
if (position >= 0 && position < inventoryList.size()) { if (position >= 0 && position < inventoryList.size()) {
@@ -252,9 +174,6 @@ public class InventoryFragment extends Fragment implements InventoryAdapter.OnIn
} }
} }
/**
* Updates the bulk deletion UI visibility and count when items are selected or deselected.
*/
@Override @Override
public void onSelectionChanged(int selectedCount) { public void onSelectionChanged(int selectedCount) {
if (bulkDeleteHandler != null) { if (bulkDeleteHandler != null) {

View File

@@ -9,7 +9,6 @@ import androidx.lifecycle.ViewModelProvider;
import androidx.navigation.fragment.NavHostFragment; import androidx.navigation.fragment.NavHostFragment;
import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.LinearLayoutManager;
import android.util.Log;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
@@ -25,8 +24,7 @@ import com.example.petstoremobile.utils.BulkDeleteHandler;
import com.example.petstoremobile.utils.Resource; import com.example.petstoremobile.utils.Resource;
import com.example.petstoremobile.utils.SpinnerUtils; import com.example.petstoremobile.utils.SpinnerUtils;
import com.example.petstoremobile.utils.UIUtils; import com.example.petstoremobile.utils.UIUtils;
import com.example.petstoremobile.viewmodels.PetViewModel; import com.example.petstoremobile.viewmodels.PetListViewModel;
import com.example.petstoremobile.viewmodels.StoreViewModel;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@@ -40,28 +38,19 @@ import dagger.hilt.android.AndroidEntryPoint;
public class PetFragment extends Fragment implements PetAdapter.OnPetClickListener { public class PetFragment extends Fragment implements PetAdapter.OnPetClickListener {
private FragmentPetBinding binding; private FragmentPetBinding binding;
private List<PetDTO> petList = new ArrayList<>(); private List<PetDTO> petList = new ArrayList<>();
private List<StoreDTO> storeList = new ArrayList<>();
private PetAdapter adapter; private PetAdapter adapter;
private PetViewModel viewModel; private PetListViewModel viewModel;
private StoreViewModel storeViewModel;
private BulkDeleteHandler bulkDeleteHandler; private BulkDeleteHandler bulkDeleteHandler;
@Inject @Named("baseUrl") String baseUrl; @Inject @Named("baseUrl") String baseUrl;
@Inject TokenManager tokenManager; @Inject TokenManager tokenManager;
/**
* Initializes the fragment and its associated ViewModels.
*/
@Override @Override
public void onCreate(@Nullable Bundle savedInstanceState) { public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
viewModel = new ViewModelProvider(this).get(PetViewModel.class); viewModel = new ViewModelProvider(this).get(PetListViewModel.class);
storeViewModel = new ViewModelProvider(this).get(StoreViewModel.class);
} }
/**
* Sets up the fragment's UI components, including RecyclerView, filters, and swipe-to-refresh.
*/
@Override @Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) { Bundle savedInstanceState) {
@@ -75,6 +64,7 @@ public class PetFragment extends Fragment implements PetAdapter.OnPetClickListen
setupSwipeRefresh(); setupSwipeRefresh();
setupFilterToggle(); setupFilterToggle();
setupBulkDelete(); setupBulkDelete();
observeViewModel();
binding.fabAddPet.setOnClickListener(v -> openPetDetails()); binding.fabAddPet.setOnClickListener(v -> openPetDetails());
@@ -83,6 +73,23 @@ public class PetFragment extends Fragment implements PetAdapter.OnPetClickListen
return binding.getRoot(); return binding.getRoot();
} }
private void observeViewModel() {
viewModel.getPets().observe(getViewLifecycleOwner(), list -> {
petList.clear();
petList.addAll(list);
adapter.notifyDataSetChanged();
});
viewModel.getStores().observe(getViewLifecycleOwner(), list -> {
SpinnerUtils.populateWhiteSpinner(requireContext(), binding.spinnerStore, list,
StoreDTO::getStoreName, "All Stores", -1L, StoreDTO::getStoreId);
});
viewModel.getIsLoading().observe(getViewLifecycleOwner(), loading -> {
binding.swipeRefreshPet.setRefreshing(loading);
});
}
private void setupBulkDelete() { private void setupBulkDelete() {
bulkDeleteHandler = new BulkDeleteHandler( bulkDeleteHandler = new BulkDeleteHandler(
this, this,
@@ -96,83 +103,62 @@ public class PetFragment extends Fragment implements PetAdapter.OnPetClickListen
); );
} }
@Override
public void onDestroyView() {
super.onDestroyView();
binding = null;
}
/**
* Reloads data every time the fragment becomes visible.
*/
@Override @Override
public void onResume() { public void onResume() {
super.onResume(); super.onResume();
loadPetData(); loadPetData();
loadStoreData(); viewModel.loadStores();
} }
/**
* Sets up the filter toggle button to show/hide the filter layout.
*/
private void setupFilterToggle() { private void setupFilterToggle() {
UIUtils.setupFilterToggle(binding.btnToggleFilter, binding.layoutFilter, binding.etSearchPet, UIUtils.setupFilterToggle(binding.btnToggleFilter, binding.layoutFilter, binding.etSearchPet,
binding.spinnerStatus, binding.spinnerSpecies, binding.spinnerStore); binding.spinnerStatus, binding.spinnerSpecies, binding.spinnerStore);
} }
/**
* Configures the search bar.
*/
private void setupSearch() { private void setupSearch() {
UIUtils.attachSearch(binding.etSearchPet, this::loadPetData); UIUtils.attachSearch(binding.etSearchPet, this::loadPetData);
} }
/**
* Configures the status filter spinner.
*/
private void setupStatusFilter() { private void setupStatusFilter() {
String[] statuses = {"All Statuses", "Available", "Adopted", "Owned"}; String[] statuses = {"All Statuses", "Available", "Adopted", "Owned"};
SpinnerUtils.setupStringFilterSpinner(requireContext(), binding.spinnerStatus, statuses, this::loadPetData); SpinnerUtils.setupStringFilterSpinner(requireContext(), binding.spinnerStatus, statuses, this::loadPetData);
} }
/**
* Configures the species filter spinner with species.
*/
private void setupSpeciesFilter() { private void setupSpeciesFilter() {
String[] species = {"All Species", "Dog", "Cat", "Bird", "Rabbit", "Fish", "Hamster"}; String[] species = {"All Species", "Dog", "Cat", "Bird", "Rabbit", "Fish", "Hamster"};
SpinnerUtils.setupStringFilterSpinner(requireContext(), binding.spinnerSpecies, species, this::loadPetData); SpinnerUtils.setupStringFilterSpinner(requireContext(), binding.spinnerSpecies, species, this::loadPetData);
} }
/**
* Configures the store filter spinner.
*/
private void setupStoreFilter() { private void setupStoreFilter() {
SpinnerUtils.setupFilterSpinner(binding.spinnerStore, this::loadPetData); SpinnerUtils.setupFilterSpinner(binding.spinnerStore, this::loadPetData);
} }
/**
* Fetches store data to populate the store filter.
*/
private void loadStoreData() {
storeViewModel.getAllStores(0, 100).observe(getViewLifecycleOwner(), resource -> {
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
storeList = resource.data.getContent();
SpinnerUtils.populateWhiteSpinner(requireContext(), binding.spinnerStore, storeList,
StoreDTO::getStoreName, "All Stores", -1L, StoreDTO::getStoreId);
}
});
}
/**
* Sets up the SwipeRefreshLayout.
*/
private void setupSwipeRefresh() { private void setupSwipeRefresh() {
binding.swipeRefreshPet.setOnRefreshListener(this::loadPetData); binding.swipeRefreshPet.setOnRefreshListener(this::loadPetData);
} }
/** private void loadPetData() {
* Navigates to the pet profile screen. String query = binding.etSearchPet.getText().toString().trim();
*/ String status = binding.spinnerStatus.getSelectedItem() != null ? binding.spinnerStatus.getSelectedItem().toString() : "All Statuses";
String species = binding.spinnerSpecies.getSelectedItem() != null ? binding.spinnerSpecies.getSelectedItem().toString() : "All Species";
Long storeId = null;
List<StoreDTO> stores = viewModel.getStores().getValue();
if (binding.spinnerStore.getSelectedItemPosition() > 0 && stores != null && !stores.isEmpty()) {
storeId = stores.get(binding.spinnerStore.getSelectedItemPosition() - 1).getStoreId();
}
viewModel.loadPets(query, status, species, storeId);
}
private void setupRecyclerView() {
adapter = new PetAdapter(petList, this);
adapter.setBaseUrl(baseUrl);
adapter.setToken(tokenManager.getToken());
binding.recyclerViewPets.setLayoutManager(new LinearLayoutManager(getContext()));
binding.recyclerViewPets.setAdapter(adapter);
}
private void openPetProfile(int position) { private void openPetProfile(int position) {
Bundle args = new Bundle(); Bundle args = new Bundle();
PetDTO pet = petList.get(position); PetDTO pet = petList.get(position);
@@ -180,9 +166,6 @@ public class PetFragment extends Fragment implements PetAdapter.OnPetClickListen
NavHostFragment.findNavController(this).navigate(R.id.nav_pet_profile, args); NavHostFragment.findNavController(this).navigate(R.id.nav_pet_profile, args);
} }
/**
* Navigates to the pet detail screen.
*/
private void openPetDetails() { private void openPetDetails() {
NavHostFragment.findNavController(this).navigate(R.id.nav_pet_detail); NavHostFragment.findNavController(this).navigate(R.id.nav_pet_detail);
} }
@@ -199,54 +182,9 @@ public class PetFragment extends Fragment implements PetAdapter.OnPetClickListen
} }
} }
/** @Override
* Fetches pet data from the server with all active filters. public void onDestroyView() {
*/ super.onDestroyView();
private void loadPetData() { binding = null;
String query = binding.etSearchPet.getText().toString().trim();
String status = binding.spinnerStatus.getSelectedItem() != null ? binding.spinnerStatus.getSelectedItem().toString() : "All Statuses";
String species = binding.spinnerSpecies.getSelectedItem() != null ? binding.spinnerSpecies.getSelectedItem().toString() : "All Species";
Long storeId = null;
if (binding.spinnerStore.getSelectedItemPosition() > 0 && !storeList.isEmpty()) {
storeId = storeList.get(binding.spinnerStore.getSelectedItemPosition() - 1).getStoreId();
}
if (status.equals("All Statuses")) status = null;
if (species.equals("All Species")) species = null;
viewModel.getAllPets(0, 100, query, status, species, storeId, "petName").observe(getViewLifecycleOwner(), resource -> {
if (resource == null) return;
switch (resource.status) {
case LOADING:
binding.swipeRefreshPet.setRefreshing(true);
break;
case SUCCESS:
binding.swipeRefreshPet.setRefreshing(false);
if (resource.data != null) {
petList.clear();
petList.addAll(resource.data.getContent());
adapter.notifyDataSetChanged();
}
break;
case ERROR:
binding.swipeRefreshPet.setRefreshing(false);
Toast.makeText(getContext(), "Failed to load pets: " + resource.message, Toast.LENGTH_SHORT).show();
Log.e("PetFragment", "Error loading pets: " + resource.message);
break;
}
});
}
/**
* Initializes the RecyclerView.
*/
private void setupRecyclerView() {
adapter = new PetAdapter(petList, this);
adapter.setBaseUrl(baseUrl);
adapter.setToken(tokenManager.getToken());
binding.recyclerViewPets.setLayoutManager(new LinearLayoutManager(getContext()));
binding.recyclerViewPets.setAdapter(adapter);
} }
} }

View File

@@ -9,21 +9,18 @@ import androidx.lifecycle.ViewModelProvider;
import androidx.navigation.fragment.NavHostFragment; import androidx.navigation.fragment.NavHostFragment;
import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.LinearLayoutManager;
import android.util.Log;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.Toast;
import com.example.petstoremobile.R; import com.example.petstoremobile.R;
import com.example.petstoremobile.adapters.ProductAdapter; import com.example.petstoremobile.adapters.ProductAdapter;
import com.example.petstoremobile.databinding.FragmentProductBinding; import com.example.petstoremobile.databinding.FragmentProductBinding;
import com.example.petstoremobile.dtos.CategoryDTO; import com.example.petstoremobile.dtos.CategoryDTO;
import com.example.petstoremobile.dtos.ProductDTO; import com.example.petstoremobile.dtos.ProductDTO;
import com.example.petstoremobile.utils.Resource;
import com.example.petstoremobile.utils.SpinnerUtils; import com.example.petstoremobile.utils.SpinnerUtils;
import com.example.petstoremobile.utils.UIUtils; import com.example.petstoremobile.utils.UIUtils;
import com.example.petstoremobile.viewmodels.ProductViewModel; import com.example.petstoremobile.viewmodels.ProductListViewModel;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@@ -38,24 +35,17 @@ public class ProductFragment extends Fragment implements ProductAdapter.OnProduc
private FragmentProductBinding binding; private FragmentProductBinding binding;
private List<ProductDTO> productList = new ArrayList<>(); private List<ProductDTO> productList = new ArrayList<>();
private List<CategoryDTO> categoryList = new ArrayList<>();
private ProductAdapter adapter; private ProductAdapter adapter;
private ProductViewModel viewModel; private ProductListViewModel viewModel;
@Inject @Named("baseUrl") String baseUrl; @Inject @Named("baseUrl") String baseUrl;
/**
* Initializes the fragment and its associated ProductViewModel.
*/
@Override @Override
public void onCreate(@Nullable Bundle savedInstanceState) { public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
viewModel = new ViewModelProvider(this).get(ProductViewModel.class); viewModel = new ViewModelProvider(this).get(ProductListViewModel.class);
} }
/**
* Sets up the fragment's UI components, including RecyclerView, search, and swipe-to-refresh.
*/
@Override @Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) { Bundle savedInstanceState) {
@@ -66,6 +56,7 @@ public class ProductFragment extends Fragment implements ProductAdapter.OnProduc
setupCategoryFilter(); setupCategoryFilter();
setupSwipeRefresh(); setupSwipeRefresh();
setupFilterToggle(); setupFilterToggle();
observeViewModel();
binding.fabAddProduct.setOnClickListener(v -> openProductDetails(-1)); binding.fabAddProduct.setOnClickListener(v -> openProductDetails(-1));
@@ -74,67 +65,67 @@ public class ProductFragment extends Fragment implements ProductAdapter.OnProduc
return binding.getRoot(); return binding.getRoot();
} }
@Override private void observeViewModel() {
public void onDestroyView() { viewModel.getProducts().observe(getViewLifecycleOwner(), list -> {
super.onDestroyView(); productList.clear();
binding = null; productList.addAll(list);
adapter.notifyDataSetChanged();
});
viewModel.getCategories().observe(getViewLifecycleOwner(), list -> {
SpinnerUtils.populateWhiteSpinner(requireContext(), binding.spinnerCategory, list,
CategoryDTO::getCategoryName, "All Categories", -1L, CategoryDTO::getCategoryId);
});
viewModel.getIsLoading().observe(getViewLifecycleOwner(), loading -> {
binding.swipeRefreshProduct.setRefreshing(loading);
});
} }
/**
* Reloads data every time the fragment becomes visible.
*/
@Override @Override
public void onResume() { public void onResume() {
super.onResume(); super.onResume();
loadProductData(); loadProductData();
loadCategoryData(); viewModel.loadCategories();
} }
/**
* Sets up the filter toggle button to show/hide the filter layout.
*/
private void setupFilterToggle() { private void setupFilterToggle() {
UIUtils.setupFilterToggle(binding.btnToggleFilter, binding.layoutFilter, UIUtils.setupFilterToggle(binding.btnToggleFilter, binding.layoutFilter,
binding.etSearchProduct, binding.spinnerCategory); binding.etSearchProduct, binding.spinnerCategory);
} }
/**
* Configures the search bar for triggering data load from backend.
*/
private void setupSearch() { private void setupSearch() {
UIUtils.attachSearch(binding.etSearchProduct, this::loadProductData); UIUtils.attachSearch(binding.etSearchProduct, this::loadProductData);
} }
/**
* Configures the category filter spinner.
*/
private void setupCategoryFilter() { private void setupCategoryFilter() {
SpinnerUtils.setupFilterSpinner(binding.spinnerCategory, this::loadProductData); SpinnerUtils.setupFilterSpinner(binding.spinnerCategory, this::loadProductData);
} }
/**
* Fetches category data to populate the category filter.
*/
private void loadCategoryData() {
viewModel.getAllCategories(0, 100).observe(getViewLifecycleOwner(), resource -> {
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
categoryList = resource.data.getContent();
SpinnerUtils.populateWhiteSpinner(requireContext(), binding.spinnerCategory, categoryList,
CategoryDTO::getCategoryName, "All Categories", -1L, CategoryDTO::getCategoryId);
}
});
}
/**
* Sets up the SwipeRefreshLayout.
*/
private void setupSwipeRefresh() { private void setupSwipeRefresh() {
binding.swipeRefreshProduct.setOnRefreshListener(this::loadProductData); binding.swipeRefreshProduct.setOnRefreshListener(this::loadProductData);
} }
/** private void loadProductData() {
* Navigates to the product detail screen. String query = binding.etSearchProduct.getText().toString().trim();
*/ if (query.isEmpty()) query = null;
Long categoryId = null;
List<CategoryDTO> categories = viewModel.getCategories().getValue();
if (binding.spinnerCategory.getSelectedItemPosition() > 0 && categories != null && !categories.isEmpty()) {
categoryId = categories.get(binding.spinnerCategory.getSelectedItemPosition() - 1).getCategoryId();
}
viewModel.loadProducts(query, categoryId);
}
private void setupRecyclerView() {
adapter = new ProductAdapter(productList, this);
adapter.setBaseUrl(baseUrl);
binding.recyclerViewProducts.setLayoutManager(new LinearLayoutManager(getContext()));
binding.recyclerViewProducts.setAdapter(adapter);
}
private void openProductDetails(int position) { private void openProductDetails(int position) {
Bundle args = new Bundle(); Bundle args = new Bundle();
if (position != -1) { if (position != -1) {
@@ -149,51 +140,9 @@ public class ProductFragment extends Fragment implements ProductAdapter.OnProduc
openProductDetails(position); openProductDetails(position);
} }
/** @Override
* Fetches product data from the server with search query, category, and sorting. public void onDestroyView() {
*/ super.onDestroyView();
private void loadProductData() { binding = null;
String query = binding.etSearchProduct.getText().toString().trim();
if (query.isEmpty()) query = null;
Long categoryId = null;
if (binding.spinnerCategory.getSelectedItemPosition() > 0 && !categoryList.isEmpty()) {
categoryId = categoryList.get(binding.spinnerCategory.getSelectedItemPosition() - 1).getCategoryId();
}
viewModel.getAllProducts(query, categoryId, 0, 100, "prodName").observe(getViewLifecycleOwner(), resource -> {
if (resource == null) return;
switch (resource.status) {
case LOADING:
binding.swipeRefreshProduct.setRefreshing(true);
break;
case SUCCESS:
binding.swipeRefreshProduct.setRefreshing(false);
if (resource.data != null) {
productList.clear();
productList.addAll(resource.data.getContent());
adapter.notifyDataSetChanged();
}
break;
case ERROR:
binding.swipeRefreshProduct.setRefreshing(false);
if (getContext() != null) {
Toast.makeText(getContext(), "Failed to load products: " + resource.message, Toast.LENGTH_SHORT).show();
}
Log.e("ProductFragment", "Error loading products: " + resource.message);
break;
}
});
}
/**
* Initializes the RecyclerView.
*/
private void setupRecyclerView() {
adapter = new ProductAdapter(productList, this);
adapter.setBaseUrl(baseUrl);
binding.recyclerViewProducts.setLayoutManager(new LinearLayoutManager(getContext()));
binding.recyclerViewProducts.setAdapter(adapter);
} }
} }

View File

@@ -1,11 +1,9 @@
package com.example.petstoremobile.fragments.listfragments; package com.example.petstoremobile.fragments.listfragments;
import android.os.Bundle; import android.os.Bundle;
import android.util.Log;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.Toast;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
@@ -21,12 +19,9 @@ import com.example.petstoremobile.dtos.ProductDTO;
import com.example.petstoremobile.dtos.ProductSupplierDTO; import com.example.petstoremobile.dtos.ProductSupplierDTO;
import com.example.petstoremobile.dtos.SupplierDTO; import com.example.petstoremobile.dtos.SupplierDTO;
import com.example.petstoremobile.utils.BulkDeleteHandler; import com.example.petstoremobile.utils.BulkDeleteHandler;
import com.example.petstoremobile.utils.Resource;
import com.example.petstoremobile.utils.SpinnerUtils; import com.example.petstoremobile.utils.SpinnerUtils;
import com.example.petstoremobile.utils.UIUtils; import com.example.petstoremobile.utils.UIUtils;
import com.example.petstoremobile.viewmodels.ProductSupplierViewModel; import com.example.petstoremobile.viewmodels.ProductSupplierListViewModel;
import com.example.petstoremobile.viewmodels.ProductViewModel;
import com.example.petstoremobile.viewmodels.SupplierViewModel;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@@ -39,29 +34,17 @@ public class ProductSupplierFragment extends Fragment
private FragmentProductSupplierBinding binding; private FragmentProductSupplierBinding binding;
private List<ProductSupplierDTO> psList = new ArrayList<>(); private List<ProductSupplierDTO> psList = new ArrayList<>();
private List<ProductDTO> productList = new ArrayList<>();
private List<SupplierDTO> supplierList = new ArrayList<>();
private ProductSupplierAdapter adapter; private ProductSupplierAdapter adapter;
private ProductSupplierViewModel viewModel; private ProductSupplierListViewModel viewModel;
private ProductViewModel productViewModel;
private SupplierViewModel supplierViewModel;
private BulkDeleteHandler bulkDeleteHandler; private BulkDeleteHandler bulkDeleteHandler;
/**
* Initializes the fragment and its associated ViewModels.
*/
@Override @Override
public void onCreate(@Nullable Bundle savedInstanceState) { public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
viewModel = new ViewModelProvider(this).get(ProductSupplierViewModel.class); viewModel = new ViewModelProvider(this).get(ProductSupplierListViewModel.class);
productViewModel = new ViewModelProvider(this).get(ProductViewModel.class);
supplierViewModel = new ViewModelProvider(this).get(SupplierViewModel.class);
} }
/**
* Sets up the fragment's UI components, including the RecyclerView, search, and swipe-to-refresh.
*/
@Override @Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) { Bundle savedInstanceState) {
@@ -74,6 +57,7 @@ public class ProductSupplierFragment extends Fragment
setupSwipeRefresh(); setupSwipeRefresh();
setupFilterToggle(); setupFilterToggle();
setupBulkDelete(); setupBulkDelete();
observeViewModel();
binding.fabAddPS.setOnClickListener(v -> openDetail(-1)); binding.fabAddPS.setOnClickListener(v -> openDetail(-1));
@@ -82,6 +66,28 @@ public class ProductSupplierFragment extends Fragment
return binding.getRoot(); return binding.getRoot();
} }
private void observeViewModel() {
viewModel.getProductSuppliers().observe(getViewLifecycleOwner(), list -> {
psList.clear();
psList.addAll(list);
adapter.notifyDataSetChanged();
});
viewModel.getProducts().observe(getViewLifecycleOwner(), list -> {
SpinnerUtils.populateWhiteSpinner(requireContext(), binding.spinnerProduct, list,
ProductDTO::getProdName, "All Products", -1L, ProductDTO::getProdId);
});
viewModel.getSuppliers().observe(getViewLifecycleOwner(), list -> {
SpinnerUtils.populateWhiteSpinner(requireContext(), binding.spinnerSupplier, list,
SupplierDTO::getSupCompany, "All Suppliers", -1L, SupplierDTO::getSupId);
});
viewModel.getIsLoading().observe(getViewLifecycleOwner(), loading -> {
binding.swipeRefreshPS.setRefreshing(loading);
});
}
private void setupBulkDelete() { private void setupBulkDelete() {
bulkDeleteHandler = new BulkDeleteHandler( bulkDeleteHandler = new BulkDeleteHandler(
this, this,
@@ -95,136 +101,65 @@ public class ProductSupplierFragment extends Fragment
); );
} }
@Override
public void onResume() {
super.onResume();
loadData();
viewModel.loadFilterData();
}
@Override @Override
public void onDestroyView() { public void onDestroyView() {
super.onDestroyView(); super.onDestroyView();
binding = null; binding = null;
} }
/**
* Reloads data every time the fragment becomes visible.
*/
@Override
public void onResume() {
super.onResume();
loadData();
loadFilterData();
}
/**
* Sets up the filter toggle button to show/hide the filter layout.
*/
private void setupFilterToggle() { private void setupFilterToggle() {
UIUtils.setupFilterToggle(binding.btnToggleFilter, binding.layoutFilter, binding.etSearchPS, UIUtils.setupFilterToggle(binding.btnToggleFilter, binding.layoutFilter, binding.etSearchPS,
binding.spinnerProduct, binding.spinnerSupplier); binding.spinnerProduct, binding.spinnerSupplier);
} }
/**
* Initializes the RecyclerView with a layout manager and adapter for product-supplier data.
*/
private void setupRecyclerView() { private void setupRecyclerView() {
adapter = new ProductSupplierAdapter(psList, this); adapter = new ProductSupplierAdapter(psList, this);
binding.recyclerViewPS.setLayoutManager(new LinearLayoutManager(getContext())); binding.recyclerViewPS.setLayoutManager(new LinearLayoutManager(getContext()));
binding.recyclerViewPS.setAdapter(adapter); binding.recyclerViewPS.setAdapter(adapter);
} }
/**
* Configures the search bar for filtering.
*/
private void setupSearch() { private void setupSearch() {
UIUtils.attachSearch(binding.etSearchPS, this::loadData); UIUtils.attachSearch(binding.etSearchPS, this::loadData);
} }
/**
* Configures the product filter spinner.
*/
private void setupProductFilter() { private void setupProductFilter() {
SpinnerUtils.setupFilterSpinner(binding.spinnerProduct, this::loadData); SpinnerUtils.setupFilterSpinner(binding.spinnerProduct, this::loadData);
} }
/**
* Configures the supplier filter spinner.
*/
private void setupSupplierFilter() { private void setupSupplierFilter() {
SpinnerUtils.setupFilterSpinner(binding.spinnerSupplier, this::loadData); SpinnerUtils.setupFilterSpinner(binding.spinnerSupplier, this::loadData);
} }
/**
* Fetches products and suppliers to populate the filters.
*/
private void loadFilterData() {
productViewModel.getAllProducts(null, null, 0, 100, "prodName").observe(getViewLifecycleOwner(), resource -> {
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
productList = resource.data.getContent();
SpinnerUtils.populateWhiteSpinner(requireContext(), binding.spinnerProduct, productList,
ProductDTO::getProdName, "All Products", -1L, ProductDTO::getProdId);
}
});
supplierViewModel.getAllSuppliers(0, 100, null, "supCompany").observe(getViewLifecycleOwner(), resource -> {
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
supplierList = resource.data.getContent();
SpinnerUtils.populateWhiteSpinner(requireContext(), binding.spinnerSupplier, supplierList,
SupplierDTO::getSupCompany, "All Suppliers", -1L, SupplierDTO::getSupId);
}
});
}
/**
* Sets up the SwipeRefreshLayout to allow manual reloading of product-supplier data.
*/
private void setupSwipeRefresh() { private void setupSwipeRefresh() {
binding.swipeRefreshPS.setOnRefreshListener(this::loadData); binding.swipeRefreshPS.setOnRefreshListener(this::loadData);
} }
/**
* Fetches product-supplier data from the server through the ViewModel with search query and filters.
*/
private void loadData() { private void loadData() {
String query = binding.etSearchPS.getText().toString().trim(); String query = binding.etSearchPS.getText().toString().trim();
if (query.isEmpty()) query = null; if (query.isEmpty()) query = null;
Long productId = null; Long productId = null;
if (binding.spinnerProduct.getSelectedItemPosition() > 0 && !productList.isEmpty()) { List<ProductDTO> products = viewModel.getProducts().getValue();
productId = productList.get(binding.spinnerProduct.getSelectedItemPosition() - 1).getProdId(); if (binding.spinnerProduct.getSelectedItemPosition() > 0 && products != null && !products.isEmpty()) {
productId = products.get(binding.spinnerProduct.getSelectedItemPosition() - 1).getProdId();
} }
Long supplierId = null; Long supplierId = null;
if (binding.spinnerSupplier.getSelectedItemPosition() > 0 && !supplierList.isEmpty()) { List<SupplierDTO> suppliers = viewModel.getSuppliers().getValue();
supplierId = supplierList.get(binding.spinnerSupplier.getSelectedItemPosition() - 1).getSupId(); if (binding.spinnerSupplier.getSelectedItemPosition() > 0 && suppliers != null && !suppliers.isEmpty()) {
supplierId = suppliers.get(binding.spinnerSupplier.getSelectedItemPosition() - 1).getSupId();
} }
viewModel.getAllProductSuppliers(0, 100, query, productId, supplierId, "productName").observe(getViewLifecycleOwner(), resource -> { viewModel.loadProductSuppliers(query, productId, supplierId);
if (resource == null) return;
// Check the status to see if the resource is loaded and display the data
switch (resource.status) {
case LOADING:
// Show loading indicator
binding.swipeRefreshPS.setRefreshing(true);
break;
case SUCCESS:
// Hide loading indicator and display data
binding.swipeRefreshPS.setRefreshing(false);
if (resource.data != null) {
psList.clear();
psList.addAll(resource.data.getContent());
adapter.notifyDataSetChanged();
}
break;
case ERROR:
// Hide loading indicator and toast error message
binding.swipeRefreshPS.setRefreshing(false);
Toast.makeText(getContext(), "Failed to load: " + resource.message, Toast.LENGTH_SHORT).show();
Log.e("PSFragment", "Error loading: " + resource.message);
break;
}
});
} }
/**
* Navigates to the product-supplier detail screen for a specific item or to add a new record.
*/
private void openDetail(int position) { private void openDetail(int position) {
Bundle args = new Bundle(); Bundle args = new Bundle();
if (position != -1) { if (position != -1) {
@@ -235,9 +170,6 @@ public class ProductSupplierFragment extends Fragment
NavHostFragment.findNavController(this).navigate(R.id.nav_product_supplier_detail, args); NavHostFragment.findNavController(this).navigate(R.id.nav_product_supplier_detail, args);
} }
/**
* Handles item click in the product-supplier list.
*/
@Override @Override
public void onProductSupplierClick(int position) { openDetail(position); } public void onProductSupplierClick(int position) { openDetail(position); }

View File

@@ -1,11 +1,9 @@
package com.example.petstoremobile.fragments.listfragments; package com.example.petstoremobile.fragments.listfragments;
import android.os.Bundle; import android.os.Bundle;
import android.util.Log;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.Toast;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
@@ -19,11 +17,9 @@ import com.example.petstoremobile.adapters.PurchaseOrderAdapter;
import com.example.petstoremobile.databinding.FragmentPurchaseOrderBinding; import com.example.petstoremobile.databinding.FragmentPurchaseOrderBinding;
import com.example.petstoremobile.dtos.PurchaseOrderDTO; import com.example.petstoremobile.dtos.PurchaseOrderDTO;
import com.example.petstoremobile.dtos.StoreDTO; import com.example.petstoremobile.dtos.StoreDTO;
import com.example.petstoremobile.utils.Resource;
import com.example.petstoremobile.utils.SpinnerUtils; import com.example.petstoremobile.utils.SpinnerUtils;
import com.example.petstoremobile.utils.UIUtils; import com.example.petstoremobile.utils.UIUtils;
import com.example.petstoremobile.viewmodels.PurchaseOrderViewModel; import com.example.petstoremobile.viewmodels.PurchaseOrderListViewModel;
import com.example.petstoremobile.viewmodels.StoreViewModel;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@@ -36,24 +32,15 @@ public class PurchaseOrderFragment extends Fragment
private FragmentPurchaseOrderBinding binding; private FragmentPurchaseOrderBinding binding;
private List<PurchaseOrderDTO> poList = new ArrayList<>(); private List<PurchaseOrderDTO> poList = new ArrayList<>();
private List<StoreDTO> storeList = new ArrayList<>();
private PurchaseOrderAdapter adapter; private PurchaseOrderAdapter adapter;
private PurchaseOrderViewModel viewModel; private PurchaseOrderListViewModel viewModel;
private StoreViewModel storeViewModel;
/**
* Initializes the fragment and its associated ViewModels.
*/
@Override @Override
public void onCreate(@Nullable Bundle savedInstanceState) { public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
viewModel = new ViewModelProvider(this).get(PurchaseOrderViewModel.class); viewModel = new ViewModelProvider(this).get(PurchaseOrderListViewModel.class);
storeViewModel = new ViewModelProvider(this).get(StoreViewModel.class);
} }
/**
* Sets up the fragment's UI components, including RecyclerView, filters, and swipe-to-refresh.
*/
@Override @Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) { Bundle savedInstanceState) {
@@ -64,121 +51,72 @@ public class PurchaseOrderFragment extends Fragment
setupStoreFilter(); setupStoreFilter();
setupSwipeRefresh(); setupSwipeRefresh();
setupFilterToggle(); setupFilterToggle();
observeViewModel();
UIUtils.setupHamburgerMenu(binding.btnHamburgerPO, this); UIUtils.setupHamburgerMenu(binding.btnHamburgerPO, this);
return binding.getRoot(); return binding.getRoot();
} }
@Override private void observeViewModel() {
public void onDestroyView() { viewModel.getPurchaseOrders().observe(getViewLifecycleOwner(), list -> {
super.onDestroyView(); poList.clear();
binding = null; poList.addAll(list);
adapter.notifyDataSetChanged();
});
viewModel.getStores().observe(getViewLifecycleOwner(), list -> {
SpinnerUtils.populateWhiteSpinner(requireContext(), binding.spinnerStore, list,
StoreDTO::getStoreName, "All Stores", -1L, StoreDTO::getStoreId);
});
viewModel.getIsLoading().observe(getViewLifecycleOwner(), loading -> {
binding.swipeRefreshPO.setRefreshing(loading);
});
} }
/**
* Reloads data every time the fragment becomes visible.
*/
@Override @Override
public void onResume() { public void onResume() {
super.onResume(); super.onResume();
loadData(); loadData();
loadStoreData(); viewModel.loadStores();
} }
/**
* Sets up the filter toggle button to show/hide the filter layout.
*/
private void setupFilterToggle() { private void setupFilterToggle() {
UIUtils.setupFilterToggle(binding.btnToggleFilter, binding.layoutFilter, binding.etSearchPO, binding.spinnerStore); UIUtils.setupFilterToggle(binding.btnToggleFilter, binding.layoutFilter, binding.etSearchPO, binding.spinnerStore);
} }
/**
* Configures the search bar for filtering.
*/
private void setupSearch() { private void setupSearch() {
UIUtils.attachSearch(binding.etSearchPO, this::loadData); UIUtils.attachSearch(binding.etSearchPO, this::loadData);
} }
/**
* Configures the store filter spinner.
*/
private void setupStoreFilter() { private void setupStoreFilter() {
SpinnerUtils.setupFilterSpinner(binding.spinnerStore, this::loadData); SpinnerUtils.setupFilterSpinner(binding.spinnerStore, this::loadData);
} }
/**
* Fetches store data to populate the store filter.
*/
private void loadStoreData() {
storeViewModel.getAllStores(0, 100).observe(getViewLifecycleOwner(), resource -> {
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
storeList = resource.data.getContent();
SpinnerUtils.populateWhiteSpinner(requireContext(), binding.spinnerStore, storeList,
StoreDTO::getStoreName, "All Stores", -1L, StoreDTO::getStoreId);
}
});
}
/**
* Initializes the RecyclerView with a layout manager and adapter.
*/
private void setupRecyclerView() { private void setupRecyclerView() {
adapter = new PurchaseOrderAdapter(poList, this); adapter = new PurchaseOrderAdapter(poList, this);
binding.recyclerViewPO.setLayoutManager(new LinearLayoutManager(getContext())); binding.recyclerViewPO.setLayoutManager(new LinearLayoutManager(getContext()));
binding.recyclerViewPO.setAdapter(adapter); binding.recyclerViewPO.setAdapter(adapter);
} }
/**
* Sets up the SwipeRefreshLayout to allow manual reloading of purchase order data.
*/
private void setupSwipeRefresh() { private void setupSwipeRefresh() {
binding.swipeRefreshPO.setOnRefreshListener(this::loadData); binding.swipeRefreshPO.setOnRefreshListener(this::loadData);
} }
/**
* Fetches purchase order data from the server with active filters and updates the UI.
*/
private void loadData() { private void loadData() {
String query = binding.etSearchPO != null ? binding.etSearchPO.getText().toString().trim() : ""; String query = binding.etSearchPO != null ? binding.etSearchPO.getText().toString().trim() : "";
if (query.isEmpty()) query = null; if (query.isEmpty()) query = null;
Long storeId = null; Long storeId = null;
if (binding.spinnerStore.getSelectedItemPosition() > 0 && !storeList.isEmpty()) { List<StoreDTO> stores = viewModel.getStores().getValue();
storeId = storeList.get(binding.spinnerStore.getSelectedItemPosition() - 1).getStoreId(); if (binding.spinnerStore.getSelectedItemPosition() > 0 && stores != null && !stores.isEmpty()) {
storeId = stores.get(binding.spinnerStore.getSelectedItemPosition() - 1).getStoreId();
} }
viewModel.getAllPurchaseOrders(0, 100, query, storeId, "purchaseOrderId,desc").observe(getViewLifecycleOwner(), resource -> { viewModel.loadPurchaseOrders(query, storeId);
if (resource == null) return;
// Check the status to see if the resource is loaded and display the data
switch (resource.status) {
case LOADING:
// Show loading indicator
binding.swipeRefreshPO.setRefreshing(true);
break;
case SUCCESS:
// Hide loading indicator and display data
binding.swipeRefreshPO.setRefreshing(false);
if (resource.data != null) {
poList.clear();
poList.addAll(resource.data.getContent());
adapter.notifyDataSetChanged();
}
break;
case ERROR:
// Hide loading indicator and toast error message
binding.swipeRefreshPO.setRefreshing(false);
Toast.makeText(getContext(), "Failed to load purchase orders: " + resource.message, Toast.LENGTH_SHORT).show();
Log.e("POFragment", "Error loading purchase orders: " + resource.message);
break;
}
});
} }
/**
* Navigates to the purchase order detail screen for a specific record.
*/
private void openDetail(int position) { private void openDetail(int position) {
Bundle args = new Bundle(); Bundle args = new Bundle();
PurchaseOrderDTO po = poList.get(position); PurchaseOrderDTO po = poList.get(position);
@@ -186,11 +124,14 @@ public class PurchaseOrderFragment extends Fragment
NavHostFragment.findNavController(this).navigate(R.id.nav_purchase_order_detail, args); NavHostFragment.findNavController(this).navigate(R.id.nav_purchase_order_detail, args);
} }
/**
* Handles item click in the purchase order list.
*/
@Override @Override
public void onPurchaseOrderClick(int position) { public void onPurchaseOrderClick(int position) {
openDetail(position); openDetail(position);
} }
@Override
public void onDestroyView() {
super.onDestroyView();
binding = null;
}
} }

View File

@@ -8,7 +8,6 @@ import androidx.lifecycle.ViewModelProvider;
import androidx.navigation.fragment.NavHostFragment; import androidx.navigation.fragment.NavHostFragment;
import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView;
import android.util.Log;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
@@ -19,11 +18,9 @@ import com.example.petstoremobile.adapters.SaleAdapter;
import com.example.petstoremobile.databinding.FragmentSaleBinding; import com.example.petstoremobile.databinding.FragmentSaleBinding;
import com.example.petstoremobile.dtos.SaleDTO; import com.example.petstoremobile.dtos.SaleDTO;
import com.example.petstoremobile.dtos.StoreDTO; import com.example.petstoremobile.dtos.StoreDTO;
import com.example.petstoremobile.utils.Resource;
import com.example.petstoremobile.utils.SpinnerUtils; import com.example.petstoremobile.utils.SpinnerUtils;
import com.example.petstoremobile.utils.UIUtils; import com.example.petstoremobile.utils.UIUtils;
import com.example.petstoremobile.viewmodels.SaleViewModel; import com.example.petstoremobile.viewmodels.SaleListViewModel;
import com.example.petstoremobile.viewmodels.StoreViewModel;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@@ -33,20 +30,10 @@ import dagger.hilt.android.AndroidEntryPoint;
@AndroidEntryPoint @AndroidEntryPoint
public class SaleFragment extends Fragment implements SaleAdapter.OnSaleClickListener { public class SaleFragment extends Fragment implements SaleAdapter.OnSaleClickListener {
private static final String TAG = "SaleFragment";
private static final int PAGE_SIZE = 200;
private FragmentSaleBinding binding; private FragmentSaleBinding binding;
private final List<SaleDTO> saleList = new ArrayList<>(); private final List<SaleDTO> saleList = new ArrayList<>();
private final List<StoreDTO> storeList = new ArrayList<>();
private SaleAdapter adapter; private SaleAdapter adapter;
private SaleViewModel saleViewModel; private SaleListViewModel viewModel;
private StoreViewModel storeViewModel;
// Pagination
private int currentPage = 0;
private boolean isLastPage = false;
private boolean isLoading = false;
@Override @Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
@@ -58,8 +45,7 @@ public class SaleFragment extends Fragment implements SaleAdapter.OnSaleClickLis
@Override @Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState); super.onViewCreated(view, savedInstanceState);
saleViewModel = new ViewModelProvider(this).get(SaleViewModel.class); viewModel = new ViewModelProvider(this).get(SaleListViewModel.class);
storeViewModel = new ViewModelProvider(this).get(StoreViewModel.class);
setupRecyclerView(); setupRecyclerView();
setupSearch(); setupSearch();
@@ -67,6 +53,8 @@ public class SaleFragment extends Fragment implements SaleAdapter.OnSaleClickLis
setupPaymentMethodFilter(); setupPaymentMethodFilter();
setupSwipeRefresh(); setupSwipeRefresh();
setupFilterToggle(); setupFilterToggle();
observeViewModel();
loadSales(true); loadSales(true);
UIUtils.setupHamburgerMenu(binding.btnHamburger, this); UIUtils.setupHamburgerMenu(binding.btnHamburger, this);
@@ -78,10 +66,27 @@ public class SaleFragment extends Fragment implements SaleAdapter.OnSaleClickLis
NavHostFragment.findNavController(this).navigate(R.id.nav_refund)); NavHostFragment.findNavController(this).navigate(R.id.nav_refund));
} }
private void observeViewModel() {
viewModel.getSales().observe(getViewLifecycleOwner(), list -> {
saleList.clear();
saleList.addAll(list);
adapter.notifyDataSetChanged();
});
viewModel.getStores().observe(getViewLifecycleOwner(), list -> {
SpinnerUtils.populateWhiteSpinner(requireContext(), binding.spinnerStore, list,
StoreDTO::getStoreName, "Stores", null, StoreDTO::getStoreId);
});
viewModel.getIsLoading().observe(getViewLifecycleOwner(), loading -> {
binding.swipeRefreshSale.setRefreshing(loading);
});
}
@Override @Override
public void onResume() { public void onResume() {
super.onResume(); super.onResume();
loadStoreData(); viewModel.loadStores();
} }
private void setupFilterToggle() { private void setupFilterToggle() {
@@ -93,28 +98,11 @@ public class SaleFragment extends Fragment implements SaleAdapter.OnSaleClickLis
SpinnerUtils.setupFilterSpinner(binding.spinnerStore, () -> loadSales(true)); SpinnerUtils.setupFilterSpinner(binding.spinnerStore, () -> loadSales(true));
} }
private void loadStoreData() {
storeViewModel.getAllStores(0, 100).observe(getViewLifecycleOwner(), resource -> {
if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) {
storeList.clear();
storeList.addAll(resource.data.getContent());
SpinnerUtils.populateWhiteSpinner(requireContext(), binding.spinnerStore, storeList,
StoreDTO::getStoreName, "Stores", null, StoreDTO::getStoreId);
}
});
}
private void setupPaymentMethodFilter() { private void setupPaymentMethodFilter() {
String[] paymentMethods = {"Payments", "Cash", "Card"}; String[] paymentMethods = {"Payments", "Cash", "Card"};
SpinnerUtils.setupStringFilterSpinner(requireContext(), binding.spinnerPaymentMethod, paymentMethods, () -> loadSales(true)); SpinnerUtils.setupStringFilterSpinner(requireContext(), binding.spinnerPaymentMethod, paymentMethods, () -> loadSales(true));
} }
@Override
public void onDestroyView() {
super.onDestroyView();
binding = null;
}
private void setupRecyclerView() { private void setupRecyclerView() {
adapter = new SaleAdapter(saleList, this); adapter = new SaleAdapter(saleList, this);
binding.recyclerViewSales.setLayoutManager(new LinearLayoutManager(getContext())); binding.recyclerViewSales.setLayoutManager(new LinearLayoutManager(getContext()));
@@ -129,7 +117,8 @@ public class SaleFragment extends Fragment implements SaleAdapter.OnSaleClickLis
int visible = lm.getChildCount(); int visible = lm.getChildCount();
int total = lm.getItemCount(); int total = lm.getItemCount();
int firstVis = lm.findFirstVisibleItemPosition(); int firstVis = lm.findFirstVisibleItemPosition();
if (!isLoading && !isLastPage && (visible + firstVis) >= total - 3) { Boolean isLoading = viewModel.getIsLoading().getValue();
if ((isLoading == null || !isLoading) && !viewModel.isLastPage() && (visible + firstVis) >= total - 3) {
loadSales(false); loadSales(false);
} }
} }
@@ -146,13 +135,6 @@ public class SaleFragment extends Fragment implements SaleAdapter.OnSaleClickLis
} }
private void loadSales(boolean reset) { private void loadSales(boolean reset) {
if (isLoading) return;
if (reset) {
currentPage = 0;
isLastPage = false;
}
String query = binding.etSearchSale != null ? binding.etSearchSale.getText().toString().trim() : ""; String query = binding.etSearchSale != null ? binding.etSearchSale.getText().toString().trim() : "";
if (query.isEmpty()) query = null; if (query.isEmpty()) query = null;
@@ -162,39 +144,12 @@ public class SaleFragment extends Fragment implements SaleAdapter.OnSaleClickLis
} }
Long storeId = null; Long storeId = null;
if (binding.spinnerStore.getSelectedItemPosition() > 0 && !storeList.isEmpty()) { List<StoreDTO> stores = viewModel.getStores().getValue();
storeId = storeList.get(binding.spinnerStore.getSelectedItemPosition() - 1).getStoreId(); if (binding.spinnerStore.getSelectedItemPosition() > 0 && stores != null && !stores.isEmpty()) {
storeId = stores.get(binding.spinnerStore.getSelectedItemPosition() - 1).getStoreId();
} }
saleViewModel.getAllSales(currentPage, PAGE_SIZE, query, paymentMethod, storeId, "saleDate,desc").observe(getViewLifecycleOwner(), resource -> { viewModel.loadSales(reset, query, paymentMethod, storeId);
if (resource == null) return;
switch (resource.status) {
case LOADING:
isLoading = true;
binding.swipeRefreshSale.setRefreshing(true);
break;
case SUCCESS:
isLoading = false;
binding.swipeRefreshSale.setRefreshing(false);
if (resource.data != null) {
if (reset) saleList.clear();
saleList.addAll(resource.data.getContent());
adapter.notifyDataSetChanged();
isLastPage = resource.data.isLast();
if (!isLastPage) currentPage++;
}
break;
case ERROR:
isLoading = false;
binding.swipeRefreshSale.setRefreshing(false);
Log.e(TAG, "Error loading sales: " + resource.message);
if (getContext() != null) {
Toast.makeText(getContext(), "Failed to load sales: " + resource.message, Toast.LENGTH_SHORT).show();
}
break;
}
});
} }
@Override @Override
@@ -210,4 +165,10 @@ public class SaleFragment extends Fragment implements SaleAdapter.OnSaleClickLis
} }
NavHostFragment.findNavController(this).navigate(R.id.nav_sale_detail, args); NavHostFragment.findNavController(this).navigate(R.id.nav_sale_detail, args);
} }
@Override
public void onDestroyView() {
super.onDestroyView();
binding = null;
}
} }

View File

@@ -1,7 +1,6 @@
package com.example.petstoremobile.fragments.listfragments; package com.example.petstoremobile.fragments.listfragments;
import android.os.Bundle; import android.os.Bundle;
import android.util.Log;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
@@ -21,7 +20,7 @@ import com.example.petstoremobile.databinding.FragmentServiceBinding;
import com.example.petstoremobile.dtos.ServiceDTO; import com.example.petstoremobile.dtos.ServiceDTO;
import com.example.petstoremobile.utils.BulkDeleteHandler; import com.example.petstoremobile.utils.BulkDeleteHandler;
import com.example.petstoremobile.utils.UIUtils; import com.example.petstoremobile.utils.UIUtils;
import com.example.petstoremobile.viewmodels.ServiceViewModel; import com.example.petstoremobile.viewmodels.ServiceListViewModel;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@@ -34,32 +33,18 @@ import dagger.hilt.android.AndroidEntryPoint;
@AndroidEntryPoint @AndroidEntryPoint
public class ServiceFragment extends Fragment implements ServiceAdapter.OnServiceClickListener { public class ServiceFragment extends Fragment implements ServiceAdapter.OnServiceClickListener {
private static final String TAG = "ServiceFragment";
private static final int PAGE_SIZE = 20;
private FragmentServiceBinding binding; private FragmentServiceBinding binding;
private final List<ServiceDTO> serviceList = new ArrayList<>(); private final List<ServiceDTO> serviceList = new ArrayList<>();
private ServiceAdapter adapter; private ServiceAdapter adapter;
private ServiceViewModel viewModel; private ServiceListViewModel viewModel;
private BulkDeleteHandler bulkDeleteHandler; private BulkDeleteHandler bulkDeleteHandler;
// Pagination
private int currentPage = 0;
private boolean isLastPage = false;
private boolean isLoading = false;
/**
* Initializes the fragment and its associated ViewModel.
*/
@Override @Override
public void onCreate(@Nullable Bundle savedInstanceState) { public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
viewModel = new ViewModelProvider(this).get(ServiceViewModel.class); viewModel = new ViewModelProvider(this).get(ServiceListViewModel.class);
} }
/**
* Sets up the fragment's UI components, including RecyclerView, search, and swipe-to-refresh.
*/
@Override @Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) { Bundle savedInstanceState) {
@@ -70,15 +55,27 @@ public class ServiceFragment extends Fragment implements ServiceAdapter.OnServic
setupSwipeRefresh(); setupSwipeRefresh();
setupFilterToggle(); setupFilterToggle();
setupBulkDelete(); setupBulkDelete();
observeViewModel();
loadServices(true); loadServices(true);
binding.fabAddService.setOnClickListener(v -> openDetail(null));
UIUtils.setupHamburgerMenu(binding.btnHamburger, this); UIUtils.setupHamburgerMenu(binding.btnHamburger, this);
return binding.getRoot(); return binding.getRoot();
} }
private void observeViewModel() {
viewModel.getServices().observe(getViewLifecycleOwner(), list -> {
serviceList.clear();
serviceList.addAll(list);
adapter.notifyDataSetChanged();
});
viewModel.getIsLoading().observe(getViewLifecycleOwner(), loading -> {
binding.swipeRefreshService.setRefreshing(loading);
});
}
private void setupBulkDelete() { private void setupBulkDelete() {
bulkDeleteHandler = new BulkDeleteHandler( bulkDeleteHandler = new BulkDeleteHandler(
this, this,
@@ -98,23 +95,14 @@ public class ServiceFragment extends Fragment implements ServiceAdapter.OnServic
binding = null; binding = null;
} }
/**
* Sets up the filter toggle button to show/hide the filter layout.
*/
private void setupFilterToggle() { private void setupFilterToggle() {
UIUtils.setupFilterToggle(binding.btnToggleFilter, binding.layoutFilter, binding.etSearchService); UIUtils.setupFilterToggle(binding.btnToggleFilter, binding.layoutFilter, binding.etSearchService);
} }
/**
* Sets up the search bar for filtering.
*/
private void setupSearch() { private void setupSearch() {
UIUtils.attachSearch(binding.etSearchService, () -> loadServices(true)); UIUtils.attachSearch(binding.etSearchService, () -> loadServices(true));
} }
/**
* Initializes the RecyclerView with a layout manager and adapter.
*/
private void setupRecyclerView() { private void setupRecyclerView() {
adapter = new ServiceAdapter(serviceList, this); adapter = new ServiceAdapter(serviceList, this);
binding.recyclerViewServices.setLayoutManager(new LinearLayoutManager(getContext())); binding.recyclerViewServices.setLayoutManager(new LinearLayoutManager(getContext()));
@@ -129,66 +117,24 @@ public class ServiceFragment extends Fragment implements ServiceAdapter.OnServic
int visible = lm.getChildCount(); int visible = lm.getChildCount();
int total = lm.getItemCount(); int total = lm.getItemCount();
int firstVis = lm.findFirstVisibleItemPosition(); int firstVis = lm.findFirstVisibleItemPosition();
if (!isLoading && !isLastPage && (visible + firstVis) >= total - 3) { Boolean isLoading = viewModel.getIsLoading().getValue();
if ((isLoading == null || !isLoading) && !viewModel.isLastPage() && (visible + firstVis) >= total - 3) {
loadServices(false); loadServices(false);
} }
} }
}); });
} }
/**
* Sets up the SwipeRefreshLayout.
*/
private void setupSwipeRefresh() { private void setupSwipeRefresh() {
binding.swipeRefreshService.setOnRefreshListener(() -> loadServices(true)); binding.swipeRefreshService.setOnRefreshListener(() -> loadServices(true));
} }
/**
* Fetches a page of services from the API.
*/
private void loadServices(boolean reset) { private void loadServices(boolean reset) {
if (isLoading) return;
if (reset) {
currentPage = 0;
isLastPage = false;
}
String query = binding.etSearchService.getText().toString().trim(); String query = binding.etSearchService.getText().toString().trim();
if (query.isEmpty()) query = null; if (query.isEmpty()) query = null;
viewModel.loadServices(reset, query);
viewModel.getAllServices(currentPage, PAGE_SIZE, query, "serviceName").observe(getViewLifecycleOwner(), resource -> {
if (resource == null) return;
switch (resource.status) {
case LOADING:
isLoading = true;
binding.swipeRefreshService.setRefreshing(true);
break;
case SUCCESS:
isLoading = false;
binding.swipeRefreshService.setRefreshing(false);
if (resource.data != null) {
if (reset) serviceList.clear();
serviceList.addAll(resource.data.getContent());
adapter.notifyDataSetChanged();
isLastPage = resource.data.isLast();
if (!isLastPage) currentPage++;
}
break;
case ERROR:
isLoading = false;
binding.swipeRefreshService.setRefreshing(false);
Log.e(TAG, "Error: " + resource.message);
Toast.makeText(getContext(), "Failed to load services: " + resource.message, Toast.LENGTH_SHORT).show();
break;
}
});
} }
/**
* Navigates to the service detail screen.
*/
private void openDetail(ServiceDTO service) { private void openDetail(ServiceDTO service) {
Bundle args = new Bundle(); Bundle args = new Bundle();
if (service != null) { if (service != null) {

View File

@@ -1,7 +1,6 @@
package com.example.petstoremobile.fragments.listfragments; package com.example.petstoremobile.fragments.listfragments;
import android.os.Bundle; import android.os.Bundle;
import android.util.Log;
import android.view.*; import android.view.*;
import android.widget.*; import android.widget.*;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
@@ -14,7 +13,7 @@ import com.example.petstoremobile.adapters.EmployeeAdapter;
import com.example.petstoremobile.databinding.FragmentStaffBinding; import com.example.petstoremobile.databinding.FragmentStaffBinding;
import com.example.petstoremobile.dtos.EmployeeDTO; import com.example.petstoremobile.dtos.EmployeeDTO;
import com.example.petstoremobile.utils.UIUtils; import com.example.petstoremobile.utils.UIUtils;
import com.example.petstoremobile.viewmodels.EmployeeViewModel; import com.example.petstoremobile.viewmodels.StaffListViewModel;
import dagger.hilt.android.AndroidEntryPoint; import dagger.hilt.android.AndroidEntryPoint;
import java.util.*; import java.util.*;
@@ -22,21 +21,22 @@ import java.util.*;
public class StaffFragment extends Fragment implements EmployeeAdapter.OnEmployeeClickListener { public class StaffFragment extends Fragment implements EmployeeAdapter.OnEmployeeClickListener {
private FragmentStaffBinding binding; private FragmentStaffBinding binding;
private EmployeeViewModel employeeViewModel; private StaffListViewModel viewModel;
private List<EmployeeDTO> employeeList = new ArrayList<>(); private List<EmployeeDTO> staffList = new ArrayList<>();
private List<EmployeeDTO> filteredList = new ArrayList<>();
private EmployeeAdapter adapter; private EmployeeAdapter adapter;
@Override @Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) { Bundle savedInstanceState) {
binding = FragmentStaffBinding.inflate(inflater, container, false); binding = FragmentStaffBinding.inflate(inflater, container, false);
employeeViewModel = new ViewModelProvider(this).get(EmployeeViewModel.class); viewModel = new ViewModelProvider(this).get(StaffListViewModel.class);
setupRecyclerView(); setupRecyclerView();
setupSearch(); setupSearch();
setupSwipeRefresh(); setupSwipeRefresh();
loadStaff(); observeViewModel();
viewModel.loadStaff();
binding.fabAddStaff.setOnClickListener(v -> openDetail(-1)); binding.fabAddStaff.setOnClickListener(v -> openDetail(-1));
@@ -46,70 +46,36 @@ public class StaffFragment extends Fragment implements EmployeeAdapter.OnEmploye
return binding.getRoot(); return binding.getRoot();
} }
private void observeViewModel() {
viewModel.getFilteredEmployees().observe(getViewLifecycleOwner(), list -> {
staffList.clear();
staffList.addAll(list);
adapter.notifyDataSetChanged();
});
viewModel.getIsLoading().observe(getViewLifecycleOwner(), loading -> {
binding.swipeRefreshStaff.setRefreshing(loading);
});
}
private void setupRecyclerView() { private void setupRecyclerView() {
adapter = new EmployeeAdapter(filteredList, this); adapter = new EmployeeAdapter(staffList, this);
binding.recyclerViewStaff.setLayoutManager(new LinearLayoutManager(getContext())); binding.recyclerViewStaff.setLayoutManager(new LinearLayoutManager(getContext()));
binding.recyclerViewStaff.setAdapter(adapter); binding.recyclerViewStaff.setAdapter(adapter);
} }
private void setupSearch() { private void setupSearch() {
UIUtils.attachSearch(binding.etSearchStaff, () -> filter(binding.etSearchStaff.getText().toString())); UIUtils.attachSearch(binding.etSearchStaff, () -> viewModel.filter(binding.etSearchStaff.getText().toString()));
} }
private void setupSwipeRefresh() { private void setupSwipeRefresh() {
binding.swipeRefreshStaff.setOnRefreshListener(this::loadStaff); binding.swipeRefreshStaff.setOnRefreshListener(viewModel::loadStaff);
}
private void filter(String query) {
filteredList.clear();
if (query.isEmpty()) {
filteredList.addAll(employeeList);
} else {
String lower = query.toLowerCase();
for (EmployeeDTO e : employeeList) {
if ((e.getFullName() != null && e.getFullName().toLowerCase().contains(lower))
|| (e.getUsername() != null && e.getUsername().toLowerCase().contains(lower))
|| (e.getEmail() != null && e.getEmail().toLowerCase().contains(lower))
|| (e.getPhone() != null && e.getPhone().toLowerCase().contains(lower))) {
filteredList.add(e);
}
}
}
adapter.notifyDataSetChanged();
}
private void loadStaff() {
binding.swipeRefreshStaff.setRefreshing(true);
employeeViewModel.getAllEmployees(0, 100).observe(getViewLifecycleOwner(), resource -> {
if (resource != null) {
switch (resource.status) {
case SUCCESS:
binding.swipeRefreshStaff.setRefreshing(false);
if (resource.data != null) {
employeeList.clear();
employeeList.addAll(resource.data.getContent());
filter(binding != null ? binding.etSearchStaff.getText().toString() : "");
}
break;
case ERROR:
binding.swipeRefreshStaff.setRefreshing(false);
if (getContext() != null) {
Toast.makeText(getContext(), resource.message != null ? resource.message : "Failed to load staff",
Toast.LENGTH_SHORT).show();
}
break;
case LOADING:
binding.swipeRefreshStaff.setRefreshing(true);
break;
}
}
});
} }
private void openDetail(int position) { private void openDetail(int position) {
Bundle args = new Bundle(); Bundle args = new Bundle();
if (position != -1) { if (position != -1) {
EmployeeDTO e = filteredList.get(position); EmployeeDTO e = staffList.get(position);
args.putLong("employeeId", e.getEmployeeId()); args.putLong("employeeId", e.getEmployeeId());
args.putString("username", e.getUsername() != null ? e.getUsername() : ""); args.putString("username", e.getUsername() != null ? e.getUsername() : "");
args.putString("firstName", e.getFirstName() != null ? e.getFirstName() : ""); args.putString("firstName", e.getFirstName() != null ? e.getFirstName() : "");

View File

@@ -9,20 +9,17 @@ import androidx.lifecycle.ViewModelProvider;
import androidx.navigation.fragment.NavHostFragment; import androidx.navigation.fragment.NavHostFragment;
import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.LinearLayoutManager;
import android.util.Log;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.Toast;
import com.example.petstoremobile.R; import com.example.petstoremobile.R;
import com.example.petstoremobile.adapters.SupplierAdapter; import com.example.petstoremobile.adapters.SupplierAdapter;
import com.example.petstoremobile.databinding.FragmentSupplierBinding; import com.example.petstoremobile.databinding.FragmentSupplierBinding;
import com.example.petstoremobile.dtos.SupplierDTO; import com.example.petstoremobile.dtos.SupplierDTO;
import com.example.petstoremobile.utils.BulkDeleteHandler; import com.example.petstoremobile.utils.BulkDeleteHandler;
import com.example.petstoremobile.utils.Resource;
import com.example.petstoremobile.utils.UIUtils; import com.example.petstoremobile.utils.UIUtils;
import com.example.petstoremobile.viewmodels.SupplierViewModel; import com.example.petstoremobile.viewmodels.SupplierListViewModel;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@@ -35,21 +32,15 @@ public class SupplierFragment extends Fragment implements SupplierAdapter.OnSupp
private FragmentSupplierBinding binding; private FragmentSupplierBinding binding;
private List<SupplierDTO> supplierList = new ArrayList<>(); private List<SupplierDTO> supplierList = new ArrayList<>();
private SupplierAdapter adapter; private SupplierAdapter adapter;
private SupplierViewModel viewModel; private SupplierListViewModel viewModel;
private BulkDeleteHandler bulkDeleteHandler; private BulkDeleteHandler bulkDeleteHandler;
/**
* Initializes the fragment and its associated SupplierViewModel.
*/
@Override @Override
public void onCreate(@Nullable Bundle savedInstanceState) { public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
viewModel = new ViewModelProvider(this).get(SupplierViewModel.class); viewModel = new ViewModelProvider(this).get(SupplierListViewModel.class);
} }
/**
* Sets up the fragment's UI components, including RecyclerView, search, and swipe-to-refresh.
*/
@Override @Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) { Bundle savedInstanceState) {
@@ -60,9 +51,10 @@ public class SupplierFragment extends Fragment implements SupplierAdapter.OnSupp
setupSwipeRefresh(); setupSwipeRefresh();
setupFilterToggle(); setupFilterToggle();
setupBulkDelete(); setupBulkDelete();
observeViewModel();
loadSupplierData(); loadSupplierData();
//Add button to opens the add dialog
binding.fabAddSupplier.setOnClickListener(v -> openSupplierDetails(-1)); binding.fabAddSupplier.setOnClickListener(v -> openSupplierDetails(-1));
UIUtils.setupHamburgerMenu(binding.btnHamburger, this); UIUtils.setupHamburgerMenu(binding.btnHamburger, this);
@@ -70,6 +62,18 @@ public class SupplierFragment extends Fragment implements SupplierAdapter.OnSupp
return binding.getRoot(); return binding.getRoot();
} }
private void observeViewModel() {
viewModel.getSuppliers().observe(getViewLifecycleOwner(), list -> {
supplierList.clear();
supplierList.addAll(list);
adapter.notifyDataSetChanged();
});
viewModel.getIsLoading().observe(getViewLifecycleOwner(), loading -> {
binding.swipeRefreshSupplier.setRefreshing(loading);
});
}
private void setupBulkDelete() { private void setupBulkDelete() {
bulkDeleteHandler = new BulkDeleteHandler( bulkDeleteHandler = new BulkDeleteHandler(
this, this,
@@ -89,47 +93,27 @@ public class SupplierFragment extends Fragment implements SupplierAdapter.OnSupp
binding = null; binding = null;
} }
/**
* Sets up the filter toggle button to show/hide the filter layout.
*/
private void setupFilterToggle() { private void setupFilterToggle() {
UIUtils.setupFilterToggle(binding.btnToggleFilter, binding.layoutFilter, binding.etSearchSupplier); UIUtils.setupFilterToggle(binding.btnToggleFilter, binding.layoutFilter, binding.etSearchSupplier);
} }
/**
* Configures the search bar for filtering.
*/
private void setupSearch() { private void setupSearch() {
UIUtils.attachSearch(binding.etSearchSupplier, this::loadSupplierData); UIUtils.attachSearch(binding.etSearchSupplier, this::loadSupplierData);
} }
/**
* Sets up the SwipeRefreshLayout to allow manual reloading of supplier data.
*/
private void setupSwipeRefresh() { private void setupSwipeRefresh() {
binding.swipeRefreshSupplier.setOnRefreshListener(this::loadSupplierData); binding.swipeRefreshSupplier.setOnRefreshListener(this::loadSupplierData);
} }
/**
* Navigates to the supplier detail screen for editing an existing record or adding a new one.
*/
private void openSupplierDetails(int position) { private void openSupplierDetails(int position) {
//Make a bundle to pass data to the detail fragment
Bundle args = new Bundle(); Bundle args = new Bundle();
//if editing a supplier, add the supplier id to the bundle
if (position != -1) { if (position != -1) {
SupplierDTO supplier = supplierList.get(position); SupplierDTO supplier = supplierList.get(position);
args.putLong("supId", supplier.getSupId()); args.putLong("supId", supplier.getSupId());
} }
NavHostFragment.findNavController(this).navigate(R.id.nav_supplier_detail, args); NavHostFragment.findNavController(this).navigate(R.id.nav_supplier_detail, args);
} }
/**
* Handles item click in the supplier list.
*/
@Override @Override
public void onSupplierClick(int position) { public void onSupplierClick(int position) {
openSupplierDetails(position); openSupplierDetails(position);
@@ -142,47 +126,12 @@ public class SupplierFragment extends Fragment implements SupplierAdapter.OnSupp
} }
} }
/**
* Fetches all supplier data from the server through the ViewModel and updates the UI.
*/
private void loadSupplierData() { private void loadSupplierData() {
String query = binding.etSearchSupplier != null ? binding.etSearchSupplier.getText().toString().trim() : ""; String query = binding.etSearchSupplier != null ? binding.etSearchSupplier.getText().toString().trim() : "";
if (query.isEmpty()) query = null; if (query.isEmpty()) query = null;
viewModel.loadSuppliers(query);
//Load suppliers from the backend with query and default sort
viewModel.getAllSuppliers(0, 100, query, "supCompany").observe(getViewLifecycleOwner(), resource -> {
if (resource == null) return;
// Check the status to see if the resource is loaded and display the data
switch (resource.status) {
case LOADING:
// Show loading indicator
binding.swipeRefreshSupplier.setRefreshing(true);
break;
case SUCCESS:
// Hide loading indicator and display data
binding.swipeRefreshSupplier.setRefreshing(false);
if (resource.data != null) {
supplierList.clear();
supplierList.addAll(resource.data.getContent());
adapter.notifyDataSetChanged();
}
break;
case ERROR:
// Hide loading indicator and toast error message
binding.swipeRefreshSupplier.setRefreshing(false);
if (getContext() != null) {
Toast.makeText(getContext(), "Failed to load suppliers: " + resource.message, Toast.LENGTH_SHORT).show();
}
Log.e("SupplierFragment", "Error loading suppliers: " + resource.message);
break;
}
});
} }
/**
* Initializes the RecyclerView with a layout manager and adapter for displaying suppliers.
*/
private void setupRecyclerView() { private void setupRecyclerView() {
adapter = new SupplierAdapter(supplierList, this); adapter = new SupplierAdapter(supplierList, this);
binding.recyclerViewSuppliers.setLayoutManager(new LinearLayoutManager(getContext())); binding.recyclerViewSuppliers.setLayoutManager(new LinearLayoutManager(getContext()));

View File

@@ -1,6 +1,5 @@
package com.example.petstoremobile.fragments.listfragments.detailfragments; package com.example.petstoremobile.fragments.listfragments.detailfragments;
import android.app.DatePickerDialog;
import android.os.Bundle; import android.os.Bundle;
import android.view.*; import android.view.*;
import android.widget.*; import android.widget.*;
@@ -12,14 +11,13 @@ import androidx.navigation.fragment.NavHostFragment;
import com.example.petstoremobile.databinding.FragmentAdoptionDetailBinding; import com.example.petstoremobile.databinding.FragmentAdoptionDetailBinding;
import com.example.petstoremobile.dtos.*; import com.example.petstoremobile.dtos.*;
import com.example.petstoremobile.utils.DateTimeUtils;
import com.example.petstoremobile.utils.DialogUtils; import com.example.petstoremobile.utils.DialogUtils;
import com.example.petstoremobile.utils.InputValidator;
import com.example.petstoremobile.utils.Resource; import com.example.petstoremobile.utils.Resource;
import com.example.petstoremobile.utils.SpinnerUtils; import com.example.petstoremobile.utils.SpinnerUtils;
import com.example.petstoremobile.viewmodels.AdoptionViewModel; import com.example.petstoremobile.utils.UIUtils;
import com.example.petstoremobile.viewmodels.CustomerViewModel; import com.example.petstoremobile.viewmodels.AdoptionDetailViewModel;
import com.example.petstoremobile.viewmodels.PetViewModel;
import com.example.petstoremobile.viewmodels.StoreViewModel;
import com.example.petstoremobile.viewmodels.UserViewModel;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.util.*; import java.util.*;
@@ -33,35 +31,19 @@ import dagger.hilt.android.AndroidEntryPoint;
public class AdoptionDetailFragment extends Fragment { public class AdoptionDetailFragment extends Fragment {
private FragmentAdoptionDetailBinding binding; private FragmentAdoptionDetailBinding binding;
private AdoptionDetailViewModel viewModel;
private long adoptionId = -1;
private boolean isEditing = false;
private long preselectedPetId = -1; private long preselectedPetId = -1;
private long preselectedCustomerId = -1; private long preselectedCustomerId = -1;
private long preselectedStoreId = -1; private long preselectedStoreId = -1;
private long preselectedEmployeeId = -1; private long preselectedEmployeeId = -1;
private List<PetDTO> petList = new ArrayList<>();
private List<CustomerDTO> customerList = new ArrayList<>();
private List<StoreDTO> storeList = new ArrayList<>();
private List<UserDTO> employeeList = new ArrayList<>();
private final String[] STATUSES = {"Pending", "Completed", "Cancelled"}; private final String[] STATUSES = {"Pending", "Completed", "Cancelled"};
private AdoptionViewModel adoptionViewModel;
private PetViewModel petViewModel;
private CustomerViewModel customerViewModel;
private StoreViewModel storeViewModel;
private UserViewModel userViewModel;
@Override @Override
public void onCreate(Bundle savedInstanceState) { public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
adoptionViewModel = new ViewModelProvider(this).get(AdoptionViewModel.class); viewModel = new ViewModelProvider(this).get(AdoptionDetailViewModel.class);
petViewModel = new ViewModelProvider(this).get(PetViewModel.class);
customerViewModel = new ViewModelProvider(this).get(CustomerViewModel.class);
storeViewModel = new ViewModelProvider(this).get(StoreViewModel.class);
userViewModel = new ViewModelProvider(this).get(UserViewModel.class);
} }
@Override @Override
@@ -76,6 +58,7 @@ public class AdoptionDetailFragment extends Fragment {
super.onViewCreated(view, savedInstanceState); super.onViewCreated(view, savedInstanceState);
setupSpinners(); setupSpinners();
setupDatePicker(); setupDatePicker();
observeViewModel();
loadSpinnersData(); loadSpinnersData();
handleArguments(); handleArguments();
@@ -84,155 +67,146 @@ public class AdoptionDetailFragment extends Fragment {
binding.btnDeleteAdoption.setOnClickListener(v -> confirmDelete()); binding.btnDeleteAdoption.setOnClickListener(v -> confirmDelete());
} }
private void observeViewModel() {
viewModel.getPetList().observe(getViewLifecycleOwner(), list -> refreshPetSpinner());
viewModel.getCustomerList().observe(getViewLifecycleOwner(), list -> refreshCustomerSpinner());
viewModel.getStoreList().observe(getViewLifecycleOwner(), list -> refreshStoreSpinner());
viewModel.getEmployeeList().observe(getViewLifecycleOwner(), list -> refreshEmployeeSpinner());
}
private void setLoading(boolean loading) {
if (binding != null && binding.progressBar != null) {
binding.progressBar.setVisibility(loading ? View.VISIBLE : View.GONE);
}
}
@Override @Override
public void onDestroyView() { public void onDestroyView() {
super.onDestroyView(); super.onDestroyView();
binding = null; binding = null;
} }
/**
* Configures the spinner for adoption status.
*/
private void setupSpinners() { private void setupSpinners() {
SpinnerUtils.setupStringSpinner(requireContext(), binding.spinnerAdoptionStatus, STATUSES); SpinnerUtils.setupStringSpinner(requireContext(), binding.spinnerAdoptionStatus, STATUSES);
UIUtils.setViewsEnabled(false, binding.spinnerAdoptionPet);
binding.spinnerAdoptionCustomer.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
@Override
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
if (position > 0) {
UIUtils.setViewsEnabled(true, binding.spinnerAdoptionPet);
} else {
if (!viewModel.isEditing()) {
binding.spinnerAdoptionPet.setSelection(0);
UIUtils.setViewsEnabled(false, binding.spinnerAdoptionPet);
}
}
}
@Override
public void onNothingSelected(AdapterView<?> parent) {}
});
binding.spinnerAdoptionStore.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
@Override
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
if (position > 0 && viewModel.getStoreList().getValue() != null && position <= viewModel.getStoreList().getValue().size()) {
DropdownDTO selectedStore = viewModel.getStoreList().getValue().get(position - 1);
loadEmployees(selectedStore.getId());
} else {
viewModel.setEmployeeList(new ArrayList<>());
}
}
@Override
public void onNothingSelected(AdapterView<?> parent) {}
});
} }
/**
* Configures the date picker dialog for the adoption date field.
*/
private void setupDatePicker() { private void setupDatePicker() {
binding.etAdoptionDate.setOnClickListener(v -> { binding.etAdoptionDate.setOnClickListener(v -> UIUtils.showDatePicker(requireContext(), binding.etAdoptionDate, null));
Calendar c = Calendar.getInstance();
new DatePickerDialog(requireContext(),
(dp, y, m, d) -> binding.etAdoptionDate.setText(
String.format("%04d-%02d-%02d", y, m + 1, d)),
c.get(Calendar.YEAR),
c.get(Calendar.MONTH),
c.get(Calendar.DAY_OF_MONTH)).show();
});
} }
/**
* Fetches required data for spinners from the backend.
*/
private void loadSpinnersData() { private void loadSpinnersData() {
loadPets(); viewModel.loadPets().observe(getViewLifecycleOwner(), resource -> {
loadCustomers(); if (resource == null) return;
loadStores(); setLoading(resource.status == Resource.Status.LOADING);
loadEmployees();
}
/**
* Loads the list of pets from the API.
*/
private void loadPets() {
petViewModel.getAllPets(0, 200, null, null, null, null, "petName").observe(getViewLifecycleOwner(), resource -> {
if (resource.status == Resource.Status.SUCCESS && resource.data != null) { if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
petList = resource.data.getContent(); viewModel.setPetList(resource.data);
refreshPetSpinner(); }
});
viewModel.loadCustomers().observe(getViewLifecycleOwner(), resource -> {
if (resource == null) return;
setLoading(resource.status == Resource.Status.LOADING);
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
viewModel.setCustomerList(resource.data);
}
});
viewModel.loadStores().observe(getViewLifecycleOwner(), resource -> {
if (resource == null) return;
setLoading(resource.status == Resource.Status.LOADING);
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
viewModel.setStoreList(resource.data);
} }
}); });
} }
/**
* Populates the pet selection spinner with data.
*/
private void refreshPetSpinner() { private void refreshPetSpinner() {
SpinnerUtils.populateSpinner(requireContext(), binding.spinnerAdoptionPet, petList, SpinnerUtils.populateSpinner(requireContext(), binding.spinnerAdoptionPet, viewModel.getPetList().getValue(),
PetDTO::getPetName, "-- Select Pet --", DropdownDTO::getLabel, "-- Select Pet --",
preselectedPetId, PetDTO::getPetId); preselectedPetId, DropdownDTO::getId);
} }
/**
* Loads the list of customers from the API.
*/
private void loadCustomers() {
customerViewModel.getAllCustomers(0, 200).observe(getViewLifecycleOwner(), resource -> {
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
customerList = resource.data.getContent();
refreshCustomerSpinner();
}
});
}
/**
* Populates the customer selection spinner with data.
*/
private void refreshCustomerSpinner() { private void refreshCustomerSpinner() {
SpinnerUtils.populateSpinner(requireContext(), binding.spinnerAdoptionCustomer, customerList, SpinnerUtils.populateSpinner(requireContext(), binding.spinnerAdoptionCustomer, viewModel.getCustomerList().getValue(),
item -> item.getFirstName() + " " + item.getLastName(), DropdownDTO::getLabel, "-- Select Customer --",
"-- Select Customer --", preselectedCustomerId, DropdownDTO::getId);
preselectedCustomerId, CustomerDTO::getCustomerId);
} }
/**
* Loads the list of stores from the API.
*/
private void loadStores() {
storeViewModel.getAllStores(0, 200).observe(getViewLifecycleOwner(), resource -> {
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
storeList = resource.data.getContent();
refreshStoreSpinner();
}
});
}
/**
* Populates the store selection spinner with data.
*/
private void refreshStoreSpinner() { private void refreshStoreSpinner() {
SpinnerUtils.populateSpinner(requireContext(), binding.spinnerAdoptionStore, storeList, SpinnerUtils.populateSpinner(requireContext(), binding.spinnerAdoptionStore, viewModel.getStoreList().getValue(),
StoreDTO::getStoreName, "-- Select Store --", DropdownDTO::getLabel, "-- Select Store --",
preselectedStoreId, StoreDTO::getStoreId); preselectedStoreId, DropdownDTO::getId);
} }
/** private void loadEmployees(Long storeId) {
* Loads the list of employees from the API. viewModel.loadEmployees(storeId).observe(getViewLifecycleOwner(), resource -> {
*/ if (resource == null) return;
private void loadEmployees() { setLoading(resource.status == Resource.Status.LOADING);
userViewModel.getUsers("STAFF", 0, 100).observe(getViewLifecycleOwner(), resource -> {
if (resource.status == Resource.Status.SUCCESS && resource.data != null) { if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
employeeList = resource.data.getContent(); viewModel.setEmployeeList(resource.data);
refreshEmployeeSpinner();
} }
}); });
} }
/**
* Populates the employee selection spinner with data.
*/
private void refreshEmployeeSpinner() { private void refreshEmployeeSpinner() {
SpinnerUtils.populateSpinner(requireContext(), binding.spinnerAdoptionEmployee, employeeList, SpinnerUtils.populateSpinner(requireContext(), binding.spinnerAdoptionEmployee, viewModel.getEmployeeList().getValue(),
UserDTO::getFullName, "-- Select Staff --", DropdownDTO::getLabel, "-- Select Staff --",
preselectedEmployeeId, UserDTO::getId); preselectedEmployeeId, DropdownDTO::getId);
} }
/**
* Handles arguments to determine if the fragment is in edit or add mode.
*/
private void handleArguments() { private void handleArguments() {
Bundle a = getArguments(); Bundle a = getArguments();
if (a != null && a.containsKey("adoptionId")) { if (a != null && a.containsKey("adoptionId")) {
isEditing = true; long adoptionId = a.getLong("adoptionId");
adoptionId = a.getLong("adoptionId"); viewModel.setAdoptionId(adoptionId);
binding.tvAdoptionMode.setText("Edit Adoption"); binding.tvAdoptionMode.setText("Edit Adoption");
binding.tvAdoptionId.setText("ID: " + adoptionId); binding.tvAdoptionId.setText(DateTimeUtils.formatId(adoptionId));
binding.tvAdoptionId.setVisibility(View.VISIBLE); binding.tvAdoptionId.setVisibility(View.VISIBLE);
binding.btnDeleteAdoption.setVisibility(View.VISIBLE); binding.btnDeleteAdoption.setVisibility(View.VISIBLE);
loadAdoptionData(); loadAdoptionData();
} else { } else {
viewModel.setAdoptionId(-1);
binding.tvAdoptionMode.setText("Add Adoption"); binding.tvAdoptionMode.setText("Add Adoption");
binding.btnDeleteAdoption.setVisibility(View.GONE); binding.btnDeleteAdoption.setVisibility(View.GONE);
binding.tvAdoptionId.setVisibility(View.GONE); binding.tvAdoptionId.setVisibility(View.GONE);
UIUtils.setViewsEnabled(false, binding.spinnerAdoptionPet);
} }
} }
/**
* Fetches specific adoption details from the backend using the ID.
*/
private void loadAdoptionData() { private void loadAdoptionData() {
adoptionViewModel.getAdoptionById(adoptionId).observe(getViewLifecycleOwner(), resource -> { viewModel.loadAdoption().observe(getViewLifecycleOwner(), resource -> {
if (resource == null) return; if (resource == null) return;
setLoading(resource.status == Resource.Status.LOADING);
if (resource.status == Resource.Status.SUCCESS && resource.data != null) { if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
AdoptionDTO a = resource.data; AdoptionDTO a = resource.data;
preselectedPetId = a.getPetId() != null ? a.getPetId() : -1; preselectedPetId = a.getPetId() != null ? a.getPetId() : -1;
@@ -247,90 +221,68 @@ public class AdoptionDetailFragment extends Fragment {
refreshPetSpinner(); refreshPetSpinner();
refreshCustomerSpinner(); refreshCustomerSpinner();
refreshStoreSpinner(); refreshStoreSpinner();
refreshEmployeeSpinner();
if (preselectedCustomerId != -1) {
UIUtils.setViewsEnabled(true, binding.spinnerAdoptionPet);
}
} else if (resource.status == Resource.Status.ERROR) { } else if (resource.status == Resource.Status.ERROR) {
Toast.makeText(getContext(), "Failed to load adoption: " + resource.message, Toast.LENGTH_SHORT).show(); Toast.makeText(getContext(), "Failed to load adoption: " + resource.message, Toast.LENGTH_SHORT).show();
} }
}); });
} }
/**
* Validates input and saves the adoption request to the backend.
*/
private void saveAdoption() { private void saveAdoption() {
if (binding.spinnerAdoptionCustomer.getSelectedItemPosition() == 0) { if (!InputValidator.isSpinnerSelected(binding.spinnerAdoptionCustomer, "Customer")) return;
Toast.makeText(getContext(), "Select a customer", Toast.LENGTH_SHORT).show(); return; if (!InputValidator.isSpinnerSelected(binding.spinnerAdoptionPet, "Pet")) return;
} if (!InputValidator.isSpinnerSelected(binding.spinnerAdoptionStore, "Store")) return;
if (binding.spinnerAdoptionPet.getSelectedItemPosition() == 0) { if (!InputValidator.isNotEmpty(binding.etAdoptionDate, "Adoption Date")) return;
Toast.makeText(getContext(), "Select a pet", Toast.LENGTH_SHORT).show(); return;
}
if (binding.spinnerAdoptionStore.getSelectedItemPosition() == 0) {
Toast.makeText(getContext(), "Select a store", Toast.LENGTH_SHORT).show(); return;
}
String date = binding.etAdoptionDate.getText().toString().trim();
if (date.isEmpty()) {
Toast.makeText(getContext(), "Select a date", Toast.LENGTH_SHORT).show(); return;
}
BigDecimal fee = BigDecimal.ZERO; BigDecimal fee = BigDecimal.ZERO;
String feeStr = binding.etAdoptionFee.getText().toString().trim(); String feeStr = binding.etAdoptionFee.getText().toString().trim();
if (!feeStr.isEmpty()) { if (!feeStr.isEmpty()) {
try { if (!InputValidator.isPositiveDecimal(binding.etAdoptionFee, "Adoption Fee")) return;
fee = new BigDecimal(feeStr); fee = new BigDecimal(feeStr);
} catch (NumberFormatException e) {
Toast.makeText(getContext(), "Invalid fee format", Toast.LENGTH_SHORT).show();
return;
}
} }
CustomerDTO customer = customerList.get(binding.spinnerAdoptionCustomer.getSelectedItemPosition() - 1); DropdownDTO customer = viewModel.getCustomerList().getValue().get(binding.spinnerAdoptionCustomer.getSelectedItemPosition() - 1);
PetDTO pet = petList.get(binding.spinnerAdoptionPet.getSelectedItemPosition() - 1); DropdownDTO pet = viewModel.getPetList().getValue().get(binding.spinnerAdoptionPet.getSelectedItemPosition() - 1);
StoreDTO store = storeList.get(binding.spinnerAdoptionStore.getSelectedItemPosition() - 1); DropdownDTO store = viewModel.getStoreList().getValue().get(binding.spinnerAdoptionStore.getSelectedItemPosition() - 1);
Long employeeId = null; Long employeeId = null;
if (binding.spinnerAdoptionEmployee.getSelectedItemPosition() > 0) { if (binding.spinnerAdoptionEmployee.getSelectedItemPosition() > 0) {
employeeId = employeeList.get(binding.spinnerAdoptionEmployee.getSelectedItemPosition() - 1).getId(); employeeId = viewModel.getEmployeeList().getValue().get(binding.spinnerAdoptionEmployee.getSelectedItemPosition() - 1).getId();
} }
String adoptionDate = binding.etAdoptionDate.getText().toString().trim();
String status = STATUSES[binding.spinnerAdoptionStatus.getSelectedItemPosition()]; String status = STATUSES[binding.spinnerAdoptionStatus.getSelectedItemPosition()];
AdoptionDTO dto = new AdoptionDTO( AdoptionDTO dto = new AdoptionDTO(
pet.getPetId(), pet.getId(),
customer.getCustomerId(), customer.getId(),
employeeId, employeeId,
store.getStoreId(), store.getId(),
date, adoptionDate,
status, status,
fee fee
); );
if (isEditing) { viewModel.saveAdoption(dto).observe(getViewLifecycleOwner(), resource -> {
adoptionViewModel.updateAdoption(adoptionId, dto).observe(getViewLifecycleOwner(), resource -> { if (resource == null) return;
if (resource.status == Resource.Status.SUCCESS) { setLoading(resource.status == Resource.Status.LOADING);
Toast.makeText(getContext(), "Updated", Toast.LENGTH_SHORT).show(); if (resource.status == Resource.Status.SUCCESS) {
navigateBack(); Toast.makeText(getContext(), viewModel.isEditing() ? "Updated" : "Saved", Toast.LENGTH_SHORT).show();
} else if (resource.status == Resource.Status.ERROR) { navigateBack();
Toast.makeText(getContext(), "Error: " + resource.message, Toast.LENGTH_SHORT).show(); } else if (resource.status == Resource.Status.ERROR) {
} Toast.makeText(getContext(), "Error: " + resource.message, Toast.LENGTH_SHORT).show();
}); }
} else { });
adoptionViewModel.createAdoption(dto).observe(getViewLifecycleOwner(), resource -> {
if (resource.status == Resource.Status.SUCCESS) {
Toast.makeText(getContext(), "Saved", Toast.LENGTH_SHORT).show();
navigateBack();
} else if (resource.status == Resource.Status.ERROR) {
Toast.makeText(getContext(), "Error: " + resource.message, Toast.LENGTH_SHORT).show();
}
});
}
} }
/**
* Shows a confirmation dialog before deleting an adoption request.
*/
private void confirmDelete() { private void confirmDelete() {
DialogUtils.showDeleteConfirmDialog(requireContext(), "Adoption", () -> DialogUtils.showDeleteConfirmDialog(requireContext(), "Adoption Record", () ->
adoptionViewModel.deleteAdoption(adoptionId).observe(getViewLifecycleOwner(), resource -> { viewModel.deleteAdoption().observe(getViewLifecycleOwner(), resource -> {
if (resource == null) return;
setLoading(resource.status == Resource.Status.LOADING);
if (resource.status == Resource.Status.SUCCESS) { if (resource.status == Resource.Status.SUCCESS) {
Toast.makeText(getContext(), "Deleted", Toast.LENGTH_SHORT).show(); Toast.makeText(getContext(), "Deleted", Toast.LENGTH_SHORT).show();
navigateBack(); navigateBack();
@@ -340,9 +292,6 @@ public class AdoptionDetailFragment extends Fragment {
})); }));
} }
/**
* Navigates back to the previous fragment.
*/
private void navigateBack() { private void navigateBack() {
NavHostFragment.findNavController(this).popBackStack(); NavHostFragment.findNavController(this).popBackStack();
} }

View File

@@ -1,10 +1,12 @@
package com.example.petstoremobile.fragments.listfragments.detailfragments; package com.example.petstoremobile.fragments.listfragments.detailfragments;
import android.app.DatePickerDialog;
import android.os.Bundle; import android.os.Bundle;
import android.util.Log; import android.view.LayoutInflater;
import android.view.*; import android.view.View;
import android.widget.*; import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.Toast;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment; import androidx.fragment.app.Fragment;
@@ -12,18 +14,16 @@ import androidx.lifecycle.ViewModelProvider;
import androidx.navigation.fragment.NavHostFragment; import androidx.navigation.fragment.NavHostFragment;
import com.example.petstoremobile.databinding.FragmentAppointmentDetailBinding; import com.example.petstoremobile.databinding.FragmentAppointmentDetailBinding;
import com.example.petstoremobile.dtos.*; import com.example.petstoremobile.dtos.AppointmentDTO;
import com.example.petstoremobile.dtos.DropdownDTO;
import com.example.petstoremobile.dtos.ServiceDTO;
import com.example.petstoremobile.utils.DateTimeUtils;
import com.example.petstoremobile.utils.DialogUtils; import com.example.petstoremobile.utils.DialogUtils;
import com.example.petstoremobile.utils.InputValidator;
import com.example.petstoremobile.utils.Resource; import com.example.petstoremobile.utils.Resource;
import com.example.petstoremobile.utils.SpinnerUtils; import com.example.petstoremobile.utils.SpinnerUtils;
import com.example.petstoremobile.viewmodels.AppointmentViewModel; import com.example.petstoremobile.utils.UIUtils;
import com.example.petstoremobile.viewmodels.CustomerViewModel; import com.example.petstoremobile.viewmodels.AppointmentDetailViewModel;
import com.example.petstoremobile.viewmodels.PetViewModel;
import com.example.petstoremobile.viewmodels.ServiceViewModel;
import com.example.petstoremobile.viewmodels.StoreViewModel;
import com.example.petstoremobile.viewmodels.UserViewModel;
import java.util.*;
import dagger.hilt.android.AndroidEntryPoint; import dagger.hilt.android.AndroidEntryPoint;
@@ -35,39 +35,22 @@ public class AppointmentDetailFragment extends Fragment {
private FragmentAppointmentDetailBinding binding; private FragmentAppointmentDetailBinding binding;
private long appointmentId = -1;
private boolean isEditing = false;
private long preselectedPetId = -1; private long preselectedPetId = -1;
private long preselectedServiceId = -1; private long preselectedServiceId = -1;
private long preselectedCustomerId = -1; private long preselectedCustomerId = -1;
private long preselectedStoreId = -1; private long preselectedStoreId = -1;
private long preselectedStaffId = -1; private long preselectedStaffId = -1;
private List<PetDTO> petList = new ArrayList<>(); private final Integer[] HOURS = {9, 10, 11, 12, 13, 14, 15, 16, 17};
private List<ServiceDTO> serviceList = new ArrayList<>(); private final Integer[] MINUTES = {0, 15, 30, 45};
private List<CustomerDTO> customerList = new ArrayList<>();
private List<StoreDTO> storeList = new ArrayList<>();
private List<UserDTO> staffList = new ArrayList<>();
private final Integer[] HOURS = {9,10,11,12,13,14,15,16,17}; private AppointmentDetailViewModel viewModel;
private final Integer[] MINUTES = {0,15,30,45}; private boolean isUpdatingUI = false;
private AppointmentViewModel appointmentViewModel;
private PetViewModel petViewModel;
private ServiceViewModel serviceViewModel;
private StoreViewModel storeViewModel;
private CustomerViewModel customerViewModel;
private UserViewModel userViewModel;
@Override @Override
public void onCreate(Bundle savedInstanceState) { public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
appointmentViewModel = new ViewModelProvider(this).get(AppointmentViewModel.class); viewModel = new ViewModelProvider(this).get(AppointmentDetailViewModel.class);
petViewModel = new ViewModelProvider(this).get(PetViewModel.class);
serviceViewModel = new ViewModelProvider(this).get(ServiceViewModel.class);
storeViewModel = new ViewModelProvider(this).get(StoreViewModel.class);
customerViewModel = new ViewModelProvider(this).get(CustomerViewModel.class);
userViewModel = new ViewModelProvider(this).get(UserViewModel.class);
} }
@Override @Override
@@ -81,7 +64,8 @@ public class AppointmentDetailFragment extends Fragment {
super.onViewCreated(view, savedInstanceState); super.onViewCreated(view, savedInstanceState);
setupSpinners(); setupSpinners();
setupDatePicker(); setupDatePicker();
loadSpinnersData(); observeViewModel();
viewModel.loadInitialFormData();
handleArguments(); handleArguments();
binding.btnApptBack.setOnClickListener(v -> navigateBack()); binding.btnApptBack.setOnClickListener(v -> navigateBack());
@@ -95,365 +79,214 @@ public class AppointmentDetailFragment extends Fragment {
binding = null; binding = null;
} }
/**
* Configures the adapters for spinners.
*/
private void setupSpinners() { private void setupSpinners() {
SpinnerUtils.setupStringSpinner(requireContext(), binding.spinnerAppointmentStatus, SpinnerUtils.setupStringSpinner(requireContext(), binding.spinnerAppointmentStatus, new String[]{});
new String[]{"Booked", "Completed", "Cancelled", "Missed"});
String[] hours = new String[HOURS.length]; String[] hours = new String[HOURS.length];
for (int i = 0; i < HOURS.length; i++) for (int i = 0; i < HOURS.length; i++)
hours[i] = String.format("%02d:00", HOURS[i]); hours[i] = DateTimeUtils.formatTime(HOURS[i], 0);
SpinnerUtils.setupStringSpinner(requireContext(), binding.spinnerHour, hours); SpinnerUtils.setupStringSpinner(requireContext(), binding.spinnerHour, hours);
SpinnerUtils.setupStringSpinner(requireContext(), binding.spinnerMinute, new String[]{"00","15","30","45"}); SpinnerUtils.setupStringSpinner(requireContext(), binding.spinnerMinute, new String[]{"00", "15", "30", "45"});
UIUtils.setViewsEnabled(false, binding.spinnerPet, binding.spinnerStaff);
binding.spinnerCustomer.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
@Override
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
viewModel.onCustomerSelected(position);
}
@Override
public void onNothingSelected(AdapterView<?> parent) {}
});
binding.spinnerStore.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
@Override
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
viewModel.onStoreSelected(position);
}
@Override
public void onNothingSelected(AdapterView<?> parent) {}
});
SpinnerUtils.setOnIndexSelectedListener(binding.spinnerService, p -> viewModel.onServiceSelected(p));
SpinnerUtils.setOnIndexSelectedListener(binding.spinnerPet, p -> viewModel.onPetSelected(p));
SpinnerUtils.setOnIndexSelectedListener(binding.spinnerStaff, p -> viewModel.onStaffSelected(p));
SpinnerUtils.setOnIndexSelectedListener(binding.spinnerHour, p -> notifyDateTimeStatusChange());
SpinnerUtils.setOnIndexSelectedListener(binding.spinnerMinute, p -> notifyDateTimeStatusChange());
SpinnerUtils.setOnIndexSelectedListener(binding.spinnerAppointmentStatus, p -> notifyDateTimeStatusChange());
} }
/**
* Configures the date picker dialog for the appointment date field.
*/
private void setupDatePicker() { private void setupDatePicker() {
binding.etAppointmentDate.setOnClickListener(v -> { binding.etAppointmentDate.setOnClickListener(v ->
Calendar c = Calendar.getInstance(); UIUtils.showDatePicker(requireContext(), binding.etAppointmentDate, this::notifyDateTimeStatusChange));
DatePickerDialog d = new DatePickerDialog(requireContext(),
(dp,y,m,d1) -> binding.etAppointmentDate.setText(
String.format("%04d-%02d-%02d", y, m+1, d1)),
c.get(Calendar.YEAR), c.get(Calendar.MONTH),
c.get(Calendar.DAY_OF_MONTH));
d.getDatePicker().setMinDate(System.currentTimeMillis() - 1000);
d.show();
});
} }
/** private void observeViewModel() {
* Fetches all required data for spinners from the backend. viewModel.getViewState().observe(getViewLifecycleOwner(), this::applyViewState);
*/
private void loadSpinnersData() { viewModel.getCustomers().observe(getViewLifecycleOwner(), list ->
loadPets(); SpinnerUtils.populateSpinner(requireContext(), binding.spinnerCustomer, list, DropdownDTO::getLabel, "-- Select Customer --", preselectedCustomerId, DropdownDTO::getId));
loadServices();
loadCustomers(); viewModel.getStores().observe(getViewLifecycleOwner(), list ->
loadStores(); SpinnerUtils.populateSpinner(requireContext(), binding.spinnerStore, list, DropdownDTO::getLabel, "-- Select Store --", preselectedStoreId, DropdownDTO::getId));
loadStaff();
viewModel.getServices().observe(getViewLifecycleOwner(), list ->
SpinnerUtils.populateSpinner(requireContext(), binding.spinnerService, list, ServiceDTO::getServiceName, "-- Select Service --", preselectedServiceId, ServiceDTO::getServiceId));
viewModel.getCustomerPets().observe(getViewLifecycleOwner(), list ->
SpinnerUtils.populateSpinner(requireContext(), binding.spinnerPet, list, DropdownDTO::getLabel, "-- Select Pet --", preselectedPetId, DropdownDTO::getId));
viewModel.getStoreEmployees().observe(getViewLifecycleOwner(), list ->
SpinnerUtils.populateSpinner(requireContext(), binding.spinnerStaff, list, DropdownDTO::getLabel, "-- Select Staff --", preselectedStaffId, DropdownDTO::getId));
} }
/** private void setLoading(boolean loading) {
* Loads the list of pets from the ViewModel. if (binding != null && binding.progressBar != null) {
*/ binding.progressBar.setVisibility(loading ? View.VISIBLE : View.GONE);
private void loadPets() { }
petViewModel.getAllPets(0, 200, null, null, null, null, "petName").observe(getViewLifecycleOwner(), resource -> {
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
petList = resource.data.getContent();
refreshPetSpinner();
}
});
} }
/** private void applyViewState(AppointmentDetailViewModel.ViewState state) {
* Populates the pet selection spinner. isUpdatingUI = true;
*/
private void refreshPetSpinner() { binding.tvApptMode.setText(state.isEditing ? "Edit Appointment" : "Add Appointment");
SpinnerUtils.populateSpinner(requireContext(), binding.spinnerPet, petList, binding.tvAppointmentId.setText(DateTimeUtils.formatId(viewModel.getAppointmentId()));
PetDTO::getPetName, "-- Select Pet --", binding.tvAppointmentId.setVisibility(state.isEditing ? View.VISIBLE : View.GONE);
preselectedPetId, PetDTO::getPetId); binding.btnDeleteAppointment.setVisibility(state.isDeleteVisible ? View.VISIBLE : View.GONE);
binding.btnSaveAppointment.setVisibility(state.isSaveVisible ? View.VISIBLE : View.GONE);
UIUtils.setFieldEnabled(state.isCustomerEnabled, binding.spinnerCustomer, binding.tvLabelCustomer);
UIUtils.setFieldEnabled(state.isStoreEnabled, binding.spinnerStore, binding.tvLabelStore);
UIUtils.setFieldEnabled(state.isPetEnabled, binding.spinnerPet, binding.tvLabelPet);
UIUtils.setFieldEnabled(state.isServiceEnabled, binding.spinnerService, binding.tvLabelService);
UIUtils.setFieldEnabled(state.isStaffEnabled, binding.spinnerStaff, binding.tvLabelStaff);
UIUtils.setFieldEnabled(state.isDateEnabled, binding.etAppointmentDate, binding.tvLabelDate);
UIUtils.setFieldEnabled(state.isTimeEnabled, binding.spinnerHour, binding.tvLabelTime);
UIUtils.setViewsEnabled(state.isTimeEnabled, binding.spinnerMinute);
UIUtils.setViewsEnabled(state.isStatusEnabled, binding.spinnerAppointmentStatus);
Object selected = binding.spinnerAppointmentStatus.getSelectedItem();
String current = selected != null ? selected.toString() : "";
SpinnerUtils.setupStringSpinner(requireContext(), binding.spinnerAppointmentStatus, state.availableStatuses);
SpinnerUtils.setSelectionByValue(binding.spinnerAppointmentStatus, current);
isUpdatingUI = false;
} }
/** private void notifyDateTimeStatusChange() {
* Loads the list of services from the API. if (isUpdatingUI) return;
*/
private void loadServices() { String date = binding.etAppointmentDate.getText().toString();
serviceViewModel.getAllServices(0, 200, null, "serviceName").observe(getViewLifecycleOwner(), resource -> { String time = buildTimeString();
if (resource.status == Resource.Status.SUCCESS && resource.data != null) { Object selected = binding.spinnerAppointmentStatus.getSelectedItem();
serviceList = resource.data.getContent(); String status = selected != null ? selected.toString() : "";
refreshServiceSpinner(); viewModel.onDateOrTimeChanged(date, time, status);
}
});
} }
/**
* 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.
*/
private void loadCustomers() {
customerViewModel.getAllCustomers(0, 200).observe(getViewLifecycleOwner(), resource -> {
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
customerList = resource.data.getContent();
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.
*/
private void loadStores() {
storeViewModel.getAllStores(0, 50).observe(getViewLifecycleOwner(), resource -> {
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
storeList = resource.data.getContent();
refreshStoreSpinner();
}
});
}
/**
* Populates the store selection spinner.
*/
private void refreshStoreSpinner() {
SpinnerUtils.populateSpinner(requireContext(), binding.spinnerStore, storeList,
StoreDTO::getStoreName, "-- Select Store --",
preselectedStoreId, StoreDTO::getStoreId);
}
/**
* Loads the list of staff from the API.
*/
private void loadStaff() {
userViewModel.getUsers("STAFF", 0, 100).observe(getViewLifecycleOwner(), resource -> {
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
staffList = resource.data.getContent();
refreshStaffSpinner();
}
});
}
/**
* Populates the staff selection spinner.
*/
private void refreshStaffSpinner() {
SpinnerUtils.populateSpinner(requireContext(), binding.spinnerStaff, staffList,
UserDTO::getFullName, "-- Select Staff --",
preselectedStaffId, UserDTO::getId);
}
/**
* Handles arguments to determine if the fragment is in edit or add mode.
*/
private void handleArguments() { private void handleArguments() {
Bundle a = getArguments(); Bundle a = getArguments();
if (a != null && a.containsKey("appointmentId")) { if (a != null && a.containsKey("appointmentId")) {
isEditing = true; viewModel.setAppointmentId(a.getLong("appointmentId"));
appointmentId = a.getLong("appointmentId");
binding.tvApptMode.setText("Edit Appointment");
binding.tvAppointmentId.setText("ID: " + appointmentId);
binding.tvAppointmentId.setVisibility(View.VISIBLE);
binding.btnDeleteAppointment.setVisibility(View.VISIBLE);
loadAppointmentData(); loadAppointmentData();
} else { } else {
binding.tvApptMode.setText("Add Appointment"); viewModel.setAppointmentId(-1);
binding.btnDeleteAppointment.setVisibility(View.GONE);
binding.tvAppointmentId.setVisibility(View.GONE);
} }
} }
/**
* Fetches specific appointment details from the backend using the ID.
*/
private void loadAppointmentData() { private void loadAppointmentData() {
appointmentViewModel.getAppointmentById(appointmentId).observe(getViewLifecycleOwner(), resource -> { viewModel.loadAppointment().observe(getViewLifecycleOwner(), resource -> {
if (resource == null) return; if (resource == null) return;
setLoading(resource.status == Resource.Status.LOADING);
if (resource.status == Resource.Status.SUCCESS && resource.data != null) { if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
AppointmentDTO a = resource.data; AppointmentDTO a = resource.data;
preselectedPetId = (a.getPetId() != null) ? a.getPetId() : -1; preselectedPetId = a.getPetId() != null ? a.getPetId() : -1;
preselectedServiceId = (a.getServiceId() != null) ? a.getServiceId() : -1; preselectedServiceId = a.getServiceId() != null ? a.getServiceId() : -1;
preselectedCustomerId = (a.getCustomerId() != null) ? a.getCustomerId() : -1; preselectedCustomerId = a.getCustomerId() != null ? a.getCustomerId() : -1;
preselectedStoreId = (a.getStoreId() != null) ? a.getStoreId() : -1; preselectedStoreId = a.getStoreId() != null ? a.getStoreId() : -1;
preselectedStaffId = (a.getEmployeeId() != null) ? a.getEmployeeId() : -1; preselectedStaffId = a.getEmployeeId() != null ? a.getEmployeeId() : -1;
binding.etAppointmentDate.setText(a.getAppointmentDate()); binding.etAppointmentDate.setText(a.getAppointmentDate());
parseAndSetTimeSpinners(a.getAppointmentTime() != null ? a.getAppointmentTime() : "09:00");
// 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) {}
}
// Match Title labels with backend values
String status = a.getAppointmentStatus(); String status = a.getAppointmentStatus();
if (status != null && !status.isEmpty()) { if (status != null && !status.isEmpty()) {
String formattedStatus = status.substring(0, 1).toUpperCase() + status.substring(1).toLowerCase(); SpinnerUtils.setSelectionByValue(binding.spinnerAppointmentStatus, DateTimeUtils.formatStatusFromBackend(status));
SpinnerUtils.setSelectionByValue(binding.spinnerAppointmentStatus, formattedStatus);
} }
notifyDateTimeStatusChange();
refreshPetSpinner();
refreshServiceSpinner();
refreshCustomerSpinner();
refreshStoreSpinner();
refreshStaffSpinner();
} 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.
*/
private void saveAppointment() { private void saveAppointment() {
if (binding.spinnerCustomer.getSelectedItemPosition() == 0) { if (!validateRequiredFields()) return;
Toast.makeText(getContext(), "Select a customer", Toast.LENGTH_SHORT).show(); return;
}
if (binding.spinnerStore.getSelectedItemPosition() == 0) {
Toast.makeText(getContext(), "Select a store", Toast.LENGTH_SHORT).show(); return;
}
if (binding.spinnerPet.getSelectedItemPosition() == 0) {
Toast.makeText(getContext(), "Select a pet", Toast.LENGTH_SHORT).show(); return;
}
if (binding.spinnerService.getSelectedItemPosition() == 0) {
Toast.makeText(getContext(), "Select a service", Toast.LENGTH_SHORT).show(); return;
}
String date = binding.etAppointmentDate.getText().toString().trim(); String date = binding.etAppointmentDate.getText().toString().trim();
if (date.isEmpty()) { String time = buildTimeString();
Toast.makeText(getContext(), "Select a date", Toast.LENGTH_SHORT).show(); return;
}
CustomerDTO customer = customerList.get(binding.spinnerCustomer.getSelectedItemPosition() - 1);
StoreDTO store = storeList.get(binding.spinnerStore.getSelectedItemPosition() - 1);
PetDTO pet = petList.get(binding.spinnerPet.getSelectedItemPosition() - 1);
ServiceDTO service = serviceList.get(binding.spinnerService.getSelectedItemPosition() - 1);
Long employeeId = null;
if (binding.spinnerStaff.getSelectedItemPosition() > 0) {
employeeId = staffList.get(binding.spinnerStaff.getSelectedItemPosition() - 1).getId();
}
String time = String.format("%02d:%02d",
HOURS[binding.spinnerHour.getSelectedItemPosition()],
MINUTES[binding.spinnerMinute.getSelectedItemPosition()]);
// Get status and convert to uppercase for backend
String status = binding.spinnerAppointmentStatus.getSelectedItem().toString().toUpperCase(); String status = binding.spinnerAppointmentStatus.getSelectedItem().toString().toUpperCase();
if (!viewModel.isValidFutureBooking(status, date, time)) {
// Validate future date+time if status is BOOKED DialogUtils.showInfoDialog(requireContext(), "Invalid Time", "Booked appointments must be in the future.");
if ("BOOKED".equalsIgnoreCase(status)) { return;
try {
String[] dateParts = date.split("-");
String[] timeParts = time.split(":");
Calendar selected = Calendar.getInstance();
selected.set(
Integer.parseInt(dateParts[0]),
Integer.parseInt(dateParts[1]) - 1,
Integer.parseInt(dateParts[2]),
Integer.parseInt(timeParts[0]),
Integer.parseInt(timeParts[1]),
0
);
if (selected.before(Calendar.getInstance())) {
DialogUtils.showInfoDialog(requireContext(), "Invalid Time",
"Booked appointments must be in the future. " +
"Please select a future date and time.");
return;
}
} catch (Exception e) {
Log.e("APPT_SAVE", "Date parse error: " + e.getMessage());
}
} }
// Build DTO with all required IDs viewModel.saveAppointment(date, time, status).observe(getViewLifecycleOwner(), resource -> {
AppointmentDTO dto = new AppointmentDTO( if (resource == null) return;
customer.getCustomerId(), setLoading(resource.status == Resource.Status.LOADING);
store.getStoreId(),
service.getServiceId(),
employeeId,
date,
time,
status,
pet.getPetId()
);
androidx.lifecycle.Observer<Resource<AppointmentDTO>> observer = resource -> {
if (resource.status == Resource.Status.SUCCESS) { if (resource.status == Resource.Status.SUCCESS) {
Toast.makeText(getContext(), isEditing ? "Updated" : "Saved", Toast.LENGTH_SHORT).show(); AppointmentDetailViewModel.ViewState state = viewModel.getViewState().getValue();
String message = (state != null && state.isEditing) ? "Updated" : "Saved";
Toast.makeText(getContext(), message, Toast.LENGTH_SHORT).show();
navigateBack(); navigateBack();
} else if (resource.status == Resource.Status.ERROR) { } else if (resource.status == Resource.Status.ERROR) {
handleSaveError(resource.message); handleSaveError(resource.message);
} }
}; });
}
if (isEditing) {
appointmentViewModel.updateAppointment(appointmentId, dto).observe(getViewLifecycleOwner(), observer); private boolean validateRequiredFields() {
} else { if (!InputValidator.isSpinnerSelected(binding.spinnerCustomer, "Customer")) return false;
appointmentViewModel.createAppointment(dto).observe(getViewLifecycleOwner(), observer); if (!InputValidator.isSpinnerSelected(binding.spinnerStore, "Store")) return false;
} if (!InputValidator.isSpinnerSelected(binding.spinnerPet, "Pet")) return false;
if (!InputValidator.isSpinnerSelected(binding.spinnerService, "Service")) return false;
if (!InputValidator.isNotEmpty(binding.etAppointmentDate, "Appointment Date")) return false;
return true;
}
private String buildTimeString() {
return DateTimeUtils.formatTime(HOURS[binding.spinnerHour.getSelectedItemPosition()], MINUTES[binding.spinnerMinute.getSelectedItemPosition()]);
} }
/**
* Handles errors that occur during the saving process.
*/
private void handleSaveError(String errorMessage) { private void handleSaveError(String errorMessage) {
if (errorMessage != null) { if (errorMessage != null && errorMessage.toLowerCase().contains("not available")) showNoAvailabilityDialog();
Log.e("APPT_SAVE", "Error: " + errorMessage); else Toast.makeText(getContext(), errorMessage != null ? errorMessage : "Error saving", Toast.LENGTH_SHORT).show();
if (errorMessage.toLowerCase().contains("future")) {
DialogUtils.showInfoDialog(requireContext(), "Invalid Date/Time",
"Booked appointments must be scheduled in the future.");
} else if (errorMessage.toLowerCase().contains("not available")) {
showNoAvailabilityDialog();
} else {
Toast.makeText(getContext(), errorMessage, Toast.LENGTH_SHORT).show();
}
} else {
Toast.makeText(getContext(), "Something went wrong", Toast.LENGTH_SHORT).show();
}
} }
/**
* Shows a specialized dialog when a time slot is not available.
*/
private void showNoAvailabilityDialog() { private void showNoAvailabilityDialog() {
new androidx.appcompat.app.AlertDialog.Builder(requireContext()) new androidx.appcompat.app.AlertDialog.Builder(requireContext())
.setTitle("No Availability") .setTitle("No Availability")
.setMessage("This time slot is already booked. Please choose a different time or date.") .setMessage("This time slot is already booked.")
.setPositiveButton("Change Time", (d, w) -> d.dismiss()) .setPositiveButton("Change Time", (d, w) -> d.dismiss())
.setNegativeButton("Cancel Booking", (d, w) -> navigateBack()) .setNegativeButton("Cancel Booking", (d, w) -> navigateBack()).show();
.setCancelable(false)
.show();
} }
/**
* Shows a confirmation dialog and handles the deletion of an appointment.
*/
private void confirmDelete() { private void confirmDelete() {
DialogUtils.showDeleteConfirmDialog(requireContext(), "Appointment", () -> DialogUtils.showDeleteConfirmDialog(requireContext(), "Appointment", () ->
appointmentViewModel.deleteAppointment(appointmentId).observe(getViewLifecycleOwner(), resource -> { viewModel.deleteAppointment().observe(getViewLifecycleOwner(), resource -> {
if (resource.status == Resource.Status.SUCCESS) { if (resource == null) return;
Toast.makeText(getContext(), "Deleted", Toast.LENGTH_SHORT).show(); setLoading(resource.status == Resource.Status.LOADING);
navigateBack(); if (resource.status == Resource.Status.SUCCESS) navigateBack();
} else if (resource.status == Resource.Status.ERROR) {
Toast.makeText(getContext(), "Delete failed", Toast.LENGTH_SHORT).show();
}
})); }));
} }
/**
* Navigates back to the previous screen.
*/
private void navigateBack() { private void navigateBack() {
NavHostFragment.findNavController(this).popBackStack(); NavHostFragment.findNavController(this).popBackStack();
} }
private void parseAndSetTimeSpinners(String time) {
int[] parsedTime = DateTimeUtils.parseTimeString(time);
if (parsedTime == null) return;
SpinnerUtils.setSelectionByValueArray(binding.spinnerHour, HOURS, parsedTime[0]);
SpinnerUtils.setSelectionByValueArray(binding.spinnerMinute, MINUTES, parsedTime[1]);
}
} }

View File

@@ -8,24 +8,20 @@ import android.widget.Toast;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.fragment.app.Fragment; import androidx.fragment.app.Fragment;
import androidx.lifecycle.ViewModelProvider; import androidx.lifecycle.ViewModelProvider;
import androidx.navigation.fragment.NavHostFragment; import androidx.navigation.fragment.NavHostFragment;
import com.example.petstoremobile.databinding.FragmentInventoryDetailBinding; import com.example.petstoremobile.databinding.FragmentInventoryDetailBinding;
import com.example.petstoremobile.dtos.DropdownDTO;
import com.example.petstoremobile.dtos.InventoryDTO; import com.example.petstoremobile.dtos.InventoryDTO;
import com.example.petstoremobile.dtos.ProductDTO; import com.example.petstoremobile.dtos.ProductDTO;
import com.example.petstoremobile.dtos.StoreDTO; import com.example.petstoremobile.utils.DialogUtils;
import com.example.petstoremobile.utils.InputValidator; import com.example.petstoremobile.utils.InputValidator;
import com.example.petstoremobile.utils.Resource; import com.example.petstoremobile.utils.Resource;
import com.example.petstoremobile.utils.SpinnerUtils; import com.example.petstoremobile.utils.SpinnerUtils;
import com.example.petstoremobile.viewmodels.InventoryViewModel; import com.example.petstoremobile.utils.UIUtils;
import com.example.petstoremobile.viewmodels.ProductViewModel; import com.example.petstoremobile.viewmodels.InventoryDetailViewModel;
import com.example.petstoremobile.viewmodels.StoreViewModel;
import java.util.ArrayList;
import java.util.List;
import dagger.hilt.android.AndroidEntryPoint; import dagger.hilt.android.AndroidEntryPoint;
@@ -36,33 +32,17 @@ import dagger.hilt.android.AndroidEntryPoint;
public class InventoryDetailFragment extends Fragment { public class InventoryDetailFragment extends Fragment {
private FragmentInventoryDetailBinding binding; private FragmentInventoryDetailBinding binding;
private InventoryDetailViewModel viewModel;
private InventoryViewModel inventoryViewModel;
private ProductViewModel productViewModel;
private StoreViewModel storeViewModel;
private boolean isEditing = false;
private long inventoryId = -1;
private long preselectedStoreId = -1; private long preselectedStoreId = -1;
private long preselectedProductId = -1; private long preselectedProductId = -1;
private List<StoreDTO> storeList = new ArrayList<>();
private List<ProductDTO> productList = new ArrayList<>();
/**
* Initializes the view models.
*/
@Override @Override
public void onCreate(Bundle savedInstanceState) { public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
inventoryViewModel = new ViewModelProvider(this).get(InventoryViewModel.class); viewModel = new ViewModelProvider(this).get(InventoryDetailViewModel.class);
productViewModel = new ViewModelProvider(this).get(ProductViewModel.class);
storeViewModel = new ViewModelProvider(this).get(StoreViewModel.class);
} }
/**
* Inflates the layout.
*/
@Override @Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) { Bundle savedInstanceState) {
@@ -70,13 +50,11 @@ public class InventoryDetailFragment extends Fragment {
return binding.getRoot(); return binding.getRoot();
} }
/**
* Sets up UI components after the view is created.
*/
@Override @Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState); super.onViewCreated(view, savedInstanceState);
observeViewModel();
loadSpinnersData(); loadSpinnersData();
handleArguments(); handleArguments();
@@ -85,64 +63,57 @@ public class InventoryDetailFragment extends Fragment {
binding.btnDeleteInventory.setOnClickListener(v -> confirmDelete()); binding.btnDeleteInventory.setOnClickListener(v -> confirmDelete());
} }
private void observeViewModel() {
viewModel.getStoreList().observe(getViewLifecycleOwner(), list -> refreshStoreSpinner());
viewModel.getProductList().observe(getViewLifecycleOwner(), list -> refreshProductSpinner());
}
private void setLoading(boolean loading) {
if (binding != null && binding.progressBar != null) {
binding.progressBar.setVisibility(loading ? View.VISIBLE : View.GONE);
}
}
@Override @Override
public void onDestroyView() { public void onDestroyView() {
super.onDestroyView(); super.onDestroyView();
binding = null; binding = null;
} }
/**
* Fetches required data for spinners from the backend.
*/
private void loadSpinnersData() { private void loadSpinnersData() {
loadStores(); viewModel.loadStores().observe(getViewLifecycleOwner(), resource -> {
loadProducts(); if (resource == null) return;
} setLoading(resource.status == Resource.Status.LOADING);
/**
* Loads the list of stores for the spinner.
*/
private void loadStores() {
storeViewModel.getAllStores(0, 100).observe(getViewLifecycleOwner(), resource -> {
if (resource.status == Resource.Status.SUCCESS && resource.data != null) { if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
storeList = resource.data.getContent(); viewModel.setStoreList(resource.data);
refreshStoreSpinner(); }
});
viewModel.loadProducts().observe(getViewLifecycleOwner(), resource -> {
if (resource == null) return;
setLoading(resource.status == Resource.Status.LOADING);
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
viewModel.setProductList(resource.data.getContent());
} }
}); });
} }
private void refreshStoreSpinner() { private void refreshStoreSpinner() {
SpinnerUtils.populateSpinner(requireContext(), binding.spinnerInventoryStore, storeList, SpinnerUtils.populateSpinner(requireContext(), binding.spinnerInventoryStore, viewModel.getStoreList().getValue(),
StoreDTO::getStoreName, "-- Select Store --", DropdownDTO::getLabel, "-- Select Store --",
preselectedStoreId, StoreDTO::getStoreId); preselectedStoreId, DropdownDTO::getId);
}
/**
* Loads the list of products for the spinner.
*/
private void loadProducts() {
productViewModel.getAllProducts(null, null, 0, 500, "prodName").observe(getViewLifecycleOwner(), resource -> {
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
productList = resource.data.getContent();
refreshProductSpinner();
}
});
} }
private void refreshProductSpinner() { private void refreshProductSpinner() {
SpinnerUtils.populateSpinner(requireContext(), binding.spinnerInventoryProduct, productList, SpinnerUtils.populateSpinner(requireContext(), binding.spinnerInventoryProduct, viewModel.getProductList().getValue(),
ProductDTO::getProdName, "-- Select Product --", ProductDTO::getProdName, "-- Select Product --",
preselectedProductId, ProductDTO::getProdId); preselectedProductId, ProductDTO::getProdId);
} }
/**
* Handles fragment arguments to determine if we are in edit or add mode.
*/
private void handleArguments() { private void handleArguments() {
Bundle args = getArguments(); Bundle args = getArguments();
if (args != null && args.containsKey("inventoryId")) { if (args != null && args.containsKey("inventoryId")) {
isEditing = true; long inventoryId = args.getLong("inventoryId");
inventoryId = args.getLong("inventoryId"); viewModel.setInventoryId(inventoryId);
binding.tvInventoryMode.setText("Edit Inventory"); binding.tvInventoryMode.setText("Edit Inventory");
binding.tvInventoryId.setText("Inventory ID: " + inventoryId); binding.tvInventoryId.setText("Inventory ID: " + inventoryId);
@@ -152,7 +123,7 @@ public class InventoryDetailFragment extends Fragment {
loadInventoryData(); loadInventoryData();
} else { } else {
isEditing = false; viewModel.setInventoryId(-1);
binding.tvInventoryMode.setText("Add Inventory"); binding.tvInventoryMode.setText("Add Inventory");
binding.tvInventoryId.setVisibility(View.GONE); binding.tvInventoryId.setVisibility(View.GONE);
binding.btnDeleteInventory.setVisibility(View.GONE); binding.btnDeleteInventory.setVisibility(View.GONE);
@@ -160,12 +131,10 @@ public class InventoryDetailFragment extends Fragment {
} }
} }
/**
* Loads existing inventory data from the backend.
*/
private void loadInventoryData() { private void loadInventoryData() {
inventoryViewModel.getInventoryById(inventoryId).observe(getViewLifecycleOwner(), resource -> { viewModel.loadInventory().observe(getViewLifecycleOwner(), resource -> {
if (resource == null) return; if (resource == null) return;
setLoading(resource.status == Resource.Status.LOADING);
if (resource.status == Resource.Status.SUCCESS && resource.data != null) { if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
InventoryDTO inv = resource.data; InventoryDTO inv = resource.data;
binding.etQuantity.setText(String.valueOf(inv.getQuantity())); binding.etQuantity.setText(String.valueOf(inv.getQuantity()));
@@ -180,95 +149,59 @@ public class InventoryDetailFragment extends Fragment {
}); });
} }
/**
* Validates input and saves the current inventory item details to the backend.
*/
private void saveInventory() { private void saveInventory() {
if (binding.spinnerInventoryStore.getSelectedItemPosition() == 0) { if (!InputValidator.isSpinnerSelected(binding.spinnerInventoryStore, "Store")) return;
Toast.makeText(getContext(), "Please select a store", Toast.LENGTH_SHORT).show(); if (!InputValidator.isSpinnerSelected(binding.spinnerInventoryProduct, "Product")) return;
return; if (!InputValidator.isPositiveInteger(binding.etQuantity, "Quantity")) return;
}
if (binding.spinnerInventoryProduct.getSelectedItemPosition() == 0) {
Toast.makeText(getContext(), "Please select a product", Toast.LENGTH_SHORT).show();
return;
}
if (!InputValidator.isNotEmpty(binding.etQuantity, "Quantity") ||
!InputValidator.isPositiveInteger(binding.etQuantity, "Quantity")) {
return;
}
int quantity = Integer.parseInt(binding.etQuantity.getText().toString().trim()); int quantity = Integer.parseInt(binding.etQuantity.getText().toString().trim());
StoreDTO store = storeList.get(binding.spinnerInventoryStore.getSelectedItemPosition() - 1); DropdownDTO store = viewModel.getStoreList().getValue().get(binding.spinnerInventoryStore.getSelectedItemPosition() - 1);
ProductDTO product = productList.get(binding.spinnerInventoryProduct.getSelectedItemPosition() - 1); ProductDTO product = viewModel.getProductList().getValue().get(binding.spinnerInventoryProduct.getSelectedItemPosition() - 1);
InventoryDTO request = new InventoryDTO(product.getProdId(), store.getStoreId(), quantity); InventoryDTO request = new InventoryDTO(product.getProdId(), store.getId(), quantity);
setButtonsEnabled(false); setButtonsEnabled(false);
if (isEditing) { viewModel.saveInventory(request).observe(getViewLifecycleOwner(), resource -> {
inventoryViewModel.updateInventory(inventoryId, request).observe(getViewLifecycleOwner(), resource -> { if (resource == null) return;
setLoading(resource.status == Resource.Status.LOADING);
if (resource.status != Resource.Status.LOADING) {
setButtonsEnabled(true); setButtonsEnabled(true);
if (resource.status == Resource.Status.SUCCESS) { if (resource.status == Resource.Status.SUCCESS) {
Toast.makeText(getContext(), "Inventory updated", Toast.LENGTH_SHORT).show(); Toast.makeText(getContext(), viewModel.isEditing() ? "Inventory updated" : "Inventory created", Toast.LENGTH_SHORT).show();
navigateBack(); navigateBack();
} else if (resource.status == Resource.Status.ERROR) { } else if (resource.status == Resource.Status.ERROR) {
Toast.makeText(getContext(), "Error: " + resource.message, Toast.LENGTH_SHORT).show(); Toast.makeText(getContext(), "Error: " + resource.message, Toast.LENGTH_SHORT).show();
} }
});
} else {
inventoryViewModel.createInventory(request).observe(getViewLifecycleOwner(), resource -> {
setButtonsEnabled(true);
if (resource.status == Resource.Status.SUCCESS) {
Toast.makeText(getContext(), "Inventory created", Toast.LENGTH_SHORT).show();
navigateBack();
} else if (resource.status == Resource.Status.ERROR) {
Toast.makeText(getContext(), "Error: " + resource.message, Toast.LENGTH_SHORT).show();
}
});
}
}
/**
* Shows a confirmation dialog before deleting an inventory item.
*/
private void confirmDelete() {
new AlertDialog.Builder(requireContext())
.setTitle("Delete inventory item?")
.setMessage("This cannot be undone.")
.setPositiveButton("Delete", (d, w) -> deleteInventory())
.setNegativeButton("Cancel", null)
.show();
}
/**
* Sends a request to the API to delete the inventory item.
*/
private void deleteInventory() {
setButtonsEnabled(false);
inventoryViewModel.deleteInventory(inventoryId).observe(getViewLifecycleOwner(), resource -> {
setButtonsEnabled(true);
if (resource.status == Resource.Status.SUCCESS) {
Toast.makeText(getContext(), "Inventory deleted", Toast.LENGTH_SHORT).show();
navigateBack();
} else if (resource.status == Resource.Status.ERROR) {
Toast.makeText(getContext(), "Delete failed: " + resource.message, Toast.LENGTH_SHORT).show();
} }
}); });
} }
/** private void confirmDelete() {
* Navigates back to the previous fragment. DialogUtils.showDeleteConfirmDialog(requireContext(), "Inventory Item", this::deleteInventory);
*/ }
private void deleteInventory() {
setButtonsEnabled(false);
viewModel.deleteInventory().observe(getViewLifecycleOwner(), resource -> {
if (resource == null) return;
setLoading(resource.status == Resource.Status.LOADING);
if (resource.status != Resource.Status.LOADING) {
setButtonsEnabled(true);
if (resource.status == Resource.Status.SUCCESS) {
Toast.makeText(getContext(), "Inventory deleted", Toast.LENGTH_SHORT).show();
navigateBack();
} else if (resource.status == Resource.Status.ERROR) {
Toast.makeText(getContext(), "Delete failed: " + resource.message, Toast.LENGTH_SHORT).show();
}
}
});
}
private void navigateBack() { private void navigateBack() {
NavHostFragment.findNavController(this).popBackStack(); NavHostFragment.findNavController(this).popBackStack();
} }
/**
* Enables or disables action buttons.
*/
private void setButtonsEnabled(boolean enabled) { private void setButtonsEnabled(boolean enabled) {
binding.btnSaveInventory.setEnabled(enabled); UIUtils.setViewsEnabled(enabled, binding.btnSaveInventory, binding.btnDeleteInventory, binding.btnInventoryBack);
binding.btnDeleteInventory.setEnabled(enabled);
binding.btnInventoryBack.setEnabled(enabled);
} }
} }

View File

@@ -18,20 +18,17 @@ import android.widget.Toast;
import com.example.petstoremobile.R; import com.example.petstoremobile.R;
import com.example.petstoremobile.databinding.FragmentPetDetailBinding; import com.example.petstoremobile.databinding.FragmentPetDetailBinding;
import com.example.petstoremobile.dtos.CustomerDTO; import com.example.petstoremobile.dtos.DropdownDTO;
import com.example.petstoremobile.dtos.PetDTO; import com.example.petstoremobile.dtos.PetDTO;
import com.example.petstoremobile.dtos.StoreDTO;
import com.example.petstoremobile.utils.ActivityLogger; import com.example.petstoremobile.utils.ActivityLogger;
import com.example.petstoremobile.utils.DateTimeUtils;
import com.example.petstoremobile.utils.DialogUtils; import com.example.petstoremobile.utils.DialogUtils;
import com.example.petstoremobile.utils.InputValidator; import com.example.petstoremobile.utils.InputValidator;
import com.example.petstoremobile.utils.Resource; import com.example.petstoremobile.utils.Resource;
import com.example.petstoremobile.utils.SpinnerUtils; import com.example.petstoremobile.utils.SpinnerUtils;
import com.example.petstoremobile.viewmodels.CustomerViewModel; import com.example.petstoremobile.utils.UIUtils;
import com.example.petstoremobile.viewmodels.PetViewModel; import com.example.petstoremobile.viewmodels.PetDetailViewModel;
import com.example.petstoremobile.viewmodels.StoreViewModel;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale; import java.util.Locale;
import dagger.hilt.android.AndroidEntryPoint; import dagger.hilt.android.AndroidEntryPoint;
@@ -43,23 +40,15 @@ import dagger.hilt.android.AndroidEntryPoint;
public class PetDetailFragment extends Fragment { public class PetDetailFragment extends Fragment {
private FragmentPetDetailBinding binding; private FragmentPetDetailBinding binding;
private long petId; private PetDetailViewModel viewModel;
private boolean isEditing = false;
private PetViewModel viewModel;
private CustomerViewModel customerViewModel;
private StoreViewModel storeViewModel;
private List<CustomerDTO> customerList = new ArrayList<>();
private List<StoreDTO> storeList = new ArrayList<>();
private Long selectedCustomerId = null; private Long selectedCustomerId = null;
private Long selectedStoreId = null; private Long selectedStoreId = null;
@Override @Override
public void onCreate(Bundle savedInstanceState) { public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
viewModel = new ViewModelProvider(this).get(PetViewModel.class); viewModel = new ViewModelProvider(this).get(PetDetailViewModel.class);
customerViewModel = new ViewModelProvider(this).get(CustomerViewModel.class);
storeViewModel = new ViewModelProvider(this).get(StoreViewModel.class);
} }
@Override @Override
@@ -74,34 +63,54 @@ public class PetDetailFragment extends Fragment {
super.onViewCreated(view, savedInstanceState); super.onViewCreated(view, savedInstanceState);
setupSpinner(); setupSpinner();
loadCustomers(); observeViewModel();
loadStores();
handleArguments(); handleArguments();
//set button click listeners
binding.btnBack.setOnClickListener(v -> navigateBack()); binding.btnBack.setOnClickListener(v -> navigateBack());
binding.btnSavePet.setOnClickListener(v -> savePet()); binding.btnSavePet.setOnClickListener(v -> savePet());
binding.btnDeletePet.setOnClickListener(v -> deletePet()); binding.btnDeletePet.setOnClickListener(v -> deletePet());
} }
private void observeViewModel() {
viewModel.getCustomerList().observe(getViewLifecycleOwner(), list -> updateCustomerSpinnerSelection());
viewModel.getStoreList().observe(getViewLifecycleOwner(), list -> updateStoreSpinnerSelection());
viewModel.loadCustomers().observe(getViewLifecycleOwner(), resource -> {
if (resource == null) return;
setLoading(resource.status == Resource.Status.LOADING);
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
viewModel.setCustomerList(resource.data);
}
});
viewModel.loadStores().observe(getViewLifecycleOwner(), resource -> {
if (resource == null) return;
setLoading(resource.status == Resource.Status.LOADING);
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
viewModel.setStoreList(resource.data);
}
});
}
private void setLoading(boolean loading) {
if (binding != null && binding.progressBar != null) {
binding.progressBar.setVisibility(loading ? View.VISIBLE : View.GONE);
}
}
@Override @Override
public void onDestroyView() { public void onDestroyView() {
super.onDestroyView(); super.onDestroyView();
binding = null; binding = null;
} }
/**
* Handles the saving of pet data (adding/updating).
*/
private void savePet() { private void savePet() {
// Validates all fields using InputValidator
if (!InputValidator.isNotEmpty(binding.etPetName, "Pet Name")) return; if (!InputValidator.isNotEmpty(binding.etPetName, "Pet Name")) return;
if (!InputValidator.isNotEmpty(binding.etPetSpecies, "Species")) return; if (!InputValidator.isNotEmpty(binding.etPetSpecies, "Species")) return;
if (!InputValidator.isNotEmpty(binding.etPetBreed, "Breed")) return; if (!InputValidator.isNotEmpty(binding.etPetBreed, "Breed")) return;
if (!InputValidator.isPositiveInteger(binding.etPetAge, "Age")) return; if (!InputValidator.isPositiveInteger(binding.etPetAge, "Age")) return;
if (!InputValidator.isPositiveDecimal(binding.etPetPrice, "Price")) return; if (!InputValidator.isPositiveDecimal(binding.etPetPrice, "Price")) return;
//get all the values from the fields
String name = binding.etPetName.getText().toString().trim(); String name = binding.etPetName.getText().toString().trim();
String species = binding.etPetSpecies.getText().toString().trim(); String species = binding.etPetSpecies.getText().toString().trim();
String breed = binding.etPetBreed.getText().toString().trim(); String breed = binding.etPetBreed.getText().toString().trim();
@@ -109,37 +118,27 @@ public class PetDetailFragment extends Fragment {
double price = Double.parseDouble(binding.etPetPrice.getText().toString().trim()); double price = Double.parseDouble(binding.etPetPrice.getText().toString().trim());
String status = binding.spinnerPetStatus.getSelectedItem().toString(); String status = binding.spinnerPetStatus.getSelectedItem().toString();
// Get selected customer
Long customerId = null; Long customerId = null;
int customerPos = binding.spinnerCustomer.getSelectedItemPosition(); if (binding.spinnerCustomer.getSelectedItemPosition() > 0) {
if (customerPos > 0) { // 0 means no customer for pet customerId = viewModel.getCustomerList().getValue().get(binding.spinnerCustomer.getSelectedItemPosition() - 1).getId();
customerId = customerList.get(customerPos - 1).getCustomerId();
} }
// Get selected store
Long storeId = null; Long storeId = null;
int storePos = binding.spinnerStore.getSelectedItemPosition(); if (binding.spinnerStore.getSelectedItemPosition() > 0) {
if (storePos > 0) { storeId = viewModel.getStoreList().getValue().get(binding.spinnerStore.getSelectedItemPosition() - 1).getId();
storeId = storeList.get(storePos - 1).getStoreId();
} }
// Validation: If status is Available, a store must be selected
if ("Available".equalsIgnoreCase(status)) { if ("Available".equalsIgnoreCase(status)) {
if (!InputValidator.isSpinnerSelected(binding.spinnerStore, "Store")) return; if (!InputValidator.isSpinnerSelected(binding.spinnerStore, "Store")) return;
} }
// Validation: If status is Owned, an owner must be selected
if ("Owned".equalsIgnoreCase(status)) { if ("Owned".equalsIgnoreCase(status)) {
if (!InputValidator.isSpinnerSelected(binding.spinnerCustomer, "Owner")) return; if (!InputValidator.isSpinnerSelected(binding.spinnerCustomer, "Owner")) return;
} }
// Validation: If status is Adopted, an owner and store must be selected
if ("Adopted".equalsIgnoreCase(status)) { if ("Adopted".equalsIgnoreCase(status)) {
if (!InputValidator.isSpinnerSelected(binding.spinnerCustomer, "Owner")) return; if (!InputValidator.isSpinnerSelected(binding.spinnerCustomer, "Owner")) return;
if (!InputValidator.isSpinnerSelected(binding.spinnerStore, "Store")) return; if (!InputValidator.isSpinnerSelected(binding.spinnerStore, "Store")) return;
} }
//create a pet object to send to the API
PetDTO petDTO = new PetDTO(); PetDTO petDTO = new PetDTO();
petDTO.setPetName(name); petDTO.setPetName(name);
petDTO.setPetSpecies(species); petDTO.setPetSpecies(species);
@@ -150,107 +149,74 @@ public class PetDetailFragment extends Fragment {
petDTO.setCustomerId(customerId); petDTO.setCustomerId(customerId);
petDTO.setStoreId(storeId); petDTO.setStoreId(storeId);
//check if the pet is being edited or added viewModel.savePet(petDTO).observe(getViewLifecycleOwner(), resource -> {
if (isEditing) { if (resource == null) return;
// Update existing pet setLoading(resource.status == Resource.Status.LOADING);
petDTO.setPetId(petId); if (resource.status == Resource.Status.SUCCESS) {
viewModel.updatePet(petId, petDTO).observe(getViewLifecycleOwner(), resource -> { if (viewModel.isEditing()) {
if (resource.status == Resource.Status.SUCCESS) { ActivityLogger.logChange(requireContext(), "Pet", "UPDATED", (int) viewModel.getPetId());
ActivityLogger.logChange(requireContext(), "Pet", "UPDATED", (int) petId);
Toast.makeText(getContext(), "Pet updated successfully!", Toast.LENGTH_SHORT).show(); Toast.makeText(getContext(), "Pet updated successfully!", Toast.LENGTH_SHORT).show();
navigateToPetList(); } else {
} else if (resource.status == Resource.Status.ERROR) {
Toast.makeText(getContext(), "Error: " + resource.message, Toast.LENGTH_SHORT).show();
}
});
} else {
// Add new pet
viewModel.createPet(petDTO).observe(getViewLifecycleOwner(), resource -> {
if (resource.status == Resource.Status.SUCCESS) {
ActivityLogger.log(requireContext(), "Added new Pet: " + name); ActivityLogger.log(requireContext(), "Added new Pet: " + name);
Toast.makeText(getContext(), "Pet added successfully!", Toast.LENGTH_SHORT).show(); Toast.makeText(getContext(), "Pet added successfully!", Toast.LENGTH_SHORT).show();
navigateToPetList();
} else if (resource.status == Resource.Status.ERROR) {
Toast.makeText(getContext(), "Error: " + resource.message, Toast.LENGTH_SHORT).show();
} }
}); navigateToPetList();
} } else if (resource.status == Resource.Status.ERROR) {
Toast.makeText(getContext(), "Error: " + resource.message, Toast.LENGTH_SHORT).show();
}
});
} }
/**
* Displays a confirmation dialog and handles the deletion of a pet.
*/
private void deletePet() { private void deletePet() {
DialogUtils.showDeleteConfirmDialog(requireContext(), "Pet", () -> DialogUtils.showDeleteConfirmDialog(requireContext(), "Pet", () -> {
viewModel.deletePet(petId).observe(getViewLifecycleOwner(), resource -> { viewModel.deletePet().observe(getViewLifecycleOwner(), resource -> {
if (resource == null) return;
setLoading(resource.status == Resource.Status.LOADING);
if (resource.status == Resource.Status.SUCCESS) { if (resource.status == Resource.Status.SUCCESS) {
ActivityLogger.logChange(requireContext(), "Pet", "DELETED", (int) petId); ActivityLogger.logChange(requireContext(), "Pet", "DELETED", (int) viewModel.getPetId());
Toast.makeText(getContext(), "Pet deleted successfully!", Toast.LENGTH_SHORT).show(); Toast.makeText(getContext(), "Pet deleted successfully!", Toast.LENGTH_SHORT).show();
navigateToPetList(); navigateToPetList();
} else if (resource.status == Resource.Status.ERROR) { } else if (resource.status == Resource.Status.ERROR) {
Toast.makeText(getContext(), "Delete failed: " + resource.message, Toast.LENGTH_SHORT).show(); Toast.makeText(getContext(), "Delete failed: " + resource.message, Toast.LENGTH_SHORT).show();
} }
})); });
});
} }
/**
* Navigates back to the pet list screen.
*/
private void navigateToPetList() { private void navigateToPetList() {
NavHostFragment.findNavController(this).popBackStack(R.id.nav_pet, false); NavHostFragment.findNavController(this).popBackStack(R.id.nav_pet, false);
} }
/**
* Navigates back to the previous screen.
*/
private void navigateBack() { private void navigateBack() {
NavHostFragment.findNavController(this).popBackStack(); NavHostFragment.findNavController(this).popBackStack();
} }
/**
* Handles arguments passed to the fragment to determine if it's in edit or add mode.
*/
private void handleArguments() { private void handleArguments() {
// Pet is being edited if the bundle contains a petId
if (getArguments() != null && getArguments().containsKey("petId")) { if (getArguments() != null && getArguments().containsKey("petId")) {
// Get pet data from arguments and populate fields long petId = getArguments().getLong("petId");
isEditing = true; viewModel.setPetId(petId);
petId = getArguments().getLong("petId");
binding.tvMode.setText("Edit Pet"); binding.tvMode.setText("Edit Pet");
binding.tvPetId.setText("ID: " + petId); binding.tvPetId.setText(DateTimeUtils.formatId(petId));
binding.tvPetId.setVisibility(View.VISIBLE); binding.tvPetId.setVisibility(View.VISIBLE);
binding.btnDeletePet.setVisibility(View.VISIBLE); binding.btnDeletePet.setVisibility(View.VISIBLE);
// Disable species and breed fields in edit mode UIUtils.setViewsEnabled(false, binding.etPetSpecies, binding.etPetBreed);
binding.etPetSpecies.setEnabled(false);
binding.etPetBreed.setEnabled(false);
binding.etPetSpecies.setAlpha(0.5f);
binding.etPetBreed.setAlpha(0.5f);
loadPetData(); loadPetData();
} else { } else {
// Pet is being added viewModel.setPetId(-1);
// Set default values for add a new pet
isEditing = false;
binding.tvMode.setText("Add Pet"); binding.tvMode.setText("Add Pet");
binding.tvPetId.setVisibility(View.GONE); binding.tvPetId.setVisibility(View.GONE);
binding.btnDeletePet.setVisibility(View.GONE); binding.btnDeletePet.setVisibility(View.GONE);
binding.btnSavePet.setText("Add"); binding.btnSavePet.setText("Add");
// Enable species and breed fields in edit mode UIUtils.setViewsEnabled(true, binding.etPetSpecies, binding.etPetBreed);
binding.etPetSpecies.setEnabled(true);
binding.etPetBreed.setEnabled(true);
binding.etPetSpecies.setAlpha(1.0f);
binding.etPetBreed.setAlpha(1.0f);
} }
} }
/**
* Fetches specific pet details from the backend using the ID.
*/
private void loadPetData() { private void loadPetData() {
viewModel.getPetById(petId).observe(getViewLifecycleOwner(), resource -> { viewModel.loadPet().observe(getViewLifecycleOwner(), resource -> {
if (resource == null) return; if (resource == null) return;
setLoading(resource.status == Resource.Status.LOADING);
if (resource.status == Resource.Status.SUCCESS && resource.data != null) { if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
PetDTO p = resource.data; PetDTO p = resource.data;
binding.etPetName.setText(p.getPetName()); binding.etPetName.setText(p.getPetName());
@@ -273,63 +239,30 @@ public class PetDetailFragment extends Fragment {
}); });
} }
/**
* Fetches the list of customers and populates the spinner.
*/
private void loadCustomers() {
customerViewModel.getAllCustomers(0, 1000).observe(getViewLifecycleOwner(), resource -> {
if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) {
customerList = resource.data.getContent();
updateCustomerSpinnerSelection();
}
});
}
/**
* Fetches the list of stores and populates the spinner.
*/
private void loadStores() {
storeViewModel.getAllStores(0, 1000).observe(getViewLifecycleOwner(), resource -> {
if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) {
storeList = resource.data.getContent();
updateStoreSpinnerSelection();
}
});
}
/**
* Updates the customer spinner with the current list and sets the selection if needed.
*/
private void updateCustomerSpinnerSelection() { private void updateCustomerSpinnerSelection() {
SpinnerUtils.populateSpinner( SpinnerUtils.populateSpinner(
requireContext(), requireContext(),
binding.spinnerCustomer, binding.spinnerCustomer,
customerList, viewModel.getCustomerList().getValue(),
CustomerDTO::getFullName, DropdownDTO::getLabel,
"No Owner", "No Owner",
selectedCustomerId, selectedCustomerId,
CustomerDTO::getCustomerId DropdownDTO::getId
); );
} }
/**
* Updates the store spinner with the current list and sets the selection if needed.
*/
private void updateStoreSpinnerSelection() { private void updateStoreSpinnerSelection() {
SpinnerUtils.populateSpinner( SpinnerUtils.populateSpinner(
requireContext(), requireContext(),
binding.spinnerStore, binding.spinnerStore,
storeList, viewModel.getStoreList().getValue(),
StoreDTO::getStoreName, DropdownDTO::getLabel,
"None", "None",
selectedStoreId, selectedStoreId,
StoreDTO::getStoreId DropdownDTO::getId
); );
} }
/**
* Initializes the spinner for pet status selection.
*/
private void setupSpinner() { private void setupSpinner() {
SpinnerUtils.setupStringSpinner(requireContext(), binding.spinnerPetStatus, SpinnerUtils.setupStringSpinner(requireContext(), binding.spinnerPetStatus,
new String[]{"Available", "Adopted", "Owned"}); new String[]{"Available", "Adopted", "Owned"});
@@ -339,28 +272,21 @@ public class PetDetailFragment extends Fragment {
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) { public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
String status = parent.getItemAtPosition(position).toString(); String status = parent.getItemAtPosition(position).toString();
// Clear any existing error icons when status changes
clearSpinnerError(binding.spinnerCustomer); clearSpinnerError(binding.spinnerCustomer);
clearSpinnerError(binding.spinnerStore); clearSpinnerError(binding.spinnerStore);
//Disable the customer spinner if the status is "Available"
if ("Available".equalsIgnoreCase(status)) { if ("Available".equalsIgnoreCase(status)) {
binding.spinnerCustomer.setSelection(0); binding.spinnerCustomer.setSelection(0);
binding.spinnerCustomer.setEnabled(false); UIUtils.setViewsEnabled(false, binding.spinnerCustomer);
binding.spinnerCustomer.setAlpha(0.5f);
} else { } else {
binding.spinnerCustomer.setEnabled(true); UIUtils.setViewsEnabled(true, binding.spinnerCustomer);
binding.spinnerCustomer.setAlpha(1.0f);
} }
//Disable the store spinner if the status is "Owned"
if ("Owned".equalsIgnoreCase(status)) { if ("Owned".equalsIgnoreCase(status)) {
binding.spinnerStore.setSelection(0); binding.spinnerStore.setSelection(0);
binding.spinnerStore.setEnabled(false); UIUtils.setViewsEnabled(false, binding.spinnerStore);
binding.spinnerStore.setAlpha(0.5f);
} else { } else {
binding.spinnerStore.setEnabled(true); UIUtils.setViewsEnabled(true, binding.spinnerStore);
binding.spinnerStore.setAlpha(1.0f);
} }
} }
@@ -370,9 +296,6 @@ public class PetDetailFragment extends Fragment {
}); });
} }
/**
* Clears error messages from a Spinner's selected view.
*/
private void clearSpinnerError(Spinner spinner) { private void clearSpinnerError(Spinner spinner) {
View selectedView = spinner.getSelectedView(); View selectedView = spinner.getSelectedView();
if (selectedView instanceof TextView) { if (selectedView instanceof TextView) {

View File

@@ -17,7 +17,8 @@ import com.example.petstoremobile.api.*;
import com.example.petstoremobile.api.auth.TokenManager; import com.example.petstoremobile.api.auth.TokenManager;
import com.example.petstoremobile.databinding.FragmentProductDetailBinding; import com.example.petstoremobile.databinding.FragmentProductDetailBinding;
import com.example.petstoremobile.dtos.*; import com.example.petstoremobile.dtos.*;
import com.example.petstoremobile.viewmodels.ProductViewModel; import com.example.petstoremobile.viewmodels.ProductDetailViewModel;
import com.example.petstoremobile.utils.DateTimeUtils;
import com.example.petstoremobile.utils.DialogUtils; import com.example.petstoremobile.utils.DialogUtils;
import com.example.petstoremobile.utils.FileUtils; import com.example.petstoremobile.utils.FileUtils;
import com.example.petstoremobile.utils.GlideUtils; import com.example.petstoremobile.utils.GlideUtils;
@@ -31,7 +32,6 @@ import java.math.BigDecimal;
import java.util.*; import java.util.*;
import javax.inject.Inject; import javax.inject.Inject;
import javax.inject.Named; import javax.inject.Named;
import dagger.hilt.android.AndroidEntryPoint; import dagger.hilt.android.AndroidEntryPoint;
@@ -46,29 +46,22 @@ import okhttp3.RequestBody;
public class ProductDetailFragment extends Fragment { public class ProductDetailFragment extends Fragment {
private FragmentProductDetailBinding binding; private FragmentProductDetailBinding binding;
private ProductDetailViewModel viewModel;
private ImagePickerHelper imagePickerHelper;
private long prodId = -1;
private boolean isEditing = false;
private long preselectedCategoryId = -1; private long preselectedCategoryId = -1;
private boolean hasImage = false; private boolean hasImage = false;
private boolean isImageChanged = false; private boolean isImageChanged = false;
private boolean isImageRemoved = false; private boolean isImageRemoved = false;
private List<CategoryDTO> categoryList = new ArrayList<>();
private Uri photoUri; private Uri photoUri;
private ProductViewModel viewModel;
private ImagePickerHelper imagePickerHelper;
@Inject @Named("baseUrl") String baseUrl; @Inject @Named("baseUrl") String baseUrl;
@Inject TokenManager tokenManager; @Inject TokenManager tokenManager;
/**
* Initializes activity launchers and the ImagePickerHelper.
*/
@Override @Override
public void onCreate(Bundle savedInstanceState) { public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
viewModel = new ViewModelProvider(this).get(ProductViewModel.class); viewModel = new ViewModelProvider(this).get(ProductDetailViewModel.class);
imagePickerHelper = new ImagePickerHelper(this, "product_photo.jpg", new ImagePickerHelper.ImagePickerListener() { imagePickerHelper = new ImagePickerHelper(this, "product_photo.jpg", new ImagePickerHelper.ImagePickerListener() {
@Override @Override
@@ -95,9 +88,6 @@ public class ProductDetailFragment extends Fragment {
}); });
} }
/**
* Inflates the layout.
*/
@Override @Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) { Bundle savedInstanceState) {
@@ -105,14 +95,11 @@ public class ProductDetailFragment extends Fragment {
return binding.getRoot(); return binding.getRoot();
} }
/**
* Sets up UI components and listeners after the view is created.
*/
@Override @Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState); super.onViewCreated(view, savedInstanceState);
loadCategories(); observeViewModel();
handleArguments(); handleArguments();
binding.btnProductBack.setOnClickListener(v -> navigateBack()); binding.btnProductBack.setOnClickListener(v -> navigateBack());
@@ -121,41 +108,49 @@ public class ProductDetailFragment extends Fragment {
binding.ivProductImage.setOnClickListener(v -> imagePickerHelper.showImagePickerDialog("Select Product Image", hasImage)); binding.ivProductImage.setOnClickListener(v -> imagePickerHelper.showImagePickerDialog("Select Product Image", hasImage));
} }
private void observeViewModel() {
viewModel.getCategoryList().observe(getViewLifecycleOwner(), list -> updateCategorySpinner());
viewModel.loadCategories().observe(getViewLifecycleOwner(), resource -> {
if (resource == null) return;
setLoading(resource.status == Resource.Status.LOADING);
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
viewModel.setCategoryList(resource.data.getContent());
}
});
}
private void setLoading(boolean loading) {
if (binding != null && binding.progressBar != null) {
binding.progressBar.setVisibility(loading ? View.VISIBLE : View.GONE);
}
}
private void updateCategorySpinner() {
SpinnerUtils.populateSpinner(requireContext(), binding.spinnerProductCategory, viewModel.getCategoryList().getValue(),
CategoryDTO::getCategoryName, "-- Select Category --",
preselectedCategoryId, CategoryDTO::getCategoryId);
}
@Override @Override
public void onDestroyView() { public void onDestroyView() {
super.onDestroyView(); super.onDestroyView();
binding = null; binding = null;
} }
/**
* Fetches all product categories for the selection spinner.
*/
private void loadCategories() {
viewModel.getAllCategories(0, 100).observe(getViewLifecycleOwner(), resource -> {
if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) {
categoryList = resource.data.getContent();
SpinnerUtils.populateSpinner(requireContext(), binding.spinnerProductCategory, categoryList,
CategoryDTO::getCategoryName, "-- Select Category --",
preselectedCategoryId, CategoryDTO::getCategoryId);
}
});
}
/**
* Checks if the fragment was opened with existing product data for editing.
*/
private void handleArguments() { private void handleArguments() {
Bundle a = getArguments(); Bundle a = getArguments();
if (a != null && a.containsKey("prodId")) { if (a != null && a.containsKey("prodId")) {
isEditing = true; long prodId = a.getLong("prodId");
prodId = a.getLong("prodId"); viewModel.setProdId(prodId);
binding.tvProductMode.setText("Edit Product"); binding.tvProductMode.setText("Edit Product");
binding.tvProductId.setText("ID: " + prodId); binding.tvProductId.setText(DateTimeUtils.formatId(prodId));
binding.tvProductId.setVisibility(View.VISIBLE); binding.tvProductId.setVisibility(View.VISIBLE);
binding.btnDeleteProduct.setVisibility(View.VISIBLE); binding.btnDeleteProduct.setVisibility(View.VISIBLE);
loadProductData(); loadProductData();
loadProductImage(); loadProductImage();
} else { } else {
viewModel.setProdId(-1);
binding.tvProductMode.setText("Add Product"); binding.tvProductMode.setText("Add Product");
binding.btnDeleteProduct.setVisibility(View.GONE); binding.btnDeleteProduct.setVisibility(View.GONE);
binding.tvProductId.setVisibility(View.GONE); binding.tvProductId.setVisibility(View.GONE);
@@ -163,36 +158,25 @@ public class ProductDetailFragment extends Fragment {
} }
} }
/**
* Loads the product data from the backend.
*/
private void loadProductData() { private void loadProductData() {
viewModel.getProductById(prodId).observe(getViewLifecycleOwner(), resource -> { viewModel.loadProduct().observe(getViewLifecycleOwner(), resource -> {
if (resource == null) return; if (resource == null) return;
setLoading(resource.status == Resource.Status.LOADING);
if (resource.status == Resource.Status.SUCCESS && resource.data != null) { if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
ProductDTO p = resource.data; ProductDTO p = resource.data;
binding.etProductName.setText(p.getProdName()); binding.etProductName.setText(p.getProdName());
binding.etProductDesc.setText(p.getProdDesc()); binding.etProductDesc.setText(p.getProdDesc());
binding.etProductPrice.setText(p.getProdPrice() != null ? p.getProdPrice().toString() : ""); binding.etProductPrice.setText(p.getProdPrice() != null ? p.getProdPrice().toString() : "");
preselectedCategoryId = p.getCategoryId() != null ? p.getCategoryId() : -1; preselectedCategoryId = p.getCategoryId() != null ? p.getCategoryId() : -1;
updateCategorySpinner();
// 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) { } else if (resource.status == Resource.Status.ERROR) {
Toast.makeText(getContext(), "Failed to load product: " + resource.message, Toast.LENGTH_SHORT).show(); Toast.makeText(getContext(), "Failed to load product: " + resource.message, Toast.LENGTH_SHORT).show();
} }
}); });
} }
/**
* Loads the product image from the backend.
*/
private void loadProductImage() { private void loadProductImage() {
String imageUrl = baseUrl + String.format(Locale.US, ProductApi.PRODUCT_IMAGE_PATH, prodId); String imageUrl = baseUrl + String.format(Locale.US, ProductApi.PRODUCT_IMAGE_PATH, viewModel.getProdId());
String token = tokenManager.getToken(); String token = tokenManager.getToken();
GlideUtils.loadImageWithToken(requireContext(), binding.ivProductImage, imageUrl, token, R.drawable.placeholder2, new GlideUtils.ImageLoadListener() { GlideUtils.loadImageWithToken(requireContext(), binding.ivProductImage, imageUrl, token, R.drawable.placeholder2, new GlideUtils.ImageLoadListener() {
@@ -208,13 +192,12 @@ public class ProductDetailFragment extends Fragment {
}); });
} }
/**
* Performs image related actions (upload/delete) after product details are saved.
*/
private void performPendingImageActions(String successMsg) { private void performPendingImageActions(String successMsg) {
if (isImageRemoved) { if (isImageRemoved) {
viewModel.deleteProductImage(prodId).observe(getViewLifecycleOwner(), resource -> { viewModel.deleteProductImage().observe(getViewLifecycleOwner(), resource -> {
if (resource != null && resource.status != Resource.Status.LOADING) { if (resource == null) return;
setLoading(resource.status == Resource.Status.LOADING);
if (resource.status != Resource.Status.LOADING) {
if (resource.status == Resource.Status.SUCCESS) { if (resource.status == Resource.Status.SUCCESS) {
Toast.makeText(getContext(), successMsg, Toast.LENGTH_SHORT).show(); Toast.makeText(getContext(), successMsg, Toast.LENGTH_SHORT).show();
} else { } else {
@@ -231,9 +214,6 @@ public class ProductDetailFragment extends Fragment {
} }
} }
/**
* Uploads the selected image file to the server.
*/
private void uploadProductImageAndNavigate(Uri uri, String successMsg) { private void uploadProductImageAndNavigate(Uri uri, String successMsg) {
File file = FileUtils.getFileFromUri(requireContext(), uri); File file = FileUtils.getFileFromUri(requireContext(), uri);
if (file == null) { if (file == null) {
@@ -245,8 +225,10 @@ public class ProductDetailFragment extends Fragment {
RequestBody requestFile = RequestBody.create(file, MediaType.parse(requireContext().getContentResolver().getType(uri))); RequestBody requestFile = RequestBody.create(file, MediaType.parse(requireContext().getContentResolver().getType(uri)));
MultipartBody.Part body = MultipartBody.Part.createFormData("image", file.getName(), requestFile); MultipartBody.Part body = MultipartBody.Part.createFormData("image", file.getName(), requestFile);
viewModel.uploadProductImage(prodId, body).observe(getViewLifecycleOwner(), resource -> { viewModel.uploadProductImage(body).observe(getViewLifecycleOwner(), resource -> {
if (resource != null && resource.status != Resource.Status.LOADING) { if (resource == null) return;
setLoading(resource.status == Resource.Status.LOADING);
if (resource.status != Resource.Status.LOADING) {
if (resource.status == Resource.Status.SUCCESS) { if (resource.status == Resource.Status.SUCCESS) {
Toast.makeText(getContext(), successMsg, Toast.LENGTH_SHORT).show(); Toast.makeText(getContext(), successMsg, Toast.LENGTH_SHORT).show();
} else { } else {
@@ -257,69 +239,47 @@ public class ProductDetailFragment extends Fragment {
}); });
} }
/**
* Validates input fields and saves product information to the backend.
*/
private void saveProduct() { private void saveProduct() {
if (!InputValidator.isNotEmpty(binding.etProductName, "Product Name")) return; if (!InputValidator.isNotEmpty(binding.etProductName, "Product Name")) return;
if (!InputValidator.isSpinnerSelected(binding.spinnerProductCategory, "Category")) return;
if (binding.spinnerProductCategory.getSelectedItemPosition() == 0) { if (!InputValidator.isPositiveDecimal(binding.etProductPrice, "Price")) return;
Toast.makeText(getContext(), "Select a category", Toast.LENGTH_SHORT).show(); return;
}
if (!InputValidator.isNotEmpty(binding.etProductPrice, "Price") ||
!InputValidator.isPositiveDecimal(binding.etProductPrice, "Price")) {
return;
}
String name = binding.etProductName.getText().toString().trim(); String name = binding.etProductName.getText().toString().trim();
String desc = binding.etProductDesc.getText().toString().trim(); String desc = binding.etProductDesc.getText().toString().trim();
BigDecimal price = new BigDecimal(binding.etProductPrice.getText().toString().trim()); BigDecimal price = new BigDecimal(binding.etProductPrice.getText().toString().trim());
CategoryDTO category = categoryList.get(binding.spinnerProductCategory.getSelectedItemPosition() - 1); CategoryDTO category = viewModel.getCategoryList().getValue().get(binding.spinnerProductCategory.getSelectedItemPosition() - 1);
ProductDTO dto = new ProductDTO(name, category.getCategoryId(), desc, price); ProductDTO dto = new ProductDTO(name, category.getCategoryId(), desc, price);
if (isEditing) { viewModel.saveProduct(dto).observe(getViewLifecycleOwner(), resource -> {
viewModel.updateProduct(prodId, dto).observe(getViewLifecycleOwner(), resource -> { if (resource == null) return;
if (resource != null && resource.status != Resource.Status.LOADING) { setLoading(resource.status == Resource.Status.LOADING);
if (resource.status == Resource.Status.SUCCESS) { if (resource.status != Resource.Status.LOADING) {
performPendingImageActions("Updated"); if (resource.status == Resource.Status.SUCCESS) {
} else { if (resource.data != null) {
Toast.makeText(getContext(), "Error: " + resource.message, Toast.LENGTH_SHORT).show(); viewModel.setProdId(resource.data.getProdId());
} }
performPendingImageActions(viewModel.isEditing() ? "Updated" : "Saved");
} else {
Toast.makeText(getContext(), "Error: " + resource.message, Toast.LENGTH_SHORT).show();
} }
}); }
} else { });
viewModel.createProduct(dto).observe(getViewLifecycleOwner(), resource -> {
if (resource != null && resource.status != Resource.Status.LOADING) {
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
prodId = resource.data.getProdId();
performPendingImageActions("Saved");
} else {
Toast.makeText(getContext(), "Error saving: " + resource.message, Toast.LENGTH_SHORT).show();
}
}
});
}
} }
/**
* Displays a confirmation dialog before deleting the product.
*/
private void confirmDelete() { private void confirmDelete() {
DialogUtils.showDeleteConfirmDialog(requireContext(), "Product", () -> DialogUtils.showDeleteConfirmDialog(requireContext(), "Product", () ->
viewModel.deleteProduct(prodId).observe(getViewLifecycleOwner(), resource -> { viewModel.deleteProduct().observe(getViewLifecycleOwner(), resource -> {
if (resource != null && resource.status == Resource.Status.SUCCESS) { if (resource == null) return;
setLoading(resource.status == Resource.Status.LOADING);
if (resource.status == Resource.Status.SUCCESS) {
navigateBack(); navigateBack();
} else if (resource != null && resource.status == Resource.Status.ERROR) { } else if (resource.status == Resource.Status.ERROR) {
Toast.makeText(getContext(), "Delete failed: " + resource.message, Toast.LENGTH_SHORT).show(); Toast.makeText(getContext(), "Delete failed: " + resource.message, Toast.LENGTH_SHORT).show();
} }
})); }));
} }
/**
* Navigates back to the previous fragment.
*/
private void navigateBack() { private void navigateBack() {
NavHostFragment.findNavController(this).popBackStack(); NavHostFragment.findNavController(this).popBackStack();
} }

View File

@@ -15,9 +15,8 @@ import com.example.petstoremobile.utils.DialogUtils;
import com.example.petstoremobile.utils.InputValidator; import com.example.petstoremobile.utils.InputValidator;
import com.example.petstoremobile.utils.Resource; import com.example.petstoremobile.utils.Resource;
import com.example.petstoremobile.utils.SpinnerUtils; import com.example.petstoremobile.utils.SpinnerUtils;
import com.example.petstoremobile.viewmodels.ProductSupplierViewModel; import com.example.petstoremobile.utils.UIUtils;
import com.example.petstoremobile.viewmodels.ProductViewModel; import com.example.petstoremobile.viewmodels.ProductSupplierDetailViewModel;
import com.example.petstoremobile.viewmodels.SupplierViewModel;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.util.*; import java.util.*;
@@ -31,26 +30,15 @@ import dagger.hilt.android.AndroidEntryPoint;
public class ProductSupplierDetailFragment extends Fragment { public class ProductSupplierDetailFragment extends Fragment {
private FragmentProductSupplierDetailBinding binding; private FragmentProductSupplierDetailBinding binding;
private ProductSupplierDetailViewModel viewModel;
private boolean isEditing = false;
private long editProductId = -1;
private long editSupplierId = -1;
private long preselectedProductId = -1; private long preselectedProductId = -1;
private long preselectedSupplierId = -1; private long preselectedSupplierId = -1;
private List<ProductDTO> productList = new ArrayList<>();
private List<SupplierDTO> supplierList = new ArrayList<>();
private ProductSupplierViewModel psViewModel;
private ProductViewModel productViewModel;
private SupplierViewModel supplierViewModel;
@Override @Override
public void onCreate(Bundle savedInstanceState) { public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
psViewModel = new ViewModelProvider(this).get(ProductSupplierViewModel.class); viewModel = new ViewModelProvider(this).get(ProductSupplierDetailViewModel.class);
productViewModel = new ViewModelProvider(this).get(ProductViewModel.class);
supplierViewModel = new ViewModelProvider(this).get(SupplierViewModel.class);
} }
@Override @Override
@@ -63,6 +51,7 @@ public class ProductSupplierDetailFragment extends Fragment {
@Override @Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState); super.onViewCreated(view, savedInstanceState);
observeViewModel();
loadSpinnersData(); loadSpinnersData();
handleArguments(); handleArguments();
@@ -71,128 +60,97 @@ public class ProductSupplierDetailFragment extends Fragment {
binding.btnDeletePS.setOnClickListener(v -> confirmDelete()); binding.btnDeletePS.setOnClickListener(v -> confirmDelete());
} }
private void observeViewModel() {
viewModel.getProductList().observe(getViewLifecycleOwner(), list -> refreshProductSpinner());
viewModel.getSupplierList().observe(getViewLifecycleOwner(), list -> refreshSupplierSpinner());
}
private void setLoading(boolean loading) {
if (binding != null && binding.progressBar != null) {
binding.progressBar.setVisibility(loading ? View.VISIBLE : View.GONE);
}
}
@Override @Override
public void onDestroyView() { public void onDestroyView() {
super.onDestroyView(); super.onDestroyView();
binding = null; binding = null;
} }
/**
* Fetches products and suppliers to populate the spinners.
*/
private void loadSpinnersData() { private void loadSpinnersData() {
loadProducts(); viewModel.loadProducts().observe(getViewLifecycleOwner(), resource -> {
loadSuppliers(); if (resource == null) return;
} setLoading(resource.status == Resource.Status.LOADING);
/**
* Loads the list of products from the API.
*/
private void loadProducts() {
productViewModel.getAllProducts(null, null, 0, 200, "prodName").observe(getViewLifecycleOwner(), resource -> {
if (resource.status == Resource.Status.SUCCESS && resource.data != null) { if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
productList = resource.data.getContent(); viewModel.setProductList(resource.data.getContent());
refreshProductSpinner(); }
});
viewModel.loadSuppliers().observe(getViewLifecycleOwner(), resource -> {
if (resource == null) return;
setLoading(resource.status == Resource.Status.LOADING);
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
viewModel.setSupplierList(resource.data.getContent());
} }
}); });
} }
private void refreshProductSpinner() { private void refreshProductSpinner() {
SpinnerUtils.populateSpinner(requireContext(), binding.spinnerPSProduct, productList, SpinnerUtils.populateSpinner(requireContext(), binding.spinnerPSProduct, viewModel.getProductList().getValue(),
ProductDTO::getProdName, "-- Select Product --", ProductDTO::getProdName, "-- Select Product --",
preselectedProductId, ProductDTO::getProdId); preselectedProductId, ProductDTO::getProdId);
} }
/**
* Loads the list of suppliers from the API.
*/
private void loadSuppliers() {
supplierViewModel.getAllSuppliers(0, 200, null, "supCompany").observe(getViewLifecycleOwner(), resource -> {
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
supplierList = resource.data.getContent();
refreshSupplierSpinner();
}
});
}
private void refreshSupplierSpinner() { private void refreshSupplierSpinner() {
SpinnerUtils.populateSpinner(requireContext(), binding.spinnerPSSupplier, supplierList, SpinnerUtils.populateSpinner(requireContext(), binding.spinnerPSSupplier, viewModel.getSupplierList().getValue(),
SupplierDTO::getSupCompany, "-- Select Supplier --", SupplierDTO::getSupCompany, "-- Select Supplier --",
preselectedSupplierId, SupplierDTO::getSupId); preselectedSupplierId, SupplierDTO::getSupId);
} }
/**
* Handles arguments to determine if the fragment is in edit or add mode.
*/
private void handleArguments() { private void handleArguments() {
Bundle a = getArguments(); Bundle a = getArguments();
if (a != null && a.containsKey("productId") && a.containsKey("supplierId")) { if (a != null && a.containsKey("productId") && a.containsKey("supplierId")) {
isEditing = true; long productId = a.getLong("productId");
editProductId = a.getLong("productId"); long supplierId = a.getLong("supplierId");
editSupplierId = a.getLong("supplierId"); viewModel.setEditMode(productId, supplierId);
preselectedProductId = editProductId; preselectedProductId = productId;
preselectedSupplierId = editSupplierId; preselectedSupplierId = supplierId;
binding.tvPSMode.setText("Edit Product Supplier"); binding.tvPSMode.setText("Edit Product Supplier");
binding.btnDeletePS.setVisibility(View.VISIBLE); binding.btnDeletePS.setVisibility(View.VISIBLE);
} else { } else {
binding.tvPSMode.setText("Add Product Supplier"); binding.tvPSMode.setText("Add Product Supplier");
binding.btnDeletePS.setVisibility(View.GONE); binding.btnDeletePS.setVisibility(View.GONE);
} }
} }
/**
* Validates input and saves the product-supplier to the backend.
*/
private void save() { private void save() {
if (binding.spinnerPSProduct.getSelectedItemPosition() == 0) { if (!InputValidator.isSpinnerSelected(binding.spinnerPSProduct, "Product")) return;
Toast.makeText(getContext(), "Select a product", Toast.LENGTH_SHORT).show(); return; if (!InputValidator.isSpinnerSelected(binding.spinnerPSSupplier, "Supplier")) return;
} if (!InputValidator.isPositiveDecimal(binding.etPSCost, "Cost")) return;
if (binding.spinnerPSSupplier.getSelectedItemPosition() == 0) {
Toast.makeText(getContext(), "Select a supplier", Toast.LENGTH_SHORT).show(); return;
}
if (!InputValidator.isNotEmpty(binding.etPSCost, "Cost") || ProductDTO product = viewModel.getProductList().getValue().get(binding.spinnerPSProduct.getSelectedItemPosition() - 1);
!InputValidator.isPositiveDecimal(binding.etPSCost, "Cost")) { SupplierDTO supplier = viewModel.getSupplierList().getValue().get(binding.spinnerPSSupplier.getSelectedItemPosition() - 1);
return;
}
ProductDTO product = productList.get(binding.spinnerPSProduct.getSelectedItemPosition() - 1);
SupplierDTO supplier = supplierList.get(binding.spinnerPSSupplier.getSelectedItemPosition() - 1);
BigDecimal cost = new BigDecimal(binding.etPSCost.getText().toString().trim()); BigDecimal cost = new BigDecimal(binding.etPSCost.getText().toString().trim());
ProductSupplierDTO dto = new ProductSupplierDTO( ProductSupplierDTO dto = new ProductSupplierDTO(product.getProdId(), supplier.getSupId(), cost);
product.getProdId(), supplier.getSupId(), cost);
if (isEditing) { viewModel.saveProductSupplier(dto).observe(getViewLifecycleOwner(), resource -> {
psViewModel.updateProductSupplier(editProductId, editSupplierId, dto).observe(getViewLifecycleOwner(), resource -> { if (resource == null) return;
if (resource.status == Resource.Status.SUCCESS) { setLoading(resource.status == Resource.Status.LOADING);
Toast.makeText(getContext(), "Updated", Toast.LENGTH_SHORT).show(); if (resource.status == Resource.Status.SUCCESS) {
navigateBack(); Toast.makeText(getContext(), viewModel.isEditing() ? "Updated" : "Saved", Toast.LENGTH_SHORT).show();
} else if (resource.status == Resource.Status.ERROR) { navigateBack();
Toast.makeText(getContext(), "Error: " + resource.message, Toast.LENGTH_SHORT).show(); } else if (resource.status == Resource.Status.ERROR) {
} Toast.makeText(getContext(), "Error: " + resource.message, Toast.LENGTH_SHORT).show();
}); }
} else { });
psViewModel.createProductSupplier(dto).observe(getViewLifecycleOwner(), resource -> {
if (resource.status == Resource.Status.SUCCESS) {
Toast.makeText(getContext(), "Saved", Toast.LENGTH_SHORT).show();
navigateBack();
} else if (resource.status == Resource.Status.ERROR) {
Toast.makeText(getContext(), "Error: " + resource.message, Toast.LENGTH_SHORT).show();
}
});
}
} }
/**
* Shows a confirmation dialog before deleting a product-supplier relationship.
*/
private void confirmDelete() { private void confirmDelete() {
DialogUtils.showDeleteConfirmDialog(requireContext(), "Product Supplier", () -> DialogUtils.showDeleteConfirmDialog(requireContext(), "Product Supplier Relationship", () ->
psViewModel.deleteProductSupplier(editProductId, editSupplierId).observe(getViewLifecycleOwner(), resource -> { viewModel.deleteProductSupplier().observe(getViewLifecycleOwner(), resource -> {
if (resource == null) return;
setLoading(resource.status == Resource.Status.LOADING);
if (resource.status == Resource.Status.SUCCESS) { if (resource.status == Resource.Status.SUCCESS) {
Toast.makeText(getContext(), "Deleted", Toast.LENGTH_SHORT).show(); Toast.makeText(getContext(), "Deleted", Toast.LENGTH_SHORT).show();
navigateBack(); navigateBack();
@@ -202,9 +160,6 @@ public class ProductSupplierDetailFragment extends Fragment {
})); }));
} }
/**
* Navigates back to the previous screen.
*/
private void navigateBack() { private void navigateBack() {
NavHostFragment.findNavController(this).popBackStack(); NavHostFragment.findNavController(this).popBackStack();
} }

View File

@@ -14,7 +14,7 @@ import androidx.navigation.fragment.NavHostFragment;
import com.example.petstoremobile.databinding.FragmentPurchaseOrderDetailBinding; import com.example.petstoremobile.databinding.FragmentPurchaseOrderDetailBinding;
import com.example.petstoremobile.dtos.PurchaseOrderDTO; import com.example.petstoremobile.dtos.PurchaseOrderDTO;
import com.example.petstoremobile.utils.Resource; import com.example.petstoremobile.utils.Resource;
import com.example.petstoremobile.viewmodels.PurchaseOrderViewModel; import com.example.petstoremobile.viewmodels.PurchaseOrderDetailViewModel;
import dagger.hilt.android.AndroidEntryPoint; import dagger.hilt.android.AndroidEntryPoint;
@@ -25,13 +25,13 @@ import dagger.hilt.android.AndroidEntryPoint;
public class PurchaseOrderDetailFragment extends Fragment { public class PurchaseOrderDetailFragment extends Fragment {
private FragmentPurchaseOrderDetailBinding binding; private FragmentPurchaseOrderDetailBinding binding;
private PurchaseOrderViewModel viewModel; private PurchaseOrderDetailViewModel viewModel;
private long purchaseOrderId; private long purchaseOrderId;
@Override @Override
public void onCreate(@Nullable Bundle savedInstanceState) { public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
viewModel = new ViewModelProvider(this).get(PurchaseOrderViewModel.class); viewModel = new ViewModelProvider(this).get(PurchaseOrderDetailViewModel.class);
} }
/** /**
@@ -66,9 +66,16 @@ public class PurchaseOrderDetailFragment extends Fragment {
} }
} }
private void setLoading(boolean loading) {
if (binding != null && binding.progressBar != null) {
binding.progressBar.setVisibility(loading ? View.VISIBLE : View.GONE);
}
}
private void loadPurchaseOrderData() { private void loadPurchaseOrderData() {
viewModel.getPurchaseOrderById(purchaseOrderId).observe(getViewLifecycleOwner(), resource -> { viewModel.loadPurchaseOrder(purchaseOrderId).observe(getViewLifecycleOwner(), resource -> {
if (resource == null) return; if (resource == null) return;
setLoading(resource.status == Resource.Status.LOADING);
if (resource.status == Resource.Status.SUCCESS && resource.data != null) { if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
PurchaseOrderDTO po = resource.data; PurchaseOrderDTO po = resource.data;
binding.tvPODetailId.setText("PO #" + po.getPurchaseOrderId()); binding.tvPODetailId.setText("PO #" + po.getPurchaseOrderId());

View File

@@ -1,6 +1,5 @@
package com.example.petstoremobile.fragments.listfragments.detailfragments; package com.example.petstoremobile.fragments.listfragments.detailfragments;
import android.app.AlertDialog;
import android.os.Bundle; import android.os.Bundle;
import android.util.Log; import android.util.Log;
import android.view.*; import android.view.*;
@@ -12,7 +11,10 @@ import androidx.navigation.fragment.NavHostFragment;
import com.example.petstoremobile.R; import com.example.petstoremobile.R;
import com.example.petstoremobile.databinding.FragmentRefundBinding; import com.example.petstoremobile.databinding.FragmentRefundBinding;
import com.example.petstoremobile.dtos.SaleDTO; import com.example.petstoremobile.dtos.SaleDTO;
import com.example.petstoremobile.viewmodels.SaleViewModel; import com.example.petstoremobile.viewmodels.RefundViewModel;
import com.example.petstoremobile.utils.DialogUtils;
import com.example.petstoremobile.utils.Resource;
import com.example.petstoremobile.utils.SpinnerUtils;
import dagger.hilt.android.AndroidEntryPoint; import dagger.hilt.android.AndroidEntryPoint;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.math.RoundingMode; import java.math.RoundingMode;
@@ -22,53 +24,23 @@ import java.util.*;
public class RefundFragment extends Fragment { public class RefundFragment extends Fragment {
private FragmentRefundBinding binding; private FragmentRefundBinding binding;
private SaleViewModel saleViewModel; private RefundViewModel viewModel;
private SaleDTO currentSale;
private List<SaleDTO> allSales = new ArrayList<>();
// Items available to refund (after accounting for previous refunds)
private List<RefundItem> availableItems = new ArrayList<>();
// Items user has added to refund cart
private List<RefundItem> refundCart = new ArrayList<>();
private final String[] PAYMENT_METHODS = {"Cash", "Card"}; private final String[] PAYMENT_METHODS = {"Cash", "Card"};
// Inner class to track refund items
static class RefundItem {
long prodId;
String productName;
int quantity;
BigDecimal unitPrice;
RefundItem(long prodId, String productName, int quantity, BigDecimal unitPrice) {
this.prodId = prodId;
this.productName = productName;
this.quantity = quantity;
this.unitPrice = unitPrice;
}
BigDecimal getTotal() {
return unitPrice != null
? unitPrice.multiply(BigDecimal.valueOf(quantity))
: BigDecimal.ZERO;
}
}
@Override @Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) { Bundle savedInstanceState) {
binding = FragmentRefundBinding.inflate(inflater, container, false); binding = FragmentRefundBinding.inflate(inflater, container, false);
saleViewModel = new ViewModelProvider(this).get(SaleViewModel.class); viewModel = new ViewModelProvider(this).get(RefundViewModel.class);
setupSpinner(); setupSpinner();
observeViewModel();
loadAllSales(); loadAllSales();
// Pre-fill sale ID if passed from SaleFragment
Bundle args = getArguments(); Bundle args = getArguments();
if (args != null && args.containsKey("saleId")) { if (args != null && args.containsKey("saleId")) {
long saleId = args.getLong("saleId"); binding.etRefundSaleId.setText(String.valueOf(args.getLong("saleId")));
binding.etRefundSaleId.setText(String.valueOf(saleId));
// Auto-load after sales are fetched
} }
binding.btnLoadSale.setOnClickListener(v -> loadSale()); binding.btnLoadSale.setOnClickListener(v -> loadSale());
@@ -79,31 +51,36 @@ public class RefundFragment extends Fragment {
} }
private void setupSpinner() { private void setupSpinner() {
binding.spinnerRefundPayment.setAdapter(new ArrayAdapter<>(requireContext(), SpinnerUtils.setupStringSpinner(requireContext(), binding.spinnerRefundPayment, PAYMENT_METHODS);
android.R.layout.simple_spinner_item, PAYMENT_METHODS)); }
private void observeViewModel() {
viewModel.getAvailableItems().observe(getViewLifecycleOwner(), items -> renderOriginalItems());
viewModel.getRefundCart().observe(getViewLifecycleOwner(), cart -> {
renderRefundCart();
updateRefundTotal();
renderOriginalItems(); // Re-render to reflect quantities in cart
});
}
private void setLoading(boolean loading) {
if (binding != null && binding.progressBar != null) {
binding.progressBar.setVisibility(loading ? View.VISIBLE : View.GONE);
}
} }
private void loadAllSales() { private void loadAllSales() {
saleViewModel.getAllSales(0, 1000, null, null, null, "saleDate,desc") viewModel.loadAllSales().observe(getViewLifecycleOwner(), resource -> {
.observe(getViewLifecycleOwner(), resource -> { if (resource == null) return;
if (resource != null) { setLoading(resource.status == Resource.Status.LOADING);
switch (resource.status) { if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
case SUCCESS: viewModel.setAllSales(resource.data.getContent());
if (resource.data != null) { Bundle args = getArguments();
allSales = resource.data.getContent(); if (args != null && args.containsKey("saleId")) {
// Auto-load if saleId was pre-filled loadSale();
Bundle args = getArguments(); }
if (args != null && args.containsKey("saleId")) { }
loadSale(); });
}
}
break;
case ERROR:
Log.e("Refund", "Failed to load sales: " + resource.message);
break;
}
}
});
} }
private void loadSale() { private void loadSale() {
@@ -120,11 +97,12 @@ public class RefundFragment extends Fragment {
return; return;
} }
// Find sale in loaded list
SaleDTO found = null; SaleDTO found = null;
for (SaleDTO s : allSales) { if (viewModel.getAllSalesList() != null) {
if (s.getSaleId() != null && s.getSaleId() == saleId) { for (SaleDTO s : viewModel.getAllSalesList()) {
found = s; break; if (s.getSaleId() != null && s.getSaleId() == saleId) {
found = s; break;
}
} }
} }
@@ -139,9 +117,9 @@ public class RefundFragment extends Fragment {
return; return;
} }
currentSale = found; viewModel.setCurrentSale(found);
SaleDTO currentSale = viewModel.getCurrentSale();
// Show sale info
binding.tvSaleInfo.setVisibility(View.VISIBLE); binding.tvSaleInfo.setVisibility(View.VISIBLE);
binding.tvSaleInfo.setText("Sale #" + currentSale.getSaleId() binding.tvSaleInfo.setText("Sale #" + currentSale.getSaleId()
+ " | " + (currentSale.getSaleDate() != null + " | " + (currentSale.getSaleDate() != null
@@ -151,94 +129,44 @@ public class RefundFragment extends Fragment {
+ " | Total: $" + currentSale.getTotalAmount() + " | Total: $" + currentSale.getTotalAmount()
+ " | Payment: " + currentSale.getPaymentMethod()); + " | Payment: " + currentSale.getPaymentMethod());
// Pre-select payment method
if (currentSale.getPaymentMethod() != null) { if (currentSale.getPaymentMethod() != null) {
for (int i = 0; i < PAYMENT_METHODS.length; i++) { SpinnerUtils.setSelectionByValue(binding.spinnerRefundPayment, currentSale.getPaymentMethod());
if (PAYMENT_METHODS[i].equalsIgnoreCase(currentSale.getPaymentMethod())) {
binding.spinnerRefundPayment.setSelection(i); break;
}
}
} }
// Build refundable items accounting for previous refunds if (viewModel.getAvailableItems().getValue() == null || viewModel.getAvailableItems().getValue().isEmpty()) {
buildRefundableItems(); Toast.makeText(getContext(), "This sale has no remaining refundable items", Toast.LENGTH_LONG).show();
if (availableItems.isEmpty()) {
Toast.makeText(getContext(),
"This sale has no remaining refundable items", Toast.LENGTH_LONG).show();
return; return;
} }
// Reset refund cart
refundCart.clear();
// Show cards
binding.cardOriginalItems.setVisibility(View.VISIBLE); binding.cardOriginalItems.setVisibility(View.VISIBLE);
binding.cardRefundItems.setVisibility(View.VISIBLE); binding.cardRefundItems.setVisibility(View.VISIBLE);
binding.cardPayment.setVisibility(View.VISIBLE); binding.cardPayment.setVisibility(View.VISIBLE);
binding.btnProcessRefund.setVisibility(View.VISIBLE); binding.btnProcessRefund.setVisibility(View.VISIBLE);
renderOriginalItems();
renderRefundCart();
updateRefundTotal();
}
private void buildRefundableItems() {
availableItems.clear();
if (currentSale.getItems() == null) return;
// Find all previous refunds for this sale
Map<Long, Integer> alreadyRefunded = new HashMap<>();
for (SaleDTO s : allSales) {
if (Boolean.TRUE.equals(s.getIsRefund())
&& currentSale.getSaleId().equals(s.getOriginalSaleId())
&& s.getItems() != null) {
for (SaleDTO.SaleItemDTO item : s.getItems()) {
if (item.getProdId() != null && item.getQuantity() != null) {
alreadyRefunded.merge(item.getProdId(),
Math.abs(item.getQuantity()), Integer::sum);
}
}
}
}
// Build available items
for (SaleDTO.SaleItemDTO item : currentSale.getItems()) {
if (item.getProdId() == null || item.getQuantity() == null) continue;
int refunded = alreadyRefunded.getOrDefault(item.getProdId(), 0);
int remaining = item.getQuantity() - refunded;
if (remaining > 0) {
availableItems.add(new RefundItem(
item.getProdId(),
item.getProductName() != null ? item.getProductName() : "Unknown",
remaining,
item.getUnitPrice()
));
}
}
} }
private void renderOriginalItems() { private void renderOriginalItems() {
binding.llOriginalItems.removeAllViews(); binding.llOriginalItems.removeAllViews();
List<RefundViewModel.RefundItem> available = viewModel.getAvailableItems().getValue();
if (available == null) return;
// Header
addTableHeader(binding.llOriginalItems); addTableHeader(binding.llOriginalItems);
for (RefundItem item : availableItems) { for (RefundViewModel.RefundItem item : available) {
// Calculate pending in cart int inCart = 0;
int pendingQty = 0; if (viewModel.getRefundCart().getValue() != null) {
for (RefundItem r : refundCart) { for (RefundViewModel.RefundItem r : viewModel.getRefundCart().getValue()) {
if (r.prodId == item.prodId) { pendingQty = r.quantity; break; } if (r.prodId == item.prodId) { inCart = r.quantity; break; }
}
} }
int displayQty = item.quantity - pendingQty; int displayQty = item.quantity - inCart;
if (displayQty <= 0) continue; if (displayQty <= 0) continue;
LinearLayout row = buildItemRow( LinearLayout row = buildItemRow(
item.productName, item.productName,
displayQty, displayQty,
item.unitPrice, item.unitPrice,
true, // show add button true,
() -> showQuantityDialog(item) () -> showQuantityDialog(item, displayQty)
); );
binding.llOriginalItems.addView(row); binding.llOriginalItems.addView(row);
} }
@@ -246,8 +174,9 @@ public class RefundFragment extends Fragment {
private void renderRefundCart() { private void renderRefundCart() {
binding.llRefundItems.removeAllViews(); binding.llRefundItems.removeAllViews();
List<RefundViewModel.RefundItem> cart = viewModel.getRefundCart().getValue();
if (refundCart.isEmpty()) { if (cart == null || cart.isEmpty()) {
TextView empty = new TextView(getContext()); TextView empty = new TextView(getContext());
empty.setText("No items added to refund yet"); empty.setText("No items added to refund yet");
empty.setTextColor(0xFF888780); empty.setTextColor(0xFF888780);
@@ -258,18 +187,13 @@ public class RefundFragment extends Fragment {
addTableHeader(binding.llRefundItems); addTableHeader(binding.llRefundItems);
for (RefundItem item : refundCart) { for (RefundViewModel.RefundItem item : cart) {
LinearLayout row = buildItemRow( LinearLayout row = buildItemRow(
item.productName, item.productName,
item.quantity, item.quantity,
item.unitPrice, item.unitPrice,
false, // show remove button false,
() -> { () -> viewModel.removeFromCart(item)
refundCart.remove(item);
renderOriginalItems();
renderRefundCart();
updateRefundTotal();
}
); );
binding.llRefundItems.addView(row); binding.llRefundItems.addView(row);
} }
@@ -342,146 +266,79 @@ public class RefundFragment extends Fragment {
return row; return row;
} }
private void showQuantityDialog(RefundItem item) { private void showQuantityDialog(RefundViewModel.RefundItem item, int available) {
// Calculate how many are already in cart
int inCart = 0;
for (RefundItem r : refundCart) {
if (r.prodId == item.prodId) { inCart = r.quantity; break; }
}
int available = item.quantity - inCart;
if (available <= 0) {
Toast.makeText(getContext(), "All units already added to refund",
Toast.LENGTH_SHORT).show();
return;
}
// Build dialog
AlertDialog.Builder builder = new AlertDialog.Builder(requireContext());
builder.setTitle("Refund Quantity");
builder.setMessage("Product: " + item.productName
+ "\nAvailable: " + available);
EditText input = new EditText(getContext()); EditText input = new EditText(getContext());
input.setInputType(android.text.InputType.TYPE_CLASS_NUMBER); input.setInputType(android.text.InputType.TYPE_CLASS_NUMBER);
input.setText(String.valueOf(available)); input.setText(String.valueOf(available));
input.setSelectAllOnFocus(true); input.setSelectAllOnFocus(true);
builder.setView(input); input.setPadding(40, 40, 40, 40);
builder.setPositiveButton("Add to Refund", (d, w) -> { new androidx.appcompat.app.AlertDialog.Builder(requireContext())
String val = input.getText().toString().trim(); .setTitle("Refund Quantity")
if (val.isEmpty()) return; .setMessage("Product: " + item.productName + "\nAvailable: " + available)
int qty; .setView(input)
try { qty = Integer.parseInt(val); } .setPositiveButton("Add to Refund", (d, w) -> {
catch (Exception e) { String val = input.getText().toString().trim();
Toast.makeText(getContext(), "Invalid quantity", Toast.LENGTH_SHORT).show(); if (val.isEmpty()) return;
return; int qty;
} try { qty = Integer.parseInt(val); }
if (qty <= 0) { catch (Exception e) {
Toast.makeText(getContext(), "Quantity must be at least 1", Toast.makeText(getContext(), "Invalid quantity", Toast.LENGTH_SHORT).show();
Toast.LENGTH_SHORT).show(); return;
return; }
} if (qty <= 0) {
if (qty > available) { Toast.makeText(getContext(), "Quantity must be at least 1", Toast.LENGTH_SHORT).show();
Toast.makeText(getContext(), "Cannot exceed " + available, return;
Toast.LENGTH_SHORT).show(); }
return; if (qty > available) {
} Toast.makeText(getContext(), "Cannot exceed " + available, Toast.LENGTH_SHORT).show();
return;
// Add or merge into cart }
boolean merged = false; viewModel.addToCart(item, qty);
for (int i = 0; i < refundCart.size(); i++) { })
if (refundCart.get(i).prodId == item.prodId) { .setNegativeButton("Cancel", null)
RefundItem existing = refundCart.get(i); .show();
refundCart.set(i, new RefundItem(existing.prodId,
existing.productName,
existing.quantity + qty,
existing.unitPrice));
merged = true; break;
}
}
if (!merged) {
refundCart.add(new RefundItem(item.prodId, item.productName,
qty, item.unitPrice));
}
renderOriginalItems();
renderRefundCart();
updateRefundTotal();
});
builder.setNegativeButton("Cancel", null);
builder.show();
} }
private void updateRefundTotal() { private void updateRefundTotal() {
BigDecimal total = BigDecimal.ZERO; BigDecimal total = BigDecimal.ZERO;
for (RefundItem item : refundCart) total = total.add(item.getTotal()); List<RefundViewModel.RefundItem> cart = viewModel.getRefundCart().getValue();
if (cart != null) {
for (RefundViewModel.RefundItem item : cart) total = total.add(item.getTotal());
}
binding.tvRefundTotal.setText("Refund Total: $" + total.setScale(2, RoundingMode.HALF_UP)); binding.tvRefundTotal.setText("Refund Total: $" + total.setScale(2, RoundingMode.HALF_UP));
} }
private void processRefund() { private void processRefund() {
if (currentSale == null) { if (viewModel.getCurrentSale() == null) {
Toast.makeText(getContext(), "Load a sale first", Toast.LENGTH_SHORT).show(); Toast.makeText(getContext(), "Load a sale first", Toast.LENGTH_SHORT).show();
return; return;
} }
if (refundCart.isEmpty()) { if (viewModel.getRefundCart().getValue() == null || viewModel.getRefundCart().getValue().isEmpty()) {
Toast.makeText(getContext(), "Add at least one item to refund", Toast.makeText(getContext(), "Add at least one item to refund", Toast.LENGTH_SHORT).show();
Toast.LENGTH_SHORT).show();
return; return;
} }
String payment = PAYMENT_METHODS[binding.spinnerRefundPayment.getSelectedItemPosition()]; String payment = PAYMENT_METHODS[binding.spinnerRefundPayment.getSelectedItemPosition()];
// Confirm dialog
BigDecimal total = BigDecimal.ZERO; BigDecimal total = BigDecimal.ZERO;
for (RefundItem item : refundCart) total = total.add(item.getTotal()); for (RefundViewModel.RefundItem item : viewModel.getRefundCart().getValue()) total = total.add(item.getTotal());
final BigDecimal finalTotal = total; final BigDecimal finalTotal = total;
new AlertDialog.Builder(requireContext()) DialogUtils.showConfirmDialog(requireContext(), "Confirm Refund",
.setTitle("Confirm Refund") "Process refund for Sale #" + viewModel.getCurrentSale().getSaleId()
.setMessage("Process refund for Sale #" + currentSale.getSaleId() + "?\nRefund amount: $" + finalTotal.setScale(2, RoundingMode.HALF_UP),
+ "?\nRefund amount: $" + finalTotal.setScale(2, RoundingMode.HALF_UP)) () -> submitRefund(payment));
.setPositiveButton("Yes", (d, w) -> submitRefund(payment))
.setNegativeButton("No", null)
.show();
} }
private void submitRefund(String payment) { private void submitRefund(String payment) {
// Build sale items list viewModel.submitRefund(payment).observe(getViewLifecycleOwner(), resource -> {
List<SaleDTO.SaleItemDTO> items = new ArrayList<>();
for (RefundItem item : refundCart) {
// Backend expects negative quantity for refunds
items.add(new SaleDTO.SaleItemDTO(item.prodId, -item.quantity));
}
SaleDTO dto = new SaleDTO(
currentSale.getStoreId(),
payment,
items,
true, // isRefund = true
currentSale.getSaleId(), // originalSaleId
null // no customer needed
);
Log.d("REFUND", "Submitting refund for saleId=" + currentSale.getSaleId()
+ " items=" + items.size());
saleViewModel.createSale(dto).observe(getViewLifecycleOwner(), resource -> {
if (resource != null) { if (resource != null) {
switch (resource.status) { setLoading(resource.status == Resource.Status.LOADING);
case SUCCESS: if (resource.status == Resource.Status.SUCCESS) {
if (resource.data != null) { Toast.makeText(getContext(), "Refund processed successfully!", Toast.LENGTH_LONG).show();
Toast.makeText(getContext(), navigateBack();
"Refund #" + resource.data.getSaleId() + " processed successfully!", } else if (resource.status == Resource.Status.ERROR) {
Toast.LENGTH_LONG).show(); Toast.makeText(getContext(), "Error: " + resource.message, Toast.LENGTH_LONG).show();
navigateBack();
}
break;
case ERROR:
Log.e("REFUND", "Error: " + resource.message);
Toast.makeText(getContext(), "Error: " + resource.message,
Toast.LENGTH_LONG).show();
break;
} }
} }
}); });

View File

@@ -11,10 +11,12 @@ import androidx.navigation.fragment.NavHostFragment;
import com.example.petstoremobile.R; import com.example.petstoremobile.R;
import com.example.petstoremobile.databinding.FragmentSaleDetailBinding; import com.example.petstoremobile.databinding.FragmentSaleDetailBinding;
import com.example.petstoremobile.dtos.*; import com.example.petstoremobile.dtos.*;
import com.example.petstoremobile.viewmodels.*; import com.example.petstoremobile.viewmodels.SaleDetailViewModel;
import com.example.petstoremobile.utils.SpinnerUtils; import com.example.petstoremobile.utils.SpinnerUtils;
import com.example.petstoremobile.utils.DialogUtils; import com.example.petstoremobile.utils.DialogUtils;
import com.example.petstoremobile.utils.InputValidator;
import com.example.petstoremobile.utils.Resource; import com.example.petstoremobile.utils.Resource;
import com.example.petstoremobile.utils.UIUtils;
import dagger.hilt.android.AndroidEntryPoint; import dagger.hilt.android.AndroidEntryPoint;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.util.*; import java.util.*;
@@ -23,18 +25,7 @@ import java.util.*;
public class SaleDetailFragment extends Fragment { public class SaleDetailFragment extends Fragment {
private FragmentSaleDetailBinding binding; private FragmentSaleDetailBinding binding;
private SaleViewModel saleViewModel; private SaleDetailViewModel viewModel;
private StoreViewModel storeViewModel;
private CustomerViewModel customerViewModel;
private ProductViewModel productViewModel;
private boolean viewOnly = false;
private long saleId = -1;
private List<StoreDTO> storeList = new ArrayList<>();
private List<CustomerDTO> customerList = new ArrayList<>();
private List<ProductDTO> productList = new ArrayList<>();
private List<SaleDTO.SaleItemDTO> cartItems = new ArrayList<>();
private final String[] PAYMENT_METHODS = { "Cash", "Card"}; private final String[] PAYMENT_METHODS = { "Cash", "Card"};
@@ -42,17 +33,14 @@ public class SaleDetailFragment extends Fragment {
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) { Bundle savedInstanceState) {
binding = FragmentSaleDetailBinding.inflate(inflater, container, false); binding = FragmentSaleDetailBinding.inflate(inflater, container, false);
viewModel = new ViewModelProvider(this).get(SaleDetailViewModel.class);
saleViewModel = new ViewModelProvider(this).get(SaleViewModel.class);
storeViewModel = new ViewModelProvider(this).get(StoreViewModel.class);
customerViewModel = new ViewModelProvider(this).get(CustomerViewModel.class);
productViewModel = new ViewModelProvider(this).get(ProductViewModel.class);
SpinnerUtils.setupStringSpinner(requireContext(), binding.spinnerPaymentMethod, PAYMENT_METHODS); SpinnerUtils.setupStringSpinner(requireContext(), binding.spinnerPaymentMethod, PAYMENT_METHODS);
observeViewModel();
handleArguments(); handleArguments();
if (!viewOnly) { if (!viewModel.isViewOnly()) {
loadData(); loadData();
setupAddItem(); setupAddItem();
} }
@@ -64,32 +52,55 @@ public class SaleDetailFragment extends Fragment {
return binding.getRoot(); return binding.getRoot();
} }
private void observeViewModel() {
viewModel.getStoreList().observe(getViewLifecycleOwner(), list -> {
SpinnerUtils.populateSpinner(requireContext(), binding.spinnerSaleStore, list,
DropdownDTO::getLabel, "-- Select Store --", -1L, DropdownDTO::getId);
});
viewModel.getCustomerList().observe(getViewLifecycleOwner(), list -> {
SpinnerUtils.populateSpinner(requireContext(), binding.spinnerSaleCustomer, list,
DropdownDTO::getLabel, "-- No Customer --", -1L, DropdownDTO::getId);
});
viewModel.getProductList().observe(getViewLifecycleOwner(), list -> {
SpinnerUtils.populateSpinner(requireContext(), binding.spinnerSaleProduct, list,
ProductDTO::getProdName, "Select Product", -1L, ProductDTO::getProdId);
});
viewModel.getCartItems().observe(getViewLifecycleOwner(), items -> {
renderCartItems();
updateTotal();
});
}
private void handleArguments() { private void handleArguments() {
Bundle a = getArguments(); Bundle a = getArguments();
if (a != null && a.containsKey("saleId")) { if (a != null && a.containsKey("saleId")) {
saleId = a.getLong("saleId"); long saleId = a.getLong("saleId");
viewOnly = a.getBoolean("viewOnly", false); boolean viewOnly = a.getBoolean("viewOnly", false);
viewModel.setSaleId(saleId, viewOnly);
binding.tvSaleMode.setText("Sale #" + saleId); binding.tvSaleMode.setText("Sale #" + saleId);
binding.tvSaleDetailId.setText("ID: " + saleId); binding.tvSaleDetailId.setText("ID: " + saleId);
// Show refund button for existing non-refund sales
if (!a.getBoolean("isRefund", false)) { if (!a.getBoolean("isRefund", false)) {
binding.btnRefundSale.setVisibility(View.VISIBLE); binding.btnRefundSale.setVisibility(View.VISIBLE);
} }
// Hide save and input controls for view only
if (viewOnly) { if (viewOnly) {
binding.btnSaveSale.setVisibility(View.GONE); binding.btnSaveSale.setVisibility(View.GONE);
binding.spinnerSaleStore.setEnabled(false); UIUtils.setViewsEnabled(false,
binding.spinnerSaleCustomer.setEnabled(false); binding.spinnerSaleStore,
binding.spinnerPaymentMethod.setEnabled(false); binding.spinnerSaleCustomer,
binding.spinnerPaymentMethod);
binding.llAddItemRow.setVisibility(View.GONE); binding.llAddItemRow.setVisibility(View.GONE);
binding.llExtraInfo.setVisibility(View.VISIBLE); binding.llExtraInfo.setVisibility(View.VISIBLE);
} }
// Load sale details
loadSaleDetails(); loadSaleDetails();
} else { } else {
viewModel.setSaleId(-1, false);
binding.tvSaleMode.setText("New Sale"); binding.tvSaleMode.setText("New Sale");
binding.tvSaleDetailId.setVisibility(View.GONE); binding.tvSaleDetailId.setVisibility(View.GONE);
binding.btnRefundSale.setVisibility(View.GONE); binding.btnRefundSale.setVisibility(View.GONE);
@@ -97,89 +108,62 @@ public class SaleDetailFragment extends Fragment {
} }
} }
private void setLoading(boolean loading) {
if (binding != null && binding.progressBar != null) {
binding.progressBar.setVisibility(loading ? View.VISIBLE : View.GONE);
}
}
private void loadData() { private void loadData() {
loadStores(); viewModel.loadStores().observe(getViewLifecycleOwner(), resource -> {
loadCustomers(); if (resource == null) return;
loadProducts(); setLoading(resource.status == Resource.Status.LOADING);
} if (resource.status == Resource.Status.SUCCESS && resource.data != null) viewModel.setStoreList(resource.data);
private void loadStores() {
storeViewModel.getAllStores(0, 50).observe(getViewLifecycleOwner(), resource -> {
if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) {
storeList = resource.data.getContent();
if (binding != null) {
SpinnerUtils.populateSpinner(requireContext(), binding.spinnerSaleStore, storeList,
StoreDTO::getStoreName, "-- Select Store --", -1L, StoreDTO::getStoreId);
}
} else if (storeList.isEmpty()) {
storeList = Collections.singletonList(new StoreDTO(1L, "Downtown Branch"));
if (binding != null) {
SpinnerUtils.populateSpinner(requireContext(), binding.spinnerSaleStore, storeList,
StoreDTO::getStoreName, "-- Select Store --", -1L, StoreDTO::getStoreId);
}
}
}); });
} viewModel.loadCustomers().observe(getViewLifecycleOwner(), resource -> {
if (resource == null) return;
private void loadCustomers() { setLoading(resource.status == Resource.Status.LOADING);
customerViewModel.getAllCustomers(0, 200).observe(getViewLifecycleOwner(), resource -> { if (resource.status == Resource.Status.SUCCESS && resource.data != null) viewModel.setCustomerList(resource.data);
if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) {
customerList = resource.data.getContent();
if (binding != null) {
SpinnerUtils.populateSpinner(requireContext(), binding.spinnerSaleCustomer, customerList,
CustomerDTO::getFullName, "-- No Customer --", -1L, CustomerDTO::getCustomerId);
}
}
}); });
} viewModel.loadProducts().observe(getViewLifecycleOwner(), resource -> {
if (resource == null) return;
private void loadProducts() { setLoading(resource.status == Resource.Status.LOADING);
productViewModel.getAllProducts(null, null, 0, 200, null).observe(getViewLifecycleOwner(), resource -> { if (resource.status == Resource.Status.SUCCESS && resource.data != null) viewModel.setProductList(resource.data.getContent());
if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) {
productList = resource.data.getContent();
if (binding != null) {
SpinnerUtils.populateSpinner(requireContext(), binding.spinnerSaleProduct, productList,
ProductDTO::getProdName, "Select Product", -1L, ProductDTO::getProdId);
}
}
}); });
} }
private void loadSaleDetails() { private void loadSaleDetails() {
saleViewModel.getSaleById(saleId).observe(getViewLifecycleOwner(), resource -> { viewModel.loadSaleDetails().observe(getViewLifecycleOwner(), resource -> {
if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) { if (resource == null) return;
setLoading(resource.status == Resource.Status.LOADING);
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
SaleDTO sale = resource.data; SaleDTO sale = resource.data;
if (binding != null) { binding.tvSaleDetailTotal.setText("Total: $" + sale.getTotalAmount());
binding.tvSaleDetailTotal.setText("Total: $" + sale.getTotalAmount()); binding.tvSaleSubtotal.setText("$" + (sale.getSubtotalAmount() != null ? sale.getSubtotalAmount() : sale.getTotalAmount()));
binding.tvSaleSubtotal.setText("$" + (sale.getSubtotalAmount() != null ? sale.getSubtotalAmount() : sale.getTotalAmount()));
if (sale.getCouponDiscountAmount() != null && sale.getCouponDiscountAmount().compareTo(BigDecimal.ZERO) > 0) {
if (sale.getCouponDiscountAmount() != null && sale.getCouponDiscountAmount().compareTo(BigDecimal.ZERO) > 0) { binding.llCouponDiscount.setVisibility(View.VISIBLE);
binding.llCouponDiscount.setVisibility(View.VISIBLE); binding.tvSaleCouponDiscount.setText("-$" + sale.getCouponDiscountAmount());
binding.tvSaleCouponDiscount.setText("-$" + sale.getCouponDiscountAmount()); } else {
} else { binding.llCouponDiscount.setVisibility(View.GONE);
binding.llCouponDiscount.setVisibility(View.GONE); }
}
if (sale.getEmployeeDiscountAmount() != null && sale.getEmployeeDiscountAmount().compareTo(BigDecimal.ZERO) > 0) { if (sale.getEmployeeDiscountAmount() != null && sale.getEmployeeDiscountAmount().compareTo(BigDecimal.ZERO) > 0) {
binding.llEmployeeDiscount.setVisibility(View.VISIBLE); binding.llEmployeeDiscount.setVisibility(View.VISIBLE);
binding.tvSaleEmployeeDiscount.setText("-$" + sale.getEmployeeDiscountAmount()); binding.tvSaleEmployeeDiscount.setText("-$" + sale.getEmployeeDiscountAmount());
} else { } else {
binding.llEmployeeDiscount.setVisibility(View.GONE); binding.llEmployeeDiscount.setVisibility(View.GONE);
} }
binding.tvSaleChannel.setText(sale.getChannel() != null ? sale.getChannel() : ""); binding.tvSaleChannel.setText(sale.getChannel() != null ? sale.getChannel() : "");
binding.tvSalePoints.setText(String.valueOf(sale.getPointsEarned() != null ? sale.getPointsEarned() : 0)); binding.tvSalePoints.setText(String.valueOf(sale.getPointsEarned() != null ? sale.getPointsEarned() : 0));
SpinnerUtils.setSelectionByValue(binding.spinnerPaymentMethod, sale.getPaymentMethod()); SpinnerUtils.setSelectionByValue(binding.spinnerPaymentMethod, sale.getPaymentMethod());
// Display items if (sale.getItems() != null) {
if (sale.getItems() != null) { binding.llSaleItems.removeAllViews();
binding.llSaleItems.removeAllViews(); for (SaleDTO.SaleItemDTO item : sale.getItems()) {
for (SaleDTO.SaleItemDTO item : sale.getItems()) { addItemRow(item.getProductName(), Math.abs(item.getQuantity()), item.getUnitPrice());
addItemRow(item.getProductName(),
Math.abs(item.getQuantity()),
item.getUnitPrice());
}
} }
} }
} }
@@ -188,41 +172,44 @@ public class SaleDetailFragment extends Fragment {
private void setupAddItem() { private void setupAddItem() {
binding.btnAddItem.setOnClickListener(v -> { binding.btnAddItem.setOnClickListener(v -> {
if (binding.spinnerSaleProduct.getSelectedItemPosition() == 0) { if (!InputValidator.isSpinnerSelected(binding.spinnerSaleProduct, "Product")) return;
Toast.makeText(getContext(), "Select a product", Toast.LENGTH_SHORT).show(); if (!InputValidator.isPositiveInteger(binding.etSaleQuantity, "Quantity")) return;
return;
}
String qtyStr = binding.etSaleQuantity.getText().toString().trim();
if (qtyStr.isEmpty()) {
binding.etSaleQuantity.setError("Enter quantity");
return;
}
int qty;
try {
qty = Integer.parseInt(qtyStr);
} catch (Exception e) {
binding.etSaleQuantity.setError("Invalid quantity");
return;
}
ProductDTO product = productList.get(binding.spinnerSaleProduct.getSelectedItemPosition() - 1); int qty = Integer.parseInt(binding.etSaleQuantity.getText().toString().trim());
ProductDTO product = viewModel.getProductList().getValue().get(binding.spinnerSaleProduct.getSelectedItemPosition() - 1);
// Check if product already in cart for (SaleDTO.SaleItemDTO existing : viewModel.getCartItems().getValue()) {
for (SaleDTO.SaleItemDTO existing : cartItems) {
if (existing.getProdId().equals(product.getProdId())) { if (existing.getProdId().equals(product.getProdId())) {
Toast.makeText(getContext(), "Product already added", Toast.LENGTH_SHORT).show(); Toast.makeText(getContext(), "Product already added", Toast.LENGTH_SHORT).show();
return; return;
} }
} }
SaleDTO.SaleItemDTO item = new SaleDTO.SaleItemDTO(product.getProdId(), qty); viewModel.addToCart(new SaleDTO.SaleItemDTO(product.getProdId(), qty));
cartItems.add(item);
addItemRow(product.getProdName(), qty, product.getProdPrice());
updateTotal();
binding.etSaleQuantity.setText(""); binding.etSaleQuantity.setText("");
}); });
} }
private void renderCartItems() {
binding.llSaleItems.removeAllViews();
List<SaleDTO.SaleItemDTO> items = viewModel.getCartItems().getValue();
List<ProductDTO> products = viewModel.getProductList().getValue();
if (items == null || products == null) return;
for (SaleDTO.SaleItemDTO item : items) {
String name = "Unknown";
BigDecimal price = BigDecimal.ZERO;
for (ProductDTO p : products) {
if (p.getProdId().equals(item.getProdId())) {
name = p.getProdName();
price = p.getProdPrice();
break;
}
}
addItemRow(name, item.getQuantity(), price);
}
}
private void addItemRow(String name, int qty, BigDecimal price) { private void addItemRow(String name, int qty, BigDecimal price) {
if (getContext() == null) return; if (getContext() == null) return;
LinearLayout row = new LinearLayout(getContext()); LinearLayout row = new LinearLayout(getContext());
@@ -230,18 +217,15 @@ public class SaleDetailFragment extends Fragment {
row.setPadding(0, 8, 0, 8); row.setPadding(0, 8, 0, 8);
TextView tvName = new TextView(getContext()); TextView tvName = new TextView(getContext());
tvName.setLayoutParams(new LinearLayout.LayoutParams( tvName.setLayoutParams(new LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 2f));
0, LinearLayout.LayoutParams.WRAP_CONTENT, 2f));
tvName.setText(name); tvName.setText(name);
TextView tvQty = new TextView(getContext()); TextView tvQty = new TextView(getContext());
tvQty.setLayoutParams(new LinearLayout.LayoutParams( tvQty.setLayoutParams(new LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f));
0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f));
tvQty.setText("x" + qty); tvQty.setText("x" + qty);
TextView tvPrice = new TextView(getContext()); TextView tvPrice = new TextView(getContext());
tvPrice.setLayoutParams(new LinearLayout.LayoutParams( tvPrice.setLayoutParams(new LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f));
0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f));
tvPrice.setText(price != null ? "$" + price : ""); tvPrice.setText(price != null ? "$" + price : "");
row.addView(tvName); row.addView(tvName);
@@ -251,59 +235,37 @@ public class SaleDetailFragment extends Fragment {
} }
private void updateTotal() { private void updateTotal() {
BigDecimal total = BigDecimal.ZERO; BigDecimal total = viewModel.calculateSubtotal();
for (SaleDTO.SaleItemDTO item : cartItems) {
for (ProductDTO p : productList) {
if (p.getProdId().equals(item.getProdId()) && p.getProdPrice() != null) {
total = total.add(p.getProdPrice()
.multiply(BigDecimal.valueOf(item.getQuantity())));
break;
}
}
}
binding.tvSaleSubtotal.setText("$" + total); binding.tvSaleSubtotal.setText("$" + total);
binding.tvSaleDetailTotal.setText("Total: $" + total); binding.tvSaleDetailTotal.setText("Total: $" + total);
} }
private void saveSale() { private void saveSale() {
if (binding.spinnerSaleStore.getSelectedItemPosition() == 0) { if (!InputValidator.isSpinnerSelected(binding.spinnerSaleStore, "Store")) return;
Toast.makeText(getContext(), "Select a store", Toast.LENGTH_SHORT).show();
return; if (viewModel.getCartItems().getValue() == null || viewModel.getCartItems().getValue().isEmpty()) {
}
if (cartItems.isEmpty()) {
Toast.makeText(getContext(), "Add at least one item", Toast.LENGTH_SHORT).show(); Toast.makeText(getContext(), "Add at least one item", Toast.LENGTH_SHORT).show();
return; return;
} }
StoreDTO store = storeList.get(binding.spinnerSaleStore.getSelectedItemPosition() - 1); DropdownDTO store = viewModel.getStoreList().getValue().get(binding.spinnerSaleStore.getSelectedItemPosition() - 1);
String payment = PAYMENT_METHODS[binding.spinnerPaymentMethod.getSelectedItemPosition()]; String payment = PAYMENT_METHODS[binding.spinnerPaymentMethod.getSelectedItemPosition()];
// Optional customer
Long customerId = null; Long customerId = null;
if (binding.spinnerSaleCustomer.getSelectedItemPosition() > 0) { if (binding.spinnerSaleCustomer.getSelectedItemPosition() > 0) {
customerId = customerList.get(binding.spinnerSaleCustomer.getSelectedItemPosition() - 1) customerId = viewModel.getCustomerList().getValue().get(binding.spinnerSaleCustomer.getSelectedItemPosition() - 1).getId();
.getCustomerId();
} }
SaleDTO dto = new SaleDTO( SaleDTO dto = new SaleDTO(store.getId(), payment, viewModel.getCartItems().getValue(), false, null, customerId);
store.getStoreId(),
payment,
cartItems,
false,
null,
customerId);
saleViewModel.createSale(dto).observe(getViewLifecycleOwner(), resource -> { viewModel.createSale(dto).observe(getViewLifecycleOwner(), resource -> {
if (resource != null) { if (resource != null) {
switch (resource.status) { setLoading(resource.status == Resource.Status.LOADING);
case SUCCESS: if (resource.status == Resource.Status.SUCCESS) {
Toast.makeText(getContext(), "Sale saved!", Toast.LENGTH_SHORT).show(); Toast.makeText(getContext(), "Sale saved!", Toast.LENGTH_SHORT).show();
navigateBack(); navigateBack();
break; } else if (resource.status == Resource.Status.ERROR) {
case ERROR: DialogUtils.showInfoDialog(requireContext(), "Save Error", resource.message);
Log.e("SALE_SAVE", "Error: " + resource.message);
DialogUtils.showInfoDialog(requireContext(), "Save Error", resource.message);
break;
} }
} }
}); });
@@ -313,7 +275,7 @@ public class SaleDetailFragment extends Fragment {
DialogUtils.showConfirmDialog(requireContext(), "Process Refund", DialogUtils.showConfirmDialog(requireContext(), "Process Refund",
"Are you sure you want to process a refund for this sale?", () -> { "Are you sure you want to process a refund for this sale?", () -> {
Bundle args = new Bundle(); Bundle args = new Bundle();
args.putLong("saleId", saleId); args.putLong("saleId", viewModel.getSaleId());
NavHostFragment.findNavController(this).navigate(R.id.nav_refund, args); NavHostFragment.findNavController(this).navigate(R.id.nav_refund, args);
}); });
} }

View File

@@ -17,10 +17,11 @@ import com.example.petstoremobile.R;
import com.example.petstoremobile.databinding.FragmentServiceDetailBinding; import com.example.petstoremobile.databinding.FragmentServiceDetailBinding;
import com.example.petstoremobile.dtos.ServiceDTO; import com.example.petstoremobile.dtos.ServiceDTO;
import com.example.petstoremobile.utils.ActivityLogger; import com.example.petstoremobile.utils.ActivityLogger;
import com.example.petstoremobile.utils.DateTimeUtils;
import com.example.petstoremobile.utils.DialogUtils; import com.example.petstoremobile.utils.DialogUtils;
import com.example.petstoremobile.utils.InputValidator; import com.example.petstoremobile.utils.InputValidator;
import com.example.petstoremobile.utils.Resource; import com.example.petstoremobile.utils.Resource;
import com.example.petstoremobile.viewmodels.ServiceViewModel; import com.example.petstoremobile.viewmodels.ServiceDetailViewModel;
import dagger.hilt.android.AndroidEntryPoint; import dagger.hilt.android.AndroidEntryPoint;
@@ -31,15 +32,12 @@ import dagger.hilt.android.AndroidEntryPoint;
public class ServiceDetailFragment extends Fragment { public class ServiceDetailFragment extends Fragment {
private FragmentServiceDetailBinding binding; private FragmentServiceDetailBinding binding;
private long serviceId; private ServiceDetailViewModel viewModel;
private boolean isEditing = false;
private ServiceViewModel viewModel;
@Override @Override
public void onCreate(Bundle savedInstanceState) { public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
viewModel = new ViewModelProvider(this).get(ServiceViewModel.class); viewModel = new ViewModelProvider(this).get(ServiceDetailViewModel.class);
} }
@Override @Override
@@ -53,78 +51,67 @@ public class ServiceDetailFragment extends Fragment {
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState); super.onViewCreated(view, savedInstanceState);
//get controls from layout and display the view depending on the mode
handleArguments(); handleArguments();
//set button click listeners
binding.btnBack.setOnClickListener(v -> navigateBack()); binding.btnBack.setOnClickListener(v -> navigateBack());
binding.btnSaveService.setOnClickListener(v -> saveService()); binding.btnSaveService.setOnClickListener(v -> saveService());
binding.btnDeleteService.setOnClickListener(v -> deleteService()); binding.btnDeleteService.setOnClickListener(v -> deleteService());
} }
private void setLoading(boolean loading) {
if (binding != null && binding.progressBar != null) {
binding.progressBar.setVisibility(loading ? View.VISIBLE : View.GONE);
}
}
@Override @Override
public void onDestroyView() { public void onDestroyView() {
super.onDestroyView(); super.onDestroyView();
binding = null; binding = null;
} }
/**
* Handles the saving of service data (adding or updating).
*/
private void saveService() { private void saveService() {
// Validates all fields using InputValidator
if (!InputValidator.isNotEmpty(binding.etServiceName, "Service Name")) return; if (!InputValidator.isNotEmpty(binding.etServiceName, "Service Name")) return;
if (!InputValidator.isNotEmpty(binding.etServiceDesc, "Description")) return; if (!InputValidator.isNotEmpty(binding.etServiceDesc, "Description")) return;
if (!InputValidator.isPositiveInteger(binding.etServiceDuration, "Duration")) return; if (!InputValidator.isPositiveInteger(binding.etServiceDuration, "Duration")) return;
if (!InputValidator.isPositiveDecimal(binding.etServicePrice, "Price")) return; if (!InputValidator.isPositiveDecimal(binding.etServicePrice, "Price")) return;
//get all the values from the fields
String name = binding.etServiceName.getText().toString().trim(); String name = binding.etServiceName.getText().toString().trim();
String desc = binding.etServiceDesc.getText().toString().trim(); String desc = binding.etServiceDesc.getText().toString().trim();
int duration = Integer.parseInt(binding.etServiceDuration.getText().toString().trim()); int duration = Integer.parseInt(binding.etServiceDuration.getText().toString().trim());
double price = Double.parseDouble(binding.etServicePrice.getText().toString().trim()); double price = Double.parseDouble(binding.etServicePrice.getText().toString().trim());
//create a service object to send to the API
ServiceDTO serviceDTO = new ServiceDTO(); ServiceDTO serviceDTO = new ServiceDTO();
serviceDTO.setServiceName(name); serviceDTO.setServiceName(name);
serviceDTO.setServiceDesc(desc); serviceDTO.setServiceDesc(desc);
serviceDTO.setServiceDuration(duration); serviceDTO.setServiceDuration(duration);
serviceDTO.setServicePrice(price); serviceDTO.setServicePrice(price);
//check if the service is being edited or added viewModel.saveService(serviceDTO).observe(getViewLifecycleOwner(), resource -> {
if (isEditing) { if (resource == null) return;
// Update existing service setLoading(resource.status == Resource.Status.LOADING);
serviceDTO.setServiceId(serviceId); if (resource.status == Resource.Status.SUCCESS) {
viewModel.updateService(serviceId, serviceDTO).observe(getViewLifecycleOwner(), resource -> { if (viewModel.isEditing()) {
if (resource.status == Resource.Status.SUCCESS) { ActivityLogger.logChange(requireContext(), "Service", "UPDATED", (int) viewModel.getServiceId());
ActivityLogger.logChange(requireContext(), "Service", "UPDATED", (int) serviceId);
Toast.makeText(getContext(), "Service updated successfully!", Toast.LENGTH_SHORT).show(); Toast.makeText(getContext(), "Service updated successfully!", Toast.LENGTH_SHORT).show();
navigateBack(); } else {
} else if (resource.status == Resource.Status.ERROR) {
Toast.makeText(getContext(), "Error: " + resource.message, Toast.LENGTH_SHORT).show();
}
});
} else {
viewModel.createService(serviceDTO).observe(getViewLifecycleOwner(), resource -> {
if (resource.status == Resource.Status.SUCCESS) {
ActivityLogger.log(requireContext(), "Added new Service: " + name); ActivityLogger.log(requireContext(), "Added new Service: " + name);
Toast.makeText(getContext(), "Service added successfully!", Toast.LENGTH_SHORT).show(); Toast.makeText(getContext(), "Service added successfully!", Toast.LENGTH_SHORT).show();
navigateBack();
} else if (resource.status == Resource.Status.ERROR) {
Toast.makeText(getContext(), "Error: " + resource.message, Toast.LENGTH_SHORT).show();
} }
}); navigateBack();
} } else if (resource.status == Resource.Status.ERROR) {
Toast.makeText(getContext(), "Error: " + resource.message, Toast.LENGTH_SHORT).show();
}
});
} }
/**
* Displays a confirmation dialog and handles the deletion of a service.
*/
private void deleteService() { private void deleteService() {
DialogUtils.showDeleteConfirmDialog(requireContext(), "Service", () -> DialogUtils.showDeleteConfirmDialog(requireContext(), "Service", () ->
viewModel.deleteService(serviceId).observe(getViewLifecycleOwner(), resource -> { viewModel.deleteService().observe(getViewLifecycleOwner(), resource -> {
if (resource == null) return;
setLoading(resource.status == Resource.Status.LOADING);
if (resource.status == Resource.Status.SUCCESS) { if (resource.status == Resource.Status.SUCCESS) {
ActivityLogger.logChange(requireContext(), "Service", "DELETED", (int) serviceId); ActivityLogger.logChange(requireContext(), "Service", "DELETED", (int) viewModel.getServiceId());
Toast.makeText(getContext(), "Service deleted successfully!", Toast.LENGTH_SHORT).show(); Toast.makeText(getContext(), "Service deleted successfully!", Toast.LENGTH_SHORT).show();
navigateBack(); navigateBack();
} else if (resource.status == Resource.Status.ERROR) { } else if (resource.status == Resource.Status.ERROR) {
@@ -133,30 +120,20 @@ public class ServiceDetailFragment extends Fragment {
})); }));
} }
/**
* Navigates back to the previous screen.
*/
private void navigateBack() { private void navigateBack() {
NavHostFragment.findNavController(this).popBackStack(); NavHostFragment.findNavController(this).popBackStack();
} }
/**
* Handles arguments passed to the fragment to determine if it's in edit or add mode.
*/
private void handleArguments() { private void handleArguments() {
// Service is being edited if the bundle contains a serviceId
if (getArguments() != null && getArguments().containsKey("serviceId")) { if (getArguments() != null && getArguments().containsKey("serviceId")) {
// Get service data from arguments and populate fields long serviceId = getArguments().getLong("serviceId");
isEditing = true; viewModel.setServiceId(serviceId);
serviceId = getArguments().getLong("serviceId");
binding.tvMode.setText("Edit Service"); binding.tvMode.setText("Edit Service");
binding.tvServiceId.setText("ID: " + serviceId); binding.tvServiceId.setText(DateTimeUtils.formatId(serviceId));
binding.btnDeleteService.setVisibility(View.VISIBLE); binding.btnDeleteService.setVisibility(View.VISIBLE);
loadServiceData(); loadServiceData();
} else { } else {
// Service is being added viewModel.setServiceId(-1);
// Set default values for add a new service
isEditing = false;
binding.tvMode.setText("Add Service"); binding.tvMode.setText("Add Service");
binding.tvServiceId.setVisibility(View.GONE); binding.tvServiceId.setVisibility(View.GONE);
binding.btnDeleteService.setVisibility(View.GONE); binding.btnDeleteService.setVisibility(View.GONE);
@@ -164,12 +141,10 @@ public class ServiceDetailFragment extends Fragment {
} }
} }
/**
* Fetches specific service details from the backend using the ID.
*/
private void loadServiceData() { private void loadServiceData() {
viewModel.getServiceById(serviceId).observe(getViewLifecycleOwner(), resource -> { viewModel.loadService().observe(getViewLifecycleOwner(), resource -> {
if (resource == null) return; if (resource == null) return;
setLoading(resource.status == Resource.Status.LOADING);
if (resource.status == Resource.Status.SUCCESS && resource.data != null) { if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
ServiceDTO s = resource.data; ServiceDTO s = resource.data;
binding.etServiceName.setText(s.getServiceName()); binding.etServiceName.setText(s.getServiceName());

View File

@@ -1,27 +1,28 @@
package com.example.petstoremobile.fragments.listfragments.detailfragments; package com.example.petstoremobile.fragments.listfragments.detailfragments;
import android.os.Bundle; import android.os.Bundle;
import android.util.Log;
import android.view.*; import android.view.*;
import android.widget.*; import android.widget.*;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import androidx.fragment.app.Fragment; import androidx.fragment.app.Fragment;
import androidx.lifecycle.ViewModelProvider; import androidx.lifecycle.ViewModelProvider;
import androidx.navigation.fragment.NavHostFragment; import androidx.navigation.fragment.NavHostFragment;
import com.example.petstoremobile.R; import com.example.petstoremobile.R;
import com.example.petstoremobile.databinding.FragmentStaffDetailBinding; import com.example.petstoremobile.databinding.FragmentStaffDetailBinding;
import com.example.petstoremobile.dtos.EmployeeDTO; import com.example.petstoremobile.dtos.EmployeeDTO;
import com.example.petstoremobile.viewmodels.EmployeeViewModel; import com.example.petstoremobile.utils.DialogUtils;
import com.example.petstoremobile.utils.InputValidator;
import com.example.petstoremobile.utils.SpinnerUtils;
import com.example.petstoremobile.utils.UIUtils;
import com.example.petstoremobile.viewmodels.StaffDetailViewModel;
import com.example.petstoremobile.utils.Resource;
import dagger.hilt.android.AndroidEntryPoint; import dagger.hilt.android.AndroidEntryPoint;
@AndroidEntryPoint @AndroidEntryPoint
public class StaffDetailFragment extends Fragment { public class StaffDetailFragment extends Fragment {
private FragmentStaffDetailBinding binding; private FragmentStaffDetailBinding binding;
private EmployeeViewModel employeeViewModel; private StaffDetailViewModel viewModel;
private long employeeId = -1;
private boolean isEditing = false;
private final String[] ROLES = {"STAFF", "ADMIN"}; private final String[] ROLES = {"STAFF", "ADMIN"};
private final String[] STATUSES = {"Active", "Inactive"}; private final String[] STATUSES = {"Active", "Inactive"};
@@ -30,7 +31,7 @@ public class StaffDetailFragment extends Fragment {
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) { Bundle savedInstanceState) {
binding = FragmentStaffDetailBinding.inflate(inflater, container, false); binding = FragmentStaffDetailBinding.inflate(inflater, container, false);
employeeViewModel = new ViewModelProvider(this).get(EmployeeViewModel.class); viewModel = new ViewModelProvider(this).get(StaffDetailViewModel.class);
setupSpinners(); setupSpinners();
handleArguments(); handleArguments();
@@ -38,21 +39,22 @@ public class StaffDetailFragment extends Fragment {
binding.btnStaffBack.setOnClickListener(v -> navigateBack()); binding.btnStaffBack.setOnClickListener(v -> navigateBack());
binding.btnSaveStaff.setOnClickListener(v -> save()); binding.btnSaveStaff.setOnClickListener(v -> save());
binding.btnDeleteStaff.setOnClickListener(v -> confirmDelete()); binding.btnDeleteStaff.setOnClickListener(v -> confirmDelete());
UIUtils.formatPhoneInput(binding.etStaffPhone);
return binding.getRoot(); return binding.getRoot();
} }
private void setupSpinners() { private void setupSpinners() {
binding.spinnerStaffRole.setAdapter(new ArrayAdapter<>(requireContext(), SpinnerUtils.setupStringSpinner(requireContext(), binding.spinnerStaffRole, ROLES);
android.R.layout.simple_spinner_item, ROLES)); SpinnerUtils.setupStringSpinner(requireContext(), binding.spinnerStaffStatus, STATUSES);
binding.spinnerStaffStatus.setAdapter(new ArrayAdapter<>(requireContext(),
android.R.layout.simple_spinner_item, STATUSES));
} }
private void handleArguments() { private void handleArguments() {
Bundle a = getArguments(); Bundle a = getArguments();
if (a != null && a.getBoolean("isEditing", false)) { if (a != null && a.getBoolean("isEditing", false)) {
isEditing = true; long employeeId = a.getLong("employeeId", -1);
employeeId = a.getLong("employeeId", -1); viewModel.setEmployeeId(employeeId, true);
binding.tvStaffMode.setText("Edit Staff Account"); binding.tvStaffMode.setText("Edit Staff Account");
binding.tvStaffId.setText("ID: " + employeeId); binding.tvStaffId.setText("ID: " + employeeId);
@@ -64,52 +66,50 @@ public class StaffDetailFragment extends Fragment {
binding.etStaffPhone.setText(a.getString("phone", "")); binding.etStaffPhone.setText(a.getString("phone", ""));
binding.btnDeleteStaff.setVisibility(View.VISIBLE); binding.btnDeleteStaff.setVisibility(View.VISIBLE);
// Pre-fill role SpinnerUtils.setSelectionByValue(binding.spinnerStaffRole, a.getString("role", "STAFF"));
String role = a.getString("role", "STAFF"); binding.spinnerStaffStatus.setSelection(a.getBoolean("active", true) ? 0 : 1);
for (int i = 0; i < ROLES.length; i++) {
if (ROLES[i].equals(role)) {
binding.spinnerStaffRole.setSelection(i);
break;
}
}
// Pre-fill status
boolean active = a.getBoolean("active", true);
binding.spinnerStaffStatus.setSelection(active ? 0 : 1);
} else { } else {
isEditing = false; viewModel.setEmployeeId(-1, false);
employeeId = -1;
binding.tvStaffMode.setText("Add Staff Account"); binding.tvStaffMode.setText("Add Staff Account");
binding.btnDeleteStaff.setVisibility(View.GONE); binding.btnDeleteStaff.setVisibility(View.GONE);
binding.tvStaffId.setVisibility(View.GONE); binding.tvStaffId.setVisibility(View.GONE);
} }
} }
private void setLoading(boolean loading) {
if (binding != null && binding.progressBar != null) {
binding.progressBar.setVisibility(loading ? View.VISIBLE : View.GONE);
}
}
private void save() { private void save() {
String username = binding.etStaffUsername.getText() != null ? binding.etStaffUsername.getText().toString().trim() : ""; if (!InputValidator.isNotEmpty(binding.etStaffUsername, "Username")) return;
String password = binding.etStaffPassword.getText() != null ? binding.etStaffPassword.getText().toString().trim() : "";
String firstName = binding.etStaffFirstName.getText() != null ? binding.etStaffFirstName.getText().toString().trim() : ""; if (!viewModel.isEditing()) {
String lastName = binding.etStaffLastName.getText() != null ? binding.etStaffLastName.getText().toString().trim() : ""; if (!InputValidator.isNotEmpty(binding.etStaffPassword, "Password")) return;
String email = binding.etStaffEmail.getText() != null ? binding.etStaffEmail.getText().toString().trim() : ""; String pass = binding.etStaffPassword.getText().toString();
String phone = binding.etStaffPhone.getText() != null ? binding.etStaffPhone.getText().toString().trim() : ""; if (pass.length() < 6) {
binding.etStaffPassword.setError("At least 6 characters");
binding.etStaffPassword.requestFocus();
return;
}
}
if (!InputValidator.isNotEmpty(binding.etStaffFirstName, "First Name")) return;
if (!InputValidator.isNotEmpty(binding.etStaffLastName, "Last Name")) return;
if (!InputValidator.isValidEmail(binding.etStaffEmail)) return;
if (!InputValidator.isValidPhone(binding.etStaffPhone)) return;
String username = binding.etStaffUsername.getText().toString().trim();
String password = binding.etStaffPassword.getText().toString().trim();
String firstName = binding.etStaffFirstName.getText().toString().trim();
String lastName = binding.etStaffLastName.getText().toString().trim();
String email = binding.etStaffEmail.getText().toString().trim();
String phone = binding.etStaffPhone.getText().toString().trim();
String role = ROLES[binding.spinnerStaffRole.getSelectedItemPosition()]; String role = ROLES[binding.spinnerStaffRole.getSelectedItemPosition()];
boolean active = binding.spinnerStaffStatus.getSelectedItemPosition() == 0; boolean active = binding.spinnerStaffStatus.getSelectedItemPosition() == 0;
// Validation
if (username.isEmpty()) { binding.etStaffUsername.setError("Required"); return; }
if (!isEditing && password.isEmpty()) {
binding.etStaffPassword.setError("Required for new account"); return;
}
if (!isEditing && password.length() < 6) {
binding.etStaffPassword.setError("At least 6 characters"); return;
}
if (firstName.isEmpty()) { binding.etStaffFirstName.setError("Required"); return; }
if (lastName.isEmpty()) { binding.etStaffLastName.setError("Required"); return; }
if (email.isEmpty()) { binding.etStaffEmail.setError("Required"); return; }
if (phone.isEmpty()) { binding.etStaffPhone.setError("Required"); return; }
EmployeeDTO dto = new EmployeeDTO( EmployeeDTO dto = new EmployeeDTO(
username, username,
password.isEmpty() ? null : password, password.isEmpty() ? null : password,
@@ -121,56 +121,32 @@ public class StaffDetailFragment extends Fragment {
active active
); );
if (isEditing && employeeId > 0) { viewModel.saveEmployee(dto).observe(getViewLifecycleOwner(), resource -> {
employeeViewModel.updateEmployee(employeeId, dto).observe(getViewLifecycleOwner(), resource -> { if (resource != null) {
if (resource != null) { setLoading(resource.status == Resource.Status.LOADING);
switch (resource.status) { if (resource.status == Resource.Status.SUCCESS) {
case SUCCESS: Toast.makeText(getContext(), viewModel.isEditing() ? "Updated successfully" : "Staff account created", Toast.LENGTH_SHORT).show();
Toast.makeText(getContext(), "Updated successfully", Toast.LENGTH_SHORT).show(); navigateBack();
navigateBack(); } else if (resource.status == Resource.Status.ERROR) {
break; Toast.makeText(getContext(), "Error: " + resource.message, Toast.LENGTH_LONG).show();
case ERROR:
Toast.makeText(getContext(), "Error: " + resource.message, Toast.LENGTH_LONG).show();
break;
}
} }
}); }
} else { });
employeeViewModel.createEmployee(dto).observe(getViewLifecycleOwner(), resource -> {
if (resource != null) {
switch (resource.status) {
case SUCCESS:
Toast.makeText(getContext(), "Staff account created", Toast.LENGTH_SHORT).show();
navigateBack();
break;
case ERROR:
Toast.makeText(getContext(), "Error: " + resource.message, Toast.LENGTH_LONG).show();
break;
}
}
});
}
} }
private void confirmDelete() { private void confirmDelete() {
new AlertDialog.Builder(requireContext()) DialogUtils.showDeleteConfirmDialog(requireContext(), "Staff Account", () ->
.setTitle("Delete Staff Account?") viewModel.deleteEmployee().observe(getViewLifecycleOwner(), resource -> {
.setMessage("This will permanently delete this staff account.") if (resource != null) {
.setPositiveButton("Yes", (d, w) -> setLoading(resource.status == Resource.Status.LOADING);
employeeViewModel.deleteEmployee(employeeId).observe(getViewLifecycleOwner(), resource -> { if (resource.status == Resource.Status.SUCCESS) {
if (resource != null) { navigateBack();
switch (resource.status) { } else if (resource.status == Resource.Status.ERROR) {
case SUCCESS: Toast.makeText(getContext(), "Delete failed: " + resource.message,
navigateBack(); Toast.LENGTH_SHORT).show();
break; }
case ERROR: }
Toast.makeText(getContext(), "Delete failed: " + resource.message, }));
Toast.LENGTH_SHORT).show();
break;
}
}
}))
.setNegativeButton("No", null).show();
} }
private void navigateBack() { private void navigateBack() {

View File

@@ -20,7 +20,7 @@ import com.example.petstoremobile.utils.DialogUtils;
import com.example.petstoremobile.utils.InputValidator; import com.example.petstoremobile.utils.InputValidator;
import com.example.petstoremobile.utils.Resource; import com.example.petstoremobile.utils.Resource;
import com.example.petstoremobile.utils.UIUtils; import com.example.petstoremobile.utils.UIUtils;
import com.example.petstoremobile.viewmodels.SupplierViewModel; import com.example.petstoremobile.viewmodels.SupplierDetailViewModel;
import dagger.hilt.android.AndroidEntryPoint; import dagger.hilt.android.AndroidEntryPoint;
@@ -31,15 +31,12 @@ import dagger.hilt.android.AndroidEntryPoint;
public class SupplierDetailFragment extends Fragment { public class SupplierDetailFragment extends Fragment {
private FragmentSupplierDetailBinding binding; private FragmentSupplierDetailBinding binding;
private long supId; private SupplierDetailViewModel viewModel;
private boolean isEditing = false;
private SupplierViewModel viewModel;
@Override @Override
public void onCreate(Bundle savedInstanceState) { public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
viewModel = new ViewModelProvider(this).get(SupplierViewModel.class); viewModel = new ViewModelProvider(this).get(SupplierDetailViewModel.class);
} }
@Override @Override
@@ -53,42 +50,39 @@ public class SupplierDetailFragment extends Fragment {
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState); super.onViewCreated(view, savedInstanceState);
// Add phone number formatting (CA) and limit length to 14 characters
UIUtils.formatPhoneInput(binding.etSupPhone); UIUtils.formatPhoneInput(binding.etSupPhone);
handleArguments(); handleArguments();
//set button click listeners
binding.btnBack.setOnClickListener(v -> navigateBack()); binding.btnBack.setOnClickListener(v -> navigateBack());
binding.btnSaveSupplier.setOnClickListener(v -> saveSupplier()); binding.btnSaveSupplier.setOnClickListener(v -> saveSupplier());
binding.btnDeleteSupplier.setOnClickListener(v -> deleteSupplier()); binding.btnDeleteSupplier.setOnClickListener(v -> deleteSupplier());
} }
private void setLoading(boolean loading) {
if (binding != null && binding.progressBar != null) {
binding.progressBar.setVisibility(loading ? View.VISIBLE : View.GONE);
}
}
@Override @Override
public void onDestroyView() { public void onDestroyView() {
super.onDestroyView(); super.onDestroyView();
binding = null; binding = null;
} }
/**
* Handles the saving of supplier data (adding or updating).
*/
private void saveSupplier() { private void saveSupplier() {
// Validates all fields using InputValidator
if (!InputValidator.isNotEmpty(binding.etSupCompany, "Company Name")) return; if (!InputValidator.isNotEmpty(binding.etSupCompany, "Company Name")) return;
if (!InputValidator.isNotEmpty(binding.etSupContactFirstName, "First Name")) return; if (!InputValidator.isNotEmpty(binding.etSupContactFirstName, "First Name")) return;
if (!InputValidator.isNotEmpty(binding.etSupContactLastName, "Last Name")) return; if (!InputValidator.isNotEmpty(binding.etSupContactLastName, "Last Name")) return;
if (!InputValidator.isValidEmail(binding.etSupEmail)) return; if (!InputValidator.isValidEmail(binding.etSupEmail)) return;
if (!InputValidator.isValidPhone(binding.etSupPhone)) return; if (!InputValidator.isValidPhone(binding.etSupPhone)) return;
//get all the values from the fields
String company = binding.etSupCompany.getText().toString().trim(); String company = binding.etSupCompany.getText().toString().trim();
String firstName = binding.etSupContactFirstName.getText().toString().trim(); String firstName = binding.etSupContactFirstName.getText().toString().trim();
String lastName = binding.etSupContactLastName.getText().toString().trim(); String lastName = binding.etSupContactLastName.getText().toString().trim();
String email = binding.etSupEmail.getText().toString().trim(); String email = binding.etSupEmail.getText().toString().trim();
String phone = binding.etSupPhone.getText().toString().trim(); String phone = binding.etSupPhone.getText().toString().trim();
//create a supplier object to send to the API
SupplierDTO supplierDTO = new SupplierDTO(); SupplierDTO supplierDTO = new SupplierDTO();
supplierDTO.setSupCompany(company); supplierDTO.setSupCompany(company);
supplierDTO.setSupContactFirstName(firstName); supplierDTO.setSupContactFirstName(firstName);
@@ -96,41 +90,31 @@ public class SupplierDetailFragment extends Fragment {
supplierDTO.setSupEmail(email); supplierDTO.setSupEmail(email);
supplierDTO.setSupPhone(phone); supplierDTO.setSupPhone(phone);
//check if the supplier is being edited or added viewModel.saveSupplier(supplierDTO).observe(getViewLifecycleOwner(), resource -> {
if (isEditing) { if (resource == null) return;
// Update existing supplier setLoading(resource.status == Resource.Status.LOADING);
supplierDTO.setSupId(supId); if (resource.status == Resource.Status.SUCCESS) {
viewModel.updateSupplier(supId, supplierDTO).observe(getViewLifecycleOwner(), resource -> { if (viewModel.isEditing()) {
if (resource.status == Resource.Status.SUCCESS) { ActivityLogger.logChange(requireContext(), "Supplier", "UPDATED", (int) viewModel.getSupId());
ActivityLogger.logChange(requireContext(), "Supplier", "UPDATED", (int) supId);
Toast.makeText(getContext(), "Supplier updated successfully!", Toast.LENGTH_SHORT).show(); Toast.makeText(getContext(), "Supplier updated successfully!", Toast.LENGTH_SHORT).show();
navigateBack(); } else {
} else if (resource.status == Resource.Status.ERROR) {
Toast.makeText(getContext(), "Error: " + resource.message, Toast.LENGTH_SHORT).show();
}
});
} else {
// Add new supplier
viewModel.createSupplier(supplierDTO).observe(getViewLifecycleOwner(), resource -> {
if (resource.status == Resource.Status.SUCCESS) {
ActivityLogger.log(requireContext(), "Added new Supplier: " + company); ActivityLogger.log(requireContext(), "Added new Supplier: " + company);
Toast.makeText(getContext(), "Supplier added successfully!", Toast.LENGTH_SHORT).show(); Toast.makeText(getContext(), "Supplier added successfully!", Toast.LENGTH_SHORT).show();
navigateBack();
} else if (resource.status == Resource.Status.ERROR) {
Toast.makeText(getContext(), "Error: " + resource.message, Toast.LENGTH_SHORT).show();
} }
}); navigateBack();
} } else if (resource.status == Resource.Status.ERROR) {
Toast.makeText(getContext(), "Error: " + resource.message, Toast.LENGTH_SHORT).show();
}
});
} }
/**
* Displays a confirmation dialog and handles the deletion of a supplier.
*/
private void deleteSupplier() { private void deleteSupplier() {
DialogUtils.showDeleteConfirmDialog(requireContext(), "Supplier", () -> DialogUtils.showDeleteConfirmDialog(requireContext(), "Supplier", () ->
viewModel.deleteSupplier(supId).observe(getViewLifecycleOwner(), resource -> { viewModel.deleteSupplier().observe(getViewLifecycleOwner(), resource -> {
if (resource == null) return;
setLoading(resource.status == Resource.Status.LOADING);
if (resource.status == Resource.Status.SUCCESS) { if (resource.status == Resource.Status.SUCCESS) {
ActivityLogger.logChange(requireContext(), "Supplier", "DELETED", (int) supId); ActivityLogger.logChange(requireContext(), "Supplier", "DELETED", (int) viewModel.getSupId());
Toast.makeText(getContext(), "Supplier deleted successfully!", Toast.LENGTH_SHORT).show(); Toast.makeText(getContext(), "Supplier deleted successfully!", Toast.LENGTH_SHORT).show();
navigateBack(); navigateBack();
} else if (resource.status == Resource.Status.ERROR) { } else if (resource.status == Resource.Status.ERROR) {
@@ -139,31 +123,21 @@ public class SupplierDetailFragment extends Fragment {
})); }));
} }
/**
* Navigates back to the previous screen.
*/
private void navigateBack() { private void navigateBack() {
NavHostFragment.findNavController(this).popBackStack(); NavHostFragment.findNavController(this).popBackStack();
} }
/**
* Handles arguments passed to the fragment to determine if it's in edit or add mode.
*/
private void handleArguments() { private void handleArguments() {
// Supplier is being edited if the bundle contains a supId
if (getArguments() != null && getArguments().containsKey("supId")) { if (getArguments() != null && getArguments().containsKey("supId")) {
// Get supplier data from arguments and populate fields long supId = getArguments().getLong("supId");
isEditing = true; viewModel.setSupId(supId);
supId = getArguments().getLong("supId");
binding.tvMode.setText("Edit Supplier"); binding.tvMode.setText("Edit Supplier");
binding.tvSupId.setText("ID: " + supId); binding.tvSupId.setText("ID: " + supId);
binding.tvSupId.setVisibility(View.VISIBLE); binding.tvSupId.setVisibility(View.VISIBLE);
binding.btnDeleteSupplier.setVisibility(View.VISIBLE); binding.btnDeleteSupplier.setVisibility(View.VISIBLE);
loadSupplierData(); loadSupplierData();
} else { } else {
// Supplier is being added viewModel.setSupId(-1);
// Set default values for add a new supplier
isEditing = false;
binding.tvMode.setText("Add Supplier"); binding.tvMode.setText("Add Supplier");
binding.tvSupId.setVisibility(View.GONE); binding.tvSupId.setVisibility(View.GONE);
binding.btnDeleteSupplier.setVisibility(View.GONE); binding.btnDeleteSupplier.setVisibility(View.GONE);
@@ -171,12 +145,10 @@ public class SupplierDetailFragment extends Fragment {
} }
} }
/**
* Fetches specific supplier details from the backend using the ID.
*/
private void loadSupplierData() { private void loadSupplierData() {
viewModel.getSupplierById(supId).observe(getViewLifecycleOwner(), resource -> { viewModel.loadSupplier().observe(getViewLifecycleOwner(), resource -> {
if (resource == null) return; if (resource == null) return;
setLoading(resource.status == Resource.Status.LOADING);
if (resource.status == Resource.Status.SUCCESS && resource.data != null) { if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
SupplierDTO s = resource.data; SupplierDTO s = resource.data;
binding.etSupCompany.setText(s.getSupCompany()); binding.etSupCompany.setText(s.getSupCompany());

View File

@@ -23,7 +23,7 @@ import com.example.petstoremobile.utils.FileUtils;
import com.example.petstoremobile.utils.GlideUtils; import com.example.petstoremobile.utils.GlideUtils;
import com.example.petstoremobile.utils.ImagePickerHelper; import com.example.petstoremobile.utils.ImagePickerHelper;
import com.example.petstoremobile.utils.Resource; import com.example.petstoremobile.utils.Resource;
import com.example.petstoremobile.viewmodels.PetViewModel; import com.example.petstoremobile.viewmodels.PetProfileViewModel;
import java.io.File; import java.io.File;
import java.util.Locale; import java.util.Locale;
@@ -46,17 +46,13 @@ public class PetProfileFragment extends Fragment {
@Inject @Named("baseUrl") String baseUrl; @Inject @Named("baseUrl") String baseUrl;
@Inject TokenManager tokenManager; @Inject TokenManager tokenManager;
private PetViewModel viewModel; private PetProfileViewModel viewModel;
private ImagePickerHelper imagePickerHelper; private ImagePickerHelper imagePickerHelper;
/**
* Initializes activity launchers for gallery, camera, and permissions.
*/
@Override @Override
public void onCreate(Bundle savedInstanceState) { public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
viewModel = new ViewModelProvider(this).get(PetViewModel.class); viewModel = new ViewModelProvider(this).get(PetProfileViewModel.class);
imagePickerHelper = new ImagePickerHelper(this, "pet_photo.jpg", new ImagePickerHelper.ImagePickerListener() { imagePickerHelper = new ImagePickerHelper(this, "pet_photo.jpg", new ImagePickerHelper.ImagePickerListener() {
@Override @Override
@@ -71,34 +67,27 @@ public class PetProfileFragment extends Fragment {
}); });
} }
/**
* Inflates the layout using view binding, initializes views, and sets up click listeners.
*/
@Override @Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) { Bundle savedInstanceState) {
binding = FragmentPetProfileBinding.inflate(inflater, container, false); binding = FragmentPetProfileBinding.inflate(inflater, container, false);
// Set pet details to display
if (getArguments() != null) { if (getArguments() != null) {
petId = getArguments().getLong("petId"); petId = getArguments().getLong("petId");
loadPetData(); loadPetData();
loadPetImage((int) petId); loadPetImage((int) petId);
} }
//set button click listeners
binding.btnBack.setOnClickListener(v -> { binding.btnBack.setOnClickListener(v -> {
NavHostFragment.findNavController(this).popBackStack(); NavHostFragment.findNavController(this).popBackStack();
}); });
//Make the edit button go to the pet detail view
binding.btnEditPet.setOnClickListener(v -> { binding.btnEditPet.setOnClickListener(v -> {
Bundle args = new Bundle(); Bundle args = new Bundle();
args.putLong("petId", petId); args.putLong("petId", petId);
NavHostFragment.findNavController(this).navigate(R.id.nav_pet_detail, args); NavHostFragment.findNavController(this).navigate(R.id.nav_pet_detail, args);
}); });
//Make change photo button ask user to select a new photo
binding.btnChangePhoto.setOnClickListener(v -> { binding.btnChangePhoto.setOnClickListener(v -> {
imagePickerHelper.showImagePickerDialog("Change Pet Photo", hasImage); imagePickerHelper.showImagePickerDialog("Change Pet Photo", hasImage);
}); });
@@ -106,18 +95,22 @@ public class PetProfileFragment extends Fragment {
return binding.getRoot(); return binding.getRoot();
} }
private void setLoading(boolean loading) {
if (binding != null && binding.progressBar != null) {
binding.progressBar.setVisibility(loading ? View.VISIBLE : View.GONE);
}
}
@Override @Override
public void onDestroyView() { public void onDestroyView() {
super.onDestroyView(); super.onDestroyView();
binding = null; binding = null;
} }
/**
* Fetches current pet data from the backend and updates the UI.
*/
private void loadPetData() { private void loadPetData() {
viewModel.getPetById(petId).observe(getViewLifecycleOwner(), resource -> { viewModel.getPetById(petId).observe(getViewLifecycleOwner(), resource -> {
if (resource == null) return; if (resource == null) return;
setLoading(resource.status == Resource.Status.LOADING);
if (resource.status == Resource.Status.SUCCESS && resource.data != null) { if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
PetDTO pet = resource.data; PetDTO pet = resource.data;
binding.tvPetName.setText(pet.getPetName()); binding.tvPetName.setText(pet.getPetName());
@@ -133,7 +126,6 @@ public class PetProfileFragment extends Fragment {
String status = pet.getPetStatus(); String status = pet.getPetStatus();
// Display owner name only if the pet is Adopted or Owned
if ("Adopted".equalsIgnoreCase(status) || "Owned".equalsIgnoreCase(status)) { if ("Adopted".equalsIgnoreCase(status) || "Owned".equalsIgnoreCase(status)) {
binding.layoutPetOwner.setVisibility(View.VISIBLE); binding.layoutPetOwner.setVisibility(View.VISIBLE);
if (pet.getCustomerName() != null && !pet.getCustomerName().isEmpty()) { if (pet.getCustomerName() != null && !pet.getCustomerName().isEmpty()) {
@@ -145,7 +137,6 @@ public class PetProfileFragment extends Fragment {
binding.layoutPetOwner.setVisibility(View.GONE); binding.layoutPetOwner.setVisibility(View.GONE);
} }
// Display store name only if the pet is Adopted or Available
if ("Available".equalsIgnoreCase(status) || "Adopted".equalsIgnoreCase(status)) { if ("Available".equalsIgnoreCase(status) || "Adopted".equalsIgnoreCase(status)) {
binding.layoutPetStore.setVisibility(View.VISIBLE); binding.layoutPetStore.setVisibility(View.VISIBLE);
if (pet.getStoreName() != null && !pet.getStoreName().isEmpty()) { if (pet.getStoreName() != null && !pet.getStoreName().isEmpty()) {
@@ -162,9 +153,6 @@ public class PetProfileFragment extends Fragment {
}); });
} }
/**
* Fetches and displays the pet\'s image from the server.
*/
private void loadPetImage(int petId) { private void loadPetImage(int petId) {
String imageUrl = baseUrl + String.format(Locale.US, PetApi.PET_IMAGE_PATH, petId); String imageUrl = baseUrl + String.format(Locale.US, PetApi.PET_IMAGE_PATH, petId);
String token = tokenManager.getToken(); String token = tokenManager.getToken();
@@ -182,27 +170,22 @@ public class PetProfileFragment extends Fragment {
}); });
} }
/**
* Uploads a selected or captured image a pet photo through the ViewModel.
*/
private void uploadPetImage(Uri uri) { private void uploadPetImage(Uri uri) {
try { try {
File file = FileUtils.getFileFromUri(requireContext(), uri); File file = FileUtils.getFileFromUri(requireContext(), uri);
if (file == null) return; if (file == null) return;
// Create RequestBody for file upload
RequestBody requestFile = RequestBody.create(file, MediaType.parse(requireContext().getContentResolver().getType(uri))); RequestBody requestFile = RequestBody.create(file, MediaType.parse(requireContext().getContentResolver().getType(uri)));
MultipartBody.Part body = MultipartBody.Part.createFormData("image", file.getName(), requestFile); MultipartBody.Part body = MultipartBody.Part.createFormData("image", file.getName(), requestFile);
// Use ViewModel to upload image
viewModel.uploadPetImage(petId, body).observe(getViewLifecycleOwner(), resource -> { viewModel.uploadPetImage(petId, body).observe(getViewLifecycleOwner(), resource -> {
if (resource != null && resource.status != Resource.Status.LOADING) { if (resource == null) return;
if (resource.status == Resource.Status.SUCCESS) { setLoading(resource.status == Resource.Status.LOADING);
Toast.makeText(getContext(), "Pet photo updated successfully", Toast.LENGTH_SHORT).show(); if (resource.status == Resource.Status.SUCCESS) {
loadPetImage((int) petId); Toast.makeText(getContext(), "Pet photo updated successfully", Toast.LENGTH_SHORT).show();
} else { loadPetImage((int) petId);
Toast.makeText(getContext(), "Upload failed: " + resource.message, Toast.LENGTH_SHORT).show(); } else if (resource.status == Resource.Status.ERROR) {
} Toast.makeText(getContext(), "Upload failed: " + resource.message, Toast.LENGTH_SHORT).show();
} }
}); });
} catch (Exception e) { } catch (Exception e) {
@@ -210,19 +193,16 @@ public class PetProfileFragment extends Fragment {
} }
} }
/**
* Sends a request to the ViewModel to remove the current pet photo.
*/
private void deletePetImage() { private void deletePetImage() {
viewModel.deletePetImage(petId).observe(getViewLifecycleOwner(), resource -> { viewModel.deletePetImage(petId).observe(getViewLifecycleOwner(), resource -> {
if (resource != null && resource.status != Resource.Status.LOADING) { if (resource == null) return;
if (resource.status == Resource.Status.SUCCESS) { setLoading(resource.status == Resource.Status.LOADING);
Toast.makeText(getContext(), "Pet photo removed", Toast.LENGTH_SHORT).show(); if (resource.status == Resource.Status.SUCCESS) {
hasImage = false; Toast.makeText(getContext(), "Pet photo removed", Toast.LENGTH_SHORT).show();
binding.imgPet.setImageResource(R.drawable.placeholder); hasImage = false;
} else { binding.imgPet.setImageResource(R.drawable.placeholder);
Toast.makeText(getContext(), "Delete failed: " + resource.message, Toast.LENGTH_SHORT).show(); } else if (resource.status == Resource.Status.ERROR) {
} Toast.makeText(getContext(), "Delete failed: " + resource.message, Toast.LENGTH_SHORT).show();
} }
}); });
} }

View File

@@ -6,13 +6,15 @@ public class Chat {
private String lastMessage; private String lastMessage;
private Long customerId; private Long customerId;
private Long staffId; private Long staffId;
private String status;
public Chat(String chatId, String customerName, String lastMessage, Long customerId, Long staffId) { public Chat(String chatId, String customerName, String lastMessage, Long customerId, Long staffId, String status) {
this.chatId = chatId; this.chatId = chatId;
this.customerName = customerName; this.customerName = customerName;
this.lastMessage = lastMessage; this.lastMessage = lastMessage;
this.customerId = customerId; this.customerId = customerId;
this.staffId = staffId; this.staffId = staffId;
this.status = status;
} }
public String getChatId() { public String getChatId() {
@@ -34,4 +36,8 @@ public class Chat {
public Long getStaffId() { public Long getStaffId() {
return staffId; return staffId;
} }
public String getStatus() {
return status;
}
} }

View File

@@ -9,7 +9,8 @@ public class Message {
private Boolean isRead; private Boolean isRead;
private String attachmentUrl; private String attachmentUrl;
private String attachmentName; private String attachmentName;
private String attachmentType; private String attachmentMimeType;
private Long attachmentSizeBytes;
public Message() {} public Message() {}
@@ -43,6 +44,9 @@ public class Message {
public String getAttachmentName() { return attachmentName; } public String getAttachmentName() { return attachmentName; }
public void setAttachmentName(String attachmentName) { this.attachmentName = attachmentName; } public void setAttachmentName(String attachmentName) { this.attachmentName = attachmentName; }
public String getAttachmentType() { return attachmentType; } public String getAttachmentMimeType() { return attachmentMimeType; }
public void setAttachmentType(String attachmentType) { this.attachmentType = attachmentType; } public void setAttachmentMimeType(String attachmentMimeType) { this.attachmentMimeType = attachmentMimeType; }
public Long getAttachmentSizeBytes() { return attachmentSizeBytes; }
public void setAttachmentSizeBytes(Long attachmentSizeBytes) { this.attachmentSizeBytes = attachmentSizeBytes; }
} }

View File

@@ -17,6 +17,10 @@ import java.util.List;
import javax.inject.Inject; import javax.inject.Inject;
import javax.inject.Singleton; import javax.inject.Singleton;
import okhttp3.MultipartBody;
import okhttp3.RequestBody;
import okhttp3.ResponseBody;
/** /**
* Repository for handling chat-related data operations. * Repository for handling chat-related data operations.
*/ */
@@ -55,6 +59,20 @@ public class ChatRepository extends BaseRepository {
return executeCall(messageApi.sendMessage(conversationId, request)); return executeCall(messageApi.sendMessage(conversationId, request));
} }
/**
* Sends a message with an attachment.
*/
public LiveData<Resource<MessageDTO>> sendMessageWithAttachment(Long conversationId, MultipartBody.Part content, MultipartBody.Part attachment) {
return executeCall(messageApi.sendMessageWithAttachment(conversationId, content, attachment));
}
/**
* Downloads an attachment for a specific message.
*/
public LiveData<Resource<ResponseBody>> downloadAttachment(Long messageId) {
return executeCall(messageApi.downloadAttachment(messageId));
}
/** /**
* Fetches a paginated list of customers. * Fetches a paginated list of customers.
*/ */

View File

@@ -4,9 +4,12 @@ import androidx.lifecycle.LiveData;
import com.example.petstoremobile.api.CustomerApi; import com.example.petstoremobile.api.CustomerApi;
import com.example.petstoremobile.dtos.CustomerDTO; import com.example.petstoremobile.dtos.CustomerDTO;
import com.example.petstoremobile.dtos.DropdownDTO;
import com.example.petstoremobile.dtos.PageResponse; import com.example.petstoremobile.dtos.PageResponse;
import com.example.petstoremobile.utils.Resource; import com.example.petstoremobile.utils.Resource;
import java.util.List;
import javax.inject.Inject; import javax.inject.Inject;
import javax.inject.Singleton; import javax.inject.Singleton;
@@ -33,4 +36,11 @@ public class CustomerRepository extends BaseRepository {
public LiveData<Resource<CustomerDTO>> getCustomerById(Long id) { public LiveData<Resource<CustomerDTO>> getCustomerById(Long id) {
return executeCall(customerApi.getCustomerById(id)); return executeCall(customerApi.getCustomerById(id));
} }
}
/**
* Retrieves a list of customer dropdowns from the API.
*/
public LiveData<Resource<List<DropdownDTO>>> getCustomerDropdowns() {
return executeCall(customerApi.getCustomerDropdowns());
}
}

View File

@@ -4,10 +4,13 @@ import androidx.lifecycle.LiveData;
import com.example.petstoremobile.api.PetApi; import com.example.petstoremobile.api.PetApi;
import com.example.petstoremobile.dtos.BulkDeleteRequest; import com.example.petstoremobile.dtos.BulkDeleteRequest;
import com.example.petstoremobile.dtos.DropdownDTO;
import com.example.petstoremobile.dtos.PageResponse; import com.example.petstoremobile.dtos.PageResponse;
import com.example.petstoremobile.dtos.PetDTO; import com.example.petstoremobile.dtos.PetDTO;
import com.example.petstoremobile.utils.Resource; import com.example.petstoremobile.utils.Resource;
import java.util.List;
import javax.inject.Inject; import javax.inject.Inject;
import javax.inject.Singleton; import javax.inject.Singleton;
@@ -26,8 +29,22 @@ public class PetRepository extends BaseRepository {
/** /**
* Retrieves a paginated list of pets from the API with optional filters. * Retrieves a paginated list of pets from the API with optional filters.
*/ */
public LiveData<Resource<PageResponse<PetDTO>>> getAllPets(int page, int size, String query, String status, String species, Long storeId, String sort) { public LiveData<Resource<PageResponse<PetDTO>>> getAllPets(int page, int size, String query, String status, String species, Long storeId, Long customerId, String sort) {
return executeCall(petApi.getAllPets(page, size, query, status, species, storeId, sort)); return executeCall(petApi.getAllPets(page, size, query, status, species, storeId, customerId, sort));
}
/**
* Retrieves a list of pets for a specific customer from the dropdowns API.
*/
public LiveData<Resource<List<DropdownDTO>>> getCustomerPets(Long customerId) {
return executeCall(petApi.getCustomerPets(customerId));
}
/**
* Retrieves a list of pets available for adoption from the dropdowns API.
*/
public LiveData<Resource<List<DropdownDTO>>> getAdoptionPets() {
return executeCall(petApi.getAdoptionPets());
} }
/** /**

View File

@@ -3,10 +3,13 @@ package com.example.petstoremobile.repositories;
import androidx.lifecycle.LiveData; import androidx.lifecycle.LiveData;
import com.example.petstoremobile.api.StoreApi; import com.example.petstoremobile.api.StoreApi;
import com.example.petstoremobile.dtos.DropdownDTO;
import com.example.petstoremobile.dtos.PageResponse; import com.example.petstoremobile.dtos.PageResponse;
import com.example.petstoremobile.dtos.StoreDTO; import com.example.petstoremobile.dtos.StoreDTO;
import com.example.petstoremobile.utils.Resource; import com.example.petstoremobile.utils.Resource;
import java.util.List;
import javax.inject.Inject; import javax.inject.Inject;
import javax.inject.Singleton; import javax.inject.Singleton;
@@ -26,4 +29,18 @@ public class StoreRepository extends BaseRepository {
public LiveData<Resource<PageResponse<StoreDTO>>> getAllStores(int page, int size) { public LiveData<Resource<PageResponse<StoreDTO>>> getAllStores(int page, int size) {
return executeCall(storeApi.getAllStores(page, size)); return executeCall(storeApi.getAllStores(page, size));
} }
/**
* Retrieves a list of store dropdowns from the API.
*/
public LiveData<Resource<List<DropdownDTO>>> getStoreDropdowns() {
return executeCall(storeApi.getStoreDropdowns());
}
/**
* Retrieves a list of employees for a specific store from the dropdowns API.
*/
public LiveData<Resource<List<DropdownDTO>>> getStoreEmployees(Long storeId) {
return executeCall(storeApi.getStoreEmployees(storeId));
}
} }

View File

@@ -0,0 +1,113 @@
package com.example.petstoremobile.utils;
import android.util.Log;
import java.util.Calendar;
import java.util.Locale;
/**
* Utility class for date and time operations.
*/
public class DateTimeUtils {
private static final String TAG = "DateTimeUtils";
/**
* Formats status from backend format to UI format
* (backend is using all caps so we lower case them with this function)
*/
public static String formatStatusFromBackend(String status) {
if (status == null || status.isEmpty()) return status;
return status.substring(0, 1).toUpperCase() + status.substring(1).toLowerCase();
}
/**
* Converts a date and time string to a Calendar object.
* format: date = "YYYY-MM-DD", time = "HH:MM"
*/
public static Calendar parseDateTimeToCalendar(String date, String time) throws Exception {
Calendar calendar = Calendar.getInstance();
String[] dateParts = date.split("-");
String[] timeParts = time.split(":");
calendar.set(
Integer.parseInt(dateParts[0]),
Integer.parseInt(dateParts[1]) - 1,
Integer.parseInt(dateParts[2]),
Integer.parseInt(timeParts[0]),
Integer.parseInt(timeParts[1]),
0
);
return calendar;
}
/**
* Checks if a given date is in the past.
* format: date = "YYYY-MM-DD"
*/
public static boolean isDateInPast(String date) {
if (date == null || date.isEmpty()) return false;
try {
Calendar selected = Calendar.getInstance();
String[] dateParts = date.split("-");
selected.set(
Integer.parseInt(dateParts[0]),
Integer.parseInt(dateParts[1]) - 1,
Integer.parseInt(dateParts[2]),
0, 0, 0
);
return selected.before(Calendar.getInstance());
} catch (Exception e) {
Log.e(TAG, "Error parsing date: " + e.getMessage());
return false;
}
}
/**
* Checks if a given date and time are in the past.
* format: date = "YYYY-MM-DD", time = "HH:MM"
*/
public static boolean isDateTimeInPast(String date, String time) {
if (date == null || date.isEmpty() || time == null || time.isEmpty()) return false;
try {
Calendar selected = parseDateTimeToCalendar(date, time);
return selected.before(Calendar.getInstance());
} catch (Exception e) {
Log.e(TAG, "Error parsing date/time: " + e.getMessage());
return false;
}
}
/**
* Parses a time string and returns hour and minute values.
*/
public static int[] parseTimeString(String time) {
if (time == null || time.isEmpty()) return null;
if (time.length() > 5) time = time.substring(0, 5);
String[] parts = time.split(":");
if (parts.length != 2) return null;
try {
int hour = Integer.parseInt(parts[0]);
int min = Integer.parseInt(parts[1]);
return new int[]{hour, min};
} catch (NumberFormatException e) {
return null;
}
}
/**
* Formats an hour and minute into an HH:mm string.
*/
public static String formatTime(int hour, int minute) {
return String.format(Locale.getDefault(), "%02d:%02d", hour, minute);
}
/**
* Formats an ID for display (e.g., "ID: 123").
*/
public static String formatId(long id) {
return String.format(Locale.getDefault(), "ID: %d", id);
}
}

View File

@@ -1,7 +1,9 @@
package com.example.petstoremobile.utils; package com.example.petstoremobile.utils;
import android.content.Context; import android.content.Context;
import android.database.Cursor;
import android.net.Uri; import android.net.Uri;
import android.provider.OpenableColumns;
import java.io.File; import java.io.File;
import java.io.FileOutputStream; import java.io.FileOutputStream;
import java.io.InputStream; import java.io.InputStream;
@@ -9,8 +11,11 @@ import java.io.InputStream;
public class FileUtils { public class FileUtils {
public static File getFileFromUri(Context context, Uri uri) { public static File getFileFromUri(Context context, Uri uri) {
try { try {
String fileName = getFileName(context, uri);
if (fileName == null) fileName = "upload_" + System.currentTimeMillis();
InputStream inputStream = context.getContentResolver().openInputStream(uri); InputStream inputStream = context.getContentResolver().openInputStream(uri);
File tempFile = new File(context.getCacheDir(), "upload_image_" + System.currentTimeMillis() + ".jpg"); File tempFile = new File(context.getCacheDir(), fileName);
FileOutputStream outputStream = new FileOutputStream(tempFile); FileOutputStream outputStream = new FileOutputStream(tempFile);
byte[] buffer = new byte[1024]; byte[] buffer = new byte[1024];
int length; int length;
@@ -24,4 +29,22 @@ public class FileUtils {
return null; return null;
} }
} }
public static String getFileName(Context context, Uri uri) {
String result = null;
if (uri.getScheme().equals("content")) {
try (Cursor cursor = context.getContentResolver().query(uri, null, null, null, null)) {
if (cursor != null && cursor.moveToFirst()) {
int index = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME);
if (index != -1) result = cursor.getString(index);
}
}
}
if (result == null) {
result = uri.getPath();
int cut = result.lastIndexOf('/');
if (cut != -1) result = result.substring(cut + 1);
}
return result;
}
} }

View File

@@ -10,8 +10,10 @@ import com.example.petstoremobile.adapters.BlackTextArrayAdapter;
import com.example.petstoremobile.adapters.WhiteTextArrayAdapter; import com.example.petstoremobile.adapters.WhiteTextArrayAdapter;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.Objects; import java.util.Objects;
import java.util.function.Consumer;
import java.util.function.Function; import java.util.function.Function;
/** /**
@@ -50,6 +52,12 @@ public class SpinnerUtils {
names.add(nameExtractor.apply(item)); names.add(nameExtractor.apply(item));
} }
// Only update adapter if contents changed to remove infinite loop when spinner is opened
if (isAdapterDataSame(spinner, names)) {
setSelectedId(spinner, data, defaultText, preselectedId, idExtractor);
return;
}
ArrayAdapter<String> adapter; ArrayAdapter<String> adapter;
if (useWhiteText) { if (useWhiteText) {
adapter = new WhiteTextArrayAdapter<>(context, android.R.layout.simple_spinner_item, names); adapter = new WhiteTextArrayAdapter<>(context, android.R.layout.simple_spinner_item, names);
@@ -60,26 +68,41 @@ public class SpinnerUtils {
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
spinner.setAdapter(adapter); spinner.setAdapter(adapter);
setSelectedId(spinner, data, defaultText, preselectedId, idExtractor);
}
private static <T> void setSelectedId(Spinner spinner, List<T> data, String defaultText, Long preselectedId, Function<T, Long> idExtractor) {
if (preselectedId != null && preselectedId != -1) { if (preselectedId != null && preselectedId != -1) {
int offset = (defaultText != null) ? 1 : 0; int offset = (defaultText != null) ? 1 : 0;
for (int i = 0; i < data.size(); i++) { for (int i = 0; i < data.size(); i++) {
Long currentId = idExtractor.apply(data.get(i)); Long currentId = idExtractor.apply(data.get(i));
if (Objects.equals(currentId, preselectedId)) { if (Objects.equals(currentId, preselectedId)) {
spinner.setSelection(i + offset); if (spinner.getSelectedItemPosition() != i + offset) {
spinner.setSelection(i + offset);
}
break; break;
} }
} }
} }
} }
/**
* Checks if the adapter data is the same as the new data.
*/
private static boolean isAdapterDataSame(Spinner spinner, List<String> newNames) {
if (spinner.getAdapter() == null) return false;
if (spinner.getAdapter().getCount() != newNames.size()) return false;
for (int i = 0; i < newNames.size(); i++) {
if (!Objects.equals(spinner.getAdapter().getItem(i), newNames.get(i))) return false;
}
return true;
}
/** /**
* Sets up a simple string spinner for filtering with a callback. * Sets up a simple string spinner for filtering with a callback.
*/ */
public static void setupStringFilterSpinner(Context context, Spinner spinner, String[] items, Runnable onSelectionChanged) { public static void setupStringFilterSpinner(Context context, Spinner spinner, String[] items, Runnable onSelectionChanged) {
WhiteTextArrayAdapter<String> adapter = new WhiteTextArrayAdapter<>(context, updateStringSpinnerIfChanged(context, spinner, items, true);
android.R.layout.simple_spinner_item, items);
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
spinner.setAdapter(adapter);
setupFilterSpinner(spinner, onSelectionChanged); setupFilterSpinner(spinner, onSelectionChanged);
} }
@@ -96,6 +119,13 @@ public class SpinnerUtils {
}); });
} }
/**
* Sets a listener that provides the selected index to a consumer.
*/
public static void setOnIndexSelectedListener(Spinner spinner, Consumer<Integer> callback) {
spinner.setOnItemSelectedListener(new OnIndexSelected(callback));
}
/** /**
* Sets the selection of a spinner based on a string value. * Sets the selection of a spinner based on a string value.
*/ */
@@ -103,18 +133,65 @@ public class SpinnerUtils {
if (value == null || spinner.getAdapter() == null) return; if (value == null || spinner.getAdapter() == null) return;
ArrayAdapter<String> adapter = (ArrayAdapter<String>) spinner.getAdapter(); ArrayAdapter<String> adapter = (ArrayAdapter<String>) spinner.getAdapter();
int pos = adapter.getPosition(value); int pos = adapter.getPosition(value);
if (pos >= 0) { if (pos >= 0 && spinner.getSelectedItemPosition() != pos) {
spinner.setSelection(pos); spinner.setSelection(pos);
} }
} }
/**
* Sets the selection of a spinner based on a value within an array.
*/
public static <T> void setSelectionByValueArray(Spinner spinner, T[] array, T value) {
if (spinner == null || array == null || value == null) return;
for (int i = 0; i < array.length; i++) {
if (Objects.equals(array[i], value)) {
if (spinner.getSelectedItemPosition() != i) {
spinner.setSelection(i);
}
return;
}
}
}
/** /**
* Configures a simple string array spinner. * Configures a simple string array spinner.
*/ */
public static void setupStringSpinner(Context context, Spinner spinner, String[] items) { public static void setupStringSpinner(Context context, Spinner spinner, String[] items) {
BlackTextArrayAdapter<String> adapter = new BlackTextArrayAdapter<>(context, updateStringSpinnerIfChanged(context, spinner, items, false);
android.R.layout.simple_spinner_item, items); }
/**
* Updates a string spinner only if the items have changed.
*/
public static void updateStringSpinnerIfChanged(Context context, Spinner spinner, String[] items, boolean useWhiteText) {
if (isAdapterDataSame(spinner, Arrays.asList(items))) return;
ArrayAdapter<String> adapter;
if (useWhiteText) {
adapter = new WhiteTextArrayAdapter<>(context, android.R.layout.simple_spinner_item, items);
} else {
adapter = new BlackTextArrayAdapter<>(context, android.R.layout.simple_spinner_item, items);
}
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
spinner.setAdapter(adapter); spinner.setAdapter(adapter);
} }
/**
* Helper listener to get selected index from a spinner.
*/
public static class OnIndexSelected implements AdapterView.OnItemSelectedListener {
private final Consumer<Integer> callback;
public OnIndexSelected(Consumer<Integer> callback) {
this.callback = callback;
}
@Override
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
callback.accept(position);
}
@Override
public void onNothingSelected(AdapterView<?> parent) {}
}
} }

View File

@@ -1,5 +1,7 @@
package com.example.petstoremobile.utils; package com.example.petstoremobile.utils;
import android.app.DatePickerDialog;
import android.content.Context;
import android.telephony.PhoneNumberFormattingTextWatcher; import android.telephony.PhoneNumberFormattingTextWatcher;
import android.text.Editable; import android.text.Editable;
import android.text.InputFilter; import android.text.InputFilter;
@@ -8,10 +10,14 @@ import android.view.View;
import android.widget.EditText; import android.widget.EditText;
import android.widget.ImageButton; import android.widget.ImageButton;
import android.widget.Spinner; import android.widget.Spinner;
import android.widget.Toast;
import androidx.fragment.app.Fragment; import androidx.fragment.app.Fragment;
import com.example.petstoremobile.fragments.ListFragment; import com.example.petstoremobile.fragments.ListFragment;
import java.util.Calendar;
import java.util.Locale;
/** /**
* Utility class for shared UI component logic and formatting. * Utility class for shared UI component logic and formatting.
*/ */
@@ -25,24 +31,16 @@ public class UIUtils {
} }
/** /**
* Sets up a toggle for a filter layout, including icon changes and field resets. * Sets up a toggle for a filter layout, including icon changes.
*/ */
public static void setupFilterToggle(ImageButton btnToggle, View layoutFilter, EditText etSearch, Spinner... spinners) { public static void setupFilterToggle(ImageButton btnToggle, View layoutFilter, EditText etSearch, Spinner... spinners) {
btnToggle.setOnClickListener(v -> { btnToggle.setOnClickListener(v -> {
boolean isVisible = layoutFilter.getVisibility() == View.VISIBLE; boolean isVisible = layoutFilter.getVisibility() == View.VISIBLE;
layoutFilter.setVisibility(isVisible ? View.GONE : View.VISIBLE); layoutFilter.setVisibility(isVisible ? View.GONE : View.VISIBLE);
// Use Android default icons or app-specific ones if available
btnToggle.setImageResource(isVisible ? btnToggle.setImageResource(isVisible ?
android.R.drawable.ic_menu_search : android.R.drawable.ic_menu_search :
android.R.drawable.ic_menu_close_clear_cancel); android.R.drawable.ic_menu_close_clear_cancel);
if (isVisible) {
if (etSearch != null) etSearch.setText("");
for (Spinner spinner : spinners) {
if (spinner != null) spinner.setSelection(0);
}
}
}); });
} }
@@ -73,4 +71,80 @@ public class UIUtils {
@Override public void afterTextChanged(Editable s) {} @Override public void afterTextChanged(Editable s) {}
}); });
} }
/**
* Sets the enabled state and alpha for multiple views, only if changed.
*/
public static void setViewsEnabled(boolean enabled, View... views) {
for (View v : views) {
if (v != null) {
if (v.isEnabled() != enabled) {
v.setEnabled(enabled);
}
float targetAlpha = enabled ? 1.0f : 0.5f;
if (Math.abs(v.getAlpha() - targetAlpha) > 0.01f) {
v.setAlpha(targetAlpha);
}
}
}
}
/**
* Sets enabled state for a field and updates alpha for both the field and its label, only if changed.
*/
public static void setFieldEnabled(boolean enabled, View field, View label) {
if (field != null) {
if (field.isEnabled() != enabled) {
field.setEnabled(enabled);
}
float targetAlpha = enabled ? 1.0f : 0.5f;
if (Math.abs(field.getAlpha() - targetAlpha) > 0.01f) {
field.setAlpha(targetAlpha);
}
}
if (label != null) {
float targetAlpha = enabled ? 1.0f : 0.5f;
if (Math.abs(label.getAlpha() - targetAlpha) > 0.01f) {
label.setAlpha(targetAlpha);
}
}
}
/**
* Sets the alpha for multiple views, only if changed.
*/
public static void setViewsAlpha(float alpha, View... views) {
for (View v : views) {
if (v != null && Math.abs(v.getAlpha() - alpha) > 0.01f) {
v.setAlpha(alpha);
}
}
}
/**
* Displays a DatePickerDialog and sets the result to an EditText.
*/
public static void showDatePicker(Context context, EditText editText, Runnable onDateSet) {
Calendar c = Calendar.getInstance();
DatePickerDialog d = new DatePickerDialog(context,
(dp, y, m, d1) -> {
String selectedDate = String.format(Locale.getDefault(), "%04d-%02d-%02d", y, m + 1, d1);
editText.setText(selectedDate);
if (onDateSet != null) onDateSet.run();
},
c.get(Calendar.YEAR), c.get(Calendar.MONTH),
c.get(Calendar.DAY_OF_MONTH));
d.getDatePicker().setMinDate(System.currentTimeMillis() - 1000);
d.show();
}
/**
* Displays a toast and returns false. Useful for validation chains.
*/
public static boolean showToast(Context context, String msg) {
if (context != null) {
Toast.makeText(context, msg, Toast.LENGTH_SHORT).show();
}
return false;
}
} }

View File

@@ -0,0 +1,102 @@
package com.example.petstoremobile.viewmodels;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.ViewModel;
import com.example.petstoremobile.dtos.AdoptionDTO;
import com.example.petstoremobile.dtos.DropdownDTO;
import com.example.petstoremobile.repositories.AdoptionRepository;
import com.example.petstoremobile.repositories.CustomerRepository;
import com.example.petstoremobile.repositories.PetRepository;
import com.example.petstoremobile.repositories.StoreRepository;
import com.example.petstoremobile.utils.Resource;
import java.util.ArrayList;
import java.util.List;
import javax.inject.Inject;
import dagger.hilt.android.lifecycle.HiltViewModel;
@HiltViewModel
public class AdoptionDetailViewModel extends ViewModel {
private final AdoptionRepository adoptionRepository;
private final PetRepository petRepository;
private final CustomerRepository customerRepository;
private final StoreRepository storeRepository;
private long adoptionId = -1;
private boolean isEditing = false;
private final MutableLiveData<List<DropdownDTO>> petList = new MutableLiveData<>(new ArrayList<>());
private final MutableLiveData<List<DropdownDTO>> customerList = new MutableLiveData<>(new ArrayList<>());
private final MutableLiveData<List<DropdownDTO>> storeList = new MutableLiveData<>(new ArrayList<>());
private final MutableLiveData<List<DropdownDTO>> employeeList = new MutableLiveData<>(new ArrayList<>());
@Inject
public AdoptionDetailViewModel(AdoptionRepository adoptionRepository, PetRepository petRepository,
CustomerRepository customerRepository, StoreRepository storeRepository) {
this.adoptionRepository = adoptionRepository;
this.petRepository = petRepository;
this.customerRepository = customerRepository;
this.storeRepository = storeRepository;
}
public void setAdoptionId(long id) {
this.adoptionId = id;
this.isEditing = id != -1;
}
public long getAdoptionId() {
return adoptionId;
}
public boolean isEditing() {
return isEditing;
}
public LiveData<Resource<AdoptionDTO>> loadAdoption() {
return adoptionRepository.getAdoptionById(adoptionId);
}
public LiveData<Resource<List<DropdownDTO>>> loadPets() {
return petRepository.getAdoptionPets();
}
public LiveData<Resource<List<DropdownDTO>>> loadCustomers() {
return customerRepository.getCustomerDropdowns();
}
public LiveData<Resource<List<DropdownDTO>>> loadStores() {
return storeRepository.getStoreDropdowns();
}
public LiveData<Resource<List<DropdownDTO>>> loadEmployees(Long storeId) {
return storeRepository.getStoreEmployees(storeId);
}
public LiveData<Resource<AdoptionDTO>> saveAdoption(AdoptionDTO dto) {
if (isEditing) {
return adoptionRepository.updateAdoption(adoptionId, dto);
} else {
return adoptionRepository.createAdoption(dto);
}
}
public LiveData<Resource<Void>> deleteAdoption() {
return adoptionRepository.deleteAdoption(adoptionId);
}
public void setPetList(List<DropdownDTO> list) { petList.setValue(list); }
public LiveData<List<DropdownDTO>> getPetList() { return petList; }
public void setCustomerList(List<DropdownDTO> list) { customerList.setValue(list); }
public LiveData<List<DropdownDTO>> getCustomerList() { return customerList; }
public void setStoreList(List<DropdownDTO> list) { storeList.setValue(list); }
public LiveData<List<DropdownDTO>> getStoreList() { return storeList; }
public void setEmployeeList(List<DropdownDTO> list) { employeeList.setValue(list); }
public LiveData<List<DropdownDTO>> getEmployeeList() { return employeeList; }
}

View File

@@ -0,0 +1,83 @@
package com.example.petstoremobile.viewmodels;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.ViewModel;
import com.example.petstoremobile.dtos.AdoptionDTO;
import com.example.petstoremobile.dtos.BulkDeleteRequest;
import com.example.petstoremobile.dtos.StoreDTO;
import com.example.petstoremobile.repositories.AdoptionRepository;
import com.example.petstoremobile.repositories.StoreRepository;
import com.example.petstoremobile.utils.Resource;
import java.util.ArrayList;
import java.util.List;
import javax.inject.Inject;
import dagger.hilt.android.lifecycle.HiltViewModel;
@HiltViewModel
public class AdoptionListViewModel extends ViewModel {
private final AdoptionRepository adoptionRepository;
private final StoreRepository storeRepository;
private final MutableLiveData<List<AdoptionDTO>> adoptions = new MutableLiveData<>(new ArrayList<>());
private final MutableLiveData<List<StoreDTO>> stores = new MutableLiveData<>(new ArrayList<>());
private final MutableLiveData<Boolean> isLoading = new MutableLiveData<>(false);
private int currentPage = 0;
private boolean isLastPage = false;
private static final int PAGE_SIZE = 20;
@Inject
public AdoptionListViewModel(AdoptionRepository adoptionRepository, StoreRepository storeRepository) {
this.adoptionRepository = adoptionRepository;
this.storeRepository = storeRepository;
}
public LiveData<List<AdoptionDTO>> getAdoptions() { return adoptions; }
public LiveData<List<StoreDTO>> getStores() { return stores; }
public LiveData<Boolean> getIsLoading() { return isLoading; }
public boolean isLastPage() { return isLastPage; }
public void loadAdoptions(boolean reset, String query, String status, Long storeId, String date, Long employeeId) {
if (isLoading.getValue() != null && isLoading.getValue() && !reset) return;
if (reset) {
currentPage = 0;
isLastPage = false;
}
if ("All Statuses".equals(status)) status = null;
isLoading.setValue(true);
adoptionRepository.getAllAdoptions(currentPage, PAGE_SIZE, query, status, storeId, date, employeeId).observeForever(resource -> {
if (resource != null) {
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
List<AdoptionDTO> currentList = reset ? new ArrayList<>() : new ArrayList<>(adoptions.getValue());
currentList.addAll(resource.data.getContent());
adoptions.setValue(currentList);
isLastPage = resource.data.isLast();
if (!isLastPage) currentPage++;
isLoading.setValue(false);
} else if (resource.status == Resource.Status.ERROR) {
isLoading.setValue(false);
}
}
});
}
public void loadStores() {
storeRepository.getAllStores(0, 100).observeForever(resource -> {
if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) {
stores.setValue(resource.data.getContent());
}
});
}
public LiveData<Resource<Void>> bulkDeleteAdoptions(List<String> ids) {
return adoptionRepository.bulkDeleteAdoptions(new BulkDeleteRequest(ids));
}
}

View File

@@ -1,68 +0,0 @@
package com.example.petstoremobile.viewmodels;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.ViewModel;
import com.example.petstoremobile.dtos.AdoptionDTO;
import com.example.petstoremobile.dtos.BulkDeleteRequest;
import com.example.petstoremobile.dtos.PageResponse;
import com.example.petstoremobile.repositories.AdoptionRepository;
import com.example.petstoremobile.utils.Resource;
import java.util.List;
import javax.inject.Inject;
import dagger.hilt.android.lifecycle.HiltViewModel;
@HiltViewModel
public class AdoptionViewModel extends ViewModel {
private final AdoptionRepository repository;
@Inject
public AdoptionViewModel(AdoptionRepository repository) {
this.repository = repository;
}
/**
* Fetches a paginated list of all adoptions with filters.
*/
public LiveData<Resource<PageResponse<AdoptionDTO>>> getAllAdoptions(int page, int size, String query, String status, Long storeId, String date, Long employeeId) {
return repository.getAllAdoptions(page, size, query, status, storeId, date, employeeId);
}
/**
* Retrieves a single adoption by its ID.
*/
public LiveData<Resource<AdoptionDTO>> getAdoptionById(Long id) {
return repository.getAdoptionById(id);
}
/**
* Creates a new adoption record.
*/
public LiveData<Resource<AdoptionDTO>> createAdoption(AdoptionDTO adoption) {
return repository.createAdoption(adoption);
}
/**
* Updates an existing adoption record by ID.
*/
public LiveData<Resource<AdoptionDTO>> updateAdoption(Long id, AdoptionDTO adoption) {
return repository.updateAdoption(id, adoption);
}
/**
* Deletes an adoption record by ID.
*/
public LiveData<Resource<Void>> deleteAdoption(Long id) {
return repository.deleteAdoption(id);
}
/**
* Deletes multiple adoption records.
*/
public LiveData<Resource<Void>> bulkDeleteAdoptions(List<String> ids) {
return repository.bulkDeleteAdoptions(new BulkDeleteRequest(ids));
}
}

View File

@@ -0,0 +1,163 @@
package com.example.petstoremobile.viewmodels;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.ViewModel;
import com.example.petstoremobile.dtos.SaleDTO;
import com.example.petstoremobile.repositories.SaleRepository;
import com.example.petstoremobile.utils.Resource;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import javax.inject.Inject;
import dagger.hilt.android.lifecycle.HiltViewModel;
@HiltViewModel
public class AnalyticsViewModel extends ViewModel {
private final SaleRepository saleRepository;
private final MutableLiveData<AnalyticsData> analyticsData = new MutableLiveData<>();
private final MutableLiveData<Boolean> isLoading = new MutableLiveData<>(false);
private final MutableLiveData<String> errorMessage = new MutableLiveData<>();
@Inject
public AnalyticsViewModel(SaleRepository saleRepository) {
this.saleRepository = saleRepository;
}
public LiveData<AnalyticsData> getAnalyticsData() { return analyticsData; }
public LiveData<Boolean> getIsLoading() { return isLoading; }
public LiveData<String> getErrorMessage() { return errorMessage; }
public void loadAnalytics() {
isLoading.setValue(true);
errorMessage.setValue(null);
saleRepository.getAllSales(0, 1000, null, null, null, "saleDate,desc").observeForever(resource -> {
if (resource != null) {
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
computeAnalytics(resource.data.getContent());
isLoading.setValue(false);
} else if (resource.status == Resource.Status.ERROR) {
errorMessage.setValue(resource.message);
isLoading.setValue(false);
}
}
});
}
private void computeAnalytics(List<SaleDTO> sales) {
List<SaleDTO> regularSales = new ArrayList<>();
for (SaleDTO s : sales) {
if (!Boolean.TRUE.equals(s.getIsRefund()))
regularSales.add(s);
}
AnalyticsData data = new AnalyticsData();
// Summary
BigDecimal totalRevenue = BigDecimal.ZERO;
int totalItems = 0;
for (SaleDTO s : regularSales) {
if (s.getTotalAmount() != null) totalRevenue = totalRevenue.add(s.getTotalAmount());
if (s.getItems() != null) {
for (SaleDTO.SaleItemDTO item : s.getItems()) {
if (item.getQuantity() != null) totalItems += Math.abs(item.getQuantity());
}
}
}
data.totalRevenue = totalRevenue;
data.totalTransactions = regularSales.size();
data.avgTransaction = data.totalTransactions > 0
? totalRevenue.divide(BigDecimal.valueOf(data.totalTransactions), 2, RoundingMode.HALF_UP)
: BigDecimal.ZERO;
data.totalItems = totalItems;
// Product Maps
Map<String, BigDecimal> revenueByProduct = new LinkedHashMap<>();
Map<String, Integer> quantityByProduct = new LinkedHashMap<>();
Map<String, Integer> paymentCount = new LinkedHashMap<>();
Map<String, BigDecimal> employeeRevenue = new LinkedHashMap<>();
for (SaleDTO s : regularSales) {
// Payments
String method = s.getPaymentMethod() != null ? s.getPaymentMethod() : "Unknown";
paymentCount.merge(method, 1, Integer::sum);
// Employee
String emp = s.getEmployeeName() != null ? s.getEmployeeName() : "Unknown";
if (s.getTotalAmount() != null) employeeRevenue.merge(emp, s.getTotalAmount(), BigDecimal::add);
// Items
if (s.getItems() != null) {
for (SaleDTO.SaleItemDTO item : s.getItems()) {
String name = item.getProductName() != null ? item.getProductName() : "Unknown";
int qty = item.getQuantity() != null ? Math.abs(item.getQuantity()) : 0;
BigDecimal lineTotal = item.getUnitPrice() != null
? item.getUnitPrice().multiply(BigDecimal.valueOf(qty))
: BigDecimal.ZERO;
revenueByProduct.merge(name, lineTotal, BigDecimal::add);
quantityByProduct.merge(name, qty, Integer::sum);
}
}
}
// Sort Top Revenue
data.topRevenueProducts = new ArrayList<>(revenueByProduct.entrySet());
data.topRevenueProducts.sort((a, b) -> b.getValue().compareTo(a.getValue()));
if (data.topRevenueProducts.size() > 5) data.topRevenueProducts = data.topRevenueProducts.subList(0, 5);
// Sort Top Quantity
data.topQuantityProducts = new ArrayList<>(quantityByProduct.entrySet());
data.topQuantityProducts.sort((a, b) -> b.getValue() - a.getValue());
if (data.topQuantityProducts.size() > 5) data.topQuantityProducts = data.topQuantityProducts.subList(0, 5);
// Payment Stats
data.paymentMethodStats = new ArrayList<>(paymentCount.entrySet());
// Employee Performance
data.employeePerformance = new ArrayList<>(employeeRevenue.entrySet());
data.employeePerformance.sort((a, b) -> b.getValue().compareTo(a.getValue()));
// Daily Revenue (last 7 days)
Map<String, BigDecimal> dailyMap = new TreeMap<>();
for (int i = 6; i >= 0; i--) {
Calendar day = Calendar.getInstance();
day.add(Calendar.DAY_OF_YEAR, -i);
String key = String.format("%04d-%02d-%02d",
day.get(Calendar.YEAR), day.get(Calendar.MONTH) + 1, day.get(Calendar.DAY_OF_MONTH));
dailyMap.put(key, BigDecimal.ZERO);
}
for (SaleDTO s : regularSales) {
if (s.getSaleDate() != null && s.getTotalAmount() != null) {
String date = s.getSaleDate().length() >= 10 ? s.getSaleDate().substring(0, 10) : s.getSaleDate();
if (dailyMap.containsKey(date)) dailyMap.merge(date, s.getTotalAmount(), BigDecimal::add);
}
}
data.dailyRevenue = new ArrayList<>(dailyMap.entrySet());
analyticsData.setValue(data);
}
public static class AnalyticsData {
public BigDecimal totalRevenue;
public int totalTransactions;
public BigDecimal avgTransaction;
public int totalItems;
public List<Map.Entry<String, BigDecimal>> topRevenueProducts;
public List<Map.Entry<String, Integer>> topQuantityProducts;
public List<Map.Entry<String, Integer>> paymentMethodStats;
public List<Map.Entry<String, BigDecimal>> employeePerformance;
public List<Map.Entry<String, BigDecimal>> dailyRevenue;
}
}

View File

@@ -0,0 +1,402 @@
package com.example.petstoremobile.viewmodels;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.ViewModel;
import com.example.petstoremobile.dtos.AppointmentDTO;
import com.example.petstoremobile.dtos.DropdownDTO;
import com.example.petstoremobile.dtos.ServiceDTO;
import com.example.petstoremobile.repositories.AppointmentRepository;
import com.example.petstoremobile.repositories.CustomerRepository;
import com.example.petstoremobile.repositories.PetRepository;
import com.example.petstoremobile.repositories.ServiceRepository;
import com.example.petstoremobile.repositories.StoreRepository;
import com.example.petstoremobile.utils.DateTimeUtils;
import com.example.petstoremobile.utils.Resource;
import java.util.ArrayList;
import java.util.List;
import javax.inject.Inject;
import dagger.hilt.android.lifecycle.HiltViewModel;
/**
* ViewModel for managing appointment details and form state.
*/
@HiltViewModel
public class AppointmentDetailViewModel extends ViewModel {
private final AppointmentRepository repository;
private final CustomerRepository customerRepository;
private final StoreRepository storeRepository;
private final PetRepository petRepository;
private final ServiceRepository serviceRepository;
private final MutableLiveData<List<DropdownDTO>> customers = new MutableLiveData<>();
private final MutableLiveData<List<DropdownDTO>> stores = new MutableLiveData<>();
private final MutableLiveData<List<ServiceDTO>> services = new MutableLiveData<>();
private final MutableLiveData<List<DropdownDTO>> customerPets = new MutableLiveData<>();
private final MutableLiveData<List<DropdownDTO>> storeEmployees = new MutableLiveData<>();
private final MutableLiveData<ViewState> viewState = new MutableLiveData<>(new ViewState());
private long appointmentId = -1;
private Long currentCustomerId;
private Long currentStoreId;
private Long currentPetId;
private Long currentServiceId;
private Long currentStaffId;
/**
* Constructor for AppointmentDetailViewModel.
*/
@Inject
public AppointmentDetailViewModel(
AppointmentRepository repository,
CustomerRepository customerRepository,
StoreRepository storeRepository,
PetRepository petRepository,
ServiceRepository serviceRepository) {
this.repository = repository;
this.customerRepository = customerRepository;
this.storeRepository = storeRepository;
this.petRepository = petRepository;
this.serviceRepository = serviceRepository;
}
// Initial Data Loading
/**
* Loads initial dropdown data for customers, stores, and services.
*/
public void loadInitialFormData() {
customerRepository.getCustomerDropdowns().observeForever(r -> {
if (r.status == Resource.Status.SUCCESS) customers.setValue(r.data);
});
storeRepository.getStoreDropdowns().observeForever(r -> {
if (r.status == Resource.Status.SUCCESS) stores.setValue(r.data);
});
serviceRepository.getAllServices(0, 200, null, "serviceName").observeForever(r -> {
if (r.status == Resource.Status.SUCCESS && r.data != null) services.setValue(r.data.getContent());
});
}
// LiveData Getters
/**
* Returns the LiveData for the list of customers.
*/
public LiveData<List<DropdownDTO>> getCustomers() { return customers; }
/**
* Returns the LiveData for the list of stores.
*/
public LiveData<List<DropdownDTO>> getStores() { return stores; }
/**
* Returns the LiveData for the list of services.
*/
public LiveData<List<ServiceDTO>> getServices() { return services; }
/**
* Returns the LiveData for the list of pets for the current customer.
*/
public LiveData<List<DropdownDTO>> getCustomerPets() { return customerPets; }
/**
* Returns the LiveData for the list of employees for the current store.
*/
public LiveData<List<DropdownDTO>> getStoreEmployees() { return storeEmployees; }
/**
* Returns the LiveData for the view state.
*/
public LiveData<ViewState> getViewState() { return viewState; }
//State Getters
/**
* Returns the current appointment ID.
*/
public long getAppointmentId() { return appointmentId; }
/**
* Sets the current appointment ID and updates the mode.
*/
public void setAppointmentId(long id) {
this.appointmentId = id;
initMode(id != -1);
}
// Selection Handlers for spinners
/**
* Handles customer selection and loads their pets.
*/
public void onCustomerSelected(int position) {
List<DropdownDTO> list = customers.getValue();
if (position > 0 && list != null && position <= list.size()) {
currentCustomerId = list.get(position - 1).getId();
loadPetsForCustomer(currentCustomerId);
updateViewState(s -> {
s.selectedCustomerId = currentCustomerId;
s.isPetEnabled = !s.isEditing;
});
} else {
currentCustomerId = null;
customerPets.setValue(new ArrayList<>());
updateViewState(s -> {
s.selectedCustomerId = null;
s.isPetEnabled = false;
});
}
}
/**
* Handles store selection and loads its employees.
*/
public void onStoreSelected(int position) {
List<DropdownDTO> list = stores.getValue();
if (position > 0 && list != null && position <= list.size()) {
currentStoreId = list.get(position - 1).getId();
loadEmployeesForStore(currentStoreId);
updateViewState(s -> {
s.selectedStoreId = currentStoreId;
s.isStaffEnabled = !s.isPast;
});
} else {
currentStoreId = null;
storeEmployees.setValue(new ArrayList<>());
updateViewState(s -> {
s.selectedStoreId = null;
s.isStaffEnabled = false;
});
}
}
/**
* Handles service selection.
*/
public void onServiceSelected(int position) {
List<ServiceDTO> list = services.getValue();
currentServiceId = (position > 0 && list != null && position <= list.size()) ? list.get(position - 1).getServiceId() : null;
updateViewState(s -> s.selectedServiceId = currentServiceId);
}
/**
* Handles pet selection.
*/
public void onPetSelected(int position) {
List<DropdownDTO> list = customerPets.getValue();
currentPetId = (position > 0 && list != null && position <= list.size()) ? list.get(position - 1).getId() : null;
updateViewState(s -> s.selectedPetId = currentPetId);
}
/**
* Handles staff selection.
*/
public void onStaffSelected(int position) {
List<DropdownDTO> list = storeEmployees.getValue();
currentStaffId = (position > 0 && list != null && position <= list.size()) ? list.get(position - 1).getId() : null;
updateViewState(s -> s.selectedStaffId = currentStaffId);
}
/**
* Loads the list of pets for a specific customer.
*/
private void loadPetsForCustomer(Long customerId) {
petRepository.getCustomerPets(customerId).observeForever(r -> {
if (r.status == Resource.Status.SUCCESS) customerPets.setValue(r.data);
});
}
/**
* Loads the list of employees for a specific store.
*/
private void loadEmployeesForStore(Long storeId) {
storeRepository.getStoreEmployees(storeId).observeForever(r -> {
if (r.status == Resource.Status.SUCCESS) storeEmployees.setValue(r.data);
});
}
// Appointment Detail CRUD
/**
* Fetches appointment details and populates internal state.
*/
public LiveData<Resource<AppointmentDTO>> loadAppointment() {
MutableLiveData<Resource<AppointmentDTO>> result = new MutableLiveData<>();
repository.getAppointmentById(appointmentId).observeForever(resource -> {
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
AppointmentDTO a = resource.data;
currentCustomerId = a.getCustomerId();
currentStoreId = a.getStoreId();
currentPetId = a.getPetId();
currentServiceId = a.getServiceId();
currentStaffId = a.getEmployeeId();
updateViewState(s -> {
s.selectedCustomerId = currentCustomerId;
s.selectedStoreId = currentStoreId;
s.selectedPetId = currentPetId;
s.selectedServiceId = currentServiceId;
s.selectedStaffId = currentStaffId;
});
if (currentCustomerId != null) loadPetsForCustomer(currentCustomerId);
if (currentStoreId != null) loadEmployeesForStore(currentStoreId);
}
result.setValue(resource);
});
return result;
}
/**
* Saves the appointment by building the DTO from tracked state.
*/
public LiveData<Resource<AppointmentDTO>> saveAppointment(String date, String time, String status) {
AppointmentDTO dto = new AppointmentDTO(currentCustomerId, currentStoreId, currentServiceId, currentStaffId, date, time, status, currentPetId);
if (appointmentId != -1) {
return repository.updateAppointment(appointmentId, dto);
} else {
return repository.createAppointment(dto);
}
}
/**
* Deletes the current appointment.
*/
public LiveData<Resource<Void>> deleteAppointment() {
return repository.deleteAppointment(appointmentId);
}
// UI Logic
/**
* Updates the UI state when date, time, or status changes.
*/
public void onDateOrTimeChanged(String date, String time, String currentStatus) {
updateViewState(s -> {
s.availableStatuses = calculateAvailableStatuses(s.isEditing, date, time, currentStatus);
boolean isPast = DateTimeUtils.isDateTimeInPast(date, time);
boolean isCancelled = "Cancelled".equalsIgnoreCase(currentStatus);
if (isCancelled) {
s.isPast = true;
setAllFieldsEnabled(s, false);
s.isStatusEnabled = false;
s.isSaveVisible = false;
} else if (isPast) {
s.isPast = true;
setAllFieldsEnabled(s, false);
s.isStatusEnabled = true;
} else {
s.isPast = false;
if (!s.isEditing) {
s.isCustomerEnabled = true;
s.isStoreEnabled = true;
s.isServiceEnabled = true;
s.isPetEnabled = currentCustomerId != null;
}
s.isDateEnabled = true;
s.isTimeEnabled = true;
s.isStatusEnabled = true;
}
});
}
/**
* Calculates available appointment statuses based on the current context.
*/
private String[] calculateAvailableStatuses(boolean isEditing, String date, String currentTime, String currentStatus) {
if (!isEditing) return new String[]{"Booked"};
if (date == null || date.isEmpty()) return new String[]{};
if ("Cancelled".equalsIgnoreCase(currentStatus)) return new String[]{"Cancelled"};
if (DateTimeUtils.isDateTimeInPast(date, currentTime)) return new String[]{"Completed", "Missed"};
return new String[]{"Booked", "Cancelled"};
}
/**
* Helper method to enable or disable all fields.
*/
private void setAllFieldsEnabled(ViewState s, boolean enabled) {
s.isCustomerEnabled = enabled;
s.isStoreEnabled = enabled;
s.isPetEnabled = enabled;
s.isServiceEnabled = enabled;
s.isStaffEnabled = enabled;
s.isDateEnabled = enabled;
s.isTimeEnabled = enabled;
}
/**
* Initializes the UI mode (Create vs Edit).
*/
public void initMode(boolean isEditing) {
updateViewState(s -> {
s.isEditing = isEditing;
s.isDeleteVisible = isEditing;
if (isEditing) {
s.isCustomerEnabled = false;
s.isStoreEnabled = false;
s.isPetEnabled = false;
s.isServiceEnabled = false;
} else {
s.isCustomerEnabled = true;
s.isStoreEnabled = true;
s.isServiceEnabled = true;
s.isPetEnabled = false; // until customer selected
s.isStaffEnabled = false; // until store selected
s.availableStatuses = new String[]{"Booked"};
}
});
}
/**
* Validates if a booking is in the future.
*/
public boolean isValidFutureBooking(String status, String date, String time) {
return !"BOOKED".equalsIgnoreCase(status) || !DateTimeUtils.isDateTimeInPast(date, time);
}
/**
* Helper to update the view state and notify observers.
*/
private void updateViewState(Action<ViewState> action) {
ViewState current = viewState.getValue();
if (current != null) {
action.run(current);
viewState.setValue(current);
}
}
private interface Action<T> {
void run(T t);
}
/**
* A Class to show the states of Appointment Detail Fragment.
*/
public static class ViewState {
public boolean isPast = false;
public boolean isEditing = false;
public boolean isSaveVisible = true;
public boolean isDeleteVisible = false;
public boolean isCustomerEnabled = true;
public boolean isStoreEnabled = true;
public boolean isPetEnabled = false;
public boolean isServiceEnabled = true;
public boolean isStaffEnabled = false;
public boolean isDateEnabled = true;
public boolean isTimeEnabled = true;
public boolean isStatusEnabled = true;
public String[] availableStatuses = new String[]{};
// Selected IDs
public Long selectedCustomerId = null;
public Long selectedStoreId = null;
public Long selectedPetId = null;
public Long selectedServiceId = null;
public Long selectedStaffId = null;
}
}

View File

@@ -0,0 +1,65 @@
package com.example.petstoremobile.viewmodels;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.ViewModel;
import com.example.petstoremobile.dtos.AppointmentDTO;
import com.example.petstoremobile.dtos.BulkDeleteRequest;
import com.example.petstoremobile.dtos.StoreDTO;
import com.example.petstoremobile.repositories.AppointmentRepository;
import com.example.petstoremobile.repositories.StoreRepository;
import com.example.petstoremobile.utils.Resource;
import java.util.ArrayList;
import java.util.List;
import javax.inject.Inject;
import dagger.hilt.android.lifecycle.HiltViewModel;
@HiltViewModel
public class AppointmentListViewModel extends ViewModel {
private final AppointmentRepository appointmentRepository;
private final StoreRepository storeRepository;
private final MutableLiveData<List<AppointmentDTO>> appointments = new MutableLiveData<>(new ArrayList<>());
private final MutableLiveData<List<StoreDTO>> stores = new MutableLiveData<>(new ArrayList<>());
private final MutableLiveData<Boolean> isLoading = new MutableLiveData<>(false);
@Inject
public AppointmentListViewModel(AppointmentRepository appointmentRepository, StoreRepository storeRepository) {
this.appointmentRepository = appointmentRepository;
this.storeRepository = storeRepository;
}
public LiveData<List<AppointmentDTO>> getAppointments() { return appointments; }
public LiveData<List<StoreDTO>> getStores() { return stores; }
public LiveData<Boolean> getIsLoading() { return isLoading; }
public void loadAppointments(String query, String status, Long storeId, String date, Long employeeId) {
isLoading.setValue(true);
appointmentRepository.getAllAppointments(0, 500, query, status, storeId, date, employeeId).observeForever(resource -> {
if (resource != null) {
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
appointments.setValue(resource.data.getContent());
isLoading.setValue(false);
} else if (resource.status == Resource.Status.ERROR) {
isLoading.setValue(false);
}
}
});
}
public void loadStores() {
storeRepository.getAllStores(0, 100).observeForever(resource -> {
if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) {
stores.setValue(resource.data.getContent());
}
});
}
public LiveData<Resource<Void>> bulkDeleteAppointments(List<String> ids) {
return appointmentRepository.bulkDeleteAppointments(new BulkDeleteRequest(ids));
}
}

View File

@@ -1,68 +0,0 @@
package com.example.petstoremobile.viewmodels;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.ViewModel;
import com.example.petstoremobile.dtos.AppointmentDTO;
import com.example.petstoremobile.dtos.BulkDeleteRequest;
import com.example.petstoremobile.dtos.PageResponse;
import com.example.petstoremobile.repositories.AppointmentRepository;
import com.example.petstoremobile.utils.Resource;
import java.util.List;
import javax.inject.Inject;
import dagger.hilt.android.lifecycle.HiltViewModel;
@HiltViewModel
public class AppointmentViewModel extends ViewModel {
private final AppointmentRepository repository;
@Inject
public AppointmentViewModel(AppointmentRepository repository) {
this.repository = repository;
}
/**
* Fetches a paginated list of all appointments with optional filters.
*/
public LiveData<Resource<PageResponse<AppointmentDTO>>> getAllAppointments(int page, int size, String query, String status, Long storeId, String date, Long employeeId) {
return repository.getAllAppointments(page, size, query, status, storeId, date, employeeId);
}
/**
* Retrieves a single appointment by its ID.
*/
public LiveData<Resource<AppointmentDTO>> getAppointmentById(Long id) {
return repository.getAppointmentById(id);
}
/**
* Creates a new appointment.
*/
public LiveData<Resource<AppointmentDTO>> createAppointment(AppointmentDTO appointment) {
return repository.createAppointment(appointment);
}
/**
* Updates an existing appointment record by ID.
*/
public LiveData<Resource<AppointmentDTO>> updateAppointment(Long id, AppointmentDTO appointment) {
return repository.updateAppointment(id, appointment);
}
/**
* Deletes an appointment record by ID.
*/
public LiveData<Resource<Void>> deleteAppointment(Long id) {
return repository.deleteAppointment(id);
}
/**
* Deletes multiple appointment records.
*/
public LiveData<Resource<Void>> bulkDeleteAppointments(List<String> ids) {
return repository.bulkDeleteAppointments(new BulkDeleteRequest(ids));
}
}

View File

@@ -0,0 +1,174 @@
package com.example.petstoremobile.viewmodels;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
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.models.Chat;
import com.example.petstoremobile.models.Message;
import com.example.petstoremobile.repositories.ChatRepository;
import com.example.petstoremobile.repositories.CustomerRepository;
import com.example.petstoremobile.utils.Resource;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import javax.inject.Inject;
import dagger.hilt.android.lifecycle.HiltViewModel;
import okhttp3.MultipartBody;
import okhttp3.RequestBody;
import okhttp3.ResponseBody;
@HiltViewModel
public class ChatListViewModel extends ViewModel {
private final ChatRepository chatRepository;
private final CustomerRepository customerRepository;
private final MutableLiveData<List<Chat>> activeChats = new MutableLiveData<>(new ArrayList<>());
private final MutableLiveData<List<Chat>> closedChats = new MutableLiveData<>(new ArrayList<>());
private final MutableLiveData<List<Message>> messageList = new MutableLiveData<>(new ArrayList<>());
private final Map<Long, String> customerNames = new HashMap<>();
private final MutableLiveData<Boolean> isLoading = new MutableLiveData<>(false);
private Long lastActiveConversationId = null;
@Inject
public ChatListViewModel(ChatRepository chatRepository, CustomerRepository customerRepository) {
this.chatRepository = chatRepository;
this.customerRepository = customerRepository;
}
public LiveData<List<Chat>> getActiveChats() { return activeChats; }
public LiveData<List<Chat>> getClosedChats() { return closedChats; }
public LiveData<List<Message>> getMessageList() { return messageList; }
public LiveData<Boolean> getIsLoading() { return isLoading; }
public Long getLastActiveConversationId() {
return lastActiveConversationId;
}
public void setLastActiveConversationId(Long conversationId) {
this.lastActiveConversationId = conversationId;
}
public void loadCustomers() {
isLoading.setValue(true);
customerRepository.getAllCustomers(0, 100).observeForever(resource -> {
if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) {
for (CustomerDTO c : resource.data.getContent()) {
customerNames.put(c.getCustomerId(), c.getFullName());
}
loadConversations();
} else if (resource != null && resource.status == Resource.Status.ERROR) {
isLoading.setValue(false);
}
});
}
public void loadConversations() {
isLoading.setValue(true);
chatRepository.getAllConversations().observeForever(resource -> {
if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) {
List<Chat> active = new ArrayList<>();
List<Chat> closed = new ArrayList<>();
for (ConversationDTO dto : resource.data) {
String name = customerNames.getOrDefault(dto.getCustomerId(), "Customer #" + dto.getCustomerId());
Chat chat = new Chat(String.valueOf(dto.getId()), name, dto.getLastMessage(), dto.getCustomerId(), dto.getStaffId(), dto.getStatus());
if ("CLOSED".equalsIgnoreCase(dto.getStatus())) {
closed.add(chat);
} else {
active.add(chat);
}
}
activeChats.setValue(active);
closedChats.setValue(closed);
isLoading.setValue(false);
} else if (resource != null && resource.status == Resource.Status.ERROR) {
isLoading.setValue(false);
}
});
}
public void loadMessageHistory(Long conversationId) {
isLoading.setValue(true);
chatRepository.getMessages(conversationId).observeForever(resource -> {
if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) {
List<Message> messages = new ArrayList<>();
for (MessageDTO dto : resource.data) {
messages.add(dtoToModel(dto));
}
messageList.setValue(messages);
isLoading.setValue(false);
} else if (resource != null && resource.status == Resource.Status.ERROR) {
isLoading.setValue(false);
}
});
}
public LiveData<Resource<MessageDTO>> sendMessage(Long conversationId, String text) {
return chatRepository.sendMessage(conversationId, new SendMessageRequest(text));
}
public LiveData<Resource<MessageDTO>> sendMessageWithAttachment(Long conversationId, MultipartBody.Part content, MultipartBody.Part attachment) {
return chatRepository.sendMessageWithAttachment(conversationId, content, attachment);
}
public LiveData<Resource<ResponseBody>> downloadAttachment(Long messageId) {
return chatRepository.downloadAttachment(messageId);
}
public void addMessageLocally(MessageDTO dto) {
List<Message> current = new ArrayList<>(messageList.getValue());
current.add(dtoToModel(dto));
messageList.setValue(current);
}
public void updateConversationLocally(ConversationDTO dto) {
updateList(activeChats, dto);
updateList(closedChats, dto);
loadConversations();
}
private void updateList(MutableLiveData<List<Chat>> liveData, ConversationDTO dto) {
List<Chat> current = new ArrayList<>(liveData.getValue());
String name = customerNames.getOrDefault(dto.getCustomerId(), "Customer #" + dto.getCustomerId());
boolean updated = false;
for (int i = 0; i < current.size(); i++) {
if (current.get(i).getChatId().equals(String.valueOf(dto.getId()))) {
current.set(i, new Chat(String.valueOf(dto.getId()), name, dto.getLastMessage(), dto.getCustomerId(), dto.getStaffId(), dto.getStatus()));
updated = true;
break;
}
}
if (updated) liveData.setValue(current);
}
private Message dtoToModel(MessageDTO dto) {
Message m = new Message();
m.setId(dto.getId());
m.setConversationId(dto.getConversationId());
m.setSenderId(dto.getSenderId());
m.setContent(dto.getContent());
m.setTimestamp(dto.getTimestamp());
m.setIsRead(dto.getIsRead());
m.setAttachmentUrl(dto.getAttachmentUrl());
m.setAttachmentName(dto.getAttachmentName());
m.setAttachmentMimeType(dto.getAttachmentMimeType());
m.setAttachmentSizeBytes(dto.getAttachmentSizeBytes());
return m;
}
public String getCustomerName(Long customerId) {
return customerNames.getOrDefault(customerId, "Customer #" + customerId);
}
}

View File

@@ -1,58 +0,0 @@
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;
/**
* 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);
}
/**
* Fetches a paginated list of customers.
*/
public LiveData<Resource<PageResponse<CustomerDTO>>> getAllCustomers(int page, int size) {
return repository.getAllCustomers(page, size);
}
}

View File

@@ -1,37 +0,0 @@
package com.example.petstoremobile.viewmodels;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.ViewModel;
import com.example.petstoremobile.dtos.CustomerDTO;
import com.example.petstoremobile.dtos.PageResponse;
import com.example.petstoremobile.repositories.CustomerRepository;
import com.example.petstoremobile.utils.Resource;
import javax.inject.Inject;
import dagger.hilt.android.lifecycle.HiltViewModel;
@HiltViewModel
public class CustomerViewModel extends ViewModel {
private final CustomerRepository repository;
@Inject
public CustomerViewModel(CustomerRepository repository) {
this.repository = repository;
}
/**
* Fetches a paginated list of all customers.
*/
public LiveData<Resource<PageResponse<CustomerDTO>>> getAllCustomers(int page, int size) {
return repository.getAllCustomers(page, size);
}
/**
* Retrieves a single customer by their ID.
*/
public LiveData<Resource<CustomerDTO>> getCustomerById(Long id) {
return repository.getCustomerById(id);
}
}

View File

@@ -1,43 +0,0 @@
package com.example.petstoremobile.viewmodels;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.ViewModel;
import com.example.petstoremobile.dtos.EmployeeDTO;
import com.example.petstoremobile.dtos.PageResponse;
import com.example.petstoremobile.repositories.EmployeeRepository;
import com.example.petstoremobile.utils.Resource;
import javax.inject.Inject;
import dagger.hilt.android.lifecycle.HiltViewModel;
@HiltViewModel
public class EmployeeViewModel extends ViewModel {
private final EmployeeRepository employeeRepository;
@Inject
public EmployeeViewModel(EmployeeRepository employeeRepository) {
this.employeeRepository = employeeRepository;
}
public LiveData<Resource<PageResponse<EmployeeDTO>>> getAllEmployees(int page, int size) {
return employeeRepository.getAllEmployees(page, size);
}
public LiveData<Resource<EmployeeDTO>> getEmployeeById(Long id) {
return employeeRepository.getEmployeeById(id);
}
public LiveData<Resource<EmployeeDTO>> createEmployee(EmployeeDTO dto) {
return employeeRepository.createEmployee(dto);
}
public LiveData<Resource<EmployeeDTO>> updateEmployee(Long id, EmployeeDTO dto) {
return employeeRepository.updateEmployee(id, dto);
}
public LiveData<Resource<Void>> deleteEmployee(Long id) {
return employeeRepository.deleteEmployee(id);
}
}

View File

@@ -0,0 +1,79 @@
package com.example.petstoremobile.viewmodels;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.ViewModel;
import com.example.petstoremobile.dtos.DropdownDTO;
import com.example.petstoremobile.dtos.InventoryDTO;
import com.example.petstoremobile.dtos.PageResponse;
import com.example.petstoremobile.dtos.ProductDTO;
import com.example.petstoremobile.repositories.InventoryRepository;
import com.example.petstoremobile.repositories.ProductRepository;
import com.example.petstoremobile.repositories.StoreRepository;
import com.example.petstoremobile.utils.Resource;
import java.util.ArrayList;
import java.util.List;
import javax.inject.Inject;
import dagger.hilt.android.lifecycle.HiltViewModel;
@HiltViewModel
public class InventoryDetailViewModel extends ViewModel {
private final InventoryRepository inventoryRepository;
private final StoreRepository storeRepository;
private final ProductRepository productRepository;
private long inventoryId = -1;
private boolean isEditing = false;
private final MutableLiveData<List<DropdownDTO>> storeList = new MutableLiveData<>(new ArrayList<>());
private final MutableLiveData<List<ProductDTO>> productList = new MutableLiveData<>(new ArrayList<>());
@Inject
public InventoryDetailViewModel(InventoryRepository inventoryRepository, StoreRepository storeRepository, ProductRepository productRepository) {
this.inventoryRepository = inventoryRepository;
this.storeRepository = storeRepository;
this.productRepository = productRepository;
}
public void setInventoryId(long id) {
this.inventoryId = id;
this.isEditing = id != -1;
}
public long getInventoryId() { return inventoryId; }
public boolean isEditing() { return isEditing; }
public LiveData<Resource<InventoryDTO>> loadInventory() {
return inventoryRepository.getInventoryById(inventoryId);
}
public LiveData<Resource<List<DropdownDTO>>> loadStores() {
return storeRepository.getStoreDropdowns();
}
public LiveData<Resource<PageResponse<ProductDTO>>> loadProducts() {
return productRepository.getAllProducts(null, null, 0, 500, "prodName");
}
public LiveData<Resource<InventoryDTO>> saveInventory(InventoryDTO dto) {
if (isEditing) {
return inventoryRepository.updateInventory(inventoryId, dto);
} else {
return inventoryRepository.createInventory(dto);
}
}
public LiveData<Resource<Void>> deleteInventory() {
return inventoryRepository.deleteInventory(inventoryId);
}
public void setStoreList(List<DropdownDTO> list) { storeList.setValue(list); }
public LiveData<List<DropdownDTO>> getStoreList() { return storeList; }
public void setProductList(List<ProductDTO> list) { productList.setValue(list); }
public LiveData<List<ProductDTO>> getProductList() { return productList; }
}

View File

@@ -0,0 +1,81 @@
package com.example.petstoremobile.viewmodels;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.ViewModel;
import com.example.petstoremobile.dtos.BulkDeleteRequest;
import com.example.petstoremobile.dtos.InventoryDTO;
import com.example.petstoremobile.dtos.StoreDTO;
import com.example.petstoremobile.repositories.InventoryRepository;
import com.example.petstoremobile.repositories.StoreRepository;
import com.example.petstoremobile.utils.Resource;
import java.util.ArrayList;
import java.util.List;
import javax.inject.Inject;
import dagger.hilt.android.lifecycle.HiltViewModel;
@HiltViewModel
public class InventoryListViewModel extends ViewModel {
private final InventoryRepository inventoryRepository;
private final StoreRepository storeRepository;
private final MutableLiveData<List<InventoryDTO>> inventory = new MutableLiveData<>(new ArrayList<>());
private final MutableLiveData<List<StoreDTO>> stores = new MutableLiveData<>(new ArrayList<>());
private final MutableLiveData<Boolean> isLoading = new MutableLiveData<>(false);
private int currentPage = 0;
private boolean isLastPage = false;
private static final int PAGE_SIZE = 20;
@Inject
public InventoryListViewModel(InventoryRepository inventoryRepository, StoreRepository storeRepository) {
this.inventoryRepository = inventoryRepository;
this.storeRepository = storeRepository;
}
public LiveData<List<InventoryDTO>> getInventory() { return inventory; }
public LiveData<List<StoreDTO>> getStores() { return stores; }
public LiveData<Boolean> getIsLoading() { return isLoading; }
public boolean isLastPage() { return isLastPage; }
public void loadInventory(boolean reset, String query, Long storeId) {
if (isLoading.getValue() != null && isLoading.getValue() && !reset) return;
if (reset) {
currentPage = 0;
isLastPage = false;
}
isLoading.setValue(true);
inventoryRepository.getAllInventory(query, storeId, currentPage, PAGE_SIZE, "product.prodName").observeForever(resource -> {
if (resource != null) {
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
List<InventoryDTO> currentList = reset ? new ArrayList<>() : new ArrayList<>(inventory.getValue());
currentList.addAll(resource.data.getContent());
inventory.setValue(currentList);
isLastPage = resource.data.isLast();
if (!isLastPage) currentPage++;
isLoading.setValue(false);
} else if (resource.status == Resource.Status.ERROR) {
isLoading.setValue(false);
}
}
});
}
public void loadStores() {
storeRepository.getAllStores(0, 100).observeForever(resource -> {
if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) {
stores.setValue(resource.data.getContent());
}
});
}
public LiveData<Resource<Void>> bulkDeleteInventory(List<String> ids) {
return inventoryRepository.bulkDeleteInventory(new BulkDeleteRequest(ids));
}
}

View File

@@ -1,90 +0,0 @@
package com.example.petstoremobile.viewmodels;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.ViewModel;
import com.example.petstoremobile.dtos.BulkDeleteRequest;
import com.example.petstoremobile.dtos.CategoryDTO;
import com.example.petstoremobile.dtos.InventoryDTO;
import com.example.petstoremobile.dtos.PageResponse;
import com.example.petstoremobile.dtos.StoreDTO;
import com.example.petstoremobile.repositories.CategoryRepository;
import com.example.petstoremobile.repositories.InventoryRepository;
import com.example.petstoremobile.repositories.StoreRepository;
import com.example.petstoremobile.utils.Resource;
import java.util.List;
import javax.inject.Inject;
import dagger.hilt.android.lifecycle.HiltViewModel;
@HiltViewModel
public class InventoryViewModel extends ViewModel {
private final InventoryRepository inventoryRepository;
private final CategoryRepository categoryRepository;
private final StoreRepository storeRepository;
@Inject
public InventoryViewModel(InventoryRepository inventoryRepository, CategoryRepository categoryRepository, StoreRepository storeRepository) {
this.inventoryRepository = inventoryRepository;
this.categoryRepository = categoryRepository;
this.storeRepository = storeRepository;
}
/**
* Retrieves a paginated list of inventory items, with optional filtering and sorting.
*/
public LiveData<Resource<PageResponse<InventoryDTO>>> getAllInventory(String query, Long storeId, int page, int size, String sort) {
return inventoryRepository.getAllInventory(query, storeId, page, size, sort);
}
/**
* Retrieves a single inventory item by its ID.
*/
public LiveData<Resource<InventoryDTO>> getInventoryById(Long id) {
return inventoryRepository.getInventoryById(id);
}
/**
* Creates a new inventory record.
*/
public LiveData<Resource<InventoryDTO>> createInventory(InventoryDTO request) {
return inventoryRepository.createInventory(request);
}
/**
* Updates an existing inventory record by ID.
*/
public LiveData<Resource<InventoryDTO>> updateInventory(Long id, InventoryDTO request) {
return inventoryRepository.updateInventory(id, request);
}
/**
* Deletes an inventory record by ID.
*/
public LiveData<Resource<Void>> deleteInventory(Long id) {
return inventoryRepository.deleteInventory(id);
}
/**
* Deletes multiple inventory records in a single request.
*/
public LiveData<Resource<Void>> bulkDeleteInventory(List<String> ids) {
return inventoryRepository.bulkDeleteInventory(new BulkDeleteRequest(ids));
}
/**
* Retrieves a paginated list of categories.
*/
public LiveData<Resource<PageResponse<CategoryDTO>>> getAllCategories(int page, int size) {
return categoryRepository.getAllCategories(page, size);
}
/**
* Retrieves a paginated list of stores.
*/
public LiveData<Resource<PageResponse<StoreDTO>>> getAllStores(int page, int size) {
return storeRepository.getAllStores(page, size);
}
}

View File

@@ -0,0 +1,103 @@
package com.example.petstoremobile.viewmodels;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.ViewModel;
import com.example.petstoremobile.dtos.DropdownDTO;
import com.example.petstoremobile.dtos.PetDTO;
import com.example.petstoremobile.repositories.CustomerRepository;
import com.example.petstoremobile.repositories.PetRepository;
import com.example.petstoremobile.repositories.StoreRepository;
import com.example.petstoremobile.utils.Resource;
import java.util.ArrayList;
import java.util.List;
import javax.inject.Inject;
import dagger.hilt.android.lifecycle.HiltViewModel;
@HiltViewModel
public class PetDetailViewModel extends ViewModel {
private final PetRepository petRepository;
private final CustomerRepository customerRepository;
private final StoreRepository storeRepository;
private final MutableLiveData<PetDTO> petState = new MutableLiveData<>();
private final MutableLiveData<List<DropdownDTO>> customerList = new MutableLiveData<>(new ArrayList<>());
private final MutableLiveData<List<DropdownDTO>> storeList = new MutableLiveData<>(new ArrayList<>());
private final MutableLiveData<Boolean> isLoading = new MutableLiveData<>(false);
private long petId = -1;
private boolean isEditing = false;
@Inject
public PetDetailViewModel(PetRepository petRepository, CustomerRepository customerRepository, StoreRepository storeRepository) {
this.petRepository = petRepository;
this.customerRepository = customerRepository;
this.storeRepository = storeRepository;
}
public void setPetId(long id) {
this.petId = id;
this.isEditing = id != -1;
}
public long getPetId() {
return petId;
}
public boolean isEditing() {
return isEditing;
}
public LiveData<Resource<PetDTO>> loadPet() {
return petRepository.getPetById(petId);
}
public LiveData<Resource<List<DropdownDTO>>> loadCustomers() {
return customerRepository.getCustomerDropdowns();
}
public LiveData<Resource<List<DropdownDTO>>> loadStores() {
return storeRepository.getStoreDropdowns();
}
public LiveData<Resource<PetDTO>> savePet(PetDTO petDTO) {
if (isEditing) {
petDTO.setPetId(petId);
return petRepository.updatePet(petId, petDTO);
} else {
return petRepository.createPet(petDTO);
}
}
public LiveData<Resource<Void>> deletePet() {
return petRepository.deletePet(petId);
}
public void setCustomerList(List<DropdownDTO> list) {
customerList.setValue(list);
}
public LiveData<List<DropdownDTO>> getCustomerList() {
return customerList;
}
public void setStoreList(List<DropdownDTO> list) {
storeList.setValue(list);
}
public LiveData<List<DropdownDTO>> getStoreList() {
return storeList;
}
public LiveData<Boolean> getIsLoading() {
return isLoading;
}
public void setLoading(boolean loading) {
isLoading.setValue(loading);
}
}

View File

@@ -0,0 +1,69 @@
package com.example.petstoremobile.viewmodels;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.ViewModel;
import com.example.petstoremobile.dtos.BulkDeleteRequest;
import com.example.petstoremobile.dtos.PageResponse;
import com.example.petstoremobile.dtos.PetDTO;
import com.example.petstoremobile.dtos.StoreDTO;
import com.example.petstoremobile.repositories.PetRepository;
import com.example.petstoremobile.repositories.StoreRepository;
import com.example.petstoremobile.utils.Resource;
import java.util.ArrayList;
import java.util.List;
import javax.inject.Inject;
import dagger.hilt.android.lifecycle.HiltViewModel;
@HiltViewModel
public class PetListViewModel extends ViewModel {
private final PetRepository petRepository;
private final StoreRepository storeRepository;
private final MutableLiveData<List<PetDTO>> pets = new MutableLiveData<>(new ArrayList<>());
private final MutableLiveData<List<StoreDTO>> stores = new MutableLiveData<>(new ArrayList<>());
private final MutableLiveData<Boolean> isLoading = new MutableLiveData<>(false);
@Inject
public PetListViewModel(PetRepository petRepository, StoreRepository storeRepository) {
this.petRepository = petRepository;
this.storeRepository = storeRepository;
}
public LiveData<List<PetDTO>> getPets() { return pets; }
public LiveData<List<StoreDTO>> getStores() { return stores; }
public LiveData<Boolean> getIsLoading() { return isLoading; }
public void loadPets(String query, String status, String species, Long storeId) {
if ("All Statuses".equals(status)) status = null;
if ("All Species".equals(species)) species = null;
isLoading.setValue(true);
petRepository.getAllPets(0, 100, query, status, species, storeId, null, "petName").observeForever(resource -> {
if (resource != null) {
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
pets.setValue(resource.data.getContent());
isLoading.setValue(false);
} else if (resource.status == Resource.Status.ERROR) {
isLoading.setValue(false);
}
}
});
}
public void loadStores() {
storeRepository.getAllStores(0, 100).observeForever(resource -> {
if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) {
stores.setValue(resource.data.getContent());
}
});
}
public LiveData<Resource<Void>> bulkDeletePets(List<String> ids) {
return petRepository.bulkDeletePets(new BulkDeleteRequest(ids));
}
}

View File

@@ -0,0 +1,35 @@
package com.example.petstoremobile.viewmodels;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.ViewModel;
import com.example.petstoremobile.dtos.PetDTO;
import com.example.petstoremobile.repositories.PetRepository;
import com.example.petstoremobile.utils.Resource;
import javax.inject.Inject;
import dagger.hilt.android.lifecycle.HiltViewModel;
import okhttp3.MultipartBody;
@HiltViewModel
public class PetProfileViewModel extends ViewModel {
private final PetRepository repository;
@Inject
public PetProfileViewModel(PetRepository repository) {
this.repository = repository;
}
public LiveData<Resource<PetDTO>> getPetById(Long id) {
return repository.getPetById(id);
}
public LiveData<Resource<Void>> uploadPetImage(Long id, MultipartBody.Part image) {
return repository.uploadPetImage(id, image);
}
public LiveData<Resource<Void>> deletePetImage(Long id) {
return repository.deletePetImage(id);
}
}

View File

@@ -1,83 +0,0 @@
package com.example.petstoremobile.viewmodels;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.ViewModel;
import com.example.petstoremobile.dtos.BulkDeleteRequest;
import com.example.petstoremobile.dtos.PageResponse;
import com.example.petstoremobile.dtos.PetDTO;
import com.example.petstoremobile.repositories.PetRepository;
import com.example.petstoremobile.utils.Resource;
import java.util.List;
import javax.inject.Inject;
import dagger.hilt.android.lifecycle.HiltViewModel;
import okhttp3.MultipartBody;
@HiltViewModel
public class PetViewModel extends ViewModel {
private final PetRepository repository;
@Inject
public PetViewModel(PetRepository repository) {
this.repository = repository;
}
/**
* Fetches a paginated list of pets with filters.
*/
public LiveData<Resource<PageResponse<PetDTO>>> getAllPets(int page, int size, String query, String status, String species, Long storeId, String sort) {
return repository.getAllPets(page, size, query, status, species, storeId, sort);
}
/**
* Retrieves a single pet by its ID.
*/
public LiveData<Resource<PetDTO>> getPetById(Long id) {
return repository.getPetById(id);
}
/**
* Creates a new pet record.
*/
public LiveData<Resource<PetDTO>> createPet(PetDTO pet) {
return repository.createPet(pet);
}
/**
* Updates an existing pet record by ID.
*/
public LiveData<Resource<PetDTO>> updatePet(Long id, PetDTO pet) {
return repository.updatePet(id, pet);
}
/**
* Deletes a pet record by ID.
*/
public LiveData<Resource<Void>> deletePet(Long id) {
return repository.deletePet(id);
}
/**
* Deletes multiple pet records.
*/
public LiveData<Resource<Void>> bulkDeletePets(List<String> ids) {
return repository.bulkDeletePets(new BulkDeleteRequest(ids));
}
/**
* Uploads an image for a specific pet.
*/
public LiveData<Resource<Void>> uploadPetImage(Long id, MultipartBody.Part image) {
return repository.uploadPetImage(id, image);
}
/**
* Deletes the image associated with a specific pet.
*/
public LiveData<Resource<Void>> deletePetImage(Long id) {
return repository.deletePetImage(id);
}
}

View File

@@ -0,0 +1,85 @@
package com.example.petstoremobile.viewmodels;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.ViewModel;
import com.example.petstoremobile.dtos.CategoryDTO;
import com.example.petstoremobile.dtos.PageResponse;
import com.example.petstoremobile.dtos.ProductDTO;
import com.example.petstoremobile.repositories.CategoryRepository;
import com.example.petstoremobile.repositories.ProductRepository;
import com.example.petstoremobile.utils.Resource;
import java.util.ArrayList;
import java.util.List;
import javax.inject.Inject;
import dagger.hilt.android.lifecycle.HiltViewModel;
import okhttp3.MultipartBody;
@HiltViewModel
public class ProductDetailViewModel extends ViewModel {
private final ProductRepository productRepository;
private final CategoryRepository categoryRepository;
private final MutableLiveData<List<CategoryDTO>> categoryList = new MutableLiveData<>(new ArrayList<>());
private long prodId = -1;
private boolean isEditing = false;
@Inject
public ProductDetailViewModel(ProductRepository productRepository, CategoryRepository categoryRepository) {
this.productRepository = productRepository;
this.categoryRepository = categoryRepository;
}
public void setProdId(long id) {
this.prodId = id;
this.isEditing = id != -1;
}
public long getProdId() {
return prodId;
}
public boolean isEditing() {
return isEditing;
}
public LiveData<Resource<PageResponse<CategoryDTO>>> loadCategories() {
return categoryRepository.getAllCategories(0, 100);
}
public LiveData<Resource<ProductDTO>> loadProduct() {
return productRepository.getProductById(prodId);
}
public LiveData<Resource<ProductDTO>> saveProduct(ProductDTO dto) {
if (isEditing) {
return productRepository.updateProduct(prodId, dto);
} else {
return productRepository.createProduct(dto);
}
}
public LiveData<Resource<Void>> deleteProduct() {
return productRepository.deleteProduct(prodId);
}
public LiveData<Resource<Void>> uploadProductImage(MultipartBody.Part image) {
return productRepository.uploadProductImage(prodId, image);
}
public LiveData<Resource<Void>> deleteProductImage() {
return productRepository.deleteProductImage(prodId);
}
public void setCategoryList(List<CategoryDTO> list) {
categoryList.setValue(list);
}
public LiveData<List<CategoryDTO>> getCategoryList() {
return categoryList;
}
}

View File

@@ -0,0 +1,60 @@
package com.example.petstoremobile.viewmodels;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.ViewModel;
import com.example.petstoremobile.dtos.CategoryDTO;
import com.example.petstoremobile.dtos.ProductDTO;
import com.example.petstoremobile.repositories.CategoryRepository;
import com.example.petstoremobile.repositories.ProductRepository;
import com.example.petstoremobile.utils.Resource;
import java.util.ArrayList;
import java.util.List;
import javax.inject.Inject;
import dagger.hilt.android.lifecycle.HiltViewModel;
@HiltViewModel
public class ProductListViewModel extends ViewModel {
private final ProductRepository productRepository;
private final CategoryRepository categoryRepository;
private final MutableLiveData<List<ProductDTO>> products = new MutableLiveData<>(new ArrayList<>());
private final MutableLiveData<List<CategoryDTO>> categories = new MutableLiveData<>(new ArrayList<>());
private final MutableLiveData<Boolean> isLoading = new MutableLiveData<>(false);
@Inject
public ProductListViewModel(ProductRepository productRepository, CategoryRepository categoryRepository) {
this.productRepository = productRepository;
this.categoryRepository = categoryRepository;
}
public LiveData<List<ProductDTO>> getProducts() { return products; }
public LiveData<List<CategoryDTO>> getCategories() { return categories; }
public LiveData<Boolean> getIsLoading() { return isLoading; }
public void loadProducts(String query, Long categoryId) {
isLoading.setValue(true);
productRepository.getAllProducts(query, categoryId, 0, 100, "prodName").observeForever(resource -> {
if (resource != null) {
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
products.setValue(resource.data.getContent());
isLoading.setValue(false);
} else if (resource.status == Resource.Status.ERROR) {
isLoading.setValue(false);
}
}
});
}
public void loadCategories() {
categoryRepository.getAllCategories(0, 100).observeForever(resource -> {
if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) {
categories.setValue(resource.data.getContent());
}
});
}
}

View File

@@ -0,0 +1,78 @@
package com.example.petstoremobile.viewmodels;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.ViewModel;
import com.example.petstoremobile.dtos.PageResponse;
import com.example.petstoremobile.dtos.ProductDTO;
import com.example.petstoremobile.dtos.ProductSupplierDTO;
import com.example.petstoremobile.dtos.SupplierDTO;
import com.example.petstoremobile.repositories.ProductRepository;
import com.example.petstoremobile.repositories.ProductSupplierRepository;
import com.example.petstoremobile.repositories.SupplierRepository;
import com.example.petstoremobile.utils.Resource;
import java.util.ArrayList;
import java.util.List;
import javax.inject.Inject;
import dagger.hilt.android.lifecycle.HiltViewModel;
@HiltViewModel
public class ProductSupplierDetailViewModel extends ViewModel {
private final ProductSupplierRepository psRepository;
private final ProductRepository productRepository;
private final SupplierRepository supplierRepository;
private boolean isEditing = false;
private long editProductId = -1;
private long editSupplierId = -1;
private final MutableLiveData<List<ProductDTO>> productList = new MutableLiveData<>(new ArrayList<>());
private final MutableLiveData<List<SupplierDTO>> supplierList = new MutableLiveData<>(new ArrayList<>());
@Inject
public ProductSupplierDetailViewModel(ProductSupplierRepository psRepository, ProductRepository productRepository, SupplierRepository supplierRepository) {
this.psRepository = psRepository;
this.productRepository = productRepository;
this.supplierRepository = supplierRepository;
}
public void setEditMode(long productId, long supplierId) {
this.isEditing = true;
this.editProductId = productId;
this.editSupplierId = supplierId;
}
public boolean isEditing() { return isEditing; }
public long getEditProductId() { return editProductId; }
public long getEditSupplierId() { return editSupplierId; }
public LiveData<Resource<PageResponse<ProductDTO>>> loadProducts() {
return productRepository.getAllProducts(null, null, 0, 200, "prodName");
}
public LiveData<Resource<PageResponse<SupplierDTO>>> loadSuppliers() {
return supplierRepository.getAllSuppliers(0, 200, null, "supCompany");
}
public LiveData<Resource<ProductSupplierDTO>> saveProductSupplier(ProductSupplierDTO dto) {
if (isEditing) {
return psRepository.updateProductSupplier(editProductId, editSupplierId, dto);
} else {
return psRepository.createProductSupplier(dto);
}
}
public LiveData<Resource<Void>> deleteProductSupplier() {
return psRepository.deleteProductSupplier(editProductId, editSupplierId);
}
public void setProductList(List<ProductDTO> list) { productList.setValue(list); }
public LiveData<List<ProductDTO>> getProductList() { return productList; }
public void setSupplierList(List<SupplierDTO> list) { supplierList.setValue(list); }
public LiveData<List<SupplierDTO>> getSupplierList() { return supplierList; }
}

View File

@@ -0,0 +1,77 @@
package com.example.petstoremobile.viewmodels;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.ViewModel;
import com.example.petstoremobile.dtos.BulkDeleteRequest;
import com.example.petstoremobile.dtos.ProductDTO;
import com.example.petstoremobile.dtos.ProductSupplierDTO;
import com.example.petstoremobile.dtos.SupplierDTO;
import com.example.petstoremobile.repositories.ProductRepository;
import com.example.petstoremobile.repositories.ProductSupplierRepository;
import com.example.petstoremobile.repositories.SupplierRepository;
import com.example.petstoremobile.utils.Resource;
import java.util.ArrayList;
import java.util.List;
import javax.inject.Inject;
import dagger.hilt.android.lifecycle.HiltViewModel;
@HiltViewModel
public class ProductSupplierListViewModel extends ViewModel {
private final ProductSupplierRepository psRepository;
private final ProductRepository productRepository;
private final SupplierRepository supplierRepository;
private final MutableLiveData<List<ProductSupplierDTO>> productSuppliers = new MutableLiveData<>(new ArrayList<>());
private final MutableLiveData<List<ProductDTO>> products = new MutableLiveData<>(new ArrayList<>());
private final MutableLiveData<List<SupplierDTO>> suppliers = new MutableLiveData<>(new ArrayList<>());
private final MutableLiveData<Boolean> isLoading = new MutableLiveData<>(false);
@Inject
public ProductSupplierListViewModel(ProductSupplierRepository psRepository, ProductRepository productRepository, SupplierRepository supplierRepository) {
this.psRepository = psRepository;
this.productRepository = productRepository;
this.supplierRepository = supplierRepository;
}
public LiveData<List<ProductSupplierDTO>> getProductSuppliers() { return productSuppliers; }
public LiveData<List<ProductDTO>> getProducts() { return products; }
public LiveData<List<SupplierDTO>> getSuppliers() { return suppliers; }
public LiveData<Boolean> getIsLoading() { return isLoading; }
public void loadProductSuppliers(String query, Long productId, Long supplierId) {
isLoading.setValue(true);
psRepository.getAllProductSuppliers(0, 100, query, productId, supplierId, "productName").observeForever(resource -> {
if (resource != null) {
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
productSuppliers.setValue(resource.data.getContent());
isLoading.setValue(false);
} else if (resource.status == Resource.Status.ERROR) {
isLoading.setValue(false);
}
}
});
}
public void loadFilterData() {
productRepository.getAllProducts(null, null, 0, 100, "prodName").observeForever(resource -> {
if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) {
products.setValue(resource.data.getContent());
}
});
supplierRepository.getAllSuppliers(0, 100, null, "supCompany").observeForever(resource -> {
if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) {
suppliers.setValue(resource.data.getContent());
}
});
}
public LiveData<Resource<Void>> bulkDeleteProductSuppliers(List<String> ids) {
return psRepository.bulkDeleteProductSuppliers(new BulkDeleteRequest(ids));
}
}

View File

@@ -1,58 +0,0 @@
package com.example.petstoremobile.viewmodels;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.ViewModel;
import com.example.petstoremobile.dtos.BulkDeleteRequest;
import com.example.petstoremobile.dtos.PageResponse;
import com.example.petstoremobile.dtos.ProductSupplierDTO;
import com.example.petstoremobile.repositories.ProductSupplierRepository;
import com.example.petstoremobile.utils.Resource;
import java.util.List;
import javax.inject.Inject;
import dagger.hilt.android.lifecycle.HiltViewModel;
@HiltViewModel
public class ProductSupplierViewModel extends ViewModel {
private final ProductSupplierRepository repository;
@Inject
public ProductSupplierViewModel(ProductSupplierRepository repository) {
this.repository = repository;
}
/**
* Fetches a paginated list of all product-supplier relationships.
*/
public LiveData<Resource<PageResponse<ProductSupplierDTO>>> getAllProductSuppliers(int page, int size, String query, Long productId, Long supplierId, String sort) {
return repository.getAllProductSuppliers(page, size, query, productId, supplierId, sort);
}
/**
* Creates a new product-supplier relationship.
*/
public LiveData<Resource<ProductSupplierDTO>> createProductSupplier(ProductSupplierDTO dto) {
return repository.createProductSupplier(dto);
}
/**
* Updates an existing product-supplier relationship.
*/
public LiveData<Resource<ProductSupplierDTO>> updateProductSupplier(Long productId, Long supplierId, ProductSupplierDTO dto) {
return repository.updateProductSupplier(productId, supplierId, dto);
}
/**
* Deletes a product-supplier relationship by product and supplier IDs.
*/
public LiveData<Resource<Void>> deleteProductSupplier(Long productId, Long supplierId) {
return repository.deleteProductSupplier(productId, supplierId);
}
public LiveData<Resource<Void>> bulkDeleteProductSuppliers(List<String> ids) {
return repository.bulkDeleteProductSuppliers(new BulkDeleteRequest(ids));
}
}

View File

@@ -1,84 +0,0 @@
package com.example.petstoremobile.viewmodels;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.ViewModel;
import com.example.petstoremobile.dtos.CategoryDTO;
import com.example.petstoremobile.dtos.PageResponse;
import com.example.petstoremobile.dtos.ProductDTO;
import com.example.petstoremobile.repositories.CategoryRepository;
import com.example.petstoremobile.repositories.ProductRepository;
import com.example.petstoremobile.utils.Resource;
import javax.inject.Inject;
import dagger.hilt.android.lifecycle.HiltViewModel;
import okhttp3.MultipartBody;
@HiltViewModel
public class ProductViewModel extends ViewModel {
private final ProductRepository productRepository;
private final CategoryRepository categoryRepository;
@Inject
public ProductViewModel(ProductRepository productRepository, CategoryRepository categoryRepository) {
this.productRepository = productRepository;
this.categoryRepository = categoryRepository;
}
/**
* Retrieves a paginated list of products, optionally filtered by a query string, category and sorted.
*/
public LiveData<Resource<PageResponse<ProductDTO>>> getAllProducts(String query, Long categoryId, int page, int size, String sort) {
return productRepository.getAllProducts(query, categoryId, page, size, sort);
}
/**
* Retrieves a single product by its ID.
*/
public LiveData<Resource<ProductDTO>> getProductById(Long id) {
return productRepository.getProductById(id);
}
/**
* Creates a new product.
*/
public LiveData<Resource<ProductDTO>> createProduct(ProductDTO product) {
return productRepository.createProduct(product);
}
/**
* Updates an existing product by ID.
*/
public LiveData<Resource<ProductDTO>> updateProduct(Long id, ProductDTO product) {
return productRepository.updateProduct(id, product);
}
/**
* Deletes a product by its ID.
*/
public LiveData<Resource<Void>> deleteProduct(Long id) {
return productRepository.deleteProduct(id);
}
/**
* Uploads an image for a specific product.
*/
public LiveData<Resource<Void>> uploadProductImage(Long id, MultipartBody.Part image) {
return productRepository.uploadProductImage(id, image);
}
/**
* Deletes the image associated with a specific product.
*/
public LiveData<Resource<Void>> deleteProductImage(Long id) {
return productRepository.deleteProductImage(id);
}
/**
* Retrieves a paginated list of all product categories.
*/
public LiveData<Resource<PageResponse<CategoryDTO>>> getAllCategories(int page, int size) {
return categoryRepository.getAllCategories(page, size);
}
}

View File

@@ -0,0 +1,26 @@
package com.example.petstoremobile.viewmodels;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.ViewModel;
import com.example.petstoremobile.dtos.PurchaseOrderDTO;
import com.example.petstoremobile.repositories.PurchaseOrderRepository;
import com.example.petstoremobile.utils.Resource;
import javax.inject.Inject;
import dagger.hilt.android.lifecycle.HiltViewModel;
@HiltViewModel
public class PurchaseOrderDetailViewModel extends ViewModel {
private final PurchaseOrderRepository repository;
@Inject
public PurchaseOrderDetailViewModel(PurchaseOrderRepository repository) {
this.repository = repository;
}
public LiveData<Resource<PurchaseOrderDTO>> loadPurchaseOrder(long id) {
return repository.getPurchaseOrderById(id);
}
}

View File

@@ -0,0 +1,60 @@
package com.example.petstoremobile.viewmodels;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.ViewModel;
import com.example.petstoremobile.dtos.PurchaseOrderDTO;
import com.example.petstoremobile.dtos.StoreDTO;
import com.example.petstoremobile.repositories.PurchaseOrderRepository;
import com.example.petstoremobile.repositories.StoreRepository;
import com.example.petstoremobile.utils.Resource;
import java.util.ArrayList;
import java.util.List;
import javax.inject.Inject;
import dagger.hilt.android.lifecycle.HiltViewModel;
@HiltViewModel
public class PurchaseOrderListViewModel extends ViewModel {
private final PurchaseOrderRepository purchaseOrderRepository;
private final StoreRepository storeRepository;
private final MutableLiveData<List<PurchaseOrderDTO>> purchaseOrders = new MutableLiveData<>(new ArrayList<>());
private final MutableLiveData<List<StoreDTO>> stores = new MutableLiveData<>(new ArrayList<>());
private final MutableLiveData<Boolean> isLoading = new MutableLiveData<>(false);
@Inject
public PurchaseOrderListViewModel(PurchaseOrderRepository purchaseOrderRepository, StoreRepository storeRepository) {
this.purchaseOrderRepository = purchaseOrderRepository;
this.storeRepository = storeRepository;
}
public LiveData<List<PurchaseOrderDTO>> getPurchaseOrders() { return purchaseOrders; }
public LiveData<List<StoreDTO>> getStores() { return stores; }
public LiveData<Boolean> getIsLoading() { return isLoading; }
public void loadPurchaseOrders(String query, Long storeId) {
isLoading.setValue(true);
purchaseOrderRepository.getAllPurchaseOrders(0, 100, query, storeId, "purchaseOrderId,desc").observeForever(resource -> {
if (resource != null) {
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
purchaseOrders.setValue(resource.data.getContent());
isLoading.setValue(false);
} else if (resource.status == Resource.Status.ERROR) {
isLoading.setValue(false);
}
}
});
}
public void loadStores() {
storeRepository.getAllStores(0, 100).observeForever(resource -> {
if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) {
stores.setValue(resource.data.getContent());
}
});
}
}

View File

@@ -1,37 +0,0 @@
package com.example.petstoremobile.viewmodels;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.ViewModel;
import com.example.petstoremobile.dtos.PageResponse;
import com.example.petstoremobile.dtos.PurchaseOrderDTO;
import com.example.petstoremobile.repositories.PurchaseOrderRepository;
import com.example.petstoremobile.utils.Resource;
import javax.inject.Inject;
import dagger.hilt.android.lifecycle.HiltViewModel;
@HiltViewModel
public class PurchaseOrderViewModel extends ViewModel {
private final PurchaseOrderRepository repository;
@Inject
public PurchaseOrderViewModel(PurchaseOrderRepository repository) {
this.repository = repository;
}
/**
* Fetches a paginated list of all purchase orders.
*/
public LiveData<Resource<PageResponse<PurchaseOrderDTO>>> getAllPurchaseOrders(int page, int size, String query, Long storeId, String sort) {
return repository.getAllPurchaseOrders(page, size, query, storeId, sort);
}
/**
* Retrieves a single purchase order by its ID.
*/
public LiveData<Resource<PurchaseOrderDTO>> getPurchaseOrderById(Long id) {
return repository.getPurchaseOrderById(id);
}
}

View File

@@ -0,0 +1,167 @@
package com.example.petstoremobile.viewmodels;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.ViewModel;
import com.example.petstoremobile.dtos.PageResponse;
import com.example.petstoremobile.dtos.SaleDTO;
import com.example.petstoremobile.repositories.SaleRepository;
import com.example.petstoremobile.utils.Resource;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.inject.Inject;
import dagger.hilt.android.lifecycle.HiltViewModel;
@HiltViewModel
public class RefundViewModel extends ViewModel {
private final SaleRepository saleRepository;
private final MutableLiveData<List<SaleDTO>> allSales = new MutableLiveData<>(new ArrayList<>());
private final MutableLiveData<SaleDTO> currentSale = new MutableLiveData<>();
private final MutableLiveData<List<RefundItem>> availableItems = new MutableLiveData<>(new ArrayList<>());
private final MutableLiveData<List<RefundItem>> refundCart = new MutableLiveData<>(new ArrayList<>());
@Inject
public RefundViewModel(SaleRepository saleRepository) {
this.saleRepository = saleRepository;
}
public LiveData<Resource<PageResponse<SaleDTO>>> loadAllSales() {
return saleRepository.getAllSales(0, 1000, null, null, null, "saleDate,desc");
}
public void setAllSales(List<SaleDTO> sales) {
allSales.setValue(sales);
}
public List<SaleDTO> getAllSalesList() {
return allSales.getValue();
}
public void setCurrentSale(SaleDTO sale) {
currentSale.setValue(sale);
buildRefundableItems();
}
public SaleDTO getCurrentSale() {
return currentSale.getValue();
}
public LiveData<List<RefundItem>> getAvailableItems() {
return availableItems;
}
public LiveData<List<RefundItem>> getRefundCart() {
return refundCart;
}
private void buildRefundableItems() {
SaleDTO sale = currentSale.getValue();
List<SaleDTO> sales = allSales.getValue();
if (sale == null || sales == null || sale.getItems() == null) {
availableItems.setValue(new ArrayList<>());
return;
}
Map<Long, Integer> alreadyRefunded = new HashMap<>();
for (SaleDTO s : sales) {
if (Boolean.TRUE.equals(s.getIsRefund())
&& sale.getSaleId().equals(s.getOriginalSaleId())
&& s.getItems() != null) {
for (SaleDTO.SaleItemDTO item : s.getItems()) {
if (item.getProdId() != null && item.getQuantity() != null) {
alreadyRefunded.merge(item.getProdId(),
Math.abs(item.getQuantity()), Integer::sum);
}
}
}
}
List<RefundItem> items = new ArrayList<>();
for (SaleDTO.SaleItemDTO item : sale.getItems()) {
if (item.getProdId() == null || item.getQuantity() == null) continue;
int refunded = alreadyRefunded.getOrDefault(item.getProdId(), 0);
int remaining = item.getQuantity() - refunded;
if (remaining > 0) {
items.add(new RefundItem(
item.getProdId(),
item.getProductName() != null ? item.getProductName() : "Unknown",
remaining,
item.getUnitPrice()
));
}
}
availableItems.setValue(items);
refundCart.setValue(new ArrayList<>());
}
public void addToCart(RefundItem item, int qty) {
List<RefundItem> cart = new ArrayList<>(refundCart.getValue());
boolean merged = false;
for (int i = 0; i < cart.size(); i++) {
if (cart.get(i).prodId == item.prodId) {
RefundItem existing = cart.get(i);
cart.set(i, new RefundItem(existing.prodId, existing.productName, existing.quantity + qty, existing.unitPrice));
merged = true;
break;
}
}
if (!merged) {
cart.add(new RefundItem(item.prodId, item.productName, qty, item.unitPrice));
}
refundCart.setValue(cart);
}
public void removeFromCart(RefundItem item) {
List<RefundItem> cart = new ArrayList<>(refundCart.getValue());
cart.remove(item);
refundCart.setValue(cart);
}
public LiveData<Resource<SaleDTO>> submitRefund(String paymentMethod) {
SaleDTO sale = currentSale.getValue();
List<RefundItem> cart = refundCart.getValue();
if (sale == null || cart == null || cart.isEmpty()) return null;
List<SaleDTO.SaleItemDTO> items = new ArrayList<>();
for (RefundItem item : cart) {
items.add(new SaleDTO.SaleItemDTO(item.prodId, -item.quantity));
}
SaleDTO dto = new SaleDTO(
sale.getStoreId(),
paymentMethod,
items,
true,
sale.getSaleId(),
null
);
return saleRepository.createSale(dto);
}
public static class RefundItem {
public long prodId;
public String productName;
public int quantity;
public BigDecimal unitPrice;
public RefundItem(long prodId, String productName, int quantity, BigDecimal unitPrice) {
this.prodId = prodId;
this.productName = productName;
this.quantity = quantity;
this.unitPrice = unitPrice;
}
public BigDecimal getTotal() {
return unitPrice != null ? unitPrice.multiply(BigDecimal.valueOf(quantity)) : BigDecimal.ZERO;
}
}
}

View File

@@ -0,0 +1,118 @@
package com.example.petstoremobile.viewmodels;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.ViewModel;
import com.example.petstoremobile.dtos.DropdownDTO;
import com.example.petstoremobile.dtos.ProductDTO;
import com.example.petstoremobile.dtos.SaleDTO;
import com.example.petstoremobile.repositories.CustomerRepository;
import com.example.petstoremobile.repositories.ProductRepository;
import com.example.petstoremobile.repositories.SaleRepository;
import com.example.petstoremobile.repositories.StoreRepository;
import com.example.petstoremobile.utils.Resource;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
import javax.inject.Inject;
import dagger.hilt.android.lifecycle.HiltViewModel;
@HiltViewModel
public class SaleDetailViewModel extends ViewModel {
private final SaleRepository saleRepository;
private final StoreRepository storeRepository;
private final CustomerRepository customerRepository;
private final ProductRepository productRepository;
private long saleId = -1;
private boolean viewOnly = false;
private final MutableLiveData<List<DropdownDTO>> storeList = new MutableLiveData<>(new ArrayList<>());
private final MutableLiveData<List<DropdownDTO>> customerList = new MutableLiveData<>(new ArrayList<>());
private final MutableLiveData<List<ProductDTO>> productList = new MutableLiveData<>(new ArrayList<>());
private final MutableLiveData<List<SaleDTO.SaleItemDTO>> cartItems = new MutableLiveData<>(new ArrayList<>());
private final MutableLiveData<Boolean> isLoading = new MutableLiveData<>(false);
@Inject
public SaleDetailViewModel(SaleRepository saleRepository, StoreRepository storeRepository,
CustomerRepository customerRepository, ProductRepository productRepository) {
this.saleRepository = saleRepository;
this.storeRepository = storeRepository;
this.customerRepository = customerRepository;
this.productRepository = productRepository;
}
public void setSaleId(long id, boolean viewOnly) {
this.saleId = id;
this.viewOnly = viewOnly;
}
public long getSaleId() { return saleId; }
public boolean isViewOnly() { return viewOnly; }
public LiveData<Resource<SaleDTO>> loadSaleDetails() {
return saleRepository.getSaleById(saleId);
}
public LiveData<Resource<List<DropdownDTO>>> loadStores() {
return storeRepository.getStoreDropdowns();
}
public LiveData<Resource<List<DropdownDTO>>> loadCustomers() {
return customerRepository.getCustomerDropdowns();
}
public LiveData<Resource<com.example.petstoremobile.dtos.PageResponse<ProductDTO>>> loadProducts() {
return productRepository.getAllProducts(null, null, 0, 200, null);
}
public LiveData<Resource<SaleDTO>> createSale(SaleDTO sale) {
return saleRepository.createSale(sale);
}
public void setStoreList(List<DropdownDTO> list) { storeList.setValue(list); }
public LiveData<List<DropdownDTO>> getStoreList() { return storeList; }
public void setCustomerList(List<DropdownDTO> list) { customerList.setValue(list); }
public LiveData<List<DropdownDTO>> getCustomerList() { return customerList; }
public void setProductList(List<ProductDTO> list) { productList.setValue(list); }
public LiveData<List<ProductDTO>> getProductList() { return productList; }
public void addToCart(SaleDTO.SaleItemDTO item) {
List<SaleDTO.SaleItemDTO> currentCart = new ArrayList<>(cartItems.getValue());
currentCart.add(item);
cartItems.setValue(currentCart);
}
public LiveData<List<SaleDTO.SaleItemDTO>> getCartItems() { return cartItems; }
public BigDecimal calculateSubtotal() {
BigDecimal total = BigDecimal.ZERO;
List<SaleDTO.SaleItemDTO> items = cartItems.getValue();
List<ProductDTO> products = productList.getValue();
if (items != null && products != null) {
for (SaleDTO.SaleItemDTO item : items) {
for (ProductDTO p : products) {
if (p.getProdId().equals(item.getProdId()) && p.getProdPrice() != null) {
total = total.add(p.getProdPrice().multiply(BigDecimal.valueOf(item.getQuantity())));
break;
}
}
}
}
return total;
}
public LiveData<Boolean> getIsLoading() {
return isLoading;
}
public void setLoading(boolean loading) {
isLoading.setValue(loading);
}
}

View File

@@ -0,0 +1,77 @@
package com.example.petstoremobile.viewmodels;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.ViewModel;
import com.example.petstoremobile.dtos.PageResponse;
import com.example.petstoremobile.dtos.SaleDTO;
import com.example.petstoremobile.dtos.StoreDTO;
import com.example.petstoremobile.repositories.SaleRepository;
import com.example.petstoremobile.repositories.StoreRepository;
import com.example.petstoremobile.utils.Resource;
import java.util.ArrayList;
import java.util.List;
import javax.inject.Inject;
import dagger.hilt.android.lifecycle.HiltViewModel;
@HiltViewModel
public class SaleListViewModel extends ViewModel {
private final SaleRepository saleRepository;
private final StoreRepository storeRepository;
private final MutableLiveData<List<SaleDTO>> sales = new MutableLiveData<>(new ArrayList<>());
private final MutableLiveData<List<StoreDTO>> stores = new MutableLiveData<>(new ArrayList<>());
private final MutableLiveData<Boolean> isLoading = new MutableLiveData<>(false);
private int currentPage = 0;
private boolean isLastPage = false;
private static final int PAGE_SIZE = 20;
@Inject
public SaleListViewModel(SaleRepository saleRepository, StoreRepository storeRepository) {
this.saleRepository = saleRepository;
this.storeRepository = storeRepository;
}
public LiveData<List<SaleDTO>> getSales() { return sales; }
public LiveData<List<StoreDTO>> getStores() { return stores; }
public LiveData<Boolean> getIsLoading() { return isLoading; }
public boolean isLastPage() { return isLastPage; }
public void loadSales(boolean reset, String query, String paymentMethod, Long storeId) {
if (isLoading.getValue() != null && isLoading.getValue() && !reset) return;
if (reset) {
currentPage = 0;
isLastPage = false;
}
isLoading.setValue(true);
saleRepository.getAllSales(currentPage, PAGE_SIZE, query, paymentMethod, storeId, "saleDate,desc").observeForever(resource -> {
if (resource != null) {
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
List<SaleDTO> currentList = reset ? new ArrayList<>() : new ArrayList<>(sales.getValue());
currentList.addAll(resource.data.getContent());
sales.setValue(currentList);
isLastPage = resource.data.isLast();
if (!isLastPage) currentPage++;
isLoading.setValue(false);
} else if (resource.status == Resource.Status.ERROR) {
isLoading.setValue(false);
}
}
});
}
public void loadStores() {
storeRepository.getAllStores(0, 100).observeForever(resource -> {
if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) {
stores.setValue(resource.data.getContent());
}
});
}
}

View File

@@ -1,35 +0,0 @@
package com.example.petstoremobile.viewmodels;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.ViewModel;
import com.example.petstoremobile.dtos.PageResponse;
import com.example.petstoremobile.dtos.SaleDTO;
import com.example.petstoremobile.repositories.SaleRepository;
import com.example.petstoremobile.utils.Resource;
import javax.inject.Inject;
import dagger.hilt.android.lifecycle.HiltViewModel;
@HiltViewModel
public class SaleViewModel extends ViewModel {
private final SaleRepository saleRepository;
@Inject
public SaleViewModel(SaleRepository saleRepository) {
this.saleRepository = saleRepository;
}
public LiveData<Resource<PageResponse<SaleDTO>>> getAllSales(int page, int size, String query, String paymentMethod, Long storeId, String sortBy) {
return saleRepository.getAllSales(page, size, query, paymentMethod, storeId, sortBy);
}
public LiveData<Resource<SaleDTO>> getSaleById(Long id) {
return saleRepository.getSaleById(id);
}
public LiveData<Resource<SaleDTO>> createSale(SaleDTO sale) {
return saleRepository.createSale(sale);
}
}

View File

@@ -0,0 +1,54 @@
package com.example.petstoremobile.viewmodels;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.ViewModel;
import com.example.petstoremobile.dtos.ServiceDTO;
import com.example.petstoremobile.repositories.ServiceRepository;
import com.example.petstoremobile.utils.Resource;
import javax.inject.Inject;
import dagger.hilt.android.lifecycle.HiltViewModel;
@HiltViewModel
public class ServiceDetailViewModel extends ViewModel {
private final ServiceRepository repository;
private long serviceId = -1;
private boolean isEditing = false;
@Inject
public ServiceDetailViewModel(ServiceRepository repository) {
this.repository = repository;
}
public void setServiceId(long id) {
this.serviceId = id;
this.isEditing = id != -1;
}
public long getServiceId() {
return serviceId;
}
public boolean isEditing() {
return isEditing;
}
public LiveData<Resource<ServiceDTO>> loadService() {
return repository.getServiceById(serviceId);
}
public LiveData<Resource<ServiceDTO>> saveService(ServiceDTO dto) {
if (isEditing) {
dto.setServiceId(serviceId);
return repository.updateService(serviceId, dto);
} else {
return repository.createService(dto);
}
}
public LiveData<Resource<Void>> deleteService() {
return repository.deleteService(serviceId);
}
}

View File

@@ -0,0 +1,68 @@
package com.example.petstoremobile.viewmodels;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.ViewModel;
import com.example.petstoremobile.dtos.BulkDeleteRequest;
import com.example.petstoremobile.dtos.PageResponse;
import com.example.petstoremobile.dtos.ServiceDTO;
import com.example.petstoremobile.repositories.ServiceRepository;
import com.example.petstoremobile.utils.Resource;
import java.util.ArrayList;
import java.util.List;
import javax.inject.Inject;
import dagger.hilt.android.lifecycle.HiltViewModel;
@HiltViewModel
public class ServiceListViewModel extends ViewModel {
private final ServiceRepository repository;
private final MutableLiveData<List<ServiceDTO>> services = new MutableLiveData<>(new ArrayList<>());
private final MutableLiveData<Boolean> isLoading = new MutableLiveData<>(false);
private int currentPage = 0;
private boolean isLastPage = false;
private static final int PAGE_SIZE = 20;
@Inject
public ServiceListViewModel(ServiceRepository repository) {
this.repository = repository;
}
public LiveData<List<ServiceDTO>> getServices() { return services; }
public LiveData<Boolean> getIsLoading() { return isLoading; }
public boolean isLastPage() { return isLastPage; }
public void loadServices(boolean reset, String query) {
if (isLoading.getValue() != null && isLoading.getValue() && !reset) return;
if (reset) {
currentPage = 0;
isLastPage = false;
}
isLoading.setValue(true);
repository.getAllServices(currentPage, PAGE_SIZE, query, "serviceName").observeForever(resource -> {
if (resource != null) {
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
List<ServiceDTO> currentList = reset ? new ArrayList<>() : new ArrayList<>(services.getValue());
currentList.addAll(resource.data.getContent());
services.setValue(currentList);
isLastPage = resource.data.isLast();
if (!isLastPage) currentPage++;
isLoading.setValue(false);
} else if (resource.status == Resource.Status.ERROR) {
isLoading.setValue(false);
}
}
});
}
public LiveData<Resource<Void>> bulkDeleteServices(List<String> ids) {
return repository.bulkDeleteServices(new BulkDeleteRequest(ids));
}
}

View File

@@ -1,68 +0,0 @@
package com.example.petstoremobile.viewmodels;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.ViewModel;
import com.example.petstoremobile.dtos.BulkDeleteRequest;
import com.example.petstoremobile.dtos.PageResponse;
import com.example.petstoremobile.dtos.ServiceDTO;
import com.example.petstoremobile.repositories.ServiceRepository;
import com.example.petstoremobile.utils.Resource;
import java.util.List;
import javax.inject.Inject;
import dagger.hilt.android.lifecycle.HiltViewModel;
@HiltViewModel
public class ServiceViewModel extends ViewModel {
private final ServiceRepository repository;
@Inject
public ServiceViewModel(ServiceRepository repository) {
this.repository = repository;
}
/**
* Fetches a paginated list of all services.
*/
public LiveData<Resource<PageResponse<ServiceDTO>>> getAllServices(int page, int size, String query, String sort) {
return repository.getAllServices(page, size, query, sort);
}
/**
* Retrieves a single service by its ID.
*/
public LiveData<Resource<ServiceDTO>> getServiceById(Long id) {
return repository.getServiceById(id);
}
/**
* Creates a new service.
*/
public LiveData<Resource<ServiceDTO>> createService(ServiceDTO service) {
return repository.createService(service);
}
/**
* Updates an existing service by ID.
*/
public LiveData<Resource<ServiceDTO>> updateService(Long id, ServiceDTO service) {
return repository.updateService(id, service);
}
/**
* Deletes a service by ID.
*/
public LiveData<Resource<Void>> deleteService(Long id) {
return repository.deleteService(id);
}
/**
* Deletes multiple services.
*/
public LiveData<Resource<Void>> bulkDeleteServices(List<String> ids) {
return repository.bulkDeleteServices(new BulkDeleteRequest(ids));
}
}

View File

@@ -0,0 +1,49 @@
package com.example.petstoremobile.viewmodels;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.ViewModel;
import com.example.petstoremobile.dtos.EmployeeDTO;
import com.example.petstoremobile.repositories.EmployeeRepository;
import com.example.petstoremobile.utils.Resource;
import javax.inject.Inject;
import dagger.hilt.android.lifecycle.HiltViewModel;
@HiltViewModel
public class StaffDetailViewModel extends ViewModel {
private final EmployeeRepository repository;
private long employeeId = -1;
private boolean isEditing = false;
@Inject
public StaffDetailViewModel(EmployeeRepository repository) {
this.repository = repository;
}
public void setEmployeeId(long id, boolean isEditing) {
this.employeeId = id;
this.isEditing = isEditing;
}
public long getEmployeeId() {
return employeeId;
}
public boolean isEditing() {
return isEditing;
}
public LiveData<Resource<EmployeeDTO>> saveEmployee(EmployeeDTO dto) {
if (isEditing && employeeId > 0) {
return repository.updateEmployee(employeeId, dto);
} else {
return repository.createEmployee(dto);
}
}
public LiveData<Resource<Void>> deleteEmployee() {
return repository.deleteEmployee(employeeId);
}
}

View File

@@ -0,0 +1,71 @@
package com.example.petstoremobile.viewmodels;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.ViewModel;
import com.example.petstoremobile.dtos.EmployeeDTO;
import com.example.petstoremobile.repositories.EmployeeRepository;
import com.example.petstoremobile.utils.Resource;
import java.util.ArrayList;
import java.util.List;
import javax.inject.Inject;
import dagger.hilt.android.lifecycle.HiltViewModel;
@HiltViewModel
public class StaffListViewModel extends ViewModel {
private final EmployeeRepository repository;
private final MutableLiveData<List<EmployeeDTO>> employees = new MutableLiveData<>(new ArrayList<>());
private final MutableLiveData<List<EmployeeDTO>> filteredEmployees = new MutableLiveData<>(new ArrayList<>());
private final MutableLiveData<Boolean> isLoading = new MutableLiveData<>(false);
private String lastQuery = "";
@Inject
public StaffListViewModel(EmployeeRepository repository) {
this.repository = repository;
}
public LiveData<List<EmployeeDTO>> getFilteredEmployees() { return filteredEmployees; }
public LiveData<Boolean> getIsLoading() { return isLoading; }
public void loadStaff() {
isLoading.setValue(true);
repository.getAllEmployees(0, 100).observeForever(resource -> {
if (resource != null) {
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
employees.setValue(resource.data.getContent());
filter(lastQuery);
isLoading.setValue(false);
} else if (resource.status == Resource.Status.ERROR) {
isLoading.setValue(false);
}
}
});
}
public void filter(String query) {
this.lastQuery = query;
List<EmployeeDTO> all = employees.getValue();
if (all == null) return;
if (query.isEmpty()) {
filteredEmployees.setValue(new ArrayList<>(all));
} else {
List<EmployeeDTO> filtered = new ArrayList<>();
String lower = query.toLowerCase();
for (EmployeeDTO e : all) {
if ((e.getFullName() != null && e.getFullName().toLowerCase().contains(lower))
|| (e.getUsername() != null && e.getUsername().toLowerCase().contains(lower))
|| (e.getEmail() != null && e.getEmail().toLowerCase().contains(lower))
|| (e.getPhone() != null && e.getPhone().toLowerCase().contains(lower))) {
filtered.add(e);
}
}
filteredEmployees.setValue(filtered);
}
}
}

View File

@@ -1,30 +0,0 @@
package com.example.petstoremobile.viewmodels;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.ViewModel;
import com.example.petstoremobile.dtos.PageResponse;
import com.example.petstoremobile.dtos.StoreDTO;
import com.example.petstoremobile.repositories.StoreRepository;
import com.example.petstoremobile.utils.Resource;
import javax.inject.Inject;
import dagger.hilt.android.lifecycle.HiltViewModel;
@HiltViewModel
public class StoreViewModel extends ViewModel {
private final StoreRepository repository;
@Inject
public StoreViewModel(StoreRepository repository) {
this.repository = repository;
}
/**
* Fetches a paginated list of all stores.
*/
public LiveData<Resource<PageResponse<StoreDTO>>> getAllStores(int page, int size) {
return repository.getAllStores(page, size);
}
}

View File

@@ -0,0 +1,54 @@
package com.example.petstoremobile.viewmodels;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.ViewModel;
import com.example.petstoremobile.dtos.SupplierDTO;
import com.example.petstoremobile.repositories.SupplierRepository;
import com.example.petstoremobile.utils.Resource;
import javax.inject.Inject;
import dagger.hilt.android.lifecycle.HiltViewModel;
@HiltViewModel
public class SupplierDetailViewModel extends ViewModel {
private final SupplierRepository repository;
private long supId = -1;
private boolean isEditing = false;
@Inject
public SupplierDetailViewModel(SupplierRepository repository) {
this.repository = repository;
}
public void setSupId(long id) {
this.supId = id;
this.isEditing = id != -1;
}
public long getSupId() {
return supId;
}
public boolean isEditing() {
return isEditing;
}
public LiveData<Resource<SupplierDTO>> loadSupplier() {
return repository.getSupplierById(supId);
}
public LiveData<Resource<SupplierDTO>> saveSupplier(SupplierDTO dto) {
if (isEditing) {
dto.setSupId(supId);
return repository.updateSupplier(supId, dto);
} else {
return repository.createSupplier(dto);
}
}
public LiveData<Resource<Void>> deleteSupplier() {
return repository.deleteSupplier(supId);
}
}

View File

@@ -0,0 +1,51 @@
package com.example.petstoremobile.viewmodels;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.ViewModel;
import com.example.petstoremobile.dtos.BulkDeleteRequest;
import com.example.petstoremobile.dtos.SupplierDTO;
import com.example.petstoremobile.repositories.SupplierRepository;
import com.example.petstoremobile.utils.Resource;
import java.util.ArrayList;
import java.util.List;
import javax.inject.Inject;
import dagger.hilt.android.lifecycle.HiltViewModel;
@HiltViewModel
public class SupplierListViewModel extends ViewModel {
private final SupplierRepository repository;
private final MutableLiveData<List<SupplierDTO>> suppliers = new MutableLiveData<>(new ArrayList<>());
private final MutableLiveData<Boolean> isLoading = new MutableLiveData<>(false);
@Inject
public SupplierListViewModel(SupplierRepository repository) {
this.repository = repository;
}
public LiveData<List<SupplierDTO>> getSuppliers() { return suppliers; }
public LiveData<Boolean> getIsLoading() { return isLoading; }
public void loadSuppliers(String query) {
isLoading.setValue(true);
repository.getAllSuppliers(0, 100, query, "supCompany").observeForever(resource -> {
if (resource != null) {
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
suppliers.setValue(resource.data.getContent());
isLoading.setValue(false);
} else if (resource.status == Resource.Status.ERROR) {
isLoading.setValue(false);
}
}
});
}
public LiveData<Resource<Void>> bulkDeleteSuppliers(List<String> ids) {
return repository.bulkDeleteSuppliers(new BulkDeleteRequest(ids));
}
}

View File

@@ -1,68 +0,0 @@
package com.example.petstoremobile.viewmodels;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.ViewModel;
import com.example.petstoremobile.dtos.BulkDeleteRequest;
import com.example.petstoremobile.dtos.PageResponse;
import com.example.petstoremobile.dtos.SupplierDTO;
import com.example.petstoremobile.repositories.SupplierRepository;
import com.example.petstoremobile.utils.Resource;
import java.util.List;
import javax.inject.Inject;
import dagger.hilt.android.lifecycle.HiltViewModel;
@HiltViewModel
public class SupplierViewModel extends ViewModel {
private final SupplierRepository repository;
@Inject
public SupplierViewModel(SupplierRepository repository) {
this.repository = repository;
}
/**
* Fetches a paginated list of all suppliers.
*/
public LiveData<Resource<PageResponse<SupplierDTO>>> getAllSuppliers(int page, int size, String query, String sort) {
return repository.getAllSuppliers(page, size, query, sort);
}
/**
* Retrieves a single supplier by its ID.
*/
public LiveData<Resource<SupplierDTO>> getSupplierById(Long id) {
return repository.getSupplierById(id);
}
/**
* Creates a new supplier record.
*/
public LiveData<Resource<SupplierDTO>> createSupplier(SupplierDTO supplier) {
return repository.createSupplier(supplier);
}
/**
* Updates an existing supplier record by ID.
*/
public LiveData<Resource<SupplierDTO>> updateSupplier(Long id, SupplierDTO supplier) {
return repository.updateSupplier(id, supplier);
}
/**
* Deletes a supplier record by ID.
*/
public LiveData<Resource<Void>> deleteSupplier(Long id) {
return repository.deleteSupplier(id);
}
/**
* Deletes multiple supplier records.
*/
public LiveData<Resource<Void>> bulkDeleteSuppliers(List<String> ids) {
return repository.bulkDeleteSuppliers(new BulkDeleteRequest(ids));
}
}

View File

@@ -1,27 +0,0 @@
package com.example.petstoremobile.viewmodels;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.ViewModel;
import com.example.petstoremobile.dtos.PageResponse;
import com.example.petstoremobile.dtos.UserDTO;
import com.example.petstoremobile.repositories.UserRepository;
import com.example.petstoremobile.utils.Resource;
import javax.inject.Inject;
import dagger.hilt.android.lifecycle.HiltViewModel;
@HiltViewModel
public class UserViewModel extends ViewModel {
private final UserRepository userRepository;
@Inject
public UserViewModel(UserRepository userRepository) {
this.userRepository = userRepository;
}
public LiveData<Resource<PageResponse<UserDTO>>> getUsers(String role, int page, int size) {
return userRepository.getUsers(role, page, size);
}
}

View File

@@ -0,0 +1,40 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/black">
<ImageView
android:id="@+id/ivFullScreen"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="fitCenter" />
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="top|end"
android:layout_margin="16dp"
android:orientation="horizontal">
<ImageButton
android:id="@+id/btnDownload"
android:layout_width="48dp"
android:layout_height="48dp"
android:background="@android:color/transparent"
android:src="@android:drawable/stat_sys_download"
android:contentDescription="Download"
android:tint="@android:color/white" />
<ImageButton
android:id="@+id/btnClose"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_marginLeft="8dp"
android:background="@android:color/transparent"
android:src="@android:drawable/ic_menu_close_clear_cancel"
android:contentDescription="Close"
android:tint="@android:color/white" />
</LinearLayout>
</FrameLayout>

View File

@@ -1,214 +1,228 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent">
android:orientation="vertical"
android:background="@color/background_grey">
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="56dp" android:layout_height="match_parent"
android:background="@color/primary_dark" android:orientation="vertical"
android:gravity="center_vertical" android:background="@color/background_grey">
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:orientation="horizontal">
<TextView
android:id="@+id/tvAdoptionMode"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Add Adoption"
android:textColor="@color/white"
android:textSize="20sp"
android:textStyle="bold"/>
<Button
android:id="@+id/btnDeleteAdoption"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:backgroundTint="@color/accent_coral"
android:text="Delete"
android:textColor="@color/white"/>
</LinearLayout>
<ScrollView
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1">
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="56dp"
android:orientation="vertical" android:background="@color/primary_dark"
android:padding="24dp"> android:gravity="center_vertical"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:orientation="horizontal">
<TextView
android:id="@+id/tvAdoptionMode"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Add Adoption"
android:textColor="@color/white"
android:textSize="20sp"
android:textStyle="bold"/>
<Button
android:id="@+id/btnDeleteAdoption"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:backgroundTint="@color/accent_coral"
android:text="Delete"
android:textColor="@color/white"/>
</LinearLayout>
<ScrollView
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1">
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="vertical" android:orientation="vertical"
android:background="@drawable/rounded_card" android:padding="24dp">
android:padding="16dp"
android:layout_marginBottom="16dp">
<TextView <LinearLayout
android:id="@+id/tvAdoptionId"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="ID: #0"
android:textColor="@color/text_light"
android:textSize="11sp"
android:textStyle="italic"
android:layout_gravity="end"
android:layout_marginBottom="8dp"/>
<!-- Customer -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Customer"
android:textColor="@color/text_dark"
android:textSize="12sp"
android:layout_marginBottom="4dp"/>
<Spinner
android:id="@+id/spinnerAdoptionCustomer"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginBottom="16dp"/> android:orientation="vertical"
android:background="@drawable/rounded_card"
android:padding="16dp"
android:layout_marginBottom="16dp">
<!-- Pet --> <TextView
<TextView android:id="@+id/tvAdoptionId"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="Pet" android:text="ID: #0"
android:textColor="@color/text_dark" android:textColor="@color/text_light"
android:textSize="12sp" android:textSize="11sp"
android:layout_marginBottom="4dp"/> android:textStyle="italic"
android:layout_gravity="end"
android:layout_marginBottom="8dp"/>
<Spinner <!-- Customer -->
android:id="@+id/spinnerAdoptionPet" <TextView
android:layout_width="match_parent" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginBottom="16dp"/> android:text="Customer"
android:textColor="@color/text_dark"
android:textSize="12sp"
android:layout_marginBottom="4dp"/>
<!-- Employee --> <Spinner
<TextView android:id="@+id/spinnerAdoptionCustomer"
android:layout_width="wrap_content" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="Handled By (Staff)" android:layout_marginBottom="16dp"/>
android:textColor="@color/text_dark"
android:textSize="12sp"
android:layout_marginBottom="4dp"/>
<Spinner <!-- Pet -->
android:id="@+id/spinnerAdoptionEmployee" <TextView
android:layout_width="match_parent" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginBottom="16dp"/> android:text="Pet"
android:textColor="@color/text_dark"
android:textSize="12sp"
android:layout_marginBottom="4dp"/>
<TextView <Spinner
android:layout_width="wrap_content" android:id="@+id/spinnerAdoptionPet"
android:layout_height="wrap_content" android:layout_width="match_parent"
android:text="Source Store" android:layout_height="wrap_content"
android:textColor="@color/text_dark" android:layout_marginBottom="16dp"/>
android:textSize="12sp"
android:layout_marginBottom="4dp"/>
<Spinner <!-- Employee -->
android:id="@+id/spinnerAdoptionStore" <TextView
android:layout_width="match_parent" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginBottom="16dp"/> android:text="Handled By (Staff)"
android:textColor="@color/text_dark"
android:textSize="12sp"
android:layout_marginBottom="4dp"/>
<!-- Adoption Date --> <Spinner
<TextView android:id="@+id/spinnerAdoptionEmployee"
android:layout_width="wrap_content" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="Adoption Date" android:layout_marginBottom="16dp"/>
android:textColor="@color/text_dark"
android:textSize="12sp"
android:layout_marginBottom="4dp"/>
<EditText <TextView
android:id="@+id/etAdoptionDate" android:layout_width="wrap_content"
android:layout_width="match_parent" android:layout_height="wrap_content"
android:layout_height="wrap_content" android:text="Source Store"
android:hint="Tap to select date" android:textColor="@color/text_dark"
android:inputType="none" android:textSize="12sp"
android:focusable="false" android:layout_marginBottom="4dp"/>
android:clickable="true"
android:drawableEnd="@android:drawable/ic_menu_my_calendar"
android:layout_marginBottom="16dp"/>
<!-- Adoption Fee --> <Spinner
<TextView android:id="@+id/spinnerAdoptionStore"
android:layout_width="wrap_content" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="Adoption Fee" android:layout_marginBottom="16dp"/>
android:textColor="@color/text_dark"
android:textSize="12sp"
android:layout_marginBottom="4dp"/>
<EditText <!-- Adoption Date -->
android:id="@+id/etAdoptionFee" <TextView
android:layout_width="match_parent" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:hint="0.00" android:text="Adoption Date"
android:inputType="numberDecimal" android:textColor="@color/text_dark"
android:layout_marginBottom="16dp"/> android:textSize="12sp"
android:layout_marginBottom="4dp"/>
<!-- Status --> <EditText
<TextView android:id="@+id/etAdoptionDate"
android:layout_width="wrap_content" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="Status" android:hint="Tap to select date"
android:textColor="@color/text_dark" android:inputType="none"
android:textSize="12sp" android:focusable="false"
android:layout_marginBottom="4dp"/> android:clickable="true"
android:drawableEnd="@android:drawable/ic_menu_my_calendar"
android:layout_marginBottom="16dp"/>
<Spinner <!-- Adoption Fee -->
android:id="@+id/spinnerAdoptionStatus" <TextView
android:layout_width="match_parent" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginBottom="8dp"/> android:text="Adoption Fee"
android:textColor="@color/text_dark"
android:textSize="12sp"
android:layout_marginBottom="4dp"/>
<EditText
android:id="@+id/etAdoptionFee"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="0.00"
android:inputType="numberDecimal"
android:layout_marginBottom="16dp"/>
<!-- Status -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Status"
android:textColor="@color/text_dark"
android:textSize="12sp"
android:layout_marginBottom="4dp"/>
<Spinner
android:id="@+id/spinnerAdoptionStatus"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"/>
</LinearLayout>
</LinearLayout> </LinearLayout>
</ScrollView>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:background="@color/white"
android:padding="16dp">
<Button
android:id="@+id/btnAdoptionBack"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginEnd="8dp"
android:text="Back"
android:backgroundTint="@color/primary_medium"
android:textColor="@color/white"/>
<Button
android:id="@+id/btnSaveAdoption"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginStart="8dp"
android:text="Save"
android:backgroundTint="@color/accent_coral"
android:textColor="@color/white"/>
</LinearLayout> </LinearLayout>
</ScrollView>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:background="@color/white"
android:padding="16dp">
<Button
android:id="@+id/btnAdoptionBack"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginEnd="8dp"
android:text="Back"
android:backgroundTint="@color/primary_medium"
android:textColor="@color/white"/>
<Button
android:id="@+id/btnSaveAdoption"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginStart="8dp"
android:text="Save"
android:backgroundTint="@color/accent_coral"
android:textColor="@color/white"/>
</LinearLayout> </LinearLayout>
</LinearLayout> <ProgressBar
android:id="@+id/progressBar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:visibility="gone"
android:indeterminateTint="@color/accent_coral"/>
</FrameLayout>

View File

@@ -1,318 +1,332 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent">
android:orientation="vertical"
android:background="@color/background_grey">
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="56dp" android:layout_height="match_parent"
android:background="@color/primary_dark" android:orientation="vertical"
android:gravity="center_vertical" android:background="@color/background_grey">
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:orientation="horizontal">
<ImageButton
android:id="@+id/btnHamburgerAnalytics"
android:layout_width="48dp"
android:layout_height="48dp"
android:src="@drawable/baseline_menu_36"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="Open menu"/>
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Analytics"
android:textColor="@color/white"
android:textSize="20sp"
android:textStyle="bold"/>
<Button
android:id="@+id/btnRefreshAnalytics"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Refresh"
android:backgroundTint="@color/accent_coral"
android:textColor="@color/white"/>
</LinearLayout>
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="56dp"
android:orientation="vertical" android:background="@color/primary_dark"
android:padding="16dp"> android:gravity="center_vertical"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:orientation="horizontal">
<!-- Summary Cards Row 1 --> <ImageButton
<LinearLayout android:id="@+id/btnHamburgerAnalytics"
android:layout_width="match_parent" android:layout_width="48dp"
android:layout_height="48dp"
android:src="@drawable/baseline_menu_36"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="Open menu"/>
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="horizontal" android:layout_weight="1"
android:layout_marginBottom="8dp"> android:text="Analytics"
android:textColor="@color/white"
android:textSize="20sp"
android:textStyle="bold"/>
<LinearLayout <Button
android:layout_width="0dp" android:id="@+id/btnRefreshAnalytics"
android:layout_height="wrap_content" android:layout_width="wrap_content"
android:layout_weight="1"
android:layout_marginEnd="4dp"
android:orientation="vertical"
android:background="@drawable/rounded_card"
android:padding="16dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Total Revenue"
android:textColor="@color/text_light"
android:textSize="11sp"/>
<TextView
android:id="@+id/tvTotalRevenue"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="$0.00"
android:textColor="@color/accent_coral"
android:textSize="20sp"
android:textStyle="bold"
android:layout_marginTop="4dp"/>
</LinearLayout>
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginStart="4dp"
android:orientation="vertical"
android:background="@drawable/rounded_card"
android:padding="16dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Transactions"
android:textColor="@color/text_light"
android:textSize="11sp"/>
<TextView
android:id="@+id/tvTotalTransactions"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="0"
android:textColor="@color/primary_dark"
android:textSize="20sp"
android:textStyle="bold"
android:layout_marginTop="4dp"/>
</LinearLayout>
</LinearLayout>
<!-- Summary Cards Row 2 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="horizontal" android:text="Refresh"
android:layout_marginBottom="16dp"> android:backgroundTint="@color/accent_coral"
android:textColor="@color/white"/>
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginEnd="4dp"
android:orientation="vertical"
android:background="@drawable/rounded_card"
android:padding="16dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Avg Transaction"
android:textColor="@color/text_light"
android:textSize="11sp"/>
<TextView
android:id="@+id/tvAvgTransaction"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="$0.00"
android:textColor="@color/primary_dark"
android:textSize="20sp"
android:textStyle="bold"
android:layout_marginTop="4dp"/>
</LinearLayout>
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginStart="4dp"
android:orientation="vertical"
android:background="@drawable/rounded_card"
android:padding="16dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Items Sold"
android:textColor="@color/text_light"
android:textSize="11sp"/>
<TextView
android:id="@+id/tvTotalItems"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="0"
android:textColor="@color/primary_dark"
android:textSize="20sp"
android:textStyle="bold"
android:layout_marginTop="4dp"/>
</LinearLayout>
</LinearLayout>
<!-- Top Products by Revenue -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:background="@drawable/rounded_card"
android:padding="16dp"
android:layout_marginBottom="16dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Top Products by Revenue"
android:textColor="@color/text_dark"
android:textSize="14sp"
android:textStyle="bold"
android:layout_marginBottom="12dp"/>
<LinearLayout
android:id="@+id/llTopRevenue"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"/>
</LinearLayout>
<!-- Top Products by Quantity -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:background="@drawable/rounded_card"
android:padding="16dp"
android:layout_marginBottom="16dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Top Products by Quantity"
android:textColor="@color/text_dark"
android:textSize="14sp"
android:textStyle="bold"
android:layout_marginBottom="12dp"/>
<LinearLayout
android:id="@+id/llTopQuantity"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"/>
</LinearLayout>
<!-- Payment Methods -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:background="@drawable/rounded_card"
android:padding="16dp"
android:layout_marginBottom="16dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Payment Methods"
android:textColor="@color/text_dark"
android:textSize="14sp"
android:textStyle="bold"
android:layout_marginBottom="12dp"/>
<LinearLayout
android:id="@+id/llPaymentMethods"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"/>
</LinearLayout>
<!-- Employee Performance -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:background="@drawable/rounded_card"
android:padding="16dp"
android:layout_marginBottom="16dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Employee Performance"
android:textColor="@color/text_dark"
android:textSize="14sp"
android:textStyle="bold"
android:layout_marginBottom="12dp"/>
<LinearLayout
android:id="@+id/llEmployeePerformance"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"/>
</LinearLayout>
<!-- Daily Revenue -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:background="@drawable/rounded_card"
android:padding="16dp"
android:layout_marginBottom="16dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Daily Revenue (Last 7 Days)"
android:textColor="@color/text_dark"
android:textSize="14sp"
android:textStyle="bold"
android:layout_marginBottom="12dp"/>
<LinearLayout
android:id="@+id/llDailyRevenue"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"/>
</LinearLayout>
</LinearLayout> </LinearLayout>
</ScrollView> <ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent">
</LinearLayout> <LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<!-- Summary Cards Row 1 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginBottom="8dp">
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginEnd="4dp"
android:orientation="vertical"
android:background="@drawable/rounded_card"
android:padding="16dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Total Revenue"
android:textColor="@color/text_light"
android:textSize="11sp"/>
<TextView
android:id="@+id/tvTotalRevenue"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="$0.00"
android:textColor="@color/accent_coral"
android:textSize="20sp"
android:textStyle="bold"
android:layout_marginTop="4dp"/>
</LinearLayout>
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginStart="4dp"
android:orientation="vertical"
android:background="@drawable/rounded_card"
android:padding="16dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Transactions"
android:textColor="@color/text_light"
android:textSize="11sp"/>
<TextView
android:id="@+id/tvTotalTransactions"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="0"
android:textColor="@color/primary_dark"
android:textSize="20sp"
android:textStyle="bold"
android:layout_marginTop="4dp"/>
</LinearLayout>
</LinearLayout>
<!-- Summary Cards Row 2 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginBottom="16dp">
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginEnd="4dp"
android:orientation="vertical"
android:background="@drawable/rounded_card"
android:padding="16dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Avg Transaction"
android:textColor="@color/text_light"
android:textSize="11sp"/>
<TextView
android:id="@+id/tvAvgTransaction"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="$0.00"
android:textColor="@color/primary_dark"
android:textSize="20sp"
android:textStyle="bold"
android:layout_marginTop="4dp"/>
</LinearLayout>
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginStart="4dp"
android:orientation="vertical"
android:background="@drawable/rounded_card"
android:padding="16dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Items Sold"
android:textColor="@color/text_light"
android:textSize="11sp"/>
<TextView
android:id="@+id/tvTotalItems"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="0"
android:textColor="@color/primary_dark"
android:textSize="20sp"
android:textStyle="bold"
android:layout_marginTop="4dp"/>
</LinearLayout>
</LinearLayout>
<!-- Top Products by Revenue -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:background="@drawable/rounded_card"
android:padding="16dp"
android:layout_marginBottom="16dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Top Products by Revenue"
android:textColor="@color/text_dark"
android:textSize="14sp"
android:textStyle="bold"
android:layout_marginBottom="12dp"/>
<LinearLayout
android:id="@+id/llTopRevenue"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"/>
</LinearLayout>
<!-- Top Products by Quantity -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:background="@drawable/rounded_card"
android:padding="16dp"
android:layout_marginBottom="16dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Top Products by Quantity"
android:textColor="@color/text_dark"
android:textSize="14sp"
android:textStyle="bold"
android:layout_marginBottom="12dp"/>
<LinearLayout
android:id="@+id/llTopQuantity"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"/>
</LinearLayout>
<!-- Payment Methods -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:background="@drawable/rounded_card"
android:padding="16dp"
android:layout_marginBottom="16dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Payment Methods"
android:textColor="@color/text_dark"
android:textSize="14sp"
android:textStyle="bold"
android:layout_marginBottom="12dp"/>
<LinearLayout
android:id="@+id/llPaymentMethods"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"/>
</LinearLayout>
<!-- Employee Performance -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:background="@drawable/rounded_card"
android:padding="16dp"
android:layout_marginBottom="16dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Employee Performance"
android:textColor="@color/text_dark"
android:textSize="14sp"
android:textStyle="bold"
android:layout_marginBottom="12dp"/>
<LinearLayout
android:id="@+id/llEmployeePerformance"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"/>
</LinearLayout>
<!-- Daily Revenue -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:background="@drawable/rounded_card"
android:padding="16dp"
android:layout_marginBottom="16dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Daily Revenue (Last 7 Days)"
android:textColor="@color/text_dark"
android:textSize="14sp"
android:textStyle="bold"
android:layout_marginBottom="12dp"/>
<LinearLayout
android:id="@+id/llDailyRevenue"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"/>
</LinearLayout>
</LinearLayout>
</ScrollView>
</LinearLayout>
<ProgressBar
android:id="@+id/progressBar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:visibility="gone"
android:indeterminateTint="@color/accent_coral"/>
</FrameLayout>

View File

@@ -1,266 +1,282 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent">
android:orientation="vertical"
android:background="@color/background_grey">
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="56dp" android:layout_height="match_parent"
android:background="@color/primary_dark" android:orientation="vertical"
android:gravity="center_vertical" android:background="@color/background_grey">
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:orientation="horizontal">
<TextView
android:id="@+id/tvApptMode"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Add Appointment"
android:textColor="@color/white"
android:textSize="20sp"
android:textStyle="bold"/>
<Button
android:id="@+id/btnDeleteAppointment"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:backgroundTint="@color/accent_coral"
android:text="Delete"
android:textColor="@color/white" />
</LinearLayout>
<ScrollView
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1">
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="56dp"
android:orientation="vertical" android:background="@color/primary_dark"
android:padding="24dp"> android:gravity="center_vertical"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:orientation="horizontal">
<TextView
android:id="@+id/tvApptMode"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Add Appointment"
android:textColor="@color/white"
android:textSize="20sp"
android:textStyle="bold"/>
<Button
android:id="@+id/btnDeleteAppointment"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:backgroundTint="@color/accent_coral"
android:text="Delete"
android:textColor="@color/white" />
</LinearLayout>
<ScrollView
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1">
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="vertical" android:orientation="vertical"
android:background="@drawable/rounded_card" android:padding="24dp">
android:padding="16dp"
android:layout_marginBottom="16dp">
<TextView
android:id="@+id/tvAppointmentId"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="ID: #0"
android:textColor="@color/text_light"
android:textSize="11sp"
android:textStyle="italic"
android:layout_gravity="end"
android:layout_marginBottom="8dp"/>
<!-- Customer -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Customer"
android:textColor="@color/text_dark"
android:textSize="12sp"
android:layout_marginBottom="4dp"/>
<Spinner
android:id="@+id/spinnerCustomer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"/>
<!-- Store -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Store"
android:textColor="@color/text_dark"
android:textSize="12sp"
android:layout_marginBottom="4dp"/>
<Spinner
android:id="@+id/spinnerStore"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"/>
<!-- Pet -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Pet"
android:textColor="@color/text_dark"
android:textSize="12sp"
android:layout_marginBottom="4dp"/>
<Spinner
android:id="@+id/spinnerPet"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"/>
<!-- Service -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Service"
android:textColor="@color/text_dark"
android:textSize="12sp"
android:layout_marginBottom="4dp"/>
<Spinner
android:id="@+id/spinnerService"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"/>
<!-- Staff -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Staff"
android:textColor="@color/text_dark"
android:textSize="12sp"
android:layout_marginBottom="4dp"/>
<Spinner
android:id="@+id/spinnerStaff"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"/>
<!-- Appointment Date -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Appointment Date"
android:textColor="@color/text_dark"
android:textSize="12sp"
android:layout_marginBottom="4dp"/>
<EditText
android:id="@+id/etAppointmentDate"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Tap to select date"
android:inputType="none"
android:focusable="false"
android:clickable="true"
android:drawableEnd="@android:drawable/ic_menu_my_calendar"
android:layout_marginBottom="16dp"/>
<!-- Appointment Time-->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Appointment Time"
android:textColor="@color/text_dark"
android:textSize="12sp"
android:layout_marginBottom="4dp"/>
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="horizontal" android:orientation="vertical"
android:layout_marginBottom="16dp" android:background="@drawable/rounded_card"
android:gravity="center_vertical"> android:padding="16dp"
android:layout_marginBottom="16dp">
<TextView <TextView
android:id="@+id/tvAppointmentId"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="Hour:" android:text="ID: #0"
android:textColor="@color/text_dark" android:textColor="@color/text_light"
android:textSize="13sp" android:textSize="11sp"
android:layout_marginEnd="6dp"/> android:textStyle="italic"
android:layout_gravity="end"
<Spinner android:layout_marginBottom="8dp"/>
android:id="@+id/spinnerHour"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginEnd="16dp"/>
<!-- Customer -->
<TextView <TextView
android:id="@+id/tvLabelCustomer"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="Min:" android:text="Customer"
android:textColor="@color/text_dark" android:textColor="@color/text_dark"
android:textSize="13sp" android:textSize="12sp"
android:layout_marginEnd="6dp"/> android:layout_marginBottom="4dp"/>
<Spinner <Spinner
android:id="@+id/spinnerMinute" android:id="@+id/spinnerCustomer"
android:layout_width="0dp" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_weight="1"/> android:layout_marginBottom="16dp"/>
<!-- Pet -->
<TextView
android:id="@+id/tvLabelPet"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Pet"
android:textColor="@color/text_dark"
android:textSize="12sp"
android:layout_marginBottom="4dp"/>
<Spinner
android:id="@+id/spinnerPet"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"/>
<!-- Store -->
<TextView
android:id="@+id/tvLabelStore"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Store"
android:textColor="@color/text_dark"
android:textSize="12sp"
android:layout_marginBottom="4dp"/>
<Spinner
android:id="@+id/spinnerStore"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"/>
<!-- Staff -->
<TextView
android:id="@+id/tvLabelStaff"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Staff"
android:textColor="@color/text_dark"
android:textSize="12sp"
android:layout_marginBottom="4dp"/>
<Spinner
android:id="@+id/spinnerStaff"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"/>
<!-- Service -->
<TextView
android:id="@+id/tvLabelService"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Service"
android:textColor="@color/text_dark"
android:textSize="12sp"
android:layout_marginBottom="4dp"/>
<Spinner
android:id="@+id/spinnerService"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"/>
<!-- Appointment Date -->
<TextView
android:id="@+id/tvLabelDate"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Appointment Date"
android:textColor="@color/text_dark"
android:textSize="12sp"
android:layout_marginBottom="4dp"/>
<EditText
android:id="@+id/etAppointmentDate"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Tap to select date"
android:inputType="none"
android:focusable="false"
android:clickable="true"
android:drawableEnd="@android:drawable/ic_menu_my_calendar"
android:layout_marginBottom="16dp"/>
<!-- Appointment Time-->
<TextView
android:id="@+id/tvLabelTime"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Appointment Time"
android:textColor="@color/text_dark"
android:textSize="12sp"
android:layout_marginBottom="4dp"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginBottom="16dp"
android:gravity="center_vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hour:"
android:textColor="@color/text_dark"
android:textSize="13sp"
android:layout_marginEnd="6dp"/>
<Spinner
android:id="@+id/spinnerHour"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginEnd="16dp"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Min:"
android:textColor="@color/text_dark"
android:textSize="13sp"
android:layout_marginEnd="6dp"/>
<Spinner
android:id="@+id/spinnerMinute"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"/>
</LinearLayout>
<!-- Status -->
<TextView
android:id="@+id/tvLabelStatus"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Status"
android:textColor="@color/text_dark"
android:textSize="12sp"
android:layout_marginBottom="4dp"/>
<Spinner
android:id="@+id/spinnerAppointmentStatus"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"/>
</LinearLayout> </LinearLayout>
<!-- Status -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Status"
android:textColor="@color/text_dark"
android:textSize="12sp"
android:layout_marginBottom="4dp"/>
<Spinner
android:id="@+id/spinnerAppointmentStatus"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"/>
</LinearLayout> </LinearLayout>
</ScrollView>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:background="@color/white"
android:padding="16dp">
<Button
android:id="@+id/btnApptBack"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginEnd="8dp"
android:text="Back"
android:backgroundTint="@color/primary_medium"
android:textColor="@color/white"/>
<Button
android:id="@+id/btnSaveAppointment"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginStart="8dp"
android:text="Save"
android:backgroundTint="@color/accent_coral"
android:textColor="@color/white"/>
</LinearLayout> </LinearLayout>
</ScrollView>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:background="@color/white"
android:padding="16dp">
<Button
android:id="@+id/btnApptBack"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginEnd="8dp"
android:text="Back"
android:backgroundTint="@color/primary_medium"
android:textColor="@color/white"/>
<Button
android:id="@+id/btnSaveAppointment"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginStart="8dp"
android:text="Save"
android:backgroundTint="@color/accent_coral"
android:textColor="@color/white"/>
</LinearLayout> </LinearLayout>
</LinearLayout> <ProgressBar
android:id="@+id/progressBar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:visibility="gone"
android:indeterminateTint="@color/accent_coral"/>
</FrameLayout>

View File

@@ -1,153 +1,240 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<androidx.drawerlayout.widget.DrawerLayout <androidx.drawerlayout.widget.DrawerLayout
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/chatDrawerLayout" android:id="@+id/chatDrawerLayout"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent">
<LinearLayout <RelativeLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:orientation="vertical"
android:background="@color/background_grey"> android:background="@color/background_grey">
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="56dp" android:layout_height="match_parent"
android:background="@color/primary_dark" android:orientation="vertical">
android:gravity="center_vertical"
android:paddingStart="16dp"
android:paddingEnd="16dp">
<ImageButton <LinearLayout
android:id="@+id/btnHamburger" android:layout_width="match_parent"
android:layout_width="48dp" android:layout_height="56dp"
android:layout_height="48dp" android:background="@color/primary_dark"
android:src="@drawable/baseline_menu_36" android:gravity="center_vertical"
android:background="?attr/selectableItemBackgroundBorderless" android:paddingStart="16dp"
android:contentDescription="Open menu"/> android:paddingEnd="16dp">
<TextView <ImageButton
android:id="@+id/tvChatTitle" android:id="@+id/btnHamburger"
android:layout_width="48dp"
android:layout_height="48dp"
android:src="@drawable/baseline_menu_36"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="Open menu"/>
<TextView
android:id="@+id/tvChatTitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Customer Chat"
android:textColor="@color/white"
android:textSize="20sp"
android:textStyle="bold"
android:paddingStart="8dp"
android:paddingEnd="8dp"/>
</LinearLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rvMessages"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:padding="8dp"
android:clipToPadding="false" />
<LinearLayout
android:id="@+id/layoutAttachmentPreview"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="Customer Chat" android:orientation="horizontal"
android:textColor="@color/white" android:padding="8dp"
android:textSize="20sp" android:background="#E0E0E0"
android:textStyle="bold" android:gravity="center_vertical"
android:paddingStart="8dp" android:visibility="gone">
android:paddingEnd="8dp"/>
<ImageView
android:id="@+id/ivPreview"
android:layout_width="48dp"
android:layout_height="48dp"
android:scaleType="centerCrop"
android:layout_marginEnd="8dp"
android:visibility="gone"/>
<TextView
android:id="@+id/tvPreviewName"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:ellipsize="middle"
android:singleLine="true"
android:textColor="@color/text_dark"/>
<ImageButton
android:id="@+id/btnRemoveAttachment"
android:layout_width="40dp"
android:layout_height="40dp"
android:src="@android:drawable/ic_menu_close_clear_cancel"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="Remove attachment"/>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="8dp"
android:gravity="center_vertical"
android:background="@color/white">
<ImageButton
android:id="@+id/btnAttach"
android:layout_width="48dp"
android:layout_height="48dp"
android:src="@android:drawable/ic_menu_add"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="Attach file"
android:layout_marginEnd="4dp"/>
<EditText
android:id="@+id/etMessage"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:hint="Type a message..."
android:inputType="text"
android:imeOptions="actionSend"
android:layout_marginEnd="8dp"
android:textColor="@color/text_dark"/>
<Button
android:id="@+id/btnSend"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Send"
android:backgroundTint="@color/accent_coral"
android:textColor="@color/white"/>
</LinearLayout>
</LinearLayout> </LinearLayout>
<androidx.recyclerview.widget.RecyclerView <ProgressBar
android:id="@+id/rvMessages" android:id="@+id/progressBar"
android:layout_width="match_parent" android:layout_width="wrap_content"
android:layout_height="0dp"
android:layout_weight="1"
android:padding="8dp"
android:clipToPadding="false" />
<LinearLayout
android:id="@+id/layoutAttachmentPreview"
android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="horizontal" android:layout_centerInParent="true"
android:padding="8dp" android:visibility="gone"
android:background="#E0E0E0" android:indeterminateTint="@color/accent_coral"/>
android:gravity="center_vertical"
android:visibility="gone">
<ImageView </RelativeLayout>
android:id="@+id/ivPreview"
android:layout_width="48dp"
android:layout_height="48dp"
android:scaleType="centerCrop"
android:layout_marginEnd="8dp"
android:visibility="gone"/>
<TextView <androidx.core.widget.NestedScrollView
android:id="@+id/tvPreviewName"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:ellipsize="middle"
android:singleLine="true"
android:textColor="@color/text_dark"/>
<ImageButton
android:id="@+id/btnRemoveAttachment"
android:layout_width="40dp"
android:layout_height="40dp"
android:src="@android:drawable/ic_menu_close_clear_cancel"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="Remove attachment"/>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="8dp"
android:gravity="center_vertical"
android:background="@color/white">
<ImageButton
android:id="@+id/btnAttach"
android:layout_width="48dp"
android:layout_height="48dp"
android:src="@android:drawable/ic_menu_add"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="Attach file"
android:layout_marginEnd="4dp"/>
<EditText
android:id="@+id/etMessage"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:hint="Type a message..."
android:inputType="text"
android:imeOptions="actionSend"
android:layout_marginEnd="8dp"
android:textColor="@color/text_dark"/>
<Button
android:id="@+id/btnSend"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Send"
android:backgroundTint="@color/accent_coral"
android:textColor="@color/white"/>
</LinearLayout>
</LinearLayout>
<LinearLayout
android:id="@+id/chatListDrawer" android:id="@+id/chatListDrawer"
android:layout_width="260dp" android:layout_width="260dp"
android:layout_height="match_parent" android:layout_height="match_parent"
android:layout_gravity="start" android:layout_gravity="start"
android:orientation="vertical" android:background="@color/primary_dark">
android:background="@color/primary_dark"
android:padding="16dp">
<TextView <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="Active Chats" android:orientation="vertical"
android:textColor="@color/white" android:paddingTop="24dp">
android:textSize="18sp"
android:textStyle="bold"
android:layout_marginBottom="16dp"/>
<androidx.recyclerview.widget.RecyclerView <LinearLayout
android:id="@+id/rvChatList" android:id="@+id/headerActiveChats"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"/> android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:clickable="true"
android:focusable="true"
android:background="?attr/selectableItemBackground"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:paddingBottom="8dp">
</LinearLayout> <TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="ACTIVE CHATS"
android:textColor="@color/text_light"
android:textSize="11sp"
android:letterSpacing="0.15"
android:textStyle="bold"/>
<ImageView
android:id="@+id/ivActiveChevron"
android:layout_width="20dp"
android:layout_height="20dp"
android:src="@android:drawable/arrow_up_float"
app:tint="@color/text_light"/>
</LinearLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rvActiveChats"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:nestedScrollingEnabled="false"
android:layout_marginBottom="16dp"
android:paddingStart="8dp"
android:paddingEnd="8dp"/>
<LinearLayout
android:id="@+id/headerClosedChats"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:clickable="true"
android:focusable="true"
android:background="?attr/selectableItemBackground"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:paddingTop="8dp"
android:paddingBottom="8dp">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="CLOSED CHATS"
android:textColor="@color/text_light"
android:textSize="11sp"
android:letterSpacing="0.15"
android:textStyle="bold"/>
<ImageView
android:id="@+id/ivClosedChevron"
android:layout_width="20dp"
android:layout_height="20dp"
android:src="@android:drawable/arrow_down_float"
app:tint="@color/text_light"/>
</LinearLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rvClosedChats"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:nestedScrollingEnabled="false"
android:visibility="gone"
android:paddingStart="8dp"
android:paddingEnd="8dp"/>
</LinearLayout>
</androidx.core.widget.NestedScrollView>
</androidx.drawerlayout.widget.DrawerLayout> </androidx.drawerlayout.widget.DrawerLayout>

View File

@@ -1,155 +1,169 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent">
android:orientation="vertical"
android:background="@color/background_grey">
<!-- Header -->
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="56dp" android:layout_height="match_parent"
android:background="@color/primary_dark" android:orientation="vertical"
android:gravity="center_vertical" android:background="@color/background_grey">
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:orientation="horizontal">
<TextView
android:id="@+id/tvInventoryMode"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Add Inventory"
android:textColor="@color/white"
android:textSize="20sp"
android:textStyle="bold"/>
<Button
android:id="@+id/btnDeleteInventory"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:backgroundTint="@color/accent_coral"
android:text="Delete"
android:textColor="@color/white"
android:visibility="gone"/>
</LinearLayout>
<ScrollView
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1">
<!-- Header -->
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="56dp"
android:orientation="vertical" android:background="@color/primary_dark"
android:padding="24dp"> android:gravity="center_vertical"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:orientation="horizontal">
<TextView
android:id="@+id/tvInventoryMode"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Add Inventory"
android:textColor="@color/white"
android:textSize="20sp"
android:textStyle="bold"/>
<Button
android:id="@+id/btnDeleteInventory"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:backgroundTint="@color/accent_coral"
android:text="Delete"
android:textColor="@color/white"
android:visibility="gone"/>
</LinearLayout>
<ScrollView
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1">
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="vertical" android:orientation="vertical"
android:background="@drawable/rounded_card" android:padding="24dp">
android:padding="16dp">
<!-- Inventory ID — edit mode only --> <LinearLayout
<TextView
android:id="@+id/tvInventoryId"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Inventory ID: —"
android:textColor="@color/text_light"
android:textSize="11sp"
android:textStyle="italic"
android:layout_gravity="end"
android:layout_marginBottom="12dp"
android:visibility="gone"/>
<!-- Store selection label -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Store"
android:textColor="@color/text_dark"
android:textSize="12sp"
android:layout_marginBottom="4dp"/>
<!-- Store Spinner -->
<Spinner
android:id="@+id/spinnerInventoryStore"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginBottom="16dp"/> android:orientation="vertical"
android:background="@drawable/rounded_card"
android:padding="16dp">
<!-- Product selection label --> <!-- Inventory ID — edit mode only -->
<TextView <TextView
android:layout_width="wrap_content" android:id="@+id/tvInventoryId"
android:layout_height="wrap_content" android:layout_width="wrap_content"
android:text="Product" android:layout_height="wrap_content"
android:textColor="@color/text_dark" android:text="Inventory ID: —"
android:textSize="12sp" android:textColor="@color/text_light"
android:layout_marginBottom="4dp"/> android:textSize="11sp"
android:textStyle="italic"
android:layout_gravity="end"
android:layout_marginBottom="12dp"
android:visibility="gone"/>
<!-- Product Spinner --> <!-- Store selection label -->
<Spinner <TextView
android:id="@+id/spinnerInventoryProduct" android:layout_width="wrap_content"
android:layout_width="match_parent" android:layout_height="wrap_content"
android:layout_height="wrap_content" android:text="Store"
android:layout_marginBottom="16dp"/> android:textColor="@color/text_dark"
android:textSize="12sp"
android:layout_marginBottom="4dp"/>
<!-- Quantity label --> <!-- Store Spinner -->
<TextView <Spinner
android:layout_width="wrap_content" android:id="@+id/spinnerInventoryStore"
android:layout_height="wrap_content" android:layout_width="match_parent"
android:text="Quantity" android:layout_height="wrap_content"
android:textColor="@color/text_dark" android:layout_marginBottom="16dp"/>
android:textSize="12sp"
android:layout_marginBottom="4dp"/>
<!-- Quantity input --> <!-- Product selection label -->
<EditText <TextView
android:id="@+id/etQuantity" android:layout_width="wrap_content"
android:layout_width="match_parent" android:layout_height="wrap_content"
android:layout_height="wrap_content" android:text="Product"
android:hint="Enter quantity" android:textColor="@color/text_dark"
android:inputType="number"/> android:textSize="12sp"
android:layout_marginBottom="4dp"/>
<!-- Product Spinner -->
<Spinner
android:id="@+id/spinnerInventoryProduct"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"/>
<!-- Quantity label -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Quantity"
android:textColor="@color/text_dark"
android:textSize="12sp"
android:layout_marginBottom="4dp"/>
<!-- Quantity input -->
<EditText
android:id="@+id/etQuantity"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Enter quantity"
android:inputType="number"/>
</LinearLayout>
</LinearLayout> </LinearLayout>
</ScrollView>
<!-- Bottom buttons -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:background="@color/white"
android:padding="16dp">
<Button
android:id="@+id/btnInventoryBack"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginEnd="8dp"
android:text="Back"
android:backgroundTint="@color/primary_medium"
android:textColor="@color/white"/>
<Button
android:id="@+id/btnSaveInventory"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginStart="8dp"
android:text="Save"
android:backgroundTint="@color/accent_coral"
android:textColor="@color/white"/>
</LinearLayout> </LinearLayout>
</ScrollView>
<!-- Bottom buttons -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:background="@color/white"
android:padding="16dp">
<Button
android:id="@+id/btnInventoryBack"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginEnd="8dp"
android:text="Back"
android:backgroundTint="@color/primary_medium"
android:textColor="@color/white"/>
<Button
android:id="@+id/btnSaveInventory"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginStart="8dp"
android:text="Save"
android:backgroundTint="@color/accent_coral"
android:textColor="@color/white"/>
</LinearLayout> </LinearLayout>
</LinearLayout> <ProgressBar
android:id="@+id/progressBar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:visibility="gone"
android:indeterminateTint="@color/accent_coral"/>
</FrameLayout>

View File

@@ -1,229 +1,244 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent">
android:orientation="vertical"
android:background="@color/background_grey">
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="56dp" android:layout_height="match_parent"
android:background="@color/primary_dark" android:orientation="vertical"
android:gravity="center_vertical" android:background="@color/background_grey">
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:orientation="horizontal">
<TextView
android:id="@+id/tvMode"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Add Pet"
android:textColor="@color/white"
android:textSize="20sp"
android:textStyle="bold" />
<Button
android:id="@+id/btnDeletePet"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:backgroundTint="@color/accent_coral"
android:text="Delete"
android:textColor="@color/white" />
</LinearLayout>
<ScrollView
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1">
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="56dp"
android:orientation="vertical" android:background="@color/primary_dark"
android:padding="24dp"> android:gravity="center_vertical"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:orientation="horizontal">
<TextView
android:id="@+id/tvMode"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Add Pet"
android:textColor="@color/white"
android:textSize="20sp"
android:textStyle="bold" />
<Button
android:id="@+id/btnDeletePet"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:backgroundTint="@color/accent_coral"
android:text="Delete"
android:textColor="@color/white" />
</LinearLayout>
<ScrollView
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1">
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="vertical" android:orientation="vertical"
android:background="@drawable/rounded_card" android:padding="24dp">
android:padding="16dp"
android:layout_marginBottom="16dp">
<TextView <LinearLayout
android:id="@+id/tvPetId"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Pet ID: #0"
android:textColor="@color/text_light"
android:textSize="11sp"
android:textStyle="italic"
android:layout_gravity="end"
android:layout_marginBottom="8dp"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Pet Name"
android:textColor="@color/text_dark"
android:textSize="12sp"
android:layout_marginBottom="4dp"/>
<EditText
android:id="@+id/etPetName"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:hint="Enter pet name" android:orientation="vertical"
android:inputType="text" android:background="@drawable/rounded_card"
android:layout_marginBottom="16dp" android:padding="16dp"
android:textColor="@color/text_dark"/> android:layout_marginBottom="16dp">
<TextView <TextView
android:layout_width="wrap_content" android:id="@+id/tvPetId"
android:layout_height="wrap_content" android:layout_width="wrap_content"
android:text="Species" android:layout_height="wrap_content"
android:textColor="@color/text_dark" android:text="Pet ID: #0"
android:textSize="12sp" android:textColor="@color/text_light"
android:layout_marginBottom="4dp"/> android:textSize="11sp"
android:textStyle="italic"
android:layout_gravity="end"
android:layout_marginBottom="8dp"/>
<EditText <TextView
android:id="@+id/etPetSpecies" android:layout_width="wrap_content"
android:layout_width="match_parent" android:layout_height="wrap_content"
android:layout_height="wrap_content" android:text="Pet Name"
android:hint="e.g. Dog, Cat, Bird" android:textColor="@color/text_dark"
android:inputType="text" android:textSize="12sp"
android:layout_marginBottom="16dp" android:layout_marginBottom="4dp"/>
android:textColor="@color/text_dark"/>
<TextView <EditText
android:layout_width="wrap_content" android:id="@+id/etPetName"
android:layout_height="wrap_content" android:layout_width="match_parent"
android:text="Breed" android:layout_height="wrap_content"
android:textColor="@color/text_dark" android:hint="Enter pet name"
android:textSize="12sp" android:inputType="text"
android:layout_marginBottom="4dp"/> android:layout_marginBottom="16dp"
android:textColor="@color/text_dark"/>
<EditText <TextView
android:id="@+id/etPetBreed" android:layout_width="wrap_content"
android:layout_width="match_parent" android:layout_height="wrap_content"
android:layout_height="wrap_content" android:text="Species"
android:hint="Enter breed" android:textColor="@color/text_dark"
android:inputType="text" android:textSize="12sp"
android:layout_marginBottom="16dp" android:layout_marginBottom="4dp"/>
android:textColor="@color/text_dark"/>
<TextView <EditText
android:layout_width="wrap_content" android:id="@+id/etPetSpecies"
android:layout_height="wrap_content" android:layout_width="match_parent"
android:text="Age" android:layout_height="wrap_content"
android:textColor="@color/text_dark" android:hint="e.g. Dog, Cat, Bird"
android:textSize="12sp" android:inputType="text"
android:layout_marginBottom="4dp"/> android:layout_marginBottom="16dp"
android:textColor="@color/text_dark"/>
<EditText <TextView
android:id="@+id/etPetAge" android:layout_width="wrap_content"
android:layout_width="match_parent" android:layout_height="wrap_content"
android:layout_height="wrap_content" android:text="Breed"
android:hint="Enter age" android:textColor="@color/text_dark"
android:inputType="number" android:textSize="12sp"
android:layout_marginBottom="16dp" android:layout_marginBottom="4dp"/>
android:textColor="@color/text_dark"/>
<TextView <EditText
android:layout_width="wrap_content" android:id="@+id/etPetBreed"
android:layout_height="wrap_content" android:layout_width="match_parent"
android:text="Price" android:layout_height="wrap_content"
android:textColor="@color/text_dark" android:hint="Enter breed"
android:textSize="12sp" android:inputType="text"
android:layout_marginBottom="4dp"/> android:layout_marginBottom="16dp"
android:textColor="@color/text_dark"/>
<EditText <TextView
android:id="@+id/etPetPrice" android:layout_width="wrap_content"
android:layout_width="match_parent" android:layout_height="wrap_content"
android:layout_height="wrap_content" android:text="Age"
android:hint="Enter price" android:textColor="@color/text_dark"
android:inputType="numberDecimal" android:textSize="12sp"
android:layout_marginBottom="16dp" android:layout_marginBottom="4dp"/>
android:textColor="@color/text_dark"/>
<TextView <EditText
android:layout_width="wrap_content" android:id="@+id/etPetAge"
android:layout_height="wrap_content" android:layout_width="match_parent"
android:text="Status" android:layout_height="wrap_content"
android:textColor="@color/text_dark" android:hint="Enter age"
android:textSize="12sp" android:inputType="number"
android:layout_marginBottom="4dp"/> android:layout_marginBottom="16dp"
android:textColor="@color/text_dark"/>
<Spinner <TextView
android:id="@+id/spinnerPetStatus" android:layout_width="wrap_content"
android:layout_width="match_parent" android:layout_height="wrap_content"
android:layout_height="wrap_content" android:text="Price"
android:layout_marginBottom="16dp"/> android:textColor="@color/text_dark"
android:textSize="12sp"
android:layout_marginBottom="4dp"/>
<TextView <EditText
android:layout_width="wrap_content" android:id="@+id/etPetPrice"
android:layout_height="wrap_content" android:layout_width="match_parent"
android:text="Owner" android:layout_height="wrap_content"
android:textColor="@color/text_dark" android:hint="Enter price"
android:textSize="12sp" android:inputType="numberDecimal"
android:layout_marginBottom="4dp"/> android:layout_marginBottom="16dp"
android:textColor="@color/text_dark"/>
<Spinner <TextView
android:id="@+id/spinnerCustomer" android:layout_width="wrap_content"
android:layout_width="match_parent" android:layout_height="wrap_content"
android:layout_height="wrap_content" android:text="Status"
android:layout_marginBottom="16dp"/> android:textColor="@color/text_dark"
android:textSize="12sp"
android:layout_marginBottom="4dp"/>
<TextView <Spinner
android:layout_width="wrap_content" android:id="@+id/spinnerPetStatus"
android:layout_height="wrap_content" android:layout_width="match_parent"
android:text="Store" android:layout_height="wrap_content"
android:textColor="@color/text_dark" android:layout_marginBottom="16dp"/>
android:textSize="12sp"
android:layout_marginBottom="4dp"/>
<Spinner <TextView
android:id="@+id/spinnerStore" android:layout_width="wrap_content"
android:layout_width="match_parent" android:layout_height="wrap_content"
android:layout_height="wrap_content"/> android:text="Owner"
android:textColor="@color/text_dark"
android:textSize="12sp"
android:layout_marginBottom="4dp"/>
<Spinner
android:id="@+id/spinnerCustomer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Store"
android:textColor="@color/text_dark"
android:textSize="12sp"
android:layout_marginBottom="4dp"/>
<Spinner
android:id="@+id/spinnerStore"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</LinearLayout>
</LinearLayout> </LinearLayout>
</ScrollView>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:background="@color/white"
android:padding="16dp">
<Button
android:id="@+id/btnBack"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginEnd="8dp"
android:text="Back"
android:backgroundTint="@color/primary_medium"
android:textColor="@color/white"/>
<Button
android:id="@+id/btnSavePet"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_weight="1"
android:backgroundTint="@color/accent_coral"
android:text="Save"
android:textColor="@color/white" />
</LinearLayout> </LinearLayout>
</ScrollView>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:background="@color/white"
android:padding="16dp">
<Button
android:id="@+id/btnBack"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginEnd="8dp"
android:text="Back"
android:backgroundTint="@color/primary_medium"
android:textColor="@color/white"/>
<Button
android:id="@+id/btnSavePet"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_weight="1"
android:backgroundTint="@color/accent_coral"
android:text="Save"
android:textColor="@color/white" />
</LinearLayout> </LinearLayout>
</LinearLayout> <ProgressBar
android:id="@+id/progressBar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:visibility="gone"
android:indeterminateTint="@color/accent_coral"/>
</FrameLayout>

View File

@@ -1,306 +1,320 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent">
android:orientation="vertical"
android:background="@color/background_grey">
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="0dp" android:layout_height="match_parent"
android:layout_weight="1"
android:orientation="vertical" android:orientation="vertical"
android:background="@color/primary_dark"> android:background="@color/background_grey">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="56dp"
android:orientation="horizontal"
android:gravity="center_vertical"
android:paddingStart="16dp"
android:paddingEnd="16dp">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Pet Profile"
android:textColor="@color/white"
android:textSize="20sp"
android:textStyle="bold"/>
<Button
android:id="@+id/btnEditPet"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Edit"
android:backgroundTint="@color/accent_coral"
android:textColor="@color/white"/>
</LinearLayout>
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="0dp" android:layout_height="0dp"
android:layout_weight="1" android:layout_weight="1"
android:orientation="vertical" android:orientation="vertical"
android:gravity="center"> android:background="@color/primary_dark">
<ImageView <LinearLayout
android:id="@+id/imgPet" android:layout_width="match_parent"
android:layout_width="146dp" android:layout_height="56dp"
android:layout_height="140dp" android:orientation="horizontal"
android:layout_marginBottom="12dp" android:gravity="center_vertical"
android:background="@drawable/circle_image" android:paddingStart="16dp"
android:clipToOutline="true" android:paddingEnd="16dp">
android:scaleType="centerCrop"
android:src="@drawable/placeholder" />
<Button <TextView
android:id="@+id/btnChangePhoto" android:layout_width="0dp"
android:layout_width="wrap_content" android:layout_height="wrap_content"
android:layout_height="wrap_content" android:layout_weight="1"
android:text="Change Photo" android:text="Pet Profile"
style="@style/Widget.Material3.Button.TextButton" android:textColor="@color/white"
android:textColor="@color/text_light" android:textSize="20sp"
android:layout_marginBottom="8dp"/> android:textStyle="bold"/>
<TextView <Button
android:id="@+id/tvPetName" android:id="@+id/btnEditPet"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="NAME" android:text="Edit"
android:textColor="@color/white" android:backgroundTint="@color/accent_coral"
android:textSize="26sp" android:textColor="@color/white"/>
android:textStyle="bold"/>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:orientation="vertical"
android:gravity="center">
<ImageView
android:id="@+id/imgPet"
android:layout_width="146dp"
android:layout_height="140dp"
android:layout_marginBottom="12dp"
android:background="@drawable/circle_image"
android:clipToOutline="true"
android:scaleType="centerCrop"
android:src="@drawable/placeholder" />
<Button
android:id="@+id/btnChangePhoto"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Change Photo"
style="@style/Widget.Material3.Button.TextButton"
android:textColor="@color/text_light"
android:layout_marginBottom="8dp"/>
<TextView
android:id="@+id/tvPetName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="NAME"
android:textColor="@color/white"
android:textSize="26sp"
android:textStyle="bold"/>
</LinearLayout>
</LinearLayout> </LinearLayout>
</LinearLayout> <ScrollView
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1">
<ScrollView <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="0dp" android:layout_height="wrap_content"
android:layout_weight="1"> android:orientation="vertical"
android:padding="16dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="120dp"
android:orientation="horizontal"
android:layout_marginBottom="8dp">
<LinearLayout
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:orientation="vertical"
android:gravity="center"
android:background="@color/white"
android:layout_marginEnd="8dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="SPECIES"
android:textSize="11sp"
android:textColor="#888888"
android:textAllCaps="true"
android:layout_marginBottom="4dp"/>
<TextView
android:id="@+id/tvPetSpecies"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Dog"
android:textSize="20sp"
android:textStyle="bold"
android:textColor="@color/text_dark"/>
</LinearLayout>
<LinearLayout
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:orientation="vertical"
android:gravity="center"
android:background="@color/white"
android:layout_marginStart="8dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="BREED"
android:textSize="11sp"
android:textColor="#888888"
android:textAllCaps="true"
android:layout_marginBottom="4dp"/>
<TextView
android:id="@+id/tvPetBreed"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Labrador"
android:textSize="20sp"
android:textStyle="bold"
android:textColor="@color/text_dark"/>
</LinearLayout>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="120dp"
android:orientation="horizontal"
android:layout_marginTop="8dp">
<LinearLayout
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:orientation="vertical"
android:gravity="center"
android:background="@color/white"
android:layout_marginEnd="8dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="AGE"
android:textSize="11sp"
android:textColor="#888888"
android:textAllCaps="true"
android:layout_marginBottom="4dp"/>
<TextView
android:id="@+id/tvPetAge"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="2 yr(s)"
android:textSize="20sp"
android:textStyle="bold"
android:textColor="@color/text_dark"/>
</LinearLayout>
<LinearLayout
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:orientation="vertical"
android:gravity="center"
android:background="@color/white"
android:layout_marginStart="8dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="PRICE"
android:textSize="11sp"
android:textColor="#888888"
android:textAllCaps="true"
android:layout_marginBottom="4dp"/>
<TextView
android:id="@+id/tvPetPrice"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="$500.00"
android:textSize="20sp"
android:textStyle="bold"
android:textColor="@color/accent_coral"/>
</LinearLayout>
</LinearLayout>
<LinearLayout
android:id="@+id/layoutPetOwner"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:gravity="center"
android:background="@color/white"
android:layout_marginTop="16dp"
android:padding="16dp"
android:visibility="gone">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="OWNER"
android:textSize="11sp"
android:textColor="#888888"
android:textAllCaps="true"
android:layout_marginBottom="4dp"/>
<TextView
android:id="@+id/tvPetOwner"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="No Owner"
android:textSize="20sp"
android:textStyle="bold"
android:textColor="@color/text_dark"/>
</LinearLayout>
<LinearLayout
android:id="@+id/layoutPetStore"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:gravity="center"
android:background="@color/white"
android:layout_marginTop="16dp"
android:padding="16dp"
android:visibility="gone">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="STORE"
android:textSize="11sp"
android:textColor="#888888"
android:textAllCaps="true"
android:layout_marginBottom="4dp"/>
<TextView
android:id="@+id/tvPetStore"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="No Store"
android:textSize="20sp"
android:textStyle="bold"
android:textColor="@color/text_dark"/>
</LinearLayout>
</LinearLayout>
</ScrollView>
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="vertical" android:orientation="horizontal"
android:padding="16dp"> android:padding="16dp">
<LinearLayout <Button
android:layout_width="match_parent" android:id="@+id/btnBack"
android:layout_height="120dp"
android:orientation="horizontal"
android:layout_marginBottom="8dp">
<LinearLayout
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:orientation="vertical"
android:gravity="center"
android:background="@color/white"
android:layout_marginEnd="8dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="SPECIES"
android:textSize="11sp"
android:textColor="#888888"
android:textAllCaps="true"
android:layout_marginBottom="4dp"/>
<TextView
android:id="@+id/tvPetSpecies"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Dog"
android:textSize="20sp"
android:textStyle="bold"
android:textColor="@color/text_dark"/>
</LinearLayout>
<LinearLayout
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:orientation="vertical"
android:gravity="center"
android:background="@color/white"
android:layout_marginStart="8dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="BREED"
android:textSize="11sp"
android:textColor="#888888"
android:textAllCaps="true"
android:layout_marginBottom="4dp"/>
<TextView
android:id="@+id/tvPetBreed"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Labrador"
android:textSize="20sp"
android:textStyle="bold"
android:textColor="@color/text_dark"/>
</LinearLayout>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="120dp"
android:orientation="horizontal"
android:layout_marginTop="8dp">
<LinearLayout
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:orientation="vertical"
android:gravity="center"
android:background="@color/white"
android:layout_marginEnd="8dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="AGE"
android:textSize="11sp"
android:textColor="#888888"
android:textAllCaps="true"
android:layout_marginBottom="4dp"/>
<TextView
android:id="@+id/tvPetAge"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="2 yr(s)"
android:textSize="20sp"
android:textStyle="bold"
android:textColor="@color/text_dark"/>
</LinearLayout>
<LinearLayout
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:orientation="vertical"
android:gravity="center"
android:background="@color/white"
android:layout_marginStart="8dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="PRICE"
android:textSize="11sp"
android:textColor="#888888"
android:textAllCaps="true"
android:layout_marginBottom="4dp"/>
<TextView
android:id="@+id/tvPetPrice"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="$500.00"
android:textSize="20sp"
android:textStyle="bold"
android:textColor="@color/accent_coral"/>
</LinearLayout>
</LinearLayout>
<LinearLayout
android:id="@+id/layoutPetOwner"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="vertical" android:text="Back"
android:gravity="center" android:backgroundTint="@color/primary_medium"
android:background="@color/white" android:textColor="@color/white"/>
android:layout_marginTop="16dp"
android:padding="16dp"
android:visibility="gone">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="OWNER"
android:textSize="11sp"
android:textColor="#888888"
android:textAllCaps="true"
android:layout_marginBottom="4dp"/>
<TextView
android:id="@+id/tvPetOwner"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="No Owner"
android:textSize="20sp"
android:textStyle="bold"
android:textColor="@color/text_dark"/>
</LinearLayout>
<LinearLayout
android:id="@+id/layoutPetStore"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:gravity="center"
android:background="@color/white"
android:layout_marginTop="16dp"
android:padding="16dp"
android:visibility="gone">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="STORE"
android:textSize="11sp"
android:textColor="#888888"
android:textAllCaps="true"
android:layout_marginBottom="4dp"/>
<TextView
android:id="@+id/tvPetStore"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="No Store"
android:textSize="20sp"
android:textStyle="bold"
android:textColor="@color/text_dark"/>
</LinearLayout>
</LinearLayout> </LinearLayout>
</ScrollView>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="16dp">
<Button
android:id="@+id/btnBack"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Back"
android:backgroundTint="@color/primary_medium"
android:textColor="@color/white"/>
</LinearLayout> </LinearLayout>
</LinearLayout> <ProgressBar
android:id="@+id/progressBar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:visibility="gone"
android:indeterminateTint="@color/accent_coral"/>
</FrameLayout>

View File

@@ -1,195 +1,209 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent">
android:orientation="vertical"
android:background="@color/background_grey">
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="56dp" android:layout_height="match_parent"
android:background="@color/primary_dark" android:orientation="vertical"
android:gravity="center_vertical" android:background="@color/background_grey">
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:orientation="horizontal">
<TextView
android:id="@+id/tvProductMode"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Add Product"
android:textColor="@color/white"
android:textSize="20sp"
android:textStyle="bold"/>
<Button
android:id="@+id/btnDeleteProduct"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:backgroundTint="@color/accent_coral"
android:text="Delete"
android:textColor="@color/white"/>
</LinearLayout>
<ScrollView
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1">
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="56dp"
android:orientation="vertical" android:background="@color/primary_dark"
android:padding="24dp"> android:gravity="center_vertical"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:orientation="horizontal">
<TextView
android:id="@+id/tvProductMode"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Add Product"
android:textColor="@color/white"
android:textSize="20sp"
android:textStyle="bold"/>
<Button
android:id="@+id/btnDeleteProduct"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:backgroundTint="@color/accent_coral"
android:text="Delete"
android:textColor="@color/white"/>
</LinearLayout>
<ScrollView
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1">
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="vertical" android:orientation="vertical"
android:background="@drawable/rounded_card" android:padding="24dp">
android:padding="16dp"
android:layout_marginBottom="16dp">
<TextView <LinearLayout
android:id="@+id/tvProductId"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="ID: #0"
android:textColor="@color/text_light"
android:textSize="11sp"
android:textStyle="italic"
android:layout_gravity="end"
android:layout_marginBottom="8dp"/>
<!-- Product Image -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Product Image"
android:textColor="@color/text_dark"
android:textSize="12sp"
android:layout_marginBottom="4dp"/>
<FrameLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="200dp" android:layout_height="wrap_content"
android:orientation="vertical"
android:background="@drawable/rounded_card"
android:padding="16dp"
android:layout_marginBottom="16dp"> android:layout_marginBottom="16dp">
<ImageView <TextView
android:id="@+id/ivProductImage" android:id="@+id/tvProductId"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="ID: #0"
android:textColor="@color/text_light"
android:textSize="11sp"
android:textStyle="italic"
android:layout_gravity="end"
android:layout_marginBottom="8dp"/>
<!-- Product Image -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Product Image"
android:textColor="@color/text_dark"
android:textSize="12sp"
android:layout_marginBottom="4dp"/>
<FrameLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="200dp"
android:scaleType="centerCrop" android:layout_marginBottom="16dp">
android:src="@drawable/placeholder2"
android:background="@color/text_light"
android:clickable="true"
android:focusable="true"/>
</FrameLayout> <ImageView
android:id="@+id/ivProductImage"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="centerCrop"
android:src="@drawable/placeholder2"
android:background="@color/text_light"
android:clickable="true"
android:focusable="true"/>
<!-- Product Name --> </FrameLayout>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Product Name"
android:textColor="@color/text_dark"
android:textSize="12sp"
android:layout_marginBottom="4dp"/>
<EditText <!-- Product Name -->
android:id="@+id/etProductName" <TextView
android:layout_width="match_parent" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:hint="Enter product name" android:text="Product Name"
android:inputType="text" android:textColor="@color/text_dark"
android:layout_marginBottom="16dp"/> android:textSize="12sp"
android:layout_marginBottom="4dp"/>
<!-- Category --> <EditText
<TextView android:id="@+id/etProductName"
android:layout_width="wrap_content" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="Category" android:hint="Enter product name"
android:textColor="@color/text_dark" android:inputType="text"
android:textSize="12sp" android:layout_marginBottom="16dp"/>
android:layout_marginBottom="4dp"/>
<Spinner <!-- Category -->
android:id="@+id/spinnerProductCategory" <TextView
android:layout_width="match_parent" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginBottom="16dp"/> android:text="Category"
android:textColor="@color/text_dark"
android:textSize="12sp"
android:layout_marginBottom="4dp"/>
<!-- Description --> <Spinner
<TextView android:id="@+id/spinnerProductCategory"
android:layout_width="wrap_content" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="Description" android:layout_marginBottom="16dp"/>
android:textColor="@color/text_dark"
android:textSize="12sp"
android:layout_marginBottom="4dp"/>
<EditText <!-- Description -->
android:id="@+id/etProductDesc" <TextView
android:layout_width="match_parent" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:hint="Enter description" android:text="Description"
android:inputType="textMultiLine" android:textColor="@color/text_dark"
android:minLines="2" android:textSize="12sp"
android:layout_marginBottom="16dp"/> android:layout_marginBottom="4dp"/>
<!-- Price --> <EditText
<TextView android:id="@+id/etProductDesc"
android:layout_width="wrap_content" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="Price" android:hint="Enter description"
android:textColor="@color/text_dark" android:inputType="textMultiLine"
android:textSize="12sp" android:minLines="2"
android:layout_marginBottom="4dp"/> android:layout_marginBottom="16dp"/>
<EditText <!-- Price -->
android:id="@+id/etProductPrice" <TextView
android:layout_width="match_parent" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:hint="0.00" android:text="Price"
android:inputType="numberDecimal" android:textColor="@color/text_dark"
android:layout_marginBottom="8dp"/> android:textSize="12sp"
android:layout_marginBottom="4dp"/>
<EditText
android:id="@+id/etProductPrice"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="0.00"
android:inputType="numberDecimal"
android:layout_marginBottom="8dp"/>
</LinearLayout>
</LinearLayout> </LinearLayout>
</ScrollView>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:background="@color/white"
android:padding="16dp">
<Button
android:id="@+id/btnProductBack"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginEnd="8dp"
android:text="Back"
android:backgroundTint="@color/primary_medium"
android:textColor="@color/white"/>
<Button
android:id="@+id/btnSaveProduct"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginStart="8dp"
android:text="Save"
android:backgroundTint="@color/accent_coral"
android:textColor="@color/white"/>
</LinearLayout> </LinearLayout>
</ScrollView>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:background="@color/white"
android:padding="16dp">
<Button
android:id="@+id/btnProductBack"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginEnd="8dp"
android:text="Back"
android:backgroundTint="@color/primary_medium"
android:textColor="@color/white"/>
<Button
android:id="@+id/btnSaveProduct"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginStart="8dp"
android:text="Save"
android:backgroundTint="@color/accent_coral"
android:textColor="@color/white"/>
</LinearLayout> </LinearLayout>
</LinearLayout> <ProgressBar
android:id="@+id/progressBar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:visibility="gone"
android:indeterminateTint="@color/accent_coral"/>
</FrameLayout>

View File

@@ -1,138 +1,152 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent">
android:orientation="vertical"
android:background="@color/background_grey">
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="56dp" android:layout_height="match_parent"
android:background="@color/primary_dark" android:orientation="vertical"
android:gravity="center_vertical" android:background="@color/background_grey">
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:orientation="horizontal">
<TextView
android:id="@+id/tvPSMode"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Add Product Supplier"
android:textColor="@color/white"
android:textSize="20sp"
android:textStyle="bold"/>
<Button
android:id="@+id/btnDeletePS"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:backgroundTint="@color/accent_coral"
android:text="Delete"
android:textColor="@color/white"/>
</LinearLayout>
<ScrollView
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1">
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="56dp"
android:orientation="vertical" android:background="@color/primary_dark"
android:padding="24dp"> android:gravity="center_vertical"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:orientation="horizontal">
<TextView
android:id="@+id/tvPSMode"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Add Product Supplier"
android:textColor="@color/white"
android:textSize="20sp"
android:textStyle="bold"/>
<Button
android:id="@+id/btnDeletePS"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:backgroundTint="@color/accent_coral"
android:text="Delete"
android:textColor="@color/white"/>
</LinearLayout>
<ScrollView
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1">
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="vertical" android:orientation="vertical"
android:background="@drawable/rounded_card" android:padding="24dp">
android:padding="16dp"
android:layout_marginBottom="16dp">
<!-- Product --> <LinearLayout
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Product"
android:textColor="@color/text_dark"
android:textSize="12sp"
android:layout_marginBottom="4dp"/>
<Spinner
android:id="@+id/spinnerPSProduct"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginBottom="16dp"/> android:orientation="vertical"
android:background="@drawable/rounded_card"
android:padding="16dp"
android:layout_marginBottom="16dp">
<!-- Supplier --> <!-- Product -->
<TextView <TextView
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="Supplier" android:text="Product"
android:textColor="@color/text_dark" android:textColor="@color/text_dark"
android:textSize="12sp" android:textSize="12sp"
android:layout_marginBottom="4dp"/> android:layout_marginBottom="4dp"/>
<Spinner <Spinner
android:id="@+id/spinnerPSSupplier" android:id="@+id/spinnerPSProduct"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginBottom="16dp"/> android:layout_marginBottom="16dp"/>
<!-- Cost --> <!-- Supplier -->
<TextView <TextView
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="Cost" android:text="Supplier"
android:textColor="@color/text_dark" android:textColor="@color/text_dark"
android:textSize="12sp" android:textSize="12sp"
android:layout_marginBottom="4dp"/> android:layout_marginBottom="4dp"/>
<EditText <Spinner
android:id="@+id/etPSCost" android:id="@+id/spinnerPSSupplier"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:hint="0.00" android:layout_marginBottom="16dp"/>
android:inputType="numberDecimal"
android:layout_marginBottom="8dp"/> <!-- Cost -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Cost"
android:textColor="@color/text_dark"
android:textSize="12sp"
android:layout_marginBottom="4dp"/>
<EditText
android:id="@+id/etPSCost"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="0.00"
android:inputType="numberDecimal"
android:layout_marginBottom="8dp"/>
</LinearLayout>
</LinearLayout> </LinearLayout>
</ScrollView>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:background="@color/white"
android:padding="16dp">
<Button
android:id="@+id/btnPSBack"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginEnd="8dp"
android:text="Back"
android:backgroundTint="@color/primary_medium"
android:textColor="@color/white"/>
<Button
android:id="@+id/btnSavePS"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginStart="8dp"
android:text="Save"
android:backgroundTint="@color/accent_coral"
android:textColor="@color/white"/>
</LinearLayout> </LinearLayout>
</ScrollView>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:background="@color/white"
android:padding="16dp">
<Button
android:id="@+id/btnPSBack"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginEnd="8dp"
android:text="Back"
android:backgroundTint="@color/primary_medium"
android:textColor="@color/white"/>
<Button
android:id="@+id/btnSavePS"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginStart="8dp"
android:text="Save"
android:backgroundTint="@color/accent_coral"
android:textColor="@color/white"/>
</LinearLayout> </LinearLayout>
</LinearLayout> <ProgressBar
android:id="@+id/progressBar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:visibility="gone"
android:indeterminateTint="@color/accent_coral"/>
</FrameLayout>

View File

@@ -1,200 +1,214 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android" <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent">
android:background="@color/background_grey">
<LinearLayout <ScrollView
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="match_parent"
android:orientation="vertical"> android:background="@color/background_grey">
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="vertical" android:orientation="vertical">
android:background="@color/primary_dark"
android:gravity="center_horizontal"
android:paddingTop="32dp"
android:paddingBottom="32dp">
<ImageView <LinearLayout
android:id="@+id/imgProfile" android:layout_width="match_parent"
android:layout_width="171dp" android:layout_height="wrap_content"
android:layout_height="166dp" android:orientation="vertical"
android:layout_marginBottom="8dp" android:background="@color/primary_dark"
android:background="@drawable/circle_image" android:gravity="center_horizontal"
android:clipToOutline="true" android:paddingTop="32dp"
android:scaleType="centerCrop" android:paddingBottom="32dp">
android:src="@drawable/placeholder" />
<ImageView
android:id="@+id/imgProfile"
android:layout_width="171dp"
android:layout_height="166dp"
android:layout_marginBottom="8dp"
android:background="@drawable/circle_image"
android:clipToOutline="true"
android:scaleType="centerCrop"
android:src="@drawable/placeholder" />
<Button
android:id="@+id/btnChangePhoto"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Change Photo"
style="@style/Widget.Material3.Button.TextButton"
android:textColor="@color/text_light"/>
<TextView
android:id="@+id/tvProfileName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="First Last"
android:textColor="@color/white"
android:textSize="22sp"
android:textStyle="bold" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:background="@color/white"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:layout_marginTop="16dp"
android:layout_marginBottom="16dp"
android:padding="16dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:paddingBottom="12dp">
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Email"
android:textSize="12sp"
android:textColor="#888888"/>
<TextView
android:id="@+id/tvProfileEmail"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="No email loaded"
android:textColor="@color/text_dark"
android:textSize="16sp" />
</LinearLayout>
<Button
android:id="@+id/btnEditEmail"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Edit"
android:textColor="@color/accent_coral"
style="@style/Widget.Material3.Button.TextButton"/>
</LinearLayout>
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="#F0F0F0"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:paddingTop="12dp"
android:paddingBottom="12dp">
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Phone"
android:textSize="12sp"
android:textColor="#888888"/>
<TextView
android:id="@+id/tvProfilePhone"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="No phone loaded"
android:textColor="@color/text_dark"
android:textSize="16sp" />
</LinearLayout>
<Button
android:id="@+id/btnEditPhone"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Edit"
android:textColor="@color/accent_coral"
style="@style/Widget.Material3.Button.TextButton"/>
</LinearLayout>
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="#F0F0F0"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:paddingTop="12dp">
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Role"
android:textSize="12sp"
android:textColor="#888888"/>
<TextView
android:id="@+id/tvProfileRole"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="No role loaded"
android:textSize="16sp"
android:textColor="@color/accent_coral"/>
</LinearLayout>
</LinearLayout>
</LinearLayout>
<Button <Button
android:id="@+id/btnChangePhoto" android:id="@+id/btnLogout"
android:layout_width="wrap_content" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="Change Photo" android:layout_marginStart="16dp"
style="@style/Widget.Material3.Button.TextButton" android:layout_marginEnd="16dp"
android:textColor="@color/text_light"/> android:layout_marginBottom="24dp"
android:text="Log Out"
<TextView android:backgroundTint="@color/accent_coral"
android:id="@+id/tvProfileName" android:textColor="@color/white"/>
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="First Last"
android:textColor="@color/white"
android:textSize="22sp"
android:textStyle="bold" />
</LinearLayout> </LinearLayout>
<LinearLayout </ScrollView>
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:background="@color/white"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:layout_marginTop="16dp"
android:layout_marginBottom="16dp"
android:padding="16dp">
<LinearLayout <ProgressBar
android:layout_width="match_parent" android:id="@+id/progressBar"
android:layout_height="wrap_content" android:layout_width="wrap_content"
android:orientation="horizontal" android:layout_height="wrap_content"
android:gravity="center_vertical" android:layout_gravity="center"
android:paddingBottom="12dp"> android:visibility="gone"
android:indeterminateTint="@color/accent_coral"/>
<LinearLayout </FrameLayout>
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Email"
android:textSize="12sp"
android:textColor="#888888"/>
<TextView
android:id="@+id/tvProfileEmail"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="No email loaded"
android:textColor="@color/text_dark"
android:textSize="16sp" />
</LinearLayout>
<Button
android:id="@+id/btnEditEmail"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Edit"
android:textColor="@color/accent_coral"
style="@style/Widget.Material3.Button.TextButton"/>
</LinearLayout>
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="#F0F0F0"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:paddingTop="12dp"
android:paddingBottom="12dp">
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Phone"
android:textSize="12sp"
android:textColor="#888888"/>
<TextView
android:id="@+id/tvProfilePhone"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="No phone loaded"
android:textColor="@color/text_dark"
android:textSize="16sp" />
</LinearLayout>
<Button
android:id="@+id/btnEditPhone"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Edit"
android:textColor="@color/accent_coral"
style="@style/Widget.Material3.Button.TextButton"/>
</LinearLayout>
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="#F0F0F0"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:paddingTop="12dp">
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Role"
android:textSize="12sp"
android:textColor="#888888"/>
<TextView
android:id="@+id/tvProfileRole"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="No role loaded"
android:textSize="16sp"
android:textColor="@color/accent_coral"/>
</LinearLayout>
</LinearLayout>
</LinearLayout>
<Button
android:id="@+id/btnLogout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="24dp"
android:text="Log Out"
android:backgroundTint="@color/accent_coral"
android:textColor="@color/white"/>
</LinearLayout>
</ScrollView>

View File

@@ -1,140 +1,154 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent">
android:orientation="vertical"
android:background="@color/background_grey">
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="56dp" android:layout_height="match_parent"
android:background="@color/primary_dark" android:orientation="vertical"
android:gravity="center_vertical" android:background="@color/background_grey">
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:orientation="horizontal">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Purchase Order Details"
android:textColor="@color/white"
android:textSize="20sp"
android:textStyle="bold"/>
</LinearLayout>
<ScrollView
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1">
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="56dp"
android:orientation="vertical" android:background="@color/primary_dark"
android:padding="24dp"> android:gravity="center_vertical"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:orientation="horizontal">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Purchase Order Details"
android:textColor="@color/white"
android:textSize="20sp"
android:textStyle="bold"/>
</LinearLayout>
<ScrollView
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1">
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="vertical" android:orientation="vertical"
android:background="@drawable/rounded_card" android:padding="24dp">
android:padding="16dp"
android:layout_marginBottom="16dp">
<TextView <LinearLayout
android:id="@+id/tvPODetailId" android:layout_width="match_parent"
android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:textColor="@color/text_light" android:orientation="vertical"
android:textSize="11sp" android:background="@drawable/rounded_card"
android:textStyle="italic" android:padding="16dp"
android:layout_gravity="end" android:layout_marginBottom="16dp">
android:layout_marginBottom="16dp"/>
<TextView <TextView
android:layout_width="wrap_content" android:id="@+id/tvPODetailId"
android:layout_height="wrap_content" android:layout_width="wrap_content"
android:text="Supplier" android:layout_height="wrap_content"
android:textColor="@color/text_light" android:textColor="@color/text_light"
android:textSize="12sp"/> android:textSize="11sp"
android:textStyle="italic"
android:layout_gravity="end"
android:layout_marginBottom="16dp"/>
<TextView <TextView
android:id="@+id/tvPODetailSupplier" android:layout_width="wrap_content"
android:layout_width="wrap_content" android:layout_height="wrap_content"
android:layout_height="wrap_content" android:text="Supplier"
android:textColor="@color/text_dark" android:textColor="@color/text_light"
android:textSize="16sp" android:textSize="12sp"/>
android:textStyle="bold"
android:layout_marginBottom="16dp"/>
<TextView <TextView
android:layout_width="wrap_content" android:id="@+id/tvPODetailSupplier"
android:layout_height="wrap_content" android:layout_width="wrap_content"
android:text="Store" android:layout_height="wrap_content"
android:textColor="@color/text_light" android:textColor="@color/text_dark"
android:textSize="12sp"/> android:textSize="16sp"
android:textStyle="bold"
android:layout_marginBottom="16dp"/>
<TextView <TextView
android:id="@+id/tvPODetailStore" android:layout_width="wrap_content"
android:layout_width="wrap_content" android:layout_height="wrap_content"
android:layout_height="wrap_content" android:text="Store"
android:textColor="@color/text_dark" android:textColor="@color/text_light"
android:textSize="16sp" android:textSize="12sp"/>
android:textStyle="bold"
android:layout_marginBottom="16dp"/>
<TextView <TextView
android:layout_width="wrap_content" android:id="@+id/tvPODetailStore"
android:layout_height="wrap_content" android:layout_width="wrap_content"
android:text="Order Date" android:layout_height="wrap_content"
android:textColor="@color/text_light" android:textColor="@color/text_dark"
android:textSize="12sp"/> android:textSize="16sp"
android:textStyle="bold"
android:layout_marginBottom="16dp"/>
<TextView <TextView
android:id="@+id/tvPODetailDate" android:layout_width="wrap_content"
android:layout_width="wrap_content" android:layout_height="wrap_content"
android:layout_height="wrap_content" android:text="Order Date"
android:textColor="@color/text_dark" android:textColor="@color/text_light"
android:textSize="15sp" android:textSize="12sp"/>
android:layout_marginBottom="16dp"/>
<TextView <TextView
android:layout_width="wrap_content" android:id="@+id/tvPODetailDate"
android:layout_height="wrap_content" android:layout_width="wrap_content"
android:text="Status" android:layout_height="wrap_content"
android:textColor="@color/text_light" android:textColor="@color/text_dark"
android:textSize="12sp"/> android:textSize="15sp"
android:layout_marginBottom="16dp"/>
<TextView <TextView
android:id="@+id/tvPODetailStatus" android:layout_width="wrap_content"
android:layout_width="wrap_content" android:layout_height="wrap_content"
android:layout_height="wrap_content" android:text="Status"
android:textSize="15sp" android:textColor="@color/text_light"
android:layout_marginBottom="16dp"/> android:textSize="12sp"/>
<TextView
android:id="@+id/tvPODetailStatus"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="15sp"
android:layout_marginBottom="16dp"/>
</LinearLayout>
</LinearLayout> </LinearLayout>
</LinearLayout> </ScrollView>
</ScrollView> <LinearLayout
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/white"
android:padding="16dp">
<Button
android:id="@+id/btnPOBack"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="Back" android:background="@color/white"
android:backgroundTint="@color/primary_medium" android:padding="16dp">
android:textColor="@color/white"/>
<Button
android:id="@+id/btnPOBack"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Back"
android:backgroundTint="@color/primary_medium"
android:textColor="@color/white"/>
</LinearLayout>
</LinearLayout> </LinearLayout>
</LinearLayout> <ProgressBar
android:id="@+id/progressBar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:visibility="gone"
android:indeterminateTint="@color/accent_coral"/>
</FrameLayout>

View File

@@ -1,222 +1,236 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent">
android:orientation="vertical"
android:background="@color/background_grey">
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="56dp" android:layout_height="match_parent"
android:background="@color/primary_dark" android:orientation="vertical"
android:gravity="center_vertical" android:background="@color/background_grey">
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:orientation="horizontal">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Process Refund"
android:textColor="@color/white"
android:textSize="20sp"
android:textStyle="bold"/>
</LinearLayout>
<ScrollView
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1">
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="56dp"
android:orientation="vertical" android:background="@color/primary_dark"
android:padding="16dp"> android:gravity="center_vertical"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:orientation="horizontal">
<!-- Load Sale Card --> <TextView
<LinearLayout android:layout_width="0dp"
android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="vertical" android:layout_weight="1"
android:background="@drawable/rounded_card" android:text="Process Refund"
android:padding="16dp" android:textColor="@color/white"
android:layout_marginBottom="16dp"> android:textSize="20sp"
android:textStyle="bold"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Load Original Sale"
android:textColor="@color/text_dark"
android:textSize="14sp"
android:textStyle="bold"
android:layout_marginBottom="8dp"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical">
<EditText
android:id="@+id/etRefundSaleId"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:hint="Enter Sale ID"
android:inputType="number"
android:layout_marginEnd="8dp"/>
<Button
android:id="@+id/btnLoadSale"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Load"
android:backgroundTint="@color/primary_medium"
android:textColor="@color/white"/>
</LinearLayout>
<TextView
android:id="@+id/tvSaleInfo"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/text_light"
android:textSize="12sp"
android:layout_marginTop="8dp"
android:visibility="gone"/>
</LinearLayout>
<!-- Original Sale Items Card -->
<LinearLayout
android:id="@+id/cardOriginalItems"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:background="@drawable/rounded_card"
android:padding="16dp"
android:layout_marginBottom="16dp"
android:visibility="gone">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Original Sale Items"
android:textColor="@color/text_dark"
android:textSize="14sp"
android:textStyle="bold"
android:layout_marginBottom="8dp"/>
<LinearLayout
android:id="@+id/llOriginalItems"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"/>
</LinearLayout>
<!-- Refund Items Card -->
<LinearLayout
android:id="@+id/cardRefundItems"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:background="@drawable/rounded_card"
android:padding="16dp"
android:layout_marginBottom="16dp"
android:visibility="gone">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Items to Refund"
android:textColor="@color/text_dark"
android:textSize="14sp"
android:textStyle="bold"
android:layout_marginBottom="8dp"/>
<LinearLayout
android:id="@+id/llRefundItems"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"/>
<TextView
android:id="@+id/tvRefundTotal"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Refund Total: $0.00"
android:textColor="@color/accent_coral"
android:textSize="16sp"
android:textStyle="bold"
android:layout_gravity="end"
android:layout_marginTop="12dp"/>
</LinearLayout>
<!-- Payment Method Card -->
<LinearLayout
android:id="@+id/cardPayment"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:background="@drawable/rounded_card"
android:padding="16dp"
android:layout_marginBottom="16dp"
android:visibility="gone">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Refund Payment Method"
android:textColor="@color/text_dark"
android:textSize="14sp"
android:textStyle="bold"
android:layout_marginBottom="8dp"/>
<Spinner
android:id="@+id/spinnerRefundPayment"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</LinearLayout>
</LinearLayout> </LinearLayout>
</ScrollView> <ScrollView
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1">
<!-- Bottom Buttons --> <LinearLayout
<LinearLayout android:layout_width="match_parent"
android:layout_width="match_parent" android:layout_height="wrap_content"
android:layout_height="wrap_content" android:orientation="vertical"
android:orientation="horizontal" android:padding="16dp">
android:background="@color/white"
android:padding="16dp">
<Button <!-- Load Sale Card -->
android:id="@+id/btnRefundBack" <LinearLayout
android:layout_width="0dp" android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:background="@drawable/rounded_card"
android:padding="16dp"
android:layout_marginBottom="16dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Load Original Sale"
android:textColor="@color/text_dark"
android:textSize="14sp"
android:textStyle="bold"
android:layout_marginBottom="8dp"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical">
<EditText
android:id="@+id/etRefundSaleId"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:hint="Enter Sale ID"
android:inputType="number"
android:layout_marginEnd="8dp"/>
<Button
android:id="@+id/btnLoadSale"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Load"
android:backgroundTint="@color/primary_medium"
android:textColor="@color/white"/>
</LinearLayout>
<TextView
android:id="@+id/tvSaleInfo"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/text_light"
android:textSize="12sp"
android:layout_marginTop="8dp"
android:visibility="gone"/>
</LinearLayout>
<!-- Original Sale Items Card -->
<LinearLayout
android:id="@+id/cardOriginalItems"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:background="@drawable/rounded_card"
android:padding="16dp"
android:layout_marginBottom="16dp"
android:visibility="gone">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Original Sale Items"
android:textColor="@color/text_dark"
android:textSize="14sp"
android:textStyle="bold"
android:layout_marginBottom="8dp"/>
<LinearLayout
android:id="@+id/llOriginalItems"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"/>
</LinearLayout>
<!-- Refund Items Card -->
<LinearLayout
android:id="@+id/cardRefundItems"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:background="@drawable/rounded_card"
android:padding="16dp"
android:layout_marginBottom="16dp"
android:visibility="gone">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Items to Refund"
android:textColor="@color/text_dark"
android:textSize="14sp"
android:textStyle="bold"
android:layout_marginBottom="8dp"/>
<LinearLayout
android:id="@+id/llRefundItems"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"/>
<TextView
android:id="@+id/tvRefundTotal"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Refund Total: $0.00"
android:textColor="@color/accent_coral"
android:textSize="16sp"
android:textStyle="bold"
android:layout_gravity="end"
android:layout_marginTop="12dp"/>
</LinearLayout>
<!-- Payment Method Card -->
<LinearLayout
android:id="@+id/cardPayment"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:background="@drawable/rounded_card"
android:padding="16dp"
android:layout_marginBottom="16dp"
android:visibility="gone">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Refund Payment Method"
android:textColor="@color/text_dark"
android:textSize="14sp"
android:textStyle="bold"
android:layout_marginBottom="8dp"/>
<Spinner
android:id="@+id/spinnerRefundPayment"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</LinearLayout>
</LinearLayout>
</ScrollView>
<!-- Bottom Buttons -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_weight="1" android:orientation="horizontal"
android:layout_marginEnd="8dp" android:background="@color/white"
android:text="Back" android:padding="16dp">
android:backgroundTint="@color/primary_medium"
android:textColor="@color/white"/>
<Button <Button
android:id="@+id/btnProcessRefund" android:id="@+id/btnRefundBack"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_weight="1" android:layout_weight="1"
android:layout_marginStart="8dp" android:layout_marginEnd="8dp"
android:text="Process Refund" android:text="Back"
android:backgroundTint="@color/accent_coral" android:backgroundTint="@color/primary_medium"
android:textColor="@color/white" android:textColor="@color/white"/>
android:visibility="gone"/>
<Button
android:id="@+id/btnProcessRefund"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginStart="8dp"
android:text="Process Refund"
android:backgroundTint="@color/accent_coral"
android:textColor="@color/white"
android:visibility="gone"/>
</LinearLayout>
</LinearLayout> </LinearLayout>
</LinearLayout> <ProgressBar
android:id="@+id/progressBar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:visibility="gone"
android:indeterminateTint="@color/accent_coral"/>
</FrameLayout>

Some files were not shown because too many files have changed in this diff Show More