Merge pull request #145 from RecentRunner/AttachmentsToChat

AttachmentsToChat
This commit was merged in pull request #145.
This commit is contained in:
2026-04-07 06:53:07 -06:00
committed by GitHub
132 changed files with 7992 additions and 4840 deletions

View File

@@ -2,6 +2,8 @@ import java.util.Properties
plugins { plugins {
alias(libs.plugins.android.application) alias(libs.plugins.android.application)
alias(libs.plugins.hilt)
alias(libs.plugins.navigation.safeargs)
} }
val localProperties = Properties().apply { val localProperties = Properties().apply {
@@ -37,6 +39,7 @@ android {
buildFeatures { buildFeatures {
buildConfig = true buildConfig = true
viewBinding = true
} }
buildTypes { buildTypes {
@@ -55,35 +58,45 @@ android {
} }
dependencies { dependencies {
// Core AndroidX & UI
implementation(libs.appcompat) implementation(libs.appcompat)
implementation(libs.material) implementation(libs.material)
implementation(libs.activity) implementation(libs.activity)
implementation(libs.constraintlayout) implementation(libs.constraintlayout)
implementation("com.squareup.retrofit2:retrofit:2.9.0")
implementation("com.squareup.retrofit2:converter-gson:2.9.0")
implementation("com.squareup.okhttp3:logging-interceptor:4.12.0")
implementation("com.squareup.okhttp3:okhttp:4.12.0")
implementation("com.google.android.material:material:1.11.0")
implementation("androidx.viewpager2:viewpager2:1.1.0")
implementation("androidx.camera:camera-core:1.4.0")
implementation("androidx.camera:camera-camera2:1.4.0")
implementation("androidx.camera:camera-lifecycle:1.4.0")
implementation("androidx.camera:camera-view:1.4.0")
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0")
implementation(libs.swiperefreshlayout) implementation(libs.swiperefreshlayout)
implementation(libs.viewpager2)
// Hilt Dependency Injection
implementation(libs.hilt.android)
annotationProcessor(libs.hilt.compiler)
// Navigation Component
implementation(libs.navigation.fragment)
implementation(libs.navigation.ui)
// Networking
implementation(libs.retrofit)
implementation(libs.retrofit.gson)
implementation(libs.okhttp)
implementation(libs.okhttp.logging)
// CameraX
implementation(libs.camera.core)
implementation(libs.camera.camera2)
implementation(libs.camera.lifecycle)
implementation(libs.camera.view)
// Image Loading
implementation(libs.glide)
annotationProcessor(libs.glide.compiler)
// Other Third-party Libraries
implementation("com.github.NaikSoftware:StompProtocolAndroid:1.6.6") implementation("com.github.NaikSoftware:StompProtocolAndroid:1.6.6")
implementation("io.reactivex.rxjava2:rxjava:2.2.21") implementation("io.reactivex.rxjava2:rxjava:2.2.21")
implementation("io.reactivex.rxjava2:rxandroid:2.1.1") implementation("io.reactivex.rxjava2:rxandroid:2.1.1")
implementation("com.github.bumptech.glide:glide:4.16.0")
annotationProcessor("com.github.bumptech.glide:compiler:4.16.0")
implementation("com.github.prolificinteractive:material-calendarview:2.0.1") implementation("com.github.prolificinteractive:material-calendarview:2.0.1")
// Testing
testImplementation(libs.junit) testImplementation(libs.junit)
androidTestImplementation(libs.ext.junit) androidTestImplementation(libs.ext.junit)
androidTestImplementation(libs.espresso.core) androidTestImplementation(libs.espresso.core)

View File

@@ -1,8 +1,9 @@
package com.example.petstoremobile; package com.example.petstoremobile;
import android.app.Application; import android.app.Application;
import com.example.petstoremobile.api.auth.TokenManager; import dagger.hilt.android.HiltAndroidApp;
@HiltAndroidApp
public class PetStoreApplication extends Application { public class PetStoreApplication extends Application {
@Override @Override
public void onCreate() { public void onCreate() {

View File

@@ -10,23 +10,25 @@ import android.util.Log;
import androidx.activity.EdgeToEdge; import androidx.activity.EdgeToEdge;
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.appcompat.app.AppCompatActivity; import androidx.appcompat.app.AppCompatActivity;
import androidx.core.content.ContextCompat; import androidx.core.content.ContextCompat;
import androidx.core.graphics.Insets; import androidx.core.graphics.Insets;
import androidx.core.view.ViewCompat; import androidx.core.view.ViewCompat;
import androidx.core.view.WindowInsetsCompat; import androidx.core.view.WindowInsetsCompat;
import androidx.fragment.app.Fragment; import androidx.navigation.NavController;
import androidx.navigation.fragment.NavHostFragment;
import androidx.navigation.ui.NavigationUI;
import com.example.petstoremobile.R; import com.example.petstoremobile.R;
import com.example.petstoremobile.fragments.ChatFragment; import com.example.petstoremobile.databinding.ActivityHomeBinding;
import com.example.petstoremobile.fragments.ListFragment;
import com.example.petstoremobile.fragments.ProfileFragment;
import com.example.petstoremobile.services.ChatNotificationService; import com.example.petstoremobile.services.ChatNotificationService;
import com.google.android.material.bottomnavigation.BottomNavigationView;
import dagger.hilt.android.AndroidEntryPoint;
@AndroidEntryPoint
public class HomeActivity extends AppCompatActivity { public class HomeActivity extends AppCompatActivity {
private BottomNavigationView bottomNav; private ActivityHomeBinding binding;
private NavController navController;
// Launcher to ask for notification permission // Launcher to ask for notification permission
private final ActivityResultLauncher<String> requestPermissionLauncher = private final ActivityResultLauncher<String> requestPermissionLauncher =
@@ -36,80 +38,73 @@ public class HomeActivity extends AppCompatActivity {
} }
}); });
/**
* Sets up the home screen, initializes bottom navigation, and handles incoming navigation intents.
*/
@Override @Override
protected void onCreate(Bundle savedInstanceState) { protected void onCreate(Bundle savedInstanceState) {
EdgeToEdge.enable(this); EdgeToEdge.enable(this);
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
setContentView(R.layout.activity_home); binding = ActivityHomeBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> { ViewCompat.setOnApplyWindowInsetsListener(binding.main, (v, insets) -> {
Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars()); Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom); v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom);
return insets; return insets;
}); });
//get the bottom navbar from the layout // Initialize Navigation Component
bottomNav = findViewById(R.id.bottom_navigation); NavHostFragment navHostFragment = (NavHostFragment) getSupportFragmentManager()
.findFragmentById(R.id.nav_host_fragment);
if (navHostFragment != null) {
navController = navHostFragment.getNavController();
NavigationUI.setupWithNavController(binding.bottomNavigation, navController);
}
//load the list fragment by default if it's a fresh start //load the list fragment by default if it's a fresh start
if (savedInstanceState == null) { if (savedInstanceState == null) {
handleIntent(getIntent()); handleIntent(getIntent());
} }
//when an item in the bottom bar is selected, load the corresponding fragment
bottomNav.setOnItemSelectedListener(item -> {
if (item.getItemId() == R.id.nav_list) {
loadFragment(new ListFragment());
return true;
} else if (item.getItemId() == R.id.nav_chat) {
loadFragment(new ChatFragment());
return true;
} else if (item.getItemId() == R.id.nav_profile) {
loadFragment(new ProfileFragment());
return true;
}
return false;
});
// Start the notification service and request for notification permission // Start the notification service and request for notification permission
startNotificationService(); startNotificationService();
requestNotificationPermission(); requestNotificationPermission();
} }
// Handle new intents when the activity is already running, /**
// like clicking a notification while the app is in use * Handles new intents received while the activity is already running (like notifications).
*/
@Override @Override
protected void onNewIntent(Intent intent) { protected void onNewIntent(Intent intent) {
super.onNewIntent(intent); super.onNewIntent(intent);
setIntent(intent); // Set the new intent so fragments can access updated extras
handleIntent(intent); handleIntent(intent);
} }
// Helper function to process intents for navigation. /**
// like clicking a notification or just launching the app from a fresh start * Processes the intent to determine if specific navigation (like opening a chat) is required.
*/
private void handleIntent(Intent intent) { private void handleIntent(Intent intent) {
if (intent != null && "chat".equals(intent.getStringExtra("navigate_to"))) { if (intent != null && "chat".equals(intent.getStringExtra("navigate_to"))) {
ChatFragment chatFragment = new ChatFragment(); if (binding.bottomNavigation != null) {
if (intent.hasExtra("conversation_id")) { // Navigate by selecting the bottom nav item.
Bundle args = new Bundle(); binding.bottomNavigation.setSelectedItemId(R.id.nav_chat);
args.putLong("conversation_id", intent.getLongExtra("conversation_id", -1));
chatFragment.setArguments(args);
} }
loadFragment(chatFragment);
bottomNav.setSelectedItemId(R.id.nav_chat);
} else {
loadFragment(new ListFragment());
bottomNav.setSelectedItemId(R.id.nav_list);
} }
} }
// Helper function to start the notification service in the background /**
// to receive notifications when a new conversation is created * Starts the background service responsible for monitoring chat notifications.
*/
private void startNotificationService() { private void startNotificationService() {
Intent serviceIntent = new Intent(this, ChatNotificationService.class); Intent serviceIntent = new Intent(this, ChatNotificationService.class);
startService(serviceIntent); startService(serviceIntent);
} }
//Helper function to request for notification permission /**
* Requests POST_NOTIFICATIONS permission from the user if running on Android 13 and above.
*/
private void requestNotificationPermission() { private void requestNotificationPermission() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
if (ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) { if (ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) {
@@ -117,12 +112,4 @@ public class HomeActivity extends AppCompatActivity {
} }
} }
} }
//Helper function to load a fragment
private void loadFragment(Fragment fragment) {
getSupportFragmentManager()
.beginTransaction()
.replace(R.id.fragment_container, fragment)
.commit();
}
} }

View File

@@ -2,10 +2,7 @@ package com.example.petstoremobile.activities;
import android.content.Intent; import android.content.Intent;
import android.os.Bundle; import android.os.Bundle;
import android.util.Log; import android.view.inputmethod.EditorInfo;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;
import android.widget.Toast; import android.widget.Toast;
import androidx.activity.EdgeToEdge; import androidx.activity.EdgeToEdge;
@@ -13,164 +10,134 @@ import androidx.appcompat.app.AppCompatActivity;
import androidx.core.graphics.Insets; import androidx.core.graphics.Insets;
import androidx.core.view.ViewCompat; import androidx.core.view.ViewCompat;
import androidx.core.view.WindowInsetsCompat; import androidx.core.view.WindowInsetsCompat;
import androidx.lifecycle.ViewModelProvider;
import com.example.petstoremobile.R;
import com.example.petstoremobile.api.auth.AuthApi;
import com.example.petstoremobile.api.auth.TokenManager; import com.example.petstoremobile.api.auth.TokenManager;
import com.example.petstoremobile.api.RetrofitClient; import com.example.petstoremobile.databinding.ActivityMainBinding;
import com.example.petstoremobile.dtos.AuthDTO; import com.example.petstoremobile.viewmodels.AuthViewModel;
import com.example.petstoremobile.dtos.UserDTO; import com.example.petstoremobile.utils.Resource;
import retrofit2.Call; import javax.inject.Inject;
import retrofit2.Callback; import javax.inject.Named;
import retrofit2.Response;
import dagger.hilt.android.AndroidEntryPoint;
//The login screen activity //The login screen activity
@AndroidEntryPoint
public class MainActivity extends AppCompatActivity { public class MainActivity extends AppCompatActivity {
private EditText etUser; private ActivityMainBinding binding;
private EditText etPassword; private AuthViewModel viewModel;
private Button btnLogin;
private TextView tvLoginStatus;
@Inject TokenManager tokenManager;
@Inject @Named("baseUrl") String baseUrl;
/**
* Initializes the activity, sets up the UI, and checks for an existing login session.
*/
@Override @Override
protected void onCreate(Bundle savedInstanceState) { protected void onCreate(Bundle savedInstanceState) {
EdgeToEdge.enable(this); EdgeToEdge.enable(this);
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
// Check if user is already logged in // Check if user is already logged in
TokenManager tokenManager = TokenManager.getInstance(this);
if (tokenManager.isLoggedIn()) { if (tokenManager.isLoggedIn()) {
if ("CUSTOMER".equalsIgnoreCase(tokenManager.getRole())) { if ("CUSTOMER".equalsIgnoreCase(tokenManager.getRole())) {
// If a customer somehow remained logged in, clear them out // If a customer somehow remained logged in, clear them out
tokenManager.clearLoginData(); tokenManager.clearLoginData();
} else { } else {
Intent intent = new Intent(this, HomeActivity.class); startActivity(new Intent(this, HomeActivity.class));
startActivity(intent);
finish(); finish();
return; return;
} }
} }
setContentView(R.layout.activity_main); binding = ActivityMainBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
viewModel = new ViewModelProvider(this).get(AuthViewModel.class);
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> { ViewCompat.setOnApplyWindowInsetsListener(binding.main, (v, insets) -> {
Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars()); Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom); v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom);
return insets; return insets;
}); });
//get all controls from layout
tvLoginStatus = findViewById(R.id.tvLoginStatus);
etUser = findViewById(R.id.etUser);
etPassword = findViewById(R.id.etPassword);
btnLogin = findViewById(R.id.btnLogin);
//clear login status //clear login status
tvLoginStatus.setText(""); binding.tvLoginStatus.setText("");
// Set editor action listener for password field to login on when enter is pressed
binding.etPassword.setOnEditorActionListener((v, actionId, event) -> {
if (actionId == EditorInfo.IME_ACTION_DONE || actionId == EditorInfo.IME_NULL) {
binding.btnLogin.performClick();
return true;
}
return false;
});
//Set click listener for login button //Set click listener for login button
btnLogin.setOnClickListener(v -> { binding.btnLogin.setOnClickListener(v -> {
//Get username and password from text fields //Get username and password from text fields
String username = etUser.getText().toString(); String username = binding.etUser.getText().toString();
String password = etPassword.getText().toString(); String password = binding.etPassword.getText().toString();
//check if fields are empty //check if fields are empty
if (username.isEmpty() || password.isEmpty()) { if (username.isEmpty() || password.isEmpty()) {
Toast.makeText(this, "Please enter username and password", Toast.LENGTH_SHORT).show(); Toast.makeText(this, "Please enter username and password", Toast.LENGTH_SHORT).show();
tvLoginStatus.setText("Please enter username and password"); binding.tvLoginStatus.setText("Please enter username and password");
return; return;
} }
AuthApi authApi = RetrofitClient.getAuthApi(this); performLogin(username, password);
});
}
//Call login from api and get response /**
authApi.login(new AuthDTO.LoginRequest(username,password)).enqueue(new Callback<AuthDTO.LoginResponse>() { * Executes the login process using the AuthViewModel and handles the authentication response.
@Override */
public void onResponse(Call<AuthDTO.LoginResponse> call, Response<AuthDTO.LoginResponse> response) { private void performLogin(String username, String password) {
if (response.isSuccessful() && response.body() != null) { viewModel.login(username, password).observe(this, resource -> {
String role = response.body().getRole(); if (resource == null) return;
// Check if the user is a CUSTOMER and deny login if so switch (resource.status) {
case LOADING:
binding.btnLogin.setEnabled(false);
binding.tvLoginStatus.setText("Logging in...");
break;
case SUCCESS:
if (resource.data != null) {
String role = resource.data.getRole();
if ("CUSTOMER".equalsIgnoreCase(role)) { if ("CUSTOMER".equalsIgnoreCase(role)) {
Toast.makeText(MainActivity.this, "Access denied: Customers are not allowed to log in.", Toast.LENGTH_LONG).show(); binding.btnLogin.setEnabled(true);
tvLoginStatus.setText("Customers are not allowed to log in"); binding.tvLoginStatus.setText("Customers are not allowed to log in");
return; Toast.makeText(this, "Access denied: Customers are not allowed to log in.", Toast.LENGTH_LONG).show();
} else {
tokenManager.saveLoginData(resource.data.getToken(), resource.data.getUsername(), role);
fetchUserIdAndNavigate();
} }
//save login data in shared preferences
TokenManager.getInstance(MainActivity.this).saveLoginData(
response.body().getToken(),
response.body().getUsername(),
role
);
//fetch user id from api then login to home activity
RetrofitClient.getAuthApi(MainActivity.this).getMe()
.enqueue(new Callback<UserDTO>() {
@Override
public void onResponse(Call<UserDTO> call,
Response<UserDTO> response) {
if (response.isSuccessful() && response.body() != null) {
TokenManager.getInstance(MainActivity.this)
.saveUserId(response.body().getId());
}
Toast.makeText(MainActivity.this, "Login successful", Toast.LENGTH_SHORT).show();
startActivity(new Intent(MainActivity.this, HomeActivity.class));
finish();
}
@Override
public void onFailure(Call<UserDTO> call,
Throwable t) {
Log.e("MainActivity", "Failed to fetch userId", t);
Toast.makeText(MainActivity.this, "Login successful", Toast.LENGTH_SHORT).show();
startActivity(new Intent(MainActivity.this, HomeActivity.class));
finish();
}
});
} else {
String errorMessage;
switch (response.code()) {
case 401:
errorMessage = "Invalid username or password";
break;
case 500:
errorMessage = "Server error. Please try again later.";
break;
case 503:
errorMessage = "Service unavailable. Backend may be starting up.";
break;
default:
errorMessage = "Login failed (Error " + response.code() + ")";
}
Toast.makeText(MainActivity.this, errorMessage, Toast.LENGTH_LONG).show();
tvLoginStatus.setText(errorMessage);
} }
break;
case ERROR:
binding.btnLogin.setEnabled(true);
binding.tvLoginStatus.setText(resource.message);
Toast.makeText(this, resource.message, Toast.LENGTH_LONG).show();
break;
}
});
}
/**
* Retrieves the logged-in user's profile information to save their ID before navigating to the home screen.
*/
private void fetchUserIdAndNavigate() {
viewModel.getMe().observe(this, resource -> {
if (resource != null && resource.status != Resource.Status.LOADING) {
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
tokenManager.saveUserId(resource.data.getId());
} }
Toast.makeText(this, "Login successful", Toast.LENGTH_SHORT).show();
@Override startActivity(new Intent(this, HomeActivity.class));
public void onFailure(Call<AuthDTO.LoginResponse> call, Throwable t) { finish();
Log.e("MainActivity", "Login request failed", t); }
String errorMessage;
if (t instanceof java.net.ConnectException ||
t instanceof java.net.SocketTimeoutException ||
t instanceof java.net.UnknownHostException) {
errorMessage = "Cannot connect to server at " + RetrofitClient.BASE_URL +
". Please check if the backend is running.";
} else if (t instanceof java.io.IOException) {
errorMessage = "Network error. Please check your connection.";
} else {
errorMessage = "Login failed: " + t.getMessage();
}
Toast.makeText(MainActivity.this, errorMessage, Toast.LENGTH_LONG).show();
tvLoginStatus.setText(errorMessage);
}
});
}); });
} }
} }

View File

@@ -59,14 +59,14 @@ public class AppointmentAdapter extends RecyclerView.Adapter<AppointmentAdapter.
String status = a.getStatus() != null ? a.getStatus() : ""; String status = a.getStatus() != null ? a.getStatus() : "";
holder.tvAppointmentStatus.setText(status); holder.tvAppointmentStatus.setText(status);
switch (status) { switch (status.toUpperCase()) {
case "Booked": case "BOOKED":
holder.tvAppointmentStatus.setBackgroundColor(Color.parseColor("#2196F3")); // blue holder.tvAppointmentStatus.setBackgroundColor(Color.parseColor("#2196F3")); // blue
break; break;
case "Completed": case "COMPLETED":
holder.tvAppointmentStatus.setBackgroundColor(Color.parseColor("#4CAF50")); // green holder.tvAppointmentStatus.setBackgroundColor(Color.parseColor("#4CAF50")); // green
break; break;
case "Cancelled": case "CANCELLED":
holder.tvAppointmentStatus.setBackgroundColor(Color.parseColor("#F44336")); // red holder.tvAppointmentStatus.setBackgroundColor(Color.parseColor("#F44336")); // red
break; break;
default: default:

View File

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

View File

@@ -9,18 +9,18 @@ import android.widget.TextView;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView;
import com.bumptech.glide.Glide;
import com.bumptech.glide.load.engine.DiskCacheStrategy;
import com.example.petstoremobile.R; import com.example.petstoremobile.R;
import com.example.petstoremobile.api.PetApi; import com.example.petstoremobile.api.PetApi;
import com.example.petstoremobile.api.RetrofitClient;
import com.example.petstoremobile.dtos.PetDTO; import com.example.petstoremobile.dtos.PetDTO;
import com.example.petstoremobile.utils.GlideUtils;
import java.util.List; import java.util.List;
public class PetAdapter extends RecyclerView.Adapter<PetAdapter.PetViewHolder> { public class PetAdapter extends RecyclerView.Adapter<PetAdapter.PetViewHolder> {
private List<PetDTO> petList; private List<PetDTO> petList;
private OnPetClickListener petClickListener; private OnPetClickListener petClickListener;
private String baseUrl;
private String token;
// Interface for pet click on recycler view // Interface for pet click on recycler view
public interface OnPetClickListener { public interface OnPetClickListener {
@@ -33,6 +33,14 @@ public class PetAdapter extends RecyclerView.Adapter<PetAdapter.PetViewHolder> {
this.petClickListener = petClickListener; this.petClickListener = petClickListener;
} }
public void setBaseUrl(String baseUrl) {
this.baseUrl = baseUrl;
}
public void setToken(String token) {
this.token = token;
}
// Get the controls of each row in recycler view // Get the controls of each row in recycler view
public static class PetViewHolder extends RecyclerView.ViewHolder { public static class PetViewHolder extends RecyclerView.ViewHolder {
TextView tvPetName, tvPetSpeciesBreed, tvPetAge, tvPetPrice, tvPetStatus; TextView tvPetName, tvPetSpeciesBreed, tvPetAge, tvPetPrice, tvPetStatus;
@@ -66,11 +74,11 @@ public class PetAdapter extends RecyclerView.Adapter<PetAdapter.PetViewHolder> {
holder.tvPetSpeciesBreed.setText(pet.getPetSpecies() + " - " + pet.getPetBreed()); holder.tvPetSpeciesBreed.setText(pet.getPetSpecies() + " - " + pet.getPetBreed());
holder.tvPetAge.setText("Age: " + pet.getPetAge() + " yr(s)"); holder.tvPetAge.setText("Age: " + pet.getPetAge() + " yr(s)");
try { Double price = pet.getPetPrice();
double price = Double.parseDouble(pet.getPetPrice()); if (price != null) {
holder.tvPetPrice.setText("$" + String.format("%.2f", price)); holder.tvPetPrice.setText("$" + String.format("%.2f", price));
} catch (Exception e) { } else {
holder.tvPetPrice.setText("$" + pet.getPetPrice()); holder.tvPetPrice.setText("$0.00");
} }
holder.tvPetStatus.setText(pet.getPetStatus()); holder.tvPetStatus.setText(pet.getPetStatus());
@@ -82,16 +90,13 @@ public class PetAdapter extends RecyclerView.Adapter<PetAdapter.PetViewHolder> {
holder.tvPetStatus.setBackgroundColor(Color.parseColor("#F44336")); holder.tvPetStatus.setBackgroundColor(Color.parseColor("#F44336"));
} }
// Load pet image using Glide with circle crop // Load pet image using Glide
String imageUrl = RetrofitClient.BASE_URL + String.format(PetApi.PET_IMAGE_PATH, pet.getPetId()); if (baseUrl != null) {
Glide.with(holder.itemView.getContext()) String imageUrl = baseUrl + String.format(PetApi.PET_IMAGE_PATH, pet.getPetId());
.load(imageUrl) GlideUtils.loadImageWithTokenCircle(holder.itemView.getContext(), holder.ivPetProfile, imageUrl, token, R.drawable.placeholder);
.circleCrop() } else {
.diskCacheStrategy(DiskCacheStrategy.NONE) holder.ivPetProfile.setImageResource(R.drawable.placeholder);
.skipMemoryCache(true) }
.placeholder(R.drawable.placeholder)
.error(R.drawable.placeholder)
.into(holder.ivPetProfile);
//when a row is clicked, open the detail view //when a row is clicked, open the detail view
holder.itemView.setOnClickListener(v -> petClickListener.onPetClick(position)); holder.itemView.setOnClickListener(v -> petClickListener.onPetClick(position));

View File

@@ -6,18 +6,18 @@ import android.widget.TextView;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView;
import com.bumptech.glide.Glide;
import com.bumptech.glide.load.engine.DiskCacheStrategy;
import com.example.petstoremobile.R; import com.example.petstoremobile.R;
import com.example.petstoremobile.api.ProductApi; import com.example.petstoremobile.api.ProductApi;
import com.example.petstoremobile.api.RetrofitClient;
import com.example.petstoremobile.dtos.ProductDTO; import com.example.petstoremobile.dtos.ProductDTO;
import com.example.petstoremobile.utils.GlideUtils;
import java.util.List; import java.util.List;
public class ProductAdapter extends RecyclerView.Adapter<ProductAdapter.ProductViewHolder> { public class ProductAdapter extends RecyclerView.Adapter<ProductAdapter.ProductViewHolder> {
private List<ProductDTO> productList; private List<ProductDTO> productList;
private OnProductClickListener listener; private OnProductClickListener listener;
private String baseUrl;
private String token;
public interface OnProductClickListener { public interface OnProductClickListener {
void onProductClick(int position); void onProductClick(int position);
@@ -28,6 +28,14 @@ public class ProductAdapter extends RecyclerView.Adapter<ProductAdapter.ProductV
this.listener = listener; this.listener = listener;
} }
public void setBaseUrl(String baseUrl) {
this.baseUrl = baseUrl;
}
public void setToken(String token) {
this.token = token;
}
public static class ProductViewHolder extends RecyclerView.ViewHolder { public static class ProductViewHolder extends RecyclerView.ViewHolder {
TextView tvName, tvCategory, tvDesc, tvPrice; TextView tvName, tvCategory, tvDesc, tvPrice;
ImageView ivProductImage; ImageView ivProductImage;
@@ -59,15 +67,12 @@ public class ProductAdapter extends RecyclerView.Adapter<ProductAdapter.ProductV
holder.tvPrice.setText(p.getProdPrice() != null ? "$" + p.getProdPrice() : ""); holder.tvPrice.setText(p.getProdPrice() != null ? "$" + p.getProdPrice() : "");
// Load product image using Glide // Load product image using Glide
String imageUrl = RetrofitClient.BASE_URL + String.format(ProductApi.PRODUCT_IMAGE_PATH, p.getProdId()); if (baseUrl != null) {
Glide.with(holder.itemView.getContext()) String imageUrl = baseUrl + String.format(ProductApi.PRODUCT_IMAGE_PATH, p.getProdId());
.load(imageUrl) GlideUtils.loadImageWithTokenCircle(holder.itemView.getContext(), holder.ivProductImage, imageUrl, token, R.drawable.placeholder);
.circleCrop() } else {
.diskCacheStrategy(DiskCacheStrategy.NONE) holder.ivProductImage.setImageResource(R.drawable.placeholder);
.skipMemoryCache(true) }
.placeholder(R.drawable.placeholder)
.error(R.drawable.placeholder)
.into(holder.ivProductImage);
holder.itemView.setOnClickListener(v -> listener.onProductClick(position)); holder.itemView.setOnClickListener(v -> listener.onProductClick(position));
} }

View File

@@ -0,0 +1,47 @@
package com.example.petstoremobile.adapters;
import android.content.Context;
import android.graphics.Color;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat;
import com.example.petstoremobile.R;
import java.util.List;
/**
* A class that overrides the arrayAdapter so the text color is white and background is transparent.
*/
public class WhiteTextArrayAdapter<T> extends ArrayAdapter<T> {
public WhiteTextArrayAdapter(@NonNull Context context, int resource, @NonNull T[] objects) {
super(context, resource, objects);
}
public WhiteTextArrayAdapter(@NonNull Context context, int resource, @NonNull List<T> objects) {
super(context, resource, objects);
}
@NonNull
@Override
public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) {
View view = super.getView(position, convertView, parent);
view.setBackgroundColor(Color.TRANSPARENT);
if (view instanceof TextView) {
((TextView) view).setTextColor(Color.WHITE);
}
return view;
}
@Override
public View getDropDownView(int position, @Nullable View convertView, @NonNull ViewGroup parent) {
View view = super.getDropDownView(position, convertView, parent);
view.setBackgroundColor(ContextCompat.getColor(getContext(), R.color.primary_dark));
if (view instanceof TextView) {
((TextView) view).setTextColor(Color.WHITE);
}
return view;
}
}

View File

@@ -17,7 +17,12 @@ public interface AppointmentApi {
@GET("api/v1/appointments") @GET("api/v1/appointments")
Call<PageResponse<AppointmentDTO>> getAllAppointments( Call<PageResponse<AppointmentDTO>> getAllAppointments(
@Query("page") int page, @Query("page") int page,
@Query("size") int size); @Query("size") int size,
@Query("q") String query,
@Query("status") String status,
@Query("storeId") Long storeId,
@Query("date") String date,
@Query("employeeId") Long employeeId);
@GET("api/v1/appointments/{id}") @GET("api/v1/appointments/{id}")
Call<AppointmentDTO> getAppointmentById(@Path("id") Long id); Call<AppointmentDTO> getAppointmentById(@Path("id") Long id);

View File

@@ -16,12 +16,14 @@ import retrofit2.http.Query;
public interface InventoryApi { public interface InventoryApi {
// GET /api/v1/inventory?q=...&page=...&size=... // GET /api/v1/inventory?q=...&page=...&size=...&category=...&storeId=...&sort=...
@GET("api/v1/inventory") @GET("api/v1/inventory")
Call<PageResponse<InventoryDTO>> getAllInventory( Call<PageResponse<InventoryDTO>> getAllInventory(
@Query("q") String query,
@Query("page") int page, @Query("page") int page,
@Query("size") int size, @Query("size") int size,
@Query("q") String query,
@Query("category") String category,
@Query("storeId") Long storeId,
@Query("sort") String sort); @Query("sort") String sort);
// GET /api/v1/inventory/{id} // GET /api/v1/inventory/{id}

View File

@@ -3,10 +3,14 @@ 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.RequestBody;
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;
//api calls to get and send messages //api calls to get and send messages
@@ -17,4 +21,12 @@ 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}/messages/attachment")
Call<MessageDTO> sendMessageWithAttachment(
@Path("id") Long conversationId,
@Part("content") RequestBody content,
@Part MultipartBody.Part file
);
} }

View File

@@ -20,11 +20,16 @@ public interface PetApi {
// endpoint for downloading the pet's image file // endpoint for downloading the pet's image file
String PET_IMAGE_PATH = "api/v1/pets/%d/image"; String PET_IMAGE_PATH = "api/v1/pets/%d/image";
// Get all pets // Get all pets with filters
@GET("api/v1/pets") @GET("api/v1/pets")
Call<PageResponse<PetDTO>> getAllPets( Call<PageResponse<PetDTO>> getAllPets(
@Query("page") int page, @Query("page") int page,
@Query("size") int size @Query("size") int size,
@Query("q") String query,
@Query("status") String status,
@Query("species") String species,
@Query("storeId") Long storeId,
@Query("sort") String sort
); );
// Get pet by id // Get pet by id

View File

@@ -12,8 +12,10 @@ public interface ProductApi {
@GET("api/v1/products") @GET("api/v1/products")
Call<PageResponse<ProductDTO>> getAllProducts( Call<PageResponse<ProductDTO>> getAllProducts(
@Query("q") String query, @Query("q") String query,
@Query("categoryId") Long categoryId,
@Query("page") int page, @Query("page") int page,
@Query("size") int size); @Query("size") int size,
@Query("sort") String sort);
@GET("api/v1/products/{id}") @GET("api/v1/products/{id}")
Call<ProductDTO> getProductById(@Path("id") Long id); Call<ProductDTO> getProductById(@Path("id") Long id);

View File

@@ -10,7 +10,16 @@ public interface ProductSupplierApi {
@GET("api/v1/product-suppliers") @GET("api/v1/product-suppliers")
Call<PageResponse<ProductSupplierDTO>> getAllProductSuppliers( Call<PageResponse<ProductSupplierDTO>> getAllProductSuppliers(
@Query("page") int page, @Query("page") int page,
@Query("size") int size); @Query("size") int size,
@Query("q") String query,
@Query("productId") Long productId,
@Query("supplierId") Long supplierId,
@Query("sort") String sort);
@GET("api/v1/product-suppliers/{productId}/{supplierId}")
Call<ProductSupplierDTO> getProductSupplierById(
@Path("productId") Long productId,
@Path("supplierId") Long supplierId);
@POST("api/v1/product-suppliers") @POST("api/v1/product-suppliers")
Call<ProductSupplierDTO> createProductSupplier(@Body ProductSupplierDTO dto); Call<ProductSupplierDTO> createProductSupplier(@Body ProductSupplierDTO dto);

View File

@@ -12,7 +12,10 @@ public interface PurchaseOrderApi {
@GET("api/v1/purchase-orders") @GET("api/v1/purchase-orders")
Call<PageResponse<PurchaseOrderDTO>> getAllPurchaseOrders( Call<PageResponse<PurchaseOrderDTO>> getAllPurchaseOrders(
@Query("page") int page, @Query("page") int page,
@Query("size") int size); @Query("size") int size,
@Query("query") String query,
@Query("storeId") Long storeId,
@Query("sort") String sort);
@GET("api/v1/purchase-orders/{id}") @GET("api/v1/purchase-orders/{id}")
Call<PurchaseOrderDTO> getPurchaseOrderById(@Path("id") Long id); Call<PurchaseOrderDTO> getPurchaseOrderById(@Path("id") Long id);

View File

@@ -7,6 +7,7 @@ import android.util.Log;
import com.example.petstoremobile.BuildConfig; import com.example.petstoremobile.BuildConfig;
import com.example.petstoremobile.api.auth.AuthApi; import com.example.petstoremobile.api.auth.AuthApi;
import com.example.petstoremobile.api.auth.AuthInterceptor; import com.example.petstoremobile.api.auth.AuthInterceptor;
import com.example.petstoremobile.api.auth.TokenManager;
import okhttp3.OkHttpClient; import okhttp3.OkHttpClient;
import okhttp3.logging.HttpLoggingInterceptor; import okhttp3.logging.HttpLoggingInterceptor;
@@ -15,7 +16,7 @@ import retrofit2.converter.gson.GsonConverterFactory;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
//Retrofit client Used for API calls //Retrofit client Used for API calls TODO: DELETE THIS FILE AFTER MERGE NOW THAT WE ARE USING HILT AND NETWORKMODULE
public class RetrofitClient { public class RetrofitClient {
private static final String TAG = "RetrofitClient"; private static final String TAG = "RetrofitClient";
public static final String BASE_URL = getBaseUrl(); public static final String BASE_URL = getBaseUrl();
@@ -50,7 +51,7 @@ public class RetrofitClient {
OkHttpClient client = new OkHttpClient.Builder() OkHttpClient client = new OkHttpClient.Builder()
.addInterceptor(interceptor) .addInterceptor(interceptor)
.addInterceptor(new AuthInterceptor(context)) .addInterceptor(new AuthInterceptor(new TokenManager(context)))
.connectTimeout(30, TimeUnit.SECONDS) .connectTimeout(30, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS) .readTimeout(30, TimeUnit.SECONDS)
.writeTimeout(30, TimeUnit.SECONDS) .writeTimeout(30, TimeUnit.SECONDS)

View File

@@ -18,7 +18,9 @@ public interface ServiceApi {
@GET("api/v1/services") @GET("api/v1/services")
Call<PageResponse<ServiceDTO>> getAllServices( Call<PageResponse<ServiceDTO>> getAllServices(
@Query("page") int page, @Query("page") int page,
@Query("size") int size @Query("size") int size,
@Query("q") String query,
@Query("sort") String sort
); );
// Get service by id // Get service by id

View File

@@ -18,7 +18,9 @@ public interface SupplierApi {
@GET("api/v1/suppliers") @GET("api/v1/suppliers")
Call<PageResponse<SupplierDTO>> getAllSuppliers( Call<PageResponse<SupplierDTO>> getAllSuppliers(
@Query("page") int page, @Query("page") int page,
@Query("size") int size @Query("size") int size,
@Query("q") String query,
@Query("sort") String sort
); );
// Get supplier by id // Get supplier by id

View File

@@ -1,7 +1,5 @@
package com.example.petstoremobile.api.auth; package com.example.petstoremobile.api.auth;
import android.content.Context;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import java.io.IOException; import java.io.IOException;
@@ -15,8 +13,8 @@ public class AuthInterceptor implements Interceptor {
private final TokenManager tokenManager; private final TokenManager tokenManager;
public AuthInterceptor(Context context) { public AuthInterceptor(TokenManager tokenManager) {
this.tokenManager = TokenManager.getInstance(context); this.tokenManager = tokenManager;
} }
@NonNull @NonNull

View File

@@ -3,7 +3,12 @@ package com.example.petstoremobile.api.auth;
import android.content.Context; import android.content.Context;
import android.content.SharedPreferences; import android.content.SharedPreferences;
//Store login token in shared preferences import javax.inject.Inject;
import javax.inject.Singleton;
import dagger.hilt.android.qualifiers.ApplicationContext;
@Singleton
public class TokenManager { public class TokenManager {
private static final String TOKEN_KEY = "token"; private static final String TOKEN_KEY = "token";
private static final String USERNAME_KEY = "username"; private static final String USERNAME_KEY = "username";
@@ -11,20 +16,13 @@ public class TokenManager {
private static final String PREFS_NAME = "auth_prefs"; private static final String PREFS_NAME = "auth_prefs";
private static final String USER_ID_KEY = "user_id"; private static final String USER_ID_KEY = "user_id";
private static TokenManager instance;
private SharedPreferences prefs; private SharedPreferences prefs;
private TokenManager(Context context) { @Inject
public TokenManager(@ApplicationContext Context context) {
prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE); prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
} }
public static TokenManager getInstance(Context context) {
if (instance == null) {
instance = new TokenManager(context);
}
return instance;
}
//save login data after login //save login data after login
public void saveLoginData(String token, String username, String role) { public void saveLoginData(String token, String username, String role) {
prefs.edit() prefs.edit()
@@ -65,6 +63,4 @@ public class TokenManager {
public void clearLoginData() { public void clearLoginData() {
prefs.edit().clear().apply(); prefs.edit().clear().apply();
} }
}
}

View File

@@ -0,0 +1,176 @@
package com.example.petstoremobile.di;
import android.content.Context;
import android.os.Build;
import com.example.petstoremobile.BuildConfig;
import com.example.petstoremobile.api.*;
import com.example.petstoremobile.api.auth.AuthApi;
import com.example.petstoremobile.api.auth.AuthInterceptor;
import com.example.petstoremobile.api.auth.TokenManager;
import java.util.concurrent.TimeUnit;
import javax.inject.Named;
import javax.inject.Singleton;
import dagger.Module;
import dagger.Provides;
import dagger.hilt.InstallIn;
import dagger.hilt.android.qualifiers.ApplicationContext;
import dagger.hilt.components.SingletonComponent;
import okhttp3.OkHttpClient;
import okhttp3.logging.HttpLoggingInterceptor;
import retrofit2.Retrofit;
import retrofit2.converter.gson.GsonConverterFactory;
//Module to provide dependencies injection for the api
@Module
@InstallIn(SingletonComponent.class)
public class NetworkModule {
@Provides
@Singleton
@Named("baseUrl")
public static String provideBaseUrl() {
return isEmulator() ? BuildConfig.EMULATOR_BACKEND_URL : BuildConfig.DEVICE_BACKEND_URL;
}
// Check if the device is an emulator
private static boolean isEmulator() {
return Build.FINGERPRINT.startsWith("generic")
|| Build.FINGERPRINT.startsWith("unknown")
|| Build.MODEL.contains("google_sdk")
|| Build.MODEL.contains("Emulator")
|| Build.MODEL.contains("Android SDK built for x86")
|| Build.MANUFACTURER.contains("Genymotion")
|| Build.HARDWARE.contains("goldfish")
|| Build.HARDWARE.contains("ranchu")
|| Build.PRODUCT.contains("sdk")
|| Build.PRODUCT.contains("sdk_gphone")
|| (Build.BRAND.startsWith("generic") && Build.DEVICE.startsWith("generic"));
}
@Provides
@Singleton
public static OkHttpClient provideOkHttpClient(TokenManager tokenManager) {
HttpLoggingInterceptor interceptor = new HttpLoggingInterceptor();
interceptor.setLevel(HttpLoggingInterceptor.Level.BODY);
return new OkHttpClient.Builder()
.addInterceptor(interceptor)
.addInterceptor(new AuthInterceptor(tokenManager))
.connectTimeout(30, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.writeTimeout(30, TimeUnit.SECONDS)
.build();
}
//build the retrofit instance
@Provides
@Singleton
public static Retrofit provideRetrofit(@Named("baseUrl") String baseUrl, OkHttpClient client) {
return new Retrofit.Builder()
.baseUrl(baseUrl)
.addConverterFactory(GsonConverterFactory.create())
.client(client)
.build();
}
//associate the api with the retrofit instance
@Provides
@Singleton
public static PetApi providePetApi(Retrofit retrofit) {
return retrofit.create(PetApi.class);
}
@Provides
@Singleton
public static ServiceApi provideServiceApi(Retrofit retrofit) {
return retrofit.create(ServiceApi.class);
}
@Provides
@Singleton
public static SupplierApi provideSupplierApi(Retrofit retrofit) {
return retrofit.create(SupplierApi.class);
}
@Provides
@Singleton
public static AdoptionApi provideAdoptionApi(Retrofit retrofit) {
return retrofit.create(AdoptionApi.class);
}
@Provides
@Singleton
public static AppointmentApi provideAppointmentApi(Retrofit retrofit) {
return retrofit.create(AppointmentApi.class);
}
@Provides
@Singleton
public static ProductApi provideProductApi(Retrofit retrofit) {
return retrofit.create(ProductApi.class);
}
@Provides
@Singleton
public static SaleApi provideSaleApi(Retrofit retrofit) {
return retrofit.create(SaleApi.class);
}
@Provides
@Singleton
public static PurchaseOrderApi providePurchaseOrderApi(Retrofit retrofit) {
return retrofit.create(PurchaseOrderApi.class);
}
@Provides
@Singleton
public static ProductSupplierApi provideProductSupplierApi(Retrofit retrofit) {
return retrofit.create(ProductSupplierApi.class);
}
@Provides
@Singleton
public static InventoryApi provideInventoryApi(Retrofit retrofit) {
return retrofit.create(InventoryApi.class);
}
@Provides
@Singleton
public static AuthApi provideAuthApi(Retrofit retrofit) {
return retrofit.create(AuthApi.class);
}
@Provides
@Singleton
public static ChatApi provideChatApi(Retrofit retrofit) {
return retrofit.create(ChatApi.class);
}
@Provides
@Singleton
public static CustomerApi provideCustomerApi(Retrofit retrofit) {
return retrofit.create(CustomerApi.class);
}
@Provides
@Singleton
public static MessageApi provideMessageApi(Retrofit retrofit) {
return retrofit.create(MessageApi.class);
}
@Provides
@Singleton
public static StoreApi provideStoreApi(Retrofit retrofit) {
return retrofit.create(StoreApi.class);
}
@Provides
@Singleton
public static CategoryApi provideCategoryApi(Retrofit retrofit) {
return retrofit.create(CategoryApi.class);
}
}

View File

@@ -1,8 +1,5 @@
package com.example.petstoremobile.dtos; package com.example.petstoremobile.dtos;
import java.math.BigDecimal;
import java.util.List;
public class AppointmentDTO { public class AppointmentDTO {
private Long appointmentId; private Long appointmentId;
@@ -17,20 +14,20 @@ public class AppointmentDTO {
private String appointmentDate; private String appointmentDate;
private String appointmentTime; private String appointmentTime;
private String appointmentStatus; private String appointmentStatus;
private List<String> petNames; private String petName;
private List<Long> petIds; private Long petId;
private String createdAt; private String createdAt;
private String updatedAt; private String updatedAt;
public AppointmentDTO(Long customerId, Long storeId, Long serviceId, public AppointmentDTO(Long customerId, Long storeId, Long serviceId,
String appointmentDate, String appointmentTime, String appointmentDate, String appointmentTime,
String appointmentStatus, List<Long> petIds) { String appointmentStatus, Long petId) {
this(customerId, storeId, serviceId, null, appointmentDate, appointmentTime, appointmentStatus, petIds); this(customerId, storeId, serviceId, null, appointmentDate, appointmentTime, appointmentStatus, petId);
} }
public AppointmentDTO(Long customerId, Long storeId, Long serviceId, Long employeeId, public AppointmentDTO(Long customerId, Long storeId, Long serviceId, Long employeeId,
String appointmentDate, String appointmentTime, String appointmentDate, String appointmentTime,
String appointmentStatus, List<Long> petIds) { String appointmentStatus, Long petId) {
this.customerId = customerId; this.customerId = customerId;
this.storeId = storeId; this.storeId = storeId;
this.serviceId = serviceId; this.serviceId = serviceId;
@@ -38,7 +35,7 @@ public class AppointmentDTO {
this.appointmentDate = appointmentDate; this.appointmentDate = appointmentDate;
this.appointmentTime = appointmentTime; this.appointmentTime = appointmentTime;
this.appointmentStatus = appointmentStatus; this.appointmentStatus = appointmentStatus;
this.petIds = petIds; this.petId = petId;
} }
public Long getAppointmentId() { public Long getAppointmentId() {
@@ -89,12 +86,12 @@ public class AppointmentDTO {
return appointmentStatus; return appointmentStatus;
} }
public List<String> getPetNames() { public String getPetName() {
return petNames; return petName;
} }
public List<Long> getPetIds() { public Long getPetId() {
return petIds; return petId;
} }
public String getCreatedAt() { public String getCreatedAt() {
@@ -105,16 +102,8 @@ public class AppointmentDTO {
return updatedAt; return updatedAt;
} }
public String getPetName() {
return (petNames != null && !petNames.isEmpty()) ? petNames.get(0) : "";
}
public Long getPetID() { public Long getPetID() {
return (petIds != null && !petIds.isEmpty()) ? petIds.get(0) : null; return petId;
}
public Long getPetId() {
return getPetID();
} }
public String getServiceType() { public String getServiceType() {

View File

@@ -1,6 +1,9 @@
package com.example.petstoremobile.dtos; package com.example.petstoremobile.dtos;
import com.google.gson.annotations.SerializedName;
public class CustomerDTO { public class CustomerDTO {
@SerializedName("id")
private Long customerId; private Long customerId;
private String firstName; private String firstName;
private String lastName; private String lastName;
@@ -12,18 +15,34 @@ public class CustomerDTO {
return customerId; return customerId;
} }
public void setCustomerId(Long customerId) {
this.customerId = customerId;
}
public String getFirstName() { public String getFirstName() {
return firstName; return firstName;
} }
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public String getLastName() { public String getLastName() {
return lastName; return lastName;
} }
public void setLastName(String lastName) {
this.lastName = lastName;
}
public String getEmail() { public String getEmail() {
return email; return email;
} }
public void setEmail(String email) {
this.email = email;
}
public String getFullName() { public String getFullName() {
return firstName + " " + lastName; return firstName + " " + lastName;
} }
@@ -32,7 +51,15 @@ public class CustomerDTO {
return createdAt; return createdAt;
} }
public void setCreatedAt(String createdAt) {
this.createdAt = createdAt;
}
public String getUpdatedAt() { public String getUpdatedAt() {
return updatedAt; return updatedAt;
} }
}
public void setUpdatedAt(String updatedAt) {
this.updatedAt = updatedAt;
}
}

View File

@@ -7,9 +7,13 @@ public class PetDTO {
private String petBreed; private String petBreed;
private Integer petAge; private Integer petAge;
private String petStatus; private String petStatus;
private String petPrice; private Double petPrice;
private String createdAt; private String createdAt;
private String updatedAt; private String updatedAt;
private Long customerId;
private String customerName;
private Long storeId;
private String storeName;
public Long getPetId() { return petId; } public Long getPetId() { return petId; }
public void setPetId(Long petId) { this.petId = petId; } public void setPetId(Long petId) { this.petId = petId; }
@@ -29,12 +33,24 @@ public class PetDTO {
public String getPetStatus() { return petStatus; } public String getPetStatus() { return petStatus; }
public void setPetStatus(String petStatus) { this.petStatus = petStatus; } public void setPetStatus(String petStatus) { this.petStatus = petStatus; }
public String getPetPrice() { return petPrice; } public Double getPetPrice() { return petPrice; }
public void setPetPrice(String petPrice) { this.petPrice = petPrice; } public void setPetPrice(Double petPrice) { this.petPrice = petPrice; }
public String getCreatedAt() { return createdAt; } public String getCreatedAt() { return createdAt; }
public void setCreatedAt(String createdAt) { this.createdAt = createdAt; } public void setCreatedAt(String createdAt) { this.createdAt = createdAt; }
public String getUpdatedAt() { return updatedAt; } public String getUpdatedAt() { return updatedAt; }
public void setUpdatedAt(String updatedAt) { this.updatedAt = updatedAt; } public void setUpdatedAt(String updatedAt) { this.updatedAt = updatedAt; }
public Long getCustomerId() { return customerId; }
public void setCustomerId(Long customerId) { this.customerId = customerId; }
public String getCustomerName() { return customerName; }
public void setCustomerName(String customerName) { this.customerName = customerName; }
public Long getStoreId() { return storeId; }
public void setStoreId(Long storeId) { this.storeId = storeId; }
public String getStoreName() { return storeName; }
public void setStoreName(String storeName) { this.storeName = storeName; }
} }

View File

@@ -8,55 +8,50 @@ import android.os.Bundle;
import android.provider.OpenableColumns; import android.provider.OpenableColumns;
import android.util.Log; import android.util.Log;
import android.view.*; import android.view.*;
import android.widget.*; import android.view.inputmethod.EditorInfo;
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;
import androidx.core.view.GravityCompat; import androidx.core.view.GravityCompat;
import androidx.drawerlayout.widget.DrawerLayout;
import androidx.fragment.app.Fragment; import androidx.fragment.app.Fragment;
import androidx.lifecycle.ViewModelProvider;
import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
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;
import com.example.petstoremobile.adapters.MessageAdapter; import com.example.petstoremobile.adapters.MessageAdapter;
import com.example.petstoremobile.api.auth.TokenManager; import com.example.petstoremobile.api.auth.TokenManager;
import com.example.petstoremobile.api.ChatApi; import com.example.petstoremobile.databinding.FragmentChatBinding;
import com.example.petstoremobile.api.CustomerApi;
import com.example.petstoremobile.api.MessageApi;
import com.example.petstoremobile.api.RetrofitClient;
import com.example.petstoremobile.dtos.ConversationDTO; import com.example.petstoremobile.dtos.ConversationDTO;
import com.example.petstoremobile.dtos.CustomerDTO;
import com.example.petstoremobile.dtos.MessageDTO; import com.example.petstoremobile.dtos.MessageDTO;
import com.example.petstoremobile.dtos.PageResponse;
import com.example.petstoremobile.dtos.SendMessageRequest; 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.FileUtils;
import com.example.petstoremobile.utils.Resource;
import com.example.petstoremobile.viewmodels.ChatViewModel;
import com.example.petstoremobile.websocket.StompChatManager; import com.example.petstoremobile.websocket.StompChatManager;
import java.util.*;
import java.util.stream.Collectors;
import retrofit2.*;
import java.io.File;
import java.util.*;
import javax.inject.Inject;
import javax.inject.Named;
import dagger.hilt.android.AndroidEntryPoint;
import okhttp3.MediaType;
import okhttp3.MultipartBody;
import okhttp3.RequestBody;
@AndroidEntryPoint
public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickListener, StompChatManager.MessageListener, public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickListener, StompChatManager.MessageListener,
StompChatManager.ConversationListener, StompChatManager.ConnectionListener { StompChatManager.ConversationListener, StompChatManager.ConnectionListener {
private static final String TAG = "ChatFragment"; private static final String TAG = "ChatFragment";
// View private FragmentChatBinding binding;
private DrawerLayout drawerLayout; private ChatViewModel viewModel;
private RecyclerView rvChatList, rvMessages;
private EditText etMessage;
private Button btnSend;
private ImageButton btnAttach;
private TextView tvChatTitle;
// Preview views
private View layoutAttachmentPreview;
private ImageView ivPreview;
private TextView tvPreviewName;
private ImageButton btnRemoveAttachment;
// Adapters // Adapters
private ChatAdapter chatAdapter; private ChatAdapter chatAdapter;
@@ -68,10 +63,8 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis
private final Map<Long, String> customerNames = new HashMap<>(); private final Map<Long, String> customerNames = new HashMap<>();
private Uri pendingAttachmentUri; private Uri pendingAttachmentUri;
// APIs @Inject TokenManager tokenManager;
private ChatApi chatApi; @Inject @Named("baseUrl") String baseUrl;
private CustomerApi customerApi;
private MessageApi messageApi;
// chat // chat
private Long currentUserId; private Long currentUserId;
@@ -79,10 +72,13 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis
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);
attachmentLauncher = registerForActivityResult( attachmentLauncher = registerForActivityResult(
new ActivityResultContracts.StartActivityForResult(), new ActivityResultContracts.StartActivityForResult(),
result -> { result -> {
@@ -96,33 +92,28 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis
); );
} }
/**
* 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) {
View view = inflater.inflate(R.layout.fragment_chat, container, false); binding = FragmentChatBinding.inflate(inflater, container, false);
chatApi = RetrofitClient.getChatApi(requireContext()); binding.btnHamburger.setOnClickListener(v -> binding.chatDrawerLayout.openDrawer(GravityCompat.START));
customerApi = RetrofitClient.getCustomerApi(requireContext());
messageApi = RetrofitClient.getMessageApi(requireContext());
drawerLayout = view.findViewById(R.id.chatDrawerLayout); // Set editor action listener for message field to send when enter is pressed
rvChatList = view.findViewById(R.id.rvChatList); binding.etMessage.setOnEditorActionListener((v, actionId, event) -> {
rvMessages = view.findViewById(R.id.rvMessages); if (actionId == EditorInfo.IME_ACTION_SEND || actionId == EditorInfo.IME_NULL) {
etMessage = view.findViewById(R.id.etMessage); binding.btnSend.performClick();
btnSend = view.findViewById(R.id.btnSend); return true;
btnAttach = view.findViewById(R.id.btnAttach); }
tvChatTitle = view.findViewById(R.id.tvChatTitle); return false;
});
layoutAttachmentPreview = view.findViewById(R.id.layoutAttachmentPreview);
ivPreview = view.findViewById(R.id.ivPreview);
tvPreviewName = view.findViewById(R.id.tvPreviewName);
btnRemoveAttachment = view.findViewById(R.id.btnRemoveAttachment);
ImageButton hamburger = view.findViewById(R.id.btnHamburger);
hamburger.setOnClickListener(v -> drawerLayout.openDrawer(GravityCompat.START));
//When the send button is clicked check if there is an attachment and send using the correct helper function //When the send button is clicked check if there is an attachment and send using the correct helper function
btnSend.setOnClickListener(v -> { binding.btnSend.setOnClickListener(v -> {
if (pendingAttachmentUri != null) { if (pendingAttachmentUri != null) {
sendWithAttachment(pendingAttachmentUri); sendWithAttachment(pendingAttachmentUri);
} else { } else {
@@ -131,43 +122,47 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis
}); });
//When the attachment button is clicked open the file picker //When the attachment button is clicked open the file picker
btnAttach.setOnClickListener(v -> selectAttachment()); binding.btnAttach.setOnClickListener(v -> selectAttachment());
btnRemoveAttachment.setOnClickListener(v -> removeAttachment()); binding.btnRemoveAttachment.setOnClickListener(v -> removeAttachment());
setupRecyclerViews(); setupRecyclerViews();
loadInitialData(); loadInitialData();
return view; return binding.getRoot();
} }
// Helper function to setup recycler views for chat and messages /**
* Configures the RecyclerViews for the conversation list and the message history.
*/
private void setupRecyclerViews() { private void setupRecyclerViews() {
// Set up Drawer menu to select conversation // Set up Drawer menu to select conversation
chatAdapter = new ChatAdapter(chatList, this); chatAdapter = new ChatAdapter(chatList, this);
rvChatList.setLayoutManager(new LinearLayoutManager(getContext())); binding.rvChatList.setLayoutManager(new LinearLayoutManager(getContext()));
rvChatList.setAdapter(chatAdapter); binding.rvChatList.setAdapter(chatAdapter);
// set up RecyclerView for selected chat to show messages // set up RecyclerView for selected chat to show messages
messageAdapter = new MessageAdapter(messageList, null); messageAdapter = new MessageAdapter(messageList, null);
LinearLayoutManager lm = new LinearLayoutManager(getContext()); LinearLayoutManager lm = new LinearLayoutManager(getContext());
lm.setStackFromEnd(true); lm.setStackFromEnd(true);
rvMessages.setLayoutManager(lm); binding.rvMessages.setLayoutManager(lm);
rvMessages.setAdapter(messageAdapter); binding.rvMessages.setAdapter(messageAdapter);
setConversationActive(false); setConversationActive(false);
} }
//Helper function to load token and user id then connect to websocket /**
* Loads authentication tokens and user info, then initializes the Stomp WebSocket connection.
*/
private void loadInitialData() { private void loadInitialData() {
TokenManager tm = TokenManager.getInstance(requireContext()); String token = tokenManager.getToken();
String token = tm.getToken(); currentUserId = tokenManager.getUserId();
currentUserId = tm.getUserId(); String role = tokenManager.getRole();
String role = tm.getRole();
messageAdapter.setCurrentUserId(currentUserId); messageAdapter.setCurrentUserId(currentUserId);
messageAdapter.setToken(token);
// if token exist then connect to websocket // if token exist then connect to websocket
if (token != null) { if (token != null) {
stompChatManager = new StompChatManager(token, role); stompChatManager = new StompChatManager(token, role, baseUrl);
stompChatManager.setMessageListener(this); stompChatManager.setMessageListener(this);
stompChatManager.setConversationListener(this); stompChatManager.setConversationListener(this);
stompChatManager.setConnectionListener(this); stompChatManager.setConnectionListener(this);
@@ -178,89 +173,74 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis
if (getArguments() != null && getArguments().containsKey("conversation_id")) { if (getArguments() != null && getArguments().containsKey("conversation_id")) {
activeConversationId = getArguments().getLong("conversation_id"); activeConversationId = getArguments().getLong("conversation_id");
} else if (getActivity() != null && getActivity().getIntent().hasExtra("conversation_id")) {
activeConversationId = getActivity().getIntent().getLongExtra("conversation_id", -1);
getActivity().getIntent().removeExtra("conversation_id");
getActivity().getIntent().removeExtra("navigate_to");
} }
loadCustomers(); loadCustomers();
} }
//Helper function to load customer names for it to be displayed on drawer menu /**
* Fetches a list of customers from the ViewModel to display customer names for the chat list.
*/
private void loadCustomers() { private void loadCustomers() {
customerApi.getAllCustomers(0, 100).enqueue(new Callback<PageResponse<CustomerDTO>>() { viewModel.getAllCustomers(0, 100).observe(getViewLifecycleOwner(), resource -> {
@Override if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) {
public void onResponse(@NonNull Call<PageResponse<CustomerDTO>> call, resource.data.getContent().forEach(c -> customerNames.put(c.getCustomerId(), c.getFullName()));
@NonNull Response<PageResponse<CustomerDTO>> response) {
if (response.isSuccessful() && response.body() != null) {
for (CustomerDTO c : response.body().getContent()) {
customerNames.put(c.getCustomerId(), c.getFullName());
}
}
loadConversations();
}
@Override
public void onFailure(@NonNull Call<PageResponse<CustomerDTO>> call,
@NonNull Throwable t) {
loadConversations(); loadConversations();
} }
}); });
} }
//helper function to load conversations entities to display with customer names in drawer menu /**
* Retrieves all conversations for the current user through the ViewModel and populates the chat drawer.
*/
private void loadConversations() { private void loadConversations() {
chatApi.getAllConversations().enqueue(new Callback<List<ConversationDTO>>() { viewModel.getAllConversations().observe(getViewLifecycleOwner(), resource -> {
@Override if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) {
public void onResponse(@NonNull Call<List<ConversationDTO>> call, chatList.clear();
@NonNull Response<List<ConversationDTO>> response) { for (ConversationDTO dto : resource.data) {
if (response.isSuccessful() && response.body() != null) { String name = customerNames.getOrDefault(
chatList.clear(); dto.getCustomerId(), "Customer #" + dto.getCustomerId());
List<Chat> loaded = response.body().stream() chatList.add(new Chat(String.valueOf(dto.getId()),
.map(dto -> { name, dto.getLastMessage(),
String name = customerNames.getOrDefault( dto.getCustomerId(), dto.getStaffId()));
dto.getCustomerId(), "Customer #" + dto.getCustomerId()); }
return new Chat(String.valueOf(dto.getId()), chatAdapter.notifyDataSetChanged();
name, dto.getLastMessage(),
dto.getCustomerId(), dto.getStaffId()); if (activeConversationId != null) {
}) setConversationActive(true);
.collect(Collectors.toList()); // Update title to customer name of active conversation
chatList.addAll(loaded); for (Chat chat : chatList) {
chatAdapter.notifyDataSetChanged(); if (chat.getChatId().equals(String.valueOf(activeConversationId))) {
binding.tvChatTitle.setText(chat.getCustomerName());
if (activeConversationId != null) { break;
setConversationActive(true); }
// Update title to customer name of active conversation }
for (Chat chat : chatList) { if (stompChatManager != null) {
if (chat.getChatId().equals(String.valueOf(activeConversationId))) { stompChatManager.subscribeToConversation(activeConversationId);
tvChatTitle.setText(chat.getCustomerName()); }
break; loadMessageHistory(activeConversationId);
} } else {
} messageList.clear();
if (stompChatManager != null) { messageAdapter.notifyDataSetChanged();
stompChatManager.subscribeToConversation(activeConversationId); setConversationActive(false);
}
loadMessageHistory(activeConversationId);
} else {
messageList.clear();
messageAdapter.notifyDataSetChanged();
setConversationActive(false);
}
} }
}
@Override
public void onFailure(@NonNull Call<List<ConversationDTO>> call,
@NonNull Throwable t) {
Log.e(TAG, "Error loading conversations", t);
} }
}); });
} }
// Called when user taps a chat in the drawer /**
// Loads messages for that chat selected * Handles selection of a chat from the drawer, updating the UI and subscribing to the WebSocket.
*/
@Override @Override
public void onChatClick(Chat chat) { public void onChatClick(Chat chat) {
activeConversationId = Long.parseLong(chat.getChatId()); activeConversationId = Long.parseLong(chat.getChatId());
setConversationActive(true); setConversationActive(true);
tvChatTitle.setText(chat.getCustomerName()); binding.tvChatTitle.setText(chat.getCustomerName());
drawerLayout.closeDrawer(GravityCompat.START); binding.chatDrawerLayout.closeDrawer(GravityCompat.START);
if (stompChatManager != null) { if (stompChatManager != null) {
stompChatManager.subscribeToConversation(activeConversationId); stompChatManager.subscribeToConversation(activeConversationId);
@@ -269,94 +249,87 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis
loadMessageHistory(activeConversationId); loadMessageHistory(activeConversationId);
} }
//helper function to load messages for selected chat /**
* Fetches the full message history for a specific conversation from the ViewModel.
*/
private void loadMessageHistory(Long conversationId) { private void loadMessageHistory(Long conversationId) {
messageApi.getMessages(conversationId).enqueue(new Callback<List<MessageDTO>>() { viewModel.getMessages(conversationId).observe(getViewLifecycleOwner(), resource -> {
@Override if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) {
public void onResponse(@NonNull Call<List<MessageDTO>> call, messageList.clear();
@NonNull Response<List<MessageDTO>> response) { for (MessageDTO dto : resource.data) {
if (response.isSuccessful() && response.body() != null) { messageList.add(dtoToModel(dto));
messageList.clear();
for (MessageDTO dto : response.body()) {
messageList.add(dtoToModel(dto));
}
messageAdapter.notifyDataSetChanged();
scrollToBottom();
} }
} messageAdapter.notifyDataSetChanged();
@Override scrollToBottom();
public void onFailure(@NonNull Call<List<MessageDTO>> call,
@NonNull Throwable t) {
Log.e(TAG, "Error loading messages", t);
} }
}); });
} }
//Helper function to send a message to the chat /**
* 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 //check if a chat is selected
if (activeConversationId == null) return; if (activeConversationId == null) return;
//get the message from text field //get the message from text field
String text = etMessage.getText().toString().trim(); String text = binding.etMessage.getText().toString().trim();
if (text.isEmpty()) return; if (text.isEmpty()) return;
//clear text field after sending //clear text field after sending
etMessage.setText(""); binding.etMessage.setText("");
//calls api to send the message //calls viewmodel to send the message
messageApi.sendMessage(activeConversationId, new SendMessageRequest(text)) viewModel.sendMessage(activeConversationId, new SendMessageRequest(text)).observe(getViewLifecycleOwner(), resource -> {
.enqueue(new Callback<MessageDTO>() { if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) {
@Override messageList.add(dtoToModel(resource.data));
public void onResponse(@NonNull Call<MessageDTO> call, messageAdapter.notifyItemInserted(messageList.size() - 1);
@NonNull Response<MessageDTO> response) { scrollToBottom();
if (response.isSuccessful() && response.body() != null) { loadConversations();
messageList.add(dtoToModel(response.body())); }
messageAdapter.notifyItemInserted(messageList.size() - 1); });
scrollToBottom();
loadConversations();
}
}
@Override
public void onFailure(@NonNull Call<MessageDTO> call,
@NonNull Throwable t) {
Log.e(TAG, "Send failed", t);
}
});
} }
//Helper function to open file picker when the attachment button is clicked /**
* Launches a file picker intent to select an attachment for the message.
*/
private void selectAttachment() { 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);
} }
//Helper function to show the attachment preview /**
* Displays a preview of the selected attachment in the UI.
*/
private void showAttachmentPreview(Uri uri) { private void showAttachmentPreview(Uri uri) {
pendingAttachmentUri = uri; pendingAttachmentUri = uri;
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); String fileName = getFileName(uri);
tvPreviewName.setText(fileName); binding.tvPreviewName.setText(fileName);
// If the file is an image, display a thumbnail of the image as well // 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/")) {
ivPreview.setVisibility(View.VISIBLE); binding.ivPreview.setVisibility(View.VISIBLE);
Glide.with(this).load(uri).into(ivPreview); Glide.with(this).load(uri).into(binding.ivPreview);
} else { } else {
ivPreview.setVisibility(View.GONE); binding.ivPreview.setVisibility(View.GONE);
} }
} }
//Helper function to remove the attachment /**
* Clears the current attachment selection and hides the preview UI.
*/
private void removeAttachment() { private void removeAttachment() {
pendingAttachmentUri = null; pendingAttachmentUri = null;
layoutAttachmentPreview.setVisibility(View.GONE); binding.layoutAttachmentPreview.setVisibility(View.GONE);
} }
//Helper function to get the file name from the uri to display in attachment preview /**
* Show the display name of the file from its Uri.
*/
private String getFileName(Uri uri) { private String getFileName(Uri uri) {
String result = null; String result = null;
if (uri.getScheme().equals("content")) { if (uri.getScheme().equals("content")) {
@@ -379,15 +352,40 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis
return result; return result;
} }
//Helper function to send the message with attachment /**
* 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;
String text = binding.etMessage.getText().toString().trim();
binding.etMessage.setText("");
removeAttachment();
//TODO: send the message with attachment when backend is done try {
Log.d(TAG, "Send with attachment happening"); File file = FileUtils.getFileFromUri(requireContext(), uri);
if (file == null) return;
String mimeType = requireContext().getContentResolver().getType(uri);
RequestBody requestFile = RequestBody.create(file, MediaType.parse(mimeType != null ? mimeType : "application/octet-stream"));
MultipartBody.Part filePart = MultipartBody.Part.createFormData("file", file.getName(), requestFile);
RequestBody contentPart = RequestBody.create(text, MediaType.parse("text/plain"));
viewModel.sendMessageWithAttachment(activeConversationId, contentPart, filePart).observe(getViewLifecycleOwner(), resource -> {
if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) {
messageList.add(dtoToModel(resource.data));
messageAdapter.notifyItemInserted(messageList.size() - 1);
scrollToBottom();
loadConversations();
}
});
} catch (Exception e) {
Log.e(TAG, "Error sending message with attachment", e);
}
} }
// When a message is received updates the chat preview /**
* Callback triggered when a new message is received via the WebSocket.
*/
@Override @Override
public void onMessageReceived(MessageDTO dto) { 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 there is no active selected conversation or the message received is for another chat, then just update the preview of last message
@@ -401,81 +399,102 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis
//else add the message to the active chat if it's not from the current user //else add the message to the active chat if it's not from the current user
messageList.add(dtoToModel(dto)); messageList.add(dtoToModel(dto));
messageAdapter.notifyItemInserted(messageList.size() - 1); requireActivity().runOnUiThread(() -> {
scrollToBottom(); messageAdapter.notifyItemInserted(messageList.size() - 1);
scrollToBottom();
});
} }
// When a new conversation is added, updates the chat preview /**
* Callback triggered when a conversation is created or updated via the WebSocket.
*/
@Override @Override
public void onConversationUpdated(ConversationDTO dto) { public void onConversationUpdated(ConversationDTO dto) {
boolean updated = false; requireActivity().runOnUiThread(() -> {
String name = customerNames.getOrDefault( boolean updated = false;
dto.getCustomerId(), "Customer #" + dto.getCustomerId()); String name = customerNames.getOrDefault(
dto.getCustomerId(), "Customer #" + dto.getCustomerId());
for (int i = 0; i < chatList.size(); i++) { for (int i = 0; i < chatList.size(); i++) {
Chat existing = chatList.get(i); Chat existing = chatList.get(i);
if (existing.getChatId().equals(String.valueOf(dto.getId()))) { if (existing.getChatId().equals(String.valueOf(dto.getId()))) {
chatList.set(i, new Chat( 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()), String.valueOf(dto.getId()),
name, name,
dto.getLastMessage(), dto.getLastMessage(),
dto.getCustomerId(), dto.getCustomerId(),
dto.getStaffId() dto.getStaffId()
)); ));
chatAdapter.notifyItemChanged(i); chatAdapter.notifyItemInserted(0);
updated = true;
break;
} }
}
if (!updated) { if (activeConversationId != null && activeConversationId.equals(dto.getId())) {
chatList.add(0, new Chat( setConversationActive(true);
String.valueOf(dto.getId()), binding.tvChatTitle.setText(name);
name, }
dto.getLastMessage(), });
dto.getCustomerId(),
dto.getStaffId()
));
chatAdapter.notifyItemInserted(0);
}
if (activeConversationId != null && activeConversationId.equals(dto.getId())) {
setConversationActive(true);
tvChatTitle.setText(name);
}
} }
/**
* Callback triggered when the WebSocket connection is successfully opened.
*/
@Override @Override
public void onSocketOpened() { public void onSocketOpened() {
if (!isAdded()) { if (!isAdded()) {
return; return;
} }
loadConversations(); requireActivity().runOnUiThread(() -> {
if (activeConversationId != null) { loadConversations();
loadMessageHistory(activeConversationId); if (activeConversationId != null) {
} loadMessageHistory(activeConversationId);
}
});
} }
/**
* Callback triggered when the WebSocket connection is closed.
*/
@Override @Override
public void onSocketClosed() { public void onSocketClosed() {
if (!isAdded()) { if (!isAdded()) {
return; return;
} }
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;
} }
loadConversations(); requireActivity().runOnUiThread(() -> {
if (activeConversationId != null) { loadConversations();
loadMessageHistory(activeConversationId); if (activeConversationId != null) {
} loadMessageHistory(activeConversationId);
}
});
} }
// Helper function to convert DTO to message /**
* Converts a MessageDTO into a Message object.
*/
private Message dtoToModel(MessageDTO dto) { private Message dtoToModel(MessageDTO dto) {
Message m = new Message(); Message m = new Message();
m.setId(dto.getId()); m.setId(dto.getId());
@@ -490,63 +509,74 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis
return m; return m;
} }
//Helper function to scroll to bottom of the chat /**
* Scrolls the message history RecyclerView to the most recent message.
*/
private void scrollToBottom() { private void scrollToBottom() {
if (!messageList.isEmpty()) { if (!messageList.isEmpty()) {
rvMessages.post(() -> binding.rvMessages.post(() ->
rvMessages.smoothScrollToPosition(messageList.size() - 1)); binding.rvMessages.smoothScrollToPosition(messageList.size() - 1));
} }
} }
// Helper function to update the chat preview last message /**
* Updates the preview snippet of the last message for a specific conversation in the drawer.
*/
private void updateConversationPreview(Long conversationId, String lastMessage) { private void updateConversationPreview(Long conversationId, String lastMessage) {
if (conversationId == null) { if (conversationId == null) {
return; return;
} }
for (int i = 0; i < chatList.size(); i++) { requireActivity().runOnUiThread(() -> {
Chat existing = chatList.get(i); for (int i = 0; i < chatList.size(); i++) {
if (existing.getChatId().equals(String.valueOf(conversationId))) { Chat existing = chatList.get(i);
Chat updated = new Chat( if (existing.getChatId().equals(String.valueOf(conversationId))) {
existing.getChatId(), Chat updated = new Chat(
existing.getCustomerName(), existing.getChatId(),
lastMessage, existing.getCustomerName(),
existing.getCustomerId(), lastMessage,
existing.getStaffId() existing.getCustomerId(),
); existing.getStaffId()
chatList.set(i, updated); );
chatAdapter.notifyItemChanged(i); chatList.set(i, updated);
return; chatAdapter.notifyItemChanged(i);
return;
}
} }
} });
} }
//Helper function to enable or disable the send button when there is no active chat /**
* Toggles the UI state based on whether a conversation is currently selected.
*/
private void setConversationActive(boolean active) { private void setConversationActive(boolean active) {
btnSend.setEnabled(active); binding.btnSend.setEnabled(active);
etMessage.setEnabled(active); binding.etMessage.setEnabled(active);
btnAttach.setEnabled(active); binding.btnAttach.setEnabled(active);
if (!active) { if (!active) {
activeConversationId = null; activeConversationId = null;
ChatNotificationService.activeConversationIdInUi = null; ChatNotificationService.activeConversationIdInUi = null;
removeAttachment(); removeAttachment();
if (tvChatTitle != null) 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();
etMessage.setText(""); binding.etMessage.setText("");
etMessage.setHint("Select a chat to start messaging"); binding.etMessage.setHint("Select a chat to start messaging");
} else { } else {
etMessage.setHint("Type a message..."); binding.etMessage.setHint("Type a message...");
ChatNotificationService.activeConversationIdInUi = activeConversationId; ChatNotificationService.activeConversationIdInUi = activeConversationId;
} }
} }
// When fragment is destroyed, disconnect from websocket /**
* Disconnects the WebSocket manager when the fragment view is destroyed.
*/
@Override @Override
public void onDestroyView() { public void onDestroyView() {
super.onDestroyView(); super.onDestroyView();
binding = null;
ChatNotificationService.activeConversationIdInUi = null; ChatNotificationService.activeConversationIdInUi = null;
if (stompChatManager != null) stompChatManager.disconnect(); if (stompChatManager != null) stompChatManager.disconnect();
} }

View File

@@ -2,92 +2,67 @@ package com.example.petstoremobile.fragments;
import android.os.Bundle; import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.view.GravityCompat; import androidx.core.view.GravityCompat;
import androidx.drawerlayout.widget.DrawerLayout; import androidx.drawerlayout.widget.DrawerLayout;
import androidx.fragment.app.Fragment; import androidx.fragment.app.Fragment;
import androidx.navigation.NavController;
import androidx.navigation.fragment.NavHostFragment;
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.LinearLayout;
import com.example.petstoremobile.R; import com.example.petstoremobile.R;
import com.example.petstoremobile.api.auth.TokenManager; import com.example.petstoremobile.api.auth.TokenManager;
import com.example.petstoremobile.fragments.listfragments.PetFragment; import com.example.petstoremobile.databinding.FragmentListBinding;
import com.example.petstoremobile.fragments.listfragments.ServiceFragment;
import com.example.petstoremobile.fragments.listfragments.SupplierFragment; import javax.inject.Inject;
import com.example.petstoremobile.fragments.listfragments.AdoptionFragment;
import com.example.petstoremobile.fragments.listfragments.AppointmentFragment; import dagger.hilt.android.AndroidEntryPoint;
import com.example.petstoremobile.fragments.listfragments.InventoryFragment;
import com.example.petstoremobile.fragments.listfragments.ProductFragment;
import com.example.petstoremobile.fragments.listfragments.ProductSupplierFragment;
import com.example.petstoremobile.fragments.listfragments.PurchaseOrderFragment;
import com.example.petstoremobile.fragments.listfragments.SaleFragment;
//The Fragment for the displaying the list of entities to be viewed //The Fragment for the displaying the list of entities to be viewed
@AndroidEntryPoint
public class ListFragment extends Fragment { public class ListFragment extends Fragment {
private DrawerLayout drawerLayout; private FragmentListBinding binding;
private LinearLayout drawerPets, drawerServices, drawerSuppliers; private NavController innerNavController;
private View touchBlocker;
// Adoptions, Appointments, Inventory, Products
private LinearLayout drawerAdoptions, drawerAppointments, drawerInventory, drawerProducts, drawerProductSupplier, drawerPurchaseOrderView, drawerSale;
@Inject TokenManager tokenManager;
/**
* Inflates the fragment layout, initializes navigation drawers, and applies role-based access control.
*/
@Override @Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) { Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_list, container, false); binding = FragmentListBinding.inflate(inflater, container, false);
//get controls from the layout
drawerLayout = view.findViewById(R.id.drawerLayout);
drawerPets = view.findViewById(R.id.drawerPets);
drawerServices = view.findViewById(R.id.drawerServices);
drawerSuppliers = view.findViewById(R.id.drawerSuppliers);
drawerAdoptions = view.findViewById(R.id.drawerAdoptions);
drawerAppointments = view.findViewById(R.id.drawerAppointments);
drawerInventory = view.findViewById(R.id.drawerInventory);
drawerProducts = view.findViewById(R.id.drawerProducts);
drawerProductSupplier=view.findViewById(R.id.drawerProductSupplier);
drawerSale=view.findViewById(R.id.drawerSale);
drawerPurchaseOrderView=view.findViewById(R.id.drawerPurchaseOrderView);
// Check user role and restrict access for STAFF // Check user role and restrict access for STAFF
String role = TokenManager.getInstance(requireContext()).getRole(); String role = tokenManager.getRole();
if ("STAFF".equalsIgnoreCase(role)) { if ("STAFF".equalsIgnoreCase(role)) {
drawerSuppliers.setVisibility(View.GONE); binding.drawerSuppliers.setVisibility(View.GONE);
drawerInventory.setVisibility(View.GONE); binding.drawerInventory.setVisibility(View.GONE);
}
//needed to disable touches on the innerContainer while the drawer is open
touchBlocker = view.findViewById(R.id.touchBlocker);
//Display pets fragment by default
if (savedInstanceState == null) {
loadFragment(new PetFragment());
} }
//add Listeners to the drawer so user won't be able to interact with the innerContainer (the list fragments) //add Listeners to the drawer so user won't be able to interact with the innerContainer (the list fragments)
//while the drawer is open //while the drawer is open
drawerLayout.addDrawerListener(new DrawerLayout.DrawerListener() { binding.drawerLayout.addDrawerListener(new DrawerLayout.DrawerListener() {
//When the drawer is opened, disable touches on the background //When the drawer is opened, disable touches on the background
@Override @Override
public void onDrawerOpened(View drawerView) { public void onDrawerOpened(View drawerView) {
touchBlocker.setVisibility(View.VISIBLE); binding.touchBlocker.setVisibility(View.VISIBLE);
touchBlocker.setClickable(true); binding.touchBlocker.setClickable(true);
} }
//When the drawer is closed, enable touches again //When the drawer is closed, enable touches again
@Override @Override
public void onDrawerClosed(View drawerView) { public void onDrawerClosed(View drawerView) {
touchBlocker.setVisibility(View.GONE); binding.touchBlocker.setVisibility(View.GONE);
touchBlocker.setClickable(false); binding.touchBlocker.setClickable(false);
} }
//unused methods //unused methods
@@ -98,84 +73,53 @@ public class ListFragment extends Fragment {
}); });
// Click listeners for each drawer // Click listeners for each drawer
//Pets binding.drawerPets.setOnClickListener(v -> navigateTo(R.id.nav_pet));
drawerPets.setOnClickListener(v -> { binding.drawerServices.setOnClickListener(v -> navigateTo(R.id.nav_service));
loadFragment(new PetFragment()); binding.drawerSuppliers.setOnClickListener(v -> navigateTo(R.id.nav_supplier));
drawerLayout.closeDrawers(); binding.drawerAdoptions.setOnClickListener(v -> navigateTo(R.id.nav_adoption));
}); binding.drawerAppointments.setOnClickListener(v -> navigateTo(R.id.nav_appointment));
binding.drawerInventory.setOnClickListener(v -> navigateTo(R.id.nav_inventory));
binding.drawerProducts.setOnClickListener(v -> navigateTo(R.id.nav_product));
binding.drawerProductSupplier.setOnClickListener(v -> navigateTo(R.id.nav_product_supplier));
binding.drawerPurchaseOrderView.setOnClickListener(v -> navigateTo(R.id.nav_purchase_order));
binding.drawerSale.setOnClickListener(v -> navigateTo(R.id.nav_sale));
//Services return binding.getRoot();
drawerServices.setOnClickListener(v -> {
loadFragment(new ServiceFragment());
drawerLayout.closeDrawers();
});
//Suppliers
drawerSuppliers.setOnClickListener(v -> {
loadFragment(new SupplierFragment());
drawerLayout.closeDrawers();
});
//Adoptions
drawerAdoptions.setOnClickListener(v -> {
loadFragment(new AdoptionFragment());
drawerLayout.closeDrawers();
});
//Appointment
drawerAppointments.setOnClickListener(v -> {
loadFragment(new AppointmentFragment());
drawerLayout.closeDrawers();
});
//Inventory
drawerInventory.setOnClickListener(v -> {
loadFragment(new InventoryFragment());
drawerLayout.closeDrawers();
});
//Products
drawerProducts.setOnClickListener(v -> {
loadFragment(new ProductFragment());
drawerLayout.closeDrawers();
});
//ProductSupplier
drawerProductSupplier.setOnClickListener(v -> {
loadFragment(new ProductSupplierFragment());
drawerLayout.closeDrawers();
});
//Purchase
drawerPurchaseOrderView.setOnClickListener(v -> {
loadFragment(new PurchaseOrderFragment());
drawerLayout.closeDrawers();
});
//Sale
drawerSale.setOnClickListener(v -> {
loadFragment(new SaleFragment());
drawerLayout.closeDrawers();
});
return view;
} }
//helper function to open the drawer @Override
public void onDestroyView() {
super.onDestroyView();
binding = null;
}
/**
* Initializes the NavController for the internal fragment container.
*/
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
NavHostFragment navHostFragment = (NavHostFragment) getChildFragmentManager()
.findFragmentById(R.id.inner_nav_host_fragment);
if (navHostFragment != null) {
innerNavController = navHostFragment.getNavController();
}
}
/**
* Navigates to a specific inner destination and closes all drawers.
*/
private void navigateTo(int destinationId) {
if (innerNavController != null) {
innerNavController.navigate(destinationId);
}
binding.drawerLayout.closeDrawers();
}
/**
* Programmatically opens the navigation drawer.
*/
public void openDrawer() { public void openDrawer() {
drawerLayout.openDrawer(GravityCompat.START); binding.drawerLayout.openDrawer(GravityCompat.START);
}
// helper function to load the fragment into the display
public void loadFragment(Fragment fragment) {
getChildFragmentManager()
.beginTransaction()
.replace(R.id.inner_fragment_container, fragment)
.addToBackStack(null)
.commit();
} }
} }

View File

@@ -1,199 +1,109 @@
package com.example.petstoremobile.fragments; package com.example.petstoremobile.fragments;
import android.Manifest;
import android.app.Activity;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.net.Uri; import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import androidx.activity.result.ActivityResultLauncher; import androidx.annotation.NonNull;
import androidx.activity.result.contract.ActivityResultContracts; import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AlertDialog;
import androidx.core.content.ContextCompat;
import androidx.core.content.FileProvider;
import androidx.fragment.app.Fragment; import androidx.fragment.app.Fragment;
import androidx.lifecycle.ViewModelProvider;
import android.provider.MediaStore;
import android.text.InputType;
import android.util.Log; 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.Button;
import android.widget.EditText; import android.widget.EditText;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast; import android.widget.Toast;
import com.bumptech.glide.Glide;
import com.bumptech.glide.load.engine.DiskCacheStrategy;
import com.bumptech.glide.load.model.GlideUrl;
import com.bumptech.glide.load.model.LazyHeaders;
import com.example.petstoremobile.R; import com.example.petstoremobile.R;
import com.example.petstoremobile.activities.MainActivity; import com.example.petstoremobile.activities.MainActivity;
import com.example.petstoremobile.api.RetrofitClient;
import com.example.petstoremobile.api.auth.AuthApi; import com.example.petstoremobile.api.auth.AuthApi;
import com.example.petstoremobile.api.auth.TokenManager; import com.example.petstoremobile.api.auth.TokenManager;
import com.example.petstoremobile.dtos.ErrorResponse; import com.example.petstoremobile.databinding.FragmentProfileBinding;
import com.example.petstoremobile.dtos.UserDTO; import com.example.petstoremobile.dtos.UserDTO;
import com.example.petstoremobile.services.ChatNotificationService; import com.example.petstoremobile.services.ChatNotificationService;
import com.example.petstoremobile.utils.FileUtils;
import com.example.petstoremobile.utils.GlideUtils;
import com.example.petstoremobile.utils.ImagePickerHelper;
import com.example.petstoremobile.utils.InputValidator; import com.example.petstoremobile.utils.InputValidator;
import com.google.gson.Gson; import com.example.petstoremobile.utils.Resource;
import com.example.petstoremobile.utils.UIUtils;
import com.example.petstoremobile.viewmodels.AuthViewModel;
import java.io.File; import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.List;
import java.util.Map; import java.util.Map;
import javax.inject.Inject;
import javax.inject.Named;
import dagger.hilt.android.AndroidEntryPoint;
import okhttp3.MediaType; import okhttp3.MediaType;
import okhttp3.MultipartBody; import okhttp3.MultipartBody;
import okhttp3.RequestBody; import okhttp3.RequestBody;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
/**
* Fragment that displays and allows editing of the user's profile information.
*/
@AndroidEntryPoint
public class ProfileFragment extends Fragment { public class ProfileFragment extends Fragment {
//initialize the view/controls private FragmentProfileBinding binding;
private ImageView imgProfile;
private TextView tvProfileName, tvProfileEmail, tvProfilePhone, tvProfileRole;
private Uri photoUri;
private UserDTO currentUser; private UserDTO currentUser;
private AuthViewModel viewModel;
private boolean hasImage = false; private boolean hasImage = false;
//Initialize the launchers for camera and gallery @Inject TokenManager tokenManager;
private ActivityResultLauncher<Intent> galleryLauncher; @Inject @Named("baseUrl") String baseUrl;
private ActivityResultLauncher<Uri> cameraLauncher;
private ActivityResultLauncher<String> permissionLauncher;
//Called when the fragment is created, sets up the launchers is set profile image private ImagePickerHelper imagePickerHelper;
/**
* Initializes activity launchers and the ImagePickerHelper for camera and gallary.
*/
@Override @Override
public void onCreate(Bundle savedInstanceState) { public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
viewModel = new ViewModelProvider(this).get(AuthViewModel.class);
// Launcher to open gallery to select profile image imagePickerHelper = new ImagePickerHelper(this, "profile_photo.jpg", new ImagePickerHelper.ImagePickerListener() {
galleryLauncher = registerForActivityResult( @Override
//open gallery public void onImagePicked(Uri uri) {
new ActivityResultContracts.StartActivityForResult(), uploadAvatar(uri);
result -> { }
//if the user selects an image and its not null
if (result.getResultCode() == Activity.RESULT_OK
&& result.getData() != null) {
//get the selected image and set the image to the profile
Uri selectedImage = result.getData().getData();
uploadAvatar(selectedImage);
}
}
);
// Launcher for camera to open and capture profile image @Override
cameraLauncher = registerForActivityResult( public void onImageRemoved() {
//open camera deleteAvatar();
new ActivityResultContracts.TakePicture(), }
success -> { });
//if a photo is taken set the image profile to it otherwise do nothing
if (success) {
uploadAvatar(photoUri);
}
}
);
// Launcher to request camera permission
permissionLauncher = registerForActivityResult(
//ask user for camera permission
new ActivityResultContracts.RequestPermission(),
granted -> {
//if the permission is granted launch the camera
if (granted) {
launchCamera();
}
else {
//if the permission is denied then tell the user to grant it
new AlertDialog.Builder(requireContext())
.setTitle("Permission Permission Required")
.setMessage("Please grant camera permission to use this feature")
.setPositiveButton("Open Settings", (dialog, which) ->{
//open the settings page to grant the permission when they click open settings
Intent intent = new Intent(android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
intent.setData(Uri.fromParts("package", requireContext().getPackageName(), null));
startActivity(intent);
})
//close the dialog when the user clicks cancel
.setNegativeButton("Cancel", null)
.show();
}
}
);
} }
/**
* Inflates the fragment layout and sets up listeners for profile.
*/
@Override @Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) { Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_profile, container, false); binding = FragmentProfileBinding.inflate(inflater, container, false);
//get all the controls from the view
imgProfile = view.findViewById(R.id.imgProfile);
tvProfileName = view.findViewById(R.id.tvProfileName);
tvProfileEmail = view.findViewById(R.id.tvProfileEmail);
tvProfilePhone = view.findViewById(R.id.tvProfilePhone);
tvProfileRole = view.findViewById(R.id.tvProfileRole);
Button btnChangePhoto = view.findViewById(R.id.btnChangePhoto);
Button btnEditEmail = view.findViewById(R.id.btnEditEmail);
Button btnEditPhone = view.findViewById(R.id.btnEditPhone);
Button btnLogout = view.findViewById(R.id.btnLogout);
//Load Profile Data from backend //Load Profile Data from backend
loadProfileData(); loadProfileData();
//Set up listeners for the buttons //Set up listeners for the buttons
//Change photo button //Change photo button
btnChangePhoto.setOnClickListener(v -> { binding.btnChangePhoto.setOnClickListener(v -> {
List<String> options = new ArrayList<>(); imagePickerHelper.showImagePickerDialog("Change Profile Photo", hasImage);
options.add("Take Photo");
options.add("Choose from Gallery");
if (hasImage) {
options.add("Remove Photo");
}
//Show alert dialog to user to select from gallery or camera
new AlertDialog.Builder(requireContext())
.setTitle("Change Profile Photo")
//set the options for the alert dialog
.setItems(options.toArray(new String[0]), (dialog, which) -> {
String selected = options.get(which);
if (selected.equals("Take Photo")) {
// Choose Camera
//Checks if the user has granted the camera permission already
if (ContextCompat.checkSelfPermission(requireContext(), Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED) {
//if the permission is already granted then launch the camera
launchCamera();
} else {
//otherwise request the permission
permissionLauncher.launch(Manifest.permission.CAMERA);
}
} else if (selected.equals("Choose from Gallery")) {
// Choose Gallery
Intent intent = new Intent(Intent.ACTION_PICK,
MediaStore.Images.Media.EXTERNAL_CONTENT_URI);
galleryLauncher.launch(intent);
} else if (selected.equals("Remove Photo")) {
deleteAvatar();
}
})
.show();
}); });
//Edit email button //Edit email button
//When clicked open a dialog to change email //When clicked open a dialog to change email
btnEditEmail.setOnClickListener(v -> { binding.btnEditEmail.setOnClickListener(v -> {
//Make a text field for the user to enter the new email //Make a text field for the user to enter the new email
EditText input = new EditText(requireContext()); EditText input = new EditText(requireContext());
input.setPadding(30,30,30,30); input.setPadding(30,30,30,30);
input.setText(tvProfileEmail.getText().toString()); input.setText(binding.tvProfileEmail.getText().toString());
//set input type to email //set input type to email
input.setInputType(android.text.InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS); input.setInputType(android.text.InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS);
@@ -216,18 +126,17 @@ public class ProfileFragment extends Fragment {
//Edit phone button //Edit phone button
//When clicked open a dialog to change phone //When clicked open a dialog to change phone
btnEditPhone.setOnClickListener(v -> { binding.btnEditPhone.setOnClickListener(v -> {
//Make a text field for the user to enter the new email //Make a text field for the user to enter the new email
EditText input = new EditText(requireContext()); EditText input = new EditText(requireContext());
input.setPadding(30,30,30,30); input.setPadding(30,30,30,30);
input.setText(tvProfilePhone.getText().toString()); input.setText(binding.tvProfilePhone.getText().toString());
//set input type to phone number //set input type to phone number
input.setInputType(InputType.TYPE_CLASS_PHONE); input.setInputType(android.view.inputmethod.EditorInfo.TYPE_CLASS_PHONE);
//add canada phone number formatting to input (XXX) XXX-XXXX //add canada phone number formatting to input
input.addTextChangedListener(new android.telephony.PhoneNumberFormattingTextWatcher("CA")); UIUtils.formatPhoneInput(input);
input.setFilters(new android.text.InputFilter[]{new android.text.InputFilter.LengthFilter(14)});
//Show alert dialog to user to enter new phone //Show alert dialog to user to enter new phone
@@ -246,107 +155,71 @@ public class ProfileFragment extends Fragment {
}); });
//Logout button //Logout button
btnLogout.setOnClickListener(v -> { binding.btnLogout.setOnClickListener(v -> {
// Stop notification service before logging out so notifications stop // Stop notification service before logging out so notifications stop
Intent serviceIntent = new Intent(requireContext(), ChatNotificationService.class); android.content.Intent serviceIntent = new android.content.Intent(requireContext(), ChatNotificationService.class);
requireContext().stopService(serviceIntent); requireContext().stopService(serviceIntent);
TokenManager.getInstance(requireContext()).clearLoginData(); // clear the token for next login tokenManager.clearLoginData(); // clear the token for next login
//get the intent to the main activity and clear the back stack so the back button won't allow the user to go back to the previous screen //get the intent to the main activity and clear the back stack so the back button won't allow the user to go back to the previous screen
Intent intent = new Intent(getActivity(), MainActivity.class); android.content.Intent intent = new android.content.Intent(getActivity(), MainActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK); intent.addFlags(android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP | android.content.Intent.FLAG_ACTIVITY_NEW_TASK);
//start the activity to go to login page and finish the current activity //start the activity to go to login page and finish the current activity
startActivity(intent); startActivity(intent);
requireActivity().finish(); requireActivity().finish();
}); });
return view; return binding.getRoot();
} }
//Helper function create a file in the cache directory to store the photo in then launch the camera to capture the photo @Override
private void launchCamera() { public void onDestroyView() {
//create a file in the cache directory to store the photo in super.onDestroyView();
File photoFile = new File(requireContext().getCacheDir(), "profile_photo.jpg"); binding = null;
//get the uri for the file made
photoUri = FileProvider.getUriForFile(requireContext(), requireContext().getPackageName() + ".fileprovider", photoFile);
//launch the camera to capture the photo and save the photo to photoUri
cameraLauncher.launch(photoUri);
} }
//Helper function to call the backend to get profile data and load it to the view /**
* Fetches current user profile data from the API and then updates the UI.
*/
private void loadProfileData() { private void loadProfileData() {
AuthApi authApi = RetrofitClient.getAuthApi(requireContext()); viewModel.getMe().observe(getViewLifecycleOwner(), resource -> {
if (resource == null) return;
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
currentUser = resource.data;
authApi.getMe().enqueue(new Callback<UserDTO>() { //set the user data to the view
@Override binding.tvProfileName.setText(currentUser.getFullName());
public void onResponse(Call<UserDTO> call, Response<UserDTO> response) { binding.tvProfileEmail.setText(currentUser.getEmail());
//if the response is successful and the body is not null then set the user to the view binding.tvProfilePhone.setText(currentUser.getPhone());
if (response.isSuccessful() && response.body() != null) { binding.tvProfileRole.setText(currentUser.getRole());
currentUser = response.body();
//set the user data to the view // get the avatar endpoint to load profile image and the token for authorization
tvProfileName.setText(currentUser.getFullName()); String avatarUrl = baseUrl + AuthApi.AVATAR_FILE_PATH;
tvProfileEmail.setText(currentUser.getEmail()); String token = tokenManager.getToken();
tvProfilePhone.setText(currentUser.getPhone());
tvProfileRole.setText(currentUser.getRole());
// get the avatar endpoint to load profile image and the token for authorization GlideUtils.loadImageWithToken(requireContext(), binding.imgProfile, avatarUrl, token, R.drawable.placeholder, new GlideUtils.ImageLoadListener() {
String avatarUrl = RetrofitClient.BASE_URL + AuthApi.AVATAR_FILE_PATH; @Override
String token = TokenManager.getInstance(requireContext()).getToken(); public void onResourceReady() {
hasImage = true;
if (token != null) {
// Create GlideUrl with token to fetch the image
GlideUrl glideUrl = new GlideUrl(avatarUrl, new LazyHeaders.Builder()
.addHeader("Authorization", "Bearer " + token)
.build());
// Load image using Glide
Glide.with(ProfileFragment.this)
.load(glideUrl)
.diskCacheStrategy(DiskCacheStrategy.NONE)
.skipMemoryCache(true)
.placeholder(R.drawable.placeholder)
.error(R.drawable.placeholder)
.listener(new com.bumptech.glide.request.RequestListener<android.graphics.drawable.Drawable>() {
@Override
public boolean onLoadFailed(@androidx.annotation.Nullable com.bumptech.glide.load.engine.GlideException e, Object model, com.bumptech.glide.request.target.Target<android.graphics.drawable.Drawable> target, boolean isFirstResource) {
hasImage = false;
return false;
}
@Override
public boolean onResourceReady(android.graphics.drawable.Drawable resource, Object model, com.bumptech.glide.request.target.Target<android.graphics.drawable.Drawable> target, com.bumptech.glide.load.DataSource dataSource, boolean isFirstResource) {
hasImage = true;
return false;
}
})
.into(imgProfile);
} else {
// load placeholder image if token is null
hasImage = false;
Glide.with(ProfileFragment.this)
.load(R.drawable.placeholder)
.into(imgProfile);
} }
}
else {
Log.e("onResponse: ", response.message());
Toast.makeText(getContext(), "Failed to load profile: ", Toast.LENGTH_SHORT).show();
}
}
@Override @Override
public void onFailure(Call<UserDTO> call, Throwable t) { public void onLoadFailed() {
Log.e("PROFILE", "onFailure: " + t.getMessage()); hasImage = false;
Toast.makeText(getContext(), "Network error: could not load profile", Toast.LENGTH_SHORT).show(); }
});
} else if (resource.status == Resource.Status.ERROR) {
Toast.makeText(getContext(), "Failed to load profile: " + resource.message, Toast.LENGTH_SHORT).show();
} }
}); });
} }
//Helper function to call the backend to upload a profile image /**
* Uploads the selected or captured image as the user's new avatar.
*/
private void uploadAvatar(Uri uri) { private void uploadAvatar(Uri uri) {
try { try {
File file = getFileFromUri(uri); File file = FileUtils.getFileFromUri(requireContext(), uri);
if (file == null) return; if (file == null) return;
// Create RequestBody for file upload // Create RequestBody for file upload
@@ -354,24 +227,15 @@ public class ProfileFragment extends Fragment {
MultipartBody.Part body = MultipartBody.Part.createFormData("avatar", file.getName(), requestFile); MultipartBody.Part body = MultipartBody.Part.createFormData("avatar", file.getName(), requestFile);
//Call the backend to upload the avatar //Call the backend to upload the avatar
AuthApi authApi = RetrofitClient.getAuthApi(requireContext()); viewModel.uploadAvatar(body).observe(getViewLifecycleOwner(), resource -> {
authApi.uploadAvatar(body).enqueue(new Callback<UserDTO>() { if (resource == null) return;
@Override if (resource.status == Resource.Status.SUCCESS) {
public void onResponse(Call<UserDTO> call, Response<UserDTO> response) { currentUser = resource.data;
if (response.isSuccessful() && response.body() != null) { Toast.makeText(getContext(), "Avatar updated successfully", Toast.LENGTH_SHORT).show();
currentUser = response.body(); // Reload image after successful upload
Toast.makeText(requireContext(), "Avatar updated successfully", Toast.LENGTH_SHORT).show(); loadProfileData();
// Reload image after successful upload } else if (resource.status == Resource.Status.ERROR) {
loadProfileData(); Toast.makeText(getContext(), "Upload failed: " + resource.message, Toast.LENGTH_SHORT).show();
} else {
Toast.makeText(requireContext(), "Failed to upload avatar", Toast.LENGTH_SHORT).show();
}
}
@Override
public void onFailure(Call<UserDTO> call, Throwable t) {
Log.e("UPLOAD_AVATAR", "Failure: " + t.getMessage());
Toast.makeText(requireContext(), "Network error", Toast.LENGTH_SHORT).show();
} }
}); });
} catch (Exception e) { } catch (Exception e) {
@@ -379,80 +243,39 @@ public class ProfileFragment extends Fragment {
} }
} }
/**
* Sends a request to the API to delete the current user's avatar image.
*/
private void deleteAvatar() { private void deleteAvatar() {
AuthApi authApi = RetrofitClient.getAuthApi(requireContext()); viewModel.deleteAvatar().observe(getViewLifecycleOwner(), resource -> {
authApi.deleteAvatar().enqueue(new Callback<Void>() { if (resource == null) return;
@Override if (resource.status == Resource.Status.SUCCESS) {
public void onResponse(Call<Void> call, Response<Void> response) { hasImage = false;
if (response.isSuccessful()) { binding.imgProfile.setImageResource(R.drawable.placeholder);
Toast.makeText(requireContext(), "Avatar removed successfully", Toast.LENGTH_SHORT).show(); Toast.makeText(getContext(), "Avatar removed successfully", Toast.LENGTH_SHORT).show();
hasImage = false; } else if (resource.status == Resource.Status.ERROR) {
imgProfile.setImageResource(R.drawable.placeholder); Toast.makeText(getContext(), "Removal failed: " + resource.message, Toast.LENGTH_SHORT).show();
} else {
Toast.makeText(requireContext(), "Failed to remove avatar", Toast.LENGTH_SHORT).show();
}
}
@Override
public void onFailure(Call<Void> call, Throwable t) {
Log.e("DELETE_AVATAR", "Failure: " + t.getMessage());
Toast.makeText(requireContext(), "Network error", Toast.LENGTH_SHORT).show();
} }
}); });
} }
// Helper function to create a temporary File object from a Uri for uploading the avatar /**
private File getFileFromUri(Uri uri) { * Updates a specific profile field (like email or phone) by sending a request to the API.
try { */
InputStream inputStream = requireContext().getContentResolver().openInputStream(uri);
File tempFile = new File(requireContext().getCacheDir(), "upload_avatar.jpg");
FileOutputStream outputStream = new FileOutputStream(tempFile);
byte[] buffer = new byte[1024];
int length;
while ((length = inputStream.read(buffer)) > 0) {
outputStream.write(buffer, 0, length);
}
outputStream.close();
inputStream.close();
return tempFile;
} catch (Exception e) {
Log.e("FILE_UTILS", "Error creating temp file", e);
return null;
}
}
//Helper function to update a profile field in the backend
private void updateProfileField(String fieldName, String value) { private void updateProfileField(String fieldName, String value) {
AuthApi authApi = RetrofitClient.getAuthApi(requireContext());
Map<String, String> updates = new HashMap<>(); Map<String, String> updates = new HashMap<>();
updates.put(fieldName, value); updates.put(fieldName, value);
authApi.updateMe(updates).enqueue(new Callback<UserDTO>() { viewModel.updateMe(updates).observe(getViewLifecycleOwner(), resource -> {
@Override if (resource == null) return;
public void onResponse(Call<UserDTO> call, Response<UserDTO> response) { if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
if (response.isSuccessful() && response.body() != null) { currentUser = resource.data;
currentUser = response.body(); Toast.makeText(getContext(), "Profile updated successfully", Toast.LENGTH_SHORT).show();
// Update the view with the new data from backend // Update the view with the new data from backend
tvProfileEmail.setText(currentUser.getEmail()); binding.tvProfileEmail.setText(currentUser.getEmail());
tvProfilePhone.setText(currentUser.getPhone()); binding.tvProfilePhone.setText(currentUser.getPhone());
Toast.makeText(requireContext(), "Profile updated successfully", Toast.LENGTH_SHORT).show(); } else if (resource.status == Resource.Status.ERROR) {
} else { Toast.makeText(getContext(), "Update failed: " + resource.message, Toast.LENGTH_SHORT).show();
try {
String errorJson = response.errorBody().string();
ErrorResponse errorResponse = new Gson().fromJson(errorJson, ErrorResponse.class);
String errorMessage = errorResponse.getMessage();
Toast.makeText(requireContext(), errorMessage, Toast.LENGTH_LONG).show();
} catch (Exception e) {
Log.e("UPDATE_PROFILE", "Error parsing error body", e);
Toast.makeText(requireContext(), "Failed to update profile", Toast.LENGTH_SHORT).show();
}
}
}
@Override
public void onFailure(Call<UserDTO> call, Throwable t) {
Log.e("UPDATE_PROFILE", "Failure: " + t.getMessage());
Toast.makeText(requireContext(), "Network error", Toast.LENGTH_SHORT).show();
} }
}); });
} }

View File

@@ -1,141 +1,252 @@
package com.example.petstoremobile.fragments.listfragments; package com.example.petstoremobile.fragments.listfragments;
import android.graphics.Color;
import android.os.Bundle; import android.os.Bundle;
import android.text.*;
import android.util.Log; import android.util.Log;
import android.view.*; import android.view.*;
import android.widget.*; import android.widget.*;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment; import androidx.fragment.app.Fragment;
import androidx.lifecycle.ViewModelProvider;
import androidx.navigation.fragment.NavHostFragment;
import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
import com.example.petstoremobile.R; import com.example.petstoremobile.R;
import com.example.petstoremobile.adapters.AdoptionAdapter; import com.example.petstoremobile.adapters.AdoptionAdapter;
import com.example.petstoremobile.api.AdoptionApi; import com.example.petstoremobile.databinding.FragmentAdoptionBinding;
import com.example.petstoremobile.api.RetrofitClient;
import com.example.petstoremobile.dtos.AdoptionDTO; import com.example.petstoremobile.dtos.AdoptionDTO;
import com.example.petstoremobile.dtos.PageResponse;
import com.example.petstoremobile.fragments.ListFragment; import com.example.petstoremobile.fragments.ListFragment;
import com.example.petstoremobile.fragments.listfragments.detailfragments.AdoptionDetailFragment; import com.example.petstoremobile.viewmodels.AdoptionViewModel;
import com.google.android.material.floatingactionbutton.FloatingActionButton; import com.example.petstoremobile.utils.EventDecorator;
import com.prolificinteractive.materialcalendarview.CalendarDay;
import com.prolificinteractive.materialcalendarview.CalendarMode;
import com.prolificinteractive.materialcalendarview.MaterialCalendarView;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.*; import java.util.*;
import retrofit2.*;
import dagger.hilt.android.AndroidEntryPoint;
@AndroidEntryPoint
public class AdoptionFragment extends Fragment implements AdoptionAdapter.OnAdoptionClickListener { public class AdoptionFragment extends Fragment implements AdoptionAdapter.OnAdoptionClickListener {
private FragmentAdoptionBinding binding;
private List<AdoptionDTO> adoptionList = new ArrayList<>(); private List<AdoptionDTO> adoptionList = new ArrayList<>();
private List<AdoptionDTO> filteredList = new ArrayList<>(); private List<AdoptionDTO> filteredList = new ArrayList<>();
private AdoptionAdapter adapter; private AdoptionAdapter adapter;
private AdoptionApi api; private AdoptionViewModel viewModel;
private SwipeRefreshLayout swipeRefresh; private CalendarDay selectedCalendarDay;
private EditText etSearch; private boolean isMonthMode = false;
private ImageButton hamburger; private final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd", Locale.getDefault());
/**
* Initializes the fragment and its ViewModel.
*/
@Override @Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
viewModel = new ViewModelProvider(this).get(AdoptionViewModel.class);
}
/**
* Sets up the fragment's UI components, including RecyclerView, Search, SwipeRefresh, and Calendar.
*/
@Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) { Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_adoption, container, false); binding = FragmentAdoptionBinding.inflate(inflater, container, false);
api = RetrofitClient.getAdoptionApi(requireContext()); setupRecyclerView();
hamburger = view.findViewById(R.id.btnHamburgerAdoption); setupSearch();
setupSwipeRefresh();
setupRecyclerView(view); setupCalendar();
setupSearch(view);
setupSwipeRefresh(view);
loadAdoptions(); loadAdoptions();
FloatingActionButton fab = view.findViewById(R.id.fabAddAdoption); binding.fabAddAdoption.setOnClickListener(v -> openDetail(-1));
fab.setOnClickListener(v -> openDetail(-1));
hamburger.setOnClickListener(v -> { binding.btnHamburgerAdoption.setOnClickListener(v -> {
ListFragment lf = (ListFragment) getParentFragment(); Fragment parent = getParentFragment();
if (lf != null) lf.openDrawer(); if (parent != null) {
Fragment grandParent = parent.getParentFragment();
if (grandParent instanceof ListFragment) {
((ListFragment) grandParent).openDrawer();
}
}
}); });
return view; binding.btnToggleCalendarModeAdoption.setOnClickListener(v -> toggleCalendarMode());
return binding.getRoot();
} }
private void setupRecyclerView(View view) { @Override
RecyclerView rv = view.findViewById(R.id.recyclerViewAdoptions); public void onDestroyView() {
super.onDestroyView();
binding = null;
}
/**
* Toggles the calendar display between week and month modes.
*/
private void toggleCalendarMode() {
isMonthMode = !isMonthMode;
binding.calendarViewAdoption.state().edit()
.setCalendarDisplayMode(isMonthMode ? CalendarMode.MONTHS : CalendarMode.WEEKS)
.commit();
}
/**
* Sets up the date selection listener for the calendar.
*/
private void setupCalendar() {
binding.calendarViewAdoption.setOnDateChangedListener((widget, date, selected) -> {
if (selected) {
if (date.equals(selectedCalendarDay)) {
selectedCalendarDay = null;
binding.calendarViewAdoption.clearSelection();
} else {
selectedCalendarDay = date;
}
} else {
selectedCalendarDay = null;
}
filter(binding.etSearchAdoption.getText().toString());
});
}
/**
* Updates the calendar decorators to highlight days with adoptions.
*/
private void updateCalendarDecorators() {
HashSet<CalendarDay> datesWithAdoptions = new HashSet<>();
for (AdoptionDTO adoption : adoptionList) {
try {
if (adoption.getAdoptionDate() != null) {
Date date = dateFormat.parse(adoption.getAdoptionDate());
if (date != null) {
Calendar cal = Calendar.getInstance();
cal.setTime(date);
datesWithAdoptions.add(CalendarDay.from(cal.get(Calendar.YEAR), cal.get(Calendar.MONTH) + 1, cal.get(Calendar.DAY_OF_MONTH)));
}
}
} catch (ParseException e) {
Log.e("AdoptionFragment", "Error parsing date: " + adoption.getAdoptionDate());
}
}
binding.calendarViewAdoption.removeDecorators();
binding.calendarViewAdoption.addDecorator(new EventDecorator(Color.RED, datesWithAdoptions));
}
/**
* Initializes the RecyclerView for displaying adoptions.
*/
private void setupRecyclerView() {
adapter = new AdoptionAdapter(filteredList, this); adapter = new AdoptionAdapter(filteredList, this);
rv.setLayoutManager(new LinearLayoutManager(getContext())); binding.recyclerViewAdoptions.setLayoutManager(new LinearLayoutManager(getContext()));
rv.setAdapter(adapter); binding.recyclerViewAdoptions.setAdapter(adapter);
} }
private void setupSearch(View view) { /**
etSearch = view.findViewById(R.id.etSearchAdoption); * Sets up the search bar for filtering
etSearch.addTextChangedListener(new TextWatcher() { */
private void setupSearch() {
binding.etSearchAdoption.addTextChangedListener(new android.text.TextWatcher() {
public void beforeTextChanged(CharSequence s, int a, int b, int c) {} public void beforeTextChanged(CharSequence s, int a, int b, int c) {}
public void afterTextChanged(Editable s) {} public void afterTextChanged(android.text.Editable s) {}
public void onTextChanged(CharSequence s, int a, int b, int c) { public void onTextChanged(CharSequence s, int a, int b, int c) {
filter(s.toString()); filter(s.toString());
} }
}); });
} }
private void setupSwipeRefresh(View view) { /**
swipeRefresh = view.findViewById(R.id.swipeRefreshAdoption); * Sets up the SwipeRefreshLayout to reload adoption data.
swipeRefresh.setOnRefreshListener(this::loadAdoptions); */
private void setupSwipeRefresh() {
binding.swipeRefreshAdoption.setOnRefreshListener(this::loadAdoptions);
} }
/**
* Filters the adoption list based on search query and selected calendar date.
*/
private void filter(String query) { private void filter(String query) {
filteredList.clear(); filteredList.clear();
if (query.isEmpty()) { String lowerQuery = query.toLowerCase();
filteredList.addAll(adoptionList);
} else { String selectedDateString = null;
String lower = query.toLowerCase(); if (selectedCalendarDay != null) {
for (AdoptionDTO a : adoptionList) { selectedDateString = String.format(Locale.getDefault(), "%04d-%02d-%02d",
if ((a.getCustomerName() != null && a.getCustomerName().toLowerCase().contains(lower)) selectedCalendarDay.getYear(), selectedCalendarDay.getMonth(), selectedCalendarDay.getDay());
|| (a.getPetName() != null && a.getPetName().toLowerCase().contains(lower)) }
|| (a.getAdoptionStatus() != null && a.getAdoptionStatus().toLowerCase().contains(lower))) {
filteredList.add(a); for (AdoptionDTO a : adoptionList) {
} boolean matchesSearch = query.isEmpty() ||
(a.getCustomerName() != null && a.getCustomerName().toLowerCase().contains(lowerQuery)) ||
(a.getPetName() != null && a.getPetName().toLowerCase().contains(lowerQuery)) ||
(a.getAdoptionStatus() != null && a.getAdoptionStatus().toLowerCase().contains(lowerQuery));
boolean matchesDate = (selectedDateString == null) ||
(a.getAdoptionDate() != null && a.getAdoptionDate().startsWith(selectedDateString));
if (matchesSearch && matchesDate) {
filteredList.add(a);
} }
} }
adapter.notifyDataSetChanged(); adapter.notifyDataSetChanged();
} }
/**
* Fetches the adoption list from the server through the ViewModel.
*/
private void loadAdoptions() { private void loadAdoptions() {
if (swipeRefresh != null) swipeRefresh.setRefreshing(true); //Load all adoptions from the backend using viewModel
api.getAllAdoptions(0, 100).enqueue(new Callback<PageResponse<AdoptionDTO>>() { viewModel.getAllAdoptions(0, 500).observe(getViewLifecycleOwner(), resource -> {
public void onResponse(Call<PageResponse<AdoptionDTO>> c, if (resource == null) return;
Response<PageResponse<AdoptionDTO>> r) {
if (swipeRefresh != null) swipeRefresh.setRefreshing(false); // Check the status to see if the resource is loaded and display the data
if (r.isSuccessful() && r.body() != null) { switch (resource.status) {
adoptionList.clear(); case LOADING:
adoptionList.addAll(r.body().getContent()); // Show loading indicator
filter(etSearch != null ? etSearch.getText().toString() : ""); binding.swipeRefreshAdoption.setRefreshing(true);
} else { break;
Toast.makeText(getContext(), "Failed to load adoptions", Toast.LENGTH_SHORT).show(); case SUCCESS:
Log.e("AdoptionFragment", "Error: " + r.message()); // Hide loading indicator and display data
} binding.swipeRefreshAdoption.setRefreshing(false);
} if (resource.data != null) {
public void onFailure(Call<PageResponse<AdoptionDTO>> c, Throwable t) { adoptionList.clear();
if (swipeRefresh != null) swipeRefresh.setRefreshing(false); adoptionList.addAll(resource.data.getContent());
Toast.makeText(getContext(), "Network error", Toast.LENGTH_SHORT).show(); updateCalendarDecorators();
Log.e("AdoptionFragment", t.getMessage()); filter(binding.etSearchAdoption != null ? binding.etSearchAdoption.getText().toString() : "");
}
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) {
AdoptionDetailFragment detail = new AdoptionDetailFragment();
Bundle args = new Bundle(); Bundle args = new Bundle();
if (position != -1) { if (position != -1) {
AdoptionDTO a = filteredList.get(position); AdoptionDTO a = filteredList.get(position);
args.putLong("adoptionId", a.getAdoptionId()); args.putLong("adoptionId", a.getAdoptionId());
args.putLong("petId", a.getPetId() != null ? a.getPetId() : -1);
args.putLong("customerId", a.getCustomerId() != null ? a.getCustomerId() : -1);
args.putString("adoptionDate", a.getAdoptionDate());
args.putString("adoptionStatus", a.getAdoptionStatus());
} }
detail.setArguments(args); NavHostFragment.findNavController(this).navigate(R.id.nav_adoption_detail, args);
ListFragment lf = (ListFragment) getParentFragment();
if (lf != null) lf.loadFragment(detail);
} }
/**
* Handles item click in the adoption list.
*/
@Override @Override
public void onAdoptionClick(int position) { openDetail(position); } public void onAdoptionClick(int position) { openDetail(position); }
} }

View File

@@ -4,38 +4,36 @@ import android.graphics.Color;
import android.os.Bundle; import android.os.Bundle;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment; import androidx.fragment.app.Fragment;
import androidx.lifecycle.ViewModelProvider;
import androidx.navigation.fragment.NavHostFragment;
import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
import android.text.Editable; import android.text.Editable;
import android.text.TextWatcher; import android.text.TextWatcher;
import android.util.Log; 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.EditText; import android.widget.AdapterView;
import android.widget.ImageButton;
import android.widget.Toast; 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;
import com.example.petstoremobile.api.AppointmentApi; import com.example.petstoremobile.adapters.WhiteTextArrayAdapter;
import com.example.petstoremobile.api.PetApi; import com.example.petstoremobile.databinding.FragmentAppointmentBinding;
import com.example.petstoremobile.api.ServiceApi;
import com.example.petstoremobile.api.RetrofitClient;
import com.example.petstoremobile.dtos.AppointmentDTO; import com.example.petstoremobile.dtos.AppointmentDTO;
import com.example.petstoremobile.dtos.ServiceDTO; import com.example.petstoremobile.dtos.StoreDTO;
import com.example.petstoremobile.dtos.PageResponse;
import com.example.petstoremobile.dtos.PetDTO;
import com.example.petstoremobile.fragments.ListFragment; import com.example.petstoremobile.fragments.ListFragment;
import com.example.petstoremobile.fragments.listfragments.detailfragments.AppointmentDetailFragment; import com.example.petstoremobile.utils.Resource;
import com.example.petstoremobile.utils.SpinnerUtils;
import com.example.petstoremobile.viewmodels.AppointmentViewModel;
import com.example.petstoremobile.utils.EventDecorator; import com.example.petstoremobile.utils.EventDecorator;
import com.google.android.material.floatingactionbutton.FloatingActionButton; 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;
import com.prolificinteractive.materialcalendarview.MaterialCalendarView;
import com.prolificinteractive.materialcalendarview.OnDateSelectedListener;
import java.text.ParseException; import java.text.ParseException;
import java.text.SimpleDateFormat; import java.text.SimpleDateFormat;
@@ -46,89 +44,160 @@ import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
import retrofit2.Call; import dagger.hilt.android.AndroidEntryPoint;
import retrofit2.Callback;
import retrofit2.Response;
@AndroidEntryPoint
public class AppointmentFragment extends Fragment implements AppointmentAdapter.OnAppointmentClickListener { public class AppointmentFragment extends Fragment implements AppointmentAdapter.OnAppointmentClickListener {
private FragmentAppointmentBinding binding;
private List<AppointmentDTO> appointmentList = new ArrayList<>(); private List<AppointmentDTO> appointmentList = new ArrayList<>();
private List<AppointmentDTO> filteredList = new ArrayList<>(); private List<StoreDTO> storeList = new ArrayList<>();
private List<PetDTO> petList = new ArrayList<>();
private List<ServiceDTO> serviceList = new ArrayList<>();
private AppointmentAdapter adapter; private AppointmentAdapter adapter;
private AppointmentApi api; private AppointmentViewModel appointmentViewModel;
private SwipeRefreshLayout swipeRefreshLayout; private StoreViewModel storeViewModel;
private EditText etSearch; private AuthViewModel authViewModel;
private ImageButton hamburger;
private ImageButton btnToggleCalendarMode;
private MaterialCalendarView calendarView;
private CalendarDay selectedCalendarDay; private CalendarDay selectedCalendarDay;
private boolean isMonthMode = false; private boolean isMonthMode = false;
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 View onCreateView(LayoutInflater inflater, ViewGroup container, public void onCreate(@Nullable Bundle savedInstanceState) {
Bundle savedInstanceState) { super.onCreate(savedInstanceState);
View view = inflater.inflate(R.layout.fragment_appointment, container, false); appointmentViewModel = new ViewModelProvider(this).get(AppointmentViewModel.class);
storeViewModel = new ViewModelProvider(this).get(StoreViewModel.class);
api = RetrofitClient.getAppointmentApi(requireContext()); authViewModel = new ViewModelProvider(this).get(AuthViewModel.class);
hamburger = view.findViewById(R.id.btnHamburger);
calendarView = view.findViewById(R.id.calendarView);
btnToggleCalendarMode = view.findViewById(R.id.btnToggleCalendarMode);
setupRecyclerView(view);
setupSearch(view);
setupSwipeRefresh(view);
setupCalendar();
loadAppointmentData();
loadPets();
loadServices();
FloatingActionButton fabAdd = view.findViewById(R.id.fabAddAppointment);
fabAdd.setOnClickListener(v -> openAppointmentDetails(-1));
hamburger.setOnClickListener(v -> {
ListFragment listFragment = (ListFragment) getParentFragment();
if (listFragment != null)
listFragment.openDrawer();
});
btnToggleCalendarMode.setOnClickListener(v -> toggleCalendarMode());
return view;
} }
// Toggle Calendar Mode from week to month and other way around /**
* Sets up the fragment's UI, including RecyclerView, search, swipe-to-refresh, and calendar.
*/
@Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
binding = FragmentAppointmentBinding.inflate(inflater, container, false);
setupRecyclerView();
setupSearch();
setupStatusFilter();
setupStoreFilter();
setupSwipeRefresh();
setupCalendar();
setupFilterToggle();
setupMyAppointmentFilter();
binding.fabAddAppointment.setOnClickListener(v -> openAppointmentDetails(-1));
binding.btnHamburger.setOnClickListener(v -> {
Fragment parent = getParentFragment();
if (parent != null) {
Fragment grandParent = parent.getParentFragment();
if (grandParent instanceof ListFragment) {
((ListFragment) grandParent).openDrawer();
}
}
});
binding.btnToggleCalendarMode.setOnClickListener(v -> toggleCalendarMode());
loadCurrentUserInfo();
return binding.getRoot();
}
@Override
public void onDestroyView() {
super.onDestroyView();
binding = null;
}
@Override
public void onResume() {
super.onResume();
loadAppointmentData();
loadStoreData();
}
/**
* Toggles the calendar between week and month display modes.
*/
private void toggleCalendarMode() { private void toggleCalendarMode() {
isMonthMode = !isMonthMode; isMonthMode = !isMonthMode;
calendarView.state().edit() binding.calendarView.state().edit()
.setCalendarDisplayMode(isMonthMode ? CalendarMode.MONTHS : CalendarMode.WEEKS) .setCalendarDisplayMode(isMonthMode ? CalendarMode.MONTHS : CalendarMode.WEEKS)
.commit(); .commit();
} }
private void setupCalendar() { /**
calendarView.setOnDateChangedListener(new OnDateSelectedListener() { * Sets up the "My Appointments" filter button.
@Override */
public void onDateSelected(@NonNull MaterialCalendarView widget, @NonNull CalendarDay date, boolean selected) { private void setupMyAppointmentFilter() {
if (selected) { binding.btnMyAppointments.setOnClickListener(v -> {
if (date.equals(selectedCalendarDay)) { loadAppointmentData();
selectedCalendarDay = null; });
calendarView.clearSelection(); }
} else {
selectedCalendarDay = date; /**
} * Fetches current user info to get the employeeId.
} else { */
selectedCalendarDay = null; private void loadCurrentUserInfo() {
} authViewModel.getMe().observe(getViewLifecycleOwner(), resource -> {
filterAppointments(etSearch.getText().toString()); if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
currentUserId = resource.data.getId();
} }
}); });
} }
//Set indicators for dates with appointments on the calendar /**
* Sets up the filter toggle button to show/hide the filter layout.
*/
private void setupFilterToggle() {
binding.btnToggleFilter.setOnClickListener(v -> {
if (binding.layoutFilter.getVisibility() == View.GONE) {
binding.layoutFilter.setVisibility(View.VISIBLE);
binding.btnToggleFilter.setImageResource(android.R.drawable.ic_menu_close_clear_cancel);
} else {
binding.layoutFilter.setVisibility(View.GONE);
binding.btnToggleFilter.setImageResource(android.R.drawable.ic_menu_search);
// Reset filters when closing
binding.etSearchAppointment.setText("");
binding.spinnerStatus.setSelection(0);
binding.spinnerStore.setSelection(0);
binding.btnMyAppointments.setChecked(false);
selectedCalendarDay = null;
binding.calendarView.clearSelection();
}
});
}
/**
* Sets up the date selection listener for the calendar.
*/
private void setupCalendar() {
binding.calendarView.setOnDateChangedListener((widget, date, selected) -> {
if (selected) {
if (date.equals(selectedCalendarDay)) {
selectedCalendarDay = null;
binding.calendarView.clearSelection();
} else {
selectedCalendarDay = date;
}
} else {
selectedCalendarDay = null;
}
loadAppointmentData();
});
}
/**
* 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<>();
for (AppointmentDTO appointment : appointmentList) { for (AppointmentDTO appointment : appointmentList) {
@@ -146,31 +215,105 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter.
} }
} }
//update the indicators to the calendar //update the indicators to the calendar
calendarView.removeDecorators(); binding.calendarView.removeDecorators();
calendarView.addDecorator(new EventDecorator(Color.RED, datesWithAppointments)); binding.calendarView.addDecorator(new EventDecorator(Color.RED, datesWithAppointments));
} }
private void setupSearch(View view) { /**
etSearch = view.findViewById(R.id.etSearchAppointment); * Configures the search bar for filtering.
etSearch.addTextChangedListener(new TextWatcher() { */
@Override private void setupSearch() {
public void beforeTextChanged(CharSequence s, int start, int count, int after) { binding.etSearchAppointment.addTextChangedListener(new TextWatcher() {
@Override public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
@Override public void onTextChanged(CharSequence s, int start, int before, int count) {
loadAppointmentData();
} }
@Override public void afterTextChanged(Editable s) {}
});
}
/**
* Configures the status filter spinner.
*/
private void setupStatusFilter() {
String[] statuses = {"All Statuses", "Booked", "Completed", "Cancelled", "Missed"};
WhiteTextArrayAdapter<String> adapter = new WhiteTextArrayAdapter<>(requireContext(), android.R.layout.simple_spinner_item, statuses);
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
binding.spinnerStatus.setAdapter(adapter);
binding.spinnerStatus.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
@Override @Override
public void onTextChanged(CharSequence s, int start, int before, int count) { public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
filterAppointments(s.toString()); loadAppointmentData();
} }
@Override public void onNothingSelected(AdapterView<?> parent) {}
});
}
/**
* Configures the store filter spinner.
*/
private void setupStoreFilter() {
binding.spinnerStore.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
@Override @Override
public void afterTextChanged(Editable s) { public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
loadAppointmentData();
}
@Override public void onNothingSelected(AdapterView<?> parent) {}
});
}
/**
* 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);
} }
}); });
} }
private void filterAppointments(String query) { /**
filteredList.clear(); * Initializes the SwipeRefreshLayout to allow manual data refreshing.
String lowerQuery = query.toLowerCase(); */
private void setupSwipeRefresh() {
binding.swipeRefreshAppointment.setOnRefreshListener(this::loadAppointmentData);
}
/**
* Navigates to the appointment detail screen for editing or creating an appointment.
*/
private void openAppointmentDetails(int position) {
Bundle args = new Bundle();
if (position != -1) {
AppointmentDTO a = appointmentList.get(position);
args.putLong("appointmentId", a.getAppointmentId());
}
NavHostFragment.findNavController(this).navigate(R.id.nav_appointment_detail, args);
}
/**
* Handles item click in the appointment list.
*/
@Override
public void onAppointmentClick(int position) {
openAppointmentDetails(position);
}
/**
* Fetches appointment data from the server with all active filters.
*/
private void loadAppointmentData() {
String query = binding.etSearchAppointment.getText().toString().trim();
String status = binding.spinnerStatus.getSelectedItem() != null ? binding.spinnerStatus.getSelectedItem().toString() : "All Statuses";
Long storeId = null;
if (binding.spinnerStore.getSelectedItemPosition() > 0 && !storeList.isEmpty()) {
storeId = storeList.get(binding.spinnerStore.getSelectedItemPosition() - 1).getStoreId();
}
String selectedDateString = null; String selectedDateString = null;
if (selectedCalendarDay != null) { if (selectedCalendarDay != null) {
@@ -178,157 +321,49 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter.
selectedCalendarDay.getYear(), selectedCalendarDay.getMonth(), selectedCalendarDay.getDay()); selectedCalendarDay.getYear(), selectedCalendarDay.getMonth(), selectedCalendarDay.getDay());
} }
for (AppointmentDTO a : appointmentList) { Long employeeId = null;
boolean matchesSearch = query.isEmpty() || if (binding.btnMyAppointments.isChecked()) {
(a.getCustomerName() != null && a.getCustomerName().toLowerCase().contains(lowerQuery)) || employeeId = currentUserId;
(a.getServiceType() != null && a.getServiceType().toLowerCase().contains(lowerQuery)) ||
(a.getPetName() != null && a.getPetName().toLowerCase().contains(lowerQuery));
boolean matchesDate = (selectedDateString == null) ||
(a.getAppointmentDate() != null && a.getAppointmentDate().equals(selectedDateString));
if (matchesSearch && matchesDate) {
filteredList.add(a);
}
}
adapter.notifyDataSetChanged();
}
private void setupSwipeRefresh(View view) {
swipeRefreshLayout = view.findViewById(R.id.swipeRefreshAppointment);
swipeRefreshLayout.setOnRefreshListener(this::loadAppointmentData);
}
private void openAppointmentDetails(int position) {
AppointmentDetailFragment detailFragment = new AppointmentDetailFragment();
Bundle args = new Bundle();
if (position != -1) {
AppointmentDTO a = filteredList.get(position);
args.putLong("appointmentId", a.getAppointmentId());
args.putString("appointmentDate", a.getAppointmentDate());
args.putString("appointmentTime", a.getAppointmentTime());
args.putString("appointmentStatus", a.getAppointmentStatus());
// IDs for pre-selecting spinners
if (a.getPetID() != null) args.putLong("petId", a.getPetID());
if (a.getServiceId() != null) args.putLong("serviceId", a.getServiceId());
if (a.getCustomerId() != null) args.putLong("customerId", a.getCustomerId());
if (a.getStoreId() != null) args.putLong("storeId", a.getStoreId());
} }
detailFragment.setArguments(args); if (status.equals("All Statuses")) status = null;
ListFragment lf = (ListFragment) getParentFragment(); else status = status.toUpperCase();
if (lf != null) lf.loadFragment(detailFragment);
}
public void onAppointmentSaved(int position, AppointmentDTO appointment) {
loadAppointmentData();
}
public void onAppointmentDeleted(int position) { appointmentViewModel.getAllAppointments(0, 500, query, status, storeId, selectedDateString, employeeId).observe(getViewLifecycleOwner(), resource -> {
loadAppointmentData(); if (resource == null) return;
}
@Override // Check the status to see if the resource is loaded and display the data
public void onAppointmentClick(int position) { switch (resource.status) {
openAppointmentDetails(position); case LOADING:
} // Show loading indicator
binding.swipeRefreshAppointment.setRefreshing(true);
private void loadAppointmentData() { break;
if (swipeRefreshLayout != null) case SUCCESS:
swipeRefreshLayout.setRefreshing(true); // Hide loading indicator and display data
api.getAllAppointments(0, 500).enqueue(new Callback<PageResponse<AppointmentDTO>>() { binding.swipeRefreshAppointment.setRefreshing(false);
@Override if (resource.data != null) {
public void onResponse(Call<PageResponse<AppointmentDTO>> call, appointmentList.clear();
Response<PageResponse<AppointmentDTO>> response) { appointmentList.addAll(resource.data.getContent());
if (swipeRefreshLayout != null) updateCalendarDecorators();
swipeRefreshLayout.setRefreshing(false); adapter.notifyDataSetChanged();
if (response.isSuccessful() && response.body() != null) { }
appointmentList.clear(); break;
appointmentList.addAll(response.body().getContent()); case ERROR:
updateCalendarDecorators(); // Hide loading indicator and toast error message
filterAppointments(etSearch != null ? etSearch.getText().toString() : ""); binding.swipeRefreshAppointment.setRefreshing(false);
} else { Toast.makeText(getContext(), "Failed to load appointments: " + resource.message, Toast.LENGTH_SHORT).show();
Log.e("AppointmentFragment", "Error: " + response.message()); Log.e("AppointmentFragment", "Error loading appointments: " + resource.message);
Toast.makeText(getContext(), "Failed to load appointments", Toast.LENGTH_SHORT).show(); break;
}
}
@Override
public void onFailure(Call<PageResponse<AppointmentDTO>> call, Throwable t) {
if (swipeRefreshLayout != null)
swipeRefreshLayout.setRefreshing(false);
Toast.makeText(getContext(), "Network error: " + t.getMessage(), Toast.LENGTH_SHORT).show();
Log.e("AppointmentFragment", t.getMessage());
} }
}); });
} }
/**
* Initializes the RecyclerView for displaying appointments.
// Load Pets */
private void loadPets() { private void setupRecyclerView() {
PetApi petApi = RetrofitClient.getPetApi(requireContext()); adapter = new AppointmentAdapter(appointmentList, this);
petApi.getAllPets(0,100).enqueue(new Callback<PageResponse<PetDTO>>() { binding.recyclerViewAppointments.setLayoutManager(new LinearLayoutManager(getContext()));
binding.recyclerViewAppointments.setAdapter(adapter);
@Override
public void onResponse(Call<PageResponse<PetDTO>> call, Response<PageResponse<PetDTO>> response) {
if (response.isSuccessful() && response.body() !=null) {
petList.clear();
petList.addAll(response.body().getContent());
}
}
@Override
public void onFailure(Call<PageResponse<PetDTO>> call, Throwable t) {
Log.e("AppointmentFragment", "Pet load error:" + t.getMessage());
}
});
} }
}
// Load Services
private void loadServices() {
ServiceApi serviceApi = RetrofitClient.getServiceApi(requireContext());
serviceApi.getAllServices(0,100).enqueue(new Callback<PageResponse<ServiceDTO>>() {
@Override
public void onResponse(Call<PageResponse<ServiceDTO>> call, Response<PageResponse<ServiceDTO>> response) {
if (response.isSuccessful() && response.body() != null) {
serviceList.clear();
serviceList.addAll(response.body().getContent());
}
}
@Override
public void onFailure(Call<PageResponse<ServiceDTO>> call, Throwable t) {
Log.e("AppointmentFragmnet", "Service load error: " + t.getMessage());
}
});
}
private String getPetName(Long id) {
for (PetDTO p : petList) {
if (p.getPetId().equals(id)) return p.getPetName();
}
return "";
}
private String getServiceName(Long id) {
for (ServiceDTO s : serviceList) {
if (s.getServiceId().equals(id))return s.getServiceName();
}
return "";
}
private void setupRecyclerView(View view) {
RecyclerView recyclerView = view.findViewById(R.id.recyclerViewAppointments);
adapter = new AppointmentAdapter(filteredList, this);
recyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
recyclerView.setAdapter(adapter);
}
}

View File

@@ -1,8 +1,6 @@
package com.example.petstoremobile.fragments.listfragments; package com.example.petstoremobile.fragments.listfragments;
import android.os.Bundle; import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.text.Editable; import android.text.Editable;
import android.text.TextWatcher; import android.text.TextWatcher;
import android.util.Log; import android.util.Log;
@@ -10,205 +8,168 @@ import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.AdapterView; import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ImageButton;
import android.widget.Spinner;
import android.widget.TextView;
import android.widget.Toast; import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment; import androidx.fragment.app.Fragment;
import androidx.lifecycle.ViewModelProvider;
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 androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
import com.example.petstoremobile.R; import com.example.petstoremobile.R;
import com.example.petstoremobile.adapters.BlackTextArrayAdapter;
import com.example.petstoremobile.adapters.InventoryAdapter; import com.example.petstoremobile.adapters.InventoryAdapter;
import com.example.petstoremobile.api.CategoryApi; import com.example.petstoremobile.databinding.FragmentInventoryBinding;
import com.example.petstoremobile.api.InventoryApi;
import com.example.petstoremobile.api.RetrofitClient;
import com.example.petstoremobile.dtos.BulkDeleteRequest;
import com.example.petstoremobile.dtos.CategoryDTO;
import com.example.petstoremobile.dtos.InventoryDTO; import com.example.petstoremobile.dtos.InventoryDTO;
import com.example.petstoremobile.dtos.PageResponse; import com.example.petstoremobile.dtos.StoreDTO;
import com.example.petstoremobile.fragments.ListFragment; import com.example.petstoremobile.fragments.ListFragment;
import com.example.petstoremobile.fragments.listfragments.detailfragments.InventoryDetailFragment; import com.example.petstoremobile.viewmodels.InventoryViewModel;
import com.google.android.material.floatingactionbutton.FloatingActionButton; import com.example.petstoremobile.utils.Resource;
import com.example.petstoremobile.utils.SpinnerUtils;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import retrofit2.Call; import dagger.hilt.android.AndroidEntryPoint;
import retrofit2.Callback;
import retrofit2.Response;
@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 String TAG = "InventoryFragment";
private static final int PAGE_SIZE = 20; private static final int PAGE_SIZE = 20;
private FragmentInventoryBinding binding;
private final List<InventoryDTO> inventoryList = new ArrayList<>(); private final List<InventoryDTO> inventoryList = new ArrayList<>();
private final List<CategoryDTO> categoryList = new ArrayList<>(); private List<StoreDTO> storeList = new ArrayList<>();
private InventoryAdapter adapter; private InventoryAdapter adapter;
private InventoryApi inventoryApi; private InventoryViewModel viewModel;
private CategoryApi categoryApi;
private SwipeRefreshLayout swipeRefreshLayout;
private EditText etSearch;
private Spinner spinnerCategory;
private ImageButton hamburger;
private Button btnBulkDelete;
private TextView tvSelectionCount;
// Debounce search
private final Handler searchHandler = new Handler(Looper.getMainLooper());
private Runnable searchRunnable;
private String currentQuery = "";
// Selected category filter — null means "All"
private String selectedCategory = null;
// Pagination // Pagination
private int currentPage = 0; private int currentPage = 0;
private boolean isLastPage = false; private boolean isLastPage = false;
private boolean isLoading = false; private boolean isLoading = false;
// Prevent spinner from firing on initial load /**
private boolean spinnerReady = false; * Initializes the fragment and its ViewModel.
*/
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
viewModel = new ViewModelProvider(this).get(InventoryViewModel.class);
}
/**
* Sets up the fragment's UI components, including the inventory list and search.
*/
@Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
binding = FragmentInventoryBinding.inflate(inflater, container, false);
setupRecyclerView();
setupSearch();
setupStoreFilter();
setupSwipeRefresh();
setupFilterToggle();
loadInventory(true);
loadStoreData();
binding.fabAddInventory.setOnClickListener(v -> openDetail(null));
binding.btnHamburger.setOnClickListener(v -> {
Fragment parent = getParentFragment();
if (parent != null) {
Fragment grandParent = parent.getParentFragment();
if (grandParent instanceof ListFragment) {
((ListFragment) grandParent).openDrawer();
}
}
});
binding.btnBulkDelete.setOnClickListener(v -> confirmBulkDelete());
return binding.getRoot();
}
@Override @Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, public void onDestroyView() {
Bundle savedInstanceState) { super.onDestroyView();
View view = inflater.inflate(R.layout.fragment_inventory, container, false); binding = null;
inventoryApi = RetrofitClient.getInventoryApi(requireContext());
categoryApi = RetrofitClient.getCategoryApi(requireContext());
hamburger = view.findViewById(R.id.btnHamburger);
btnBulkDelete = view.findViewById(R.id.btnBulkDelete);
tvSelectionCount = view.findViewById(R.id.tvSelectionCount);
spinnerCategory = view.findViewById(R.id.spinnerCategory);
setupRecyclerView(view);
setupSearch(view);
setupSwipeRefresh(view);
loadCategories(); // loads categories then triggers loadInventory
loadInventory(true);
view.findViewById(R.id.fabAddInventory)
.setOnClickListener(v -> openDetail(null));
hamburger.setOnClickListener(v -> {
ListFragment lf = (ListFragment) getParentFragment();
if (lf != null)
lf.openDrawer();
});
btnBulkDelete.setOnClickListener(v -> confirmBulkDelete());
return view;
} }
// Categories /**
private void loadCategories() { * Sets up the filter toggle button to show/hide the filter layout.
categoryApi.getAllCategories(0, 100).enqueue(new Callback<PageResponse<CategoryDTO>>() { */
@Override private void setupFilterToggle() {
public void onResponse(Call<PageResponse<CategoryDTO>> call, binding.btnToggleFilter.setOnClickListener(v -> {
Response<PageResponse<CategoryDTO>> response) { if (binding.layoutFilter.getVisibility() == View.GONE) {
if (response.isSuccessful() && response.body() != null) { binding.layoutFilter.setVisibility(View.VISIBLE);
categoryList.clear(); binding.btnToggleFilter.setImageResource(android.R.drawable.ic_menu_close_clear_cancel);
categoryList.addAll(response.body().getContent()); } else {
setupCategorySpinner(); binding.layoutFilter.setVisibility(View.GONE);
} binding.btnToggleFilter.setImageResource(android.R.drawable.ic_menu_search);
}
@Override // Reset filters when closing
public void onFailure(Call<PageResponse<CategoryDTO>> call, Throwable t) { binding.etSearchInventory.setText("");
Log.e(TAG, "Failed to load categories", t); binding.spinnerStore.setSelection(0);
// Still setup spinner with just "All"
setupCategorySpinner();
} }
}); });
} }
private void setupCategorySpinner() { /**
// First item is always "All Categories" * Sets up the search bar for filtering.
List<String> categoryNames = new ArrayList<>(); */
categoryNames.add("All Categories"); private void setupSearch() {
for (CategoryDTO c : categoryList) { binding.etSearchInventory.addTextChangedListener(new TextWatcher() {
categoryNames.add(c.getCategoryName()); @Override public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
} @Override public void onTextChanged(CharSequence s, int start, int before, int count) {
BlackTextArrayAdapter<String> spinnerAdapter = new BlackTextArrayAdapter<>(
requireContext(),
android.R.layout.simple_spinner_item,
categoryNames);
spinnerAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
spinnerCategory.setAdapter(spinnerAdapter);
spinnerCategory.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
@Override
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
if (!spinnerReady) {
// Skip the first automatic trigger on setup
spinnerReady = true;
return;
}
if (position == 0) {
selectedCategory = null; // "All Categories"
} else {
selectedCategory = categoryList.get(position - 1).getCategoryName();
}
loadInventory(true); loadInventory(true);
} }
@Override public void afterTextChanged(Editable s) {}
});
}
/**
* Configures the store filter spinner.
*/
private void setupStoreFilter() {
binding.spinnerStore.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
@Override @Override
public void onNothingSelected(AdapterView<?> parent) { public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
loadInventory(true);
}
@Override public void onNothingSelected(AdapterView<?> parent) {}
});
}
/**
* 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);
} }
}); });
} }
// Search /**
* Initializes the RecyclerView with a layout manager, and adapter.
private void setupSearch(View view) { */
etSearch = view.findViewById(R.id.etSearchInventory); private void setupRecyclerView() {
etSearch.addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int i, int i1, int i2) {
}
@Override
public void afterTextChanged(Editable s) {
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
if (searchRunnable != null)
searchHandler.removeCallbacks(searchRunnable);
searchRunnable = () -> {
currentQuery = s.toString().trim();
loadInventory(true);
};
searchHandler.postDelayed(searchRunnable, 400);
}
});
}
// RecyclerView + infinite scroll
private void setupRecyclerView(View view) {
RecyclerView rv = view.findViewById(R.id.recyclerViewInventory);
adapter = new InventoryAdapter(inventoryList, this); adapter = new InventoryAdapter(inventoryList, this);
rv.setLayoutManager(new LinearLayoutManager(getContext())); binding.recyclerViewInventory.setLayoutManager(new LinearLayoutManager(getContext()));
rv.setAdapter(adapter); binding.recyclerViewInventory.setAdapter(adapter);
rv.addOnScrollListener(new RecyclerView.OnScrollListener() { binding.recyclerViewInventory.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override @Override
public void onScrolled(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) rv.getLayoutManager(); LinearLayoutManager lm = (LinearLayoutManager) binding.recyclerViewInventory.getLayoutManager();
if (lm == null) if (lm == null)
return; return;
int visible = lm.getChildCount(); int visible = lm.getChildCount();
@@ -221,75 +182,70 @@ public class InventoryFragment extends Fragment implements InventoryAdapter.OnIn
}); });
} }
private void setupSwipeRefresh(View view) { /**
swipeRefreshLayout = view.findViewById(R.id.swipeRefreshInventory); * Sets up the SwipeRefreshLayout to reload the first page of inventory items.
swipeRefreshLayout.setOnRefreshListener(() -> loadInventory(true)); */
private void setupSwipeRefresh() {
binding.swipeRefreshInventory.setOnRefreshListener(() -> loadInventory(true));
} }
// Load inventory /**
* Fetches a page of inventory items from the API.
*/
private void loadInventory(boolean reset) { private void loadInventory(boolean reset) {
if (isLoading) if (isLoading) return;
return;
isLoading = true;
if (reset) { if (reset) {
currentPage = 0; currentPage = 0;
isLastPage = false; isLastPage = false;
} }
// Build query: combine search text + selected category // Search text from input
String q = buildQuery(); String query = binding.etSearchInventory != null ? binding.etSearchInventory.getText().toString().trim() : "";
if (query.isEmpty()) query = null;
inventoryApi.getAllInventory(q, currentPage, PAGE_SIZE, "inventoryId,asc") Long storeId = null;
.enqueue(new Callback<PageResponse<InventoryDTO>>() { if (binding.spinnerStore.getSelectedItemPosition() > 0 && !storeList.isEmpty()) {
@Override storeId = storeList.get(binding.spinnerStore.getSelectedItemPosition() - 1).getStoreId();
public void onResponse(Call<PageResponse<InventoryDTO>> call,
Response<PageResponse<InventoryDTO>> response) {
isLoading = false;
if (swipeRefreshLayout != null)
swipeRefreshLayout.setRefreshing(false);
if (response.isSuccessful() && response.body() != null) {
PageResponse<InventoryDTO> page = response.body();
if (reset)
inventoryList.clear();
inventoryList.addAll(page.getContent());
adapter.notifyDataSetChanged();
isLastPage = page.isLast();
if (!isLastPage)
currentPage++;
} else {
Log.e(TAG, "Error " + response.code());
Toast.makeText(getContext(), "Failed to load inventory", Toast.LENGTH_SHORT).show();
}
}
@Override
public void onFailure(Call<PageResponse<InventoryDTO>> call, Throwable t) {
isLoading = false;
if (swipeRefreshLayout != null)
swipeRefreshLayout.setRefreshing(false);
Log.e(TAG, "Network error", t);
Toast.makeText(getContext(), "Network error: " + t.getMessage(), Toast.LENGTH_SHORT).show();
}
});
}
// Combines search text and category into one query string for ?q=
private String buildQuery() {
String q = null;
if (!currentQuery.isEmpty() && selectedCategory != null) {
// Both active — prioritize search text, category acts as context
q = currentQuery;
} else if (!currentQuery.isEmpty()) {
q = currentQuery;
} else if (selectedCategory != null) {
q = selectedCategory;
} }
return q;
//Load all inventory items from the backend using viewModel
viewModel.getAllInventory(query, null, 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;
}
});
} }
// Bulk delete /**
* Displays a confirmation dialog before performing a bulk deletion of selected items.
*/
private void confirmBulkDelete() { private void confirmBulkDelete() {
List<Long> ids = adapter.getSelectedIds(); List<Long> ids = adapter.getSelectedIds();
if (ids.isEmpty()) if (ids.isEmpty())
@@ -303,62 +259,57 @@ public class InventoryFragment extends Fragment implements InventoryAdapter.OnIn
.show(); .show();
} }
/**
* Executes the bulk deletion of inventory items through the ViewModel.
*/
private void bulkDelete(List<Long> ids) { private void bulkDelete(List<Long> ids) {
inventoryApi.bulkDeleteInventory(new BulkDeleteRequest(ids)) viewModel.bulkDeleteInventory(ids).observe(getViewLifecycleOwner(), resource -> {
.enqueue(new Callback<Void>() { if (resource != null && resource.status != Resource.Status.LOADING) {
@Override if (resource.status == Resource.Status.SUCCESS) {
public void onResponse(Call<Void> call, Response<Void> response) { adapter.clearSelection();
if (response.isSuccessful()) { hideBulkDeleteBar();
adapter.clearSelection(); loadInventory(true);
hideBulkDeleteBar(); Toast.makeText(getContext(), ids.size() + " item(s) deleted", Toast.LENGTH_SHORT).show();
loadInventory(true); } else {
Toast.makeText(getContext(), ids.size() + " item(s) deleted", Toast.LENGTH_SHORT).show(); Toast.makeText(getContext(), "Delete failed: " + resource.message, Toast.LENGTH_SHORT).show();
} else { }
Toast.makeText(getContext(), "Delete failed", Toast.LENGTH_SHORT).show(); }
} });
}
@Override
public void onFailure(Call<Void> call, Throwable t) {
Toast.makeText(getContext(), "Network error", Toast.LENGTH_SHORT).show();
}
});
} }
/**
* Hides the bulk deletion UI bar.
*/
private void hideBulkDeleteBar() { private void hideBulkDeleteBar() {
if (btnBulkDelete != null) if (binding != null) {
btnBulkDelete.setVisibility(View.GONE); binding.btnBulkDelete.setVisibility(View.GONE);
if (tvSelectionCount != null) binding.tvSelectionCount.setVisibility(View.GONE);
tvSelectionCount.setVisibility(View.GONE); }
} }
// Navigation /**
* Navigates to the inventory detail screen for a specific item or to add a new one.
*/
private void openDetail(InventoryDTO inv) { private void openDetail(InventoryDTO inv) {
InventoryDetailFragment detail = new InventoryDetailFragment();
Bundle args = new Bundle(); Bundle args = new Bundle();
if (inv != null) { if (inv != null) {
args.putLong("inventoryId", inv.getInventoryId()); args.putLong("inventoryId", inv.getInventoryId());
args.putLong("prodId", inv.getProdId() != null ? inv.getProdId() : -1);
args.putString("productName", inv.getProductName());
args.putString("categoryName", inv.getCategoryName());
args.putInt("quantity", inv.getQuantity() != null ? inv.getQuantity() : 0);
} }
detail.setArguments(args); NavHostFragment.findNavController(this).navigate(R.id.nav_inventory_detail, args);
detail.setInventoryFragment(this);
ListFragment lf = (ListFragment) getParentFragment();
if (lf != null)
lf.loadFragment(detail);
} }
/**
* Reloads inventory data when changes occur.
*/
public void onInventoryChanged() { public void onInventoryChanged() {
loadInventory(true); loadInventory(true);
} }
// Adapter callbacks /**
* 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()) {
@@ -366,14 +317,17 @@ 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 (selectedCount > 0) { if (selectedCount > 0) {
btnBulkDelete.setVisibility(View.VISIBLE); binding.btnBulkDelete.setVisibility(View.VISIBLE);
tvSelectionCount.setVisibility(View.VISIBLE); binding.tvSelectionCount.setVisibility(View.VISIBLE);
tvSelectionCount.setText(selectedCount + " selected"); binding.tvSelectionCount.setText(selectedCount + " selected");
} else { } else {
hideBulkDeleteBar(); hideBulkDeleteBar();
} }
} }
} }

View File

@@ -2,10 +2,12 @@ package com.example.petstoremobile.fragments.listfragments;
import android.os.Bundle; import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment; import androidx.fragment.app.Fragment;
import androidx.lifecycle.ViewModelProvider;
import androidx.navigation.fragment.NavHostFragment;
import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
import android.text.Editable; import android.text.Editable;
import android.text.TextWatcher; import android.text.TextWatcher;
@@ -14,224 +16,268 @@ import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.AdapterView; import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.EditText;
import android.widget.ImageButton;
import android.widget.Spinner;
import android.widget.Toast; import android.widget.Toast;
import com.example.petstoremobile.R; import com.example.petstoremobile.R;
import com.example.petstoremobile.adapters.BlackTextArrayAdapter;
import com.example.petstoremobile.adapters.PetAdapter; import com.example.petstoremobile.adapters.PetAdapter;
import com.example.petstoremobile.api.PetApi; import com.example.petstoremobile.adapters.WhiteTextArrayAdapter;
import com.example.petstoremobile.api.RetrofitClient; import com.example.petstoremobile.databinding.FragmentPetBinding;
import com.example.petstoremobile.dtos.PageResponse;
import com.example.petstoremobile.dtos.PetDTO; import com.example.petstoremobile.dtos.PetDTO;
import com.example.petstoremobile.dtos.StoreDTO;
import com.example.petstoremobile.fragments.ListFragment; import com.example.petstoremobile.fragments.ListFragment;
import com.example.petstoremobile.fragments.listfragments.detailfragments.PetDetailFragment; import com.example.petstoremobile.utils.Resource;
import com.example.petstoremobile.fragments.listfragments.listprofilefragments.PetProfileFragment; import com.example.petstoremobile.utils.SpinnerUtils;
import com.google.android.material.floatingactionbutton.FloatingActionButton; import com.example.petstoremobile.viewmodels.PetViewModel;
import com.example.petstoremobile.viewmodels.StoreViewModel;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import retrofit2.Call; import javax.inject.Inject;
import retrofit2.Callback; import javax.inject.Named;
import retrofit2.Response;
import dagger.hilt.android.AndroidEntryPoint;
@AndroidEntryPoint
public class PetFragment extends Fragment implements PetAdapter.OnPetClickListener { public class PetFragment extends Fragment implements PetAdapter.OnPetClickListener {
private FragmentPetBinding binding;
private List<PetDTO> petList = new ArrayList<>(); private List<PetDTO> petList = new ArrayList<>();
private List<PetDTO> filteredList = new ArrayList<>(); private List<StoreDTO> storeList = new ArrayList<>();
private ImageButton hamburger;
private PetAdapter adapter; private PetAdapter adapter;
private PetApi api; private PetViewModel viewModel;
private SwipeRefreshLayout swipeRefreshLayout; private StoreViewModel storeViewModel;
private EditText etSearch;
private Spinner spinnerStatus;
//load pet view @Inject @Named("baseUrl") String baseUrl;
/**
* Initializes the fragment and its associated ViewModels.
*/
@Override @Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
viewModel = new ViewModelProvider(this).get(PetViewModel.class);
storeViewModel = new ViewModelProvider(this).get(StoreViewModel.class);
}
/**
* Sets up the fragment's UI components, including RecyclerView, filters, and swipe-to-refresh.
*/
@Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) { Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_pet, container, false); binding = FragmentPetBinding.inflate(inflater, container, false);
//get retrofit setupRecyclerView();
api = RetrofitClient.getPetApi(requireContext()); setupSearch();
setupStatusFilter();
setupSpeciesFilter();
setupStoreFilter();
setupSwipeRefresh();
setupFilterToggle();
hamburger = view.findViewById(R.id.btnHamburger); binding.fabAddPet.setOnClickListener(v -> openPetDetails());
setupRecyclerView(view); binding.btnHamburger.setOnClickListener(v -> {
setupSearch(view); Fragment parent = getParentFragment();
setupStatusFilter(view); if (parent != null) {
setupSwipeRefresh(view); Fragment grandParent = parent.getParentFragment();
if (grandParent instanceof ListFragment) {
((ListFragment) grandParent).openDrawer();
//Add button to opens the add dialog }
FloatingActionButton fabAddPet = view.findViewById(R.id.fabAddPet);
fabAddPet.setOnClickListener(v -> openPetDetails(-1));
//Make the hamburger button open the drawer from listFragment
hamburger.setOnClickListener(v -> {
ListFragment listFragment = (ListFragment) getParentFragment();
//if list fragment is found then use its helper function to open the drawer
if (listFragment != null) {
listFragment.openDrawer();
} }
}); });
return view; return binding.getRoot();
} }
@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();
} }
private void setupSearch(View view) { /**
etSearch = view.findViewById(R.id.etSearchPet); * Sets up the filter toggle button to show/hide the filter layout.
etSearch.addTextChangedListener(new TextWatcher() { */
private void setupFilterToggle() {
binding.btnToggleFilter.setOnClickListener(v -> {
if (binding.layoutFilter.getVisibility() == View.GONE) {
binding.layoutFilter.setVisibility(View.VISIBLE);
binding.btnToggleFilter.setImageResource(android.R.drawable.ic_menu_close_clear_cancel);
} else {
binding.layoutFilter.setVisibility(View.GONE);
binding.btnToggleFilter.setImageResource(android.R.drawable.ic_menu_search);
// Reset filters when closing
binding.etSearchPet.setText("");
binding.spinnerStatus.setSelection(0);
binding.spinnerSpecies.setSelection(0);
binding.spinnerStore.setSelection(0);
}
});
}
/**
* Configures the search bar.
*/
private void setupSearch() {
binding.etSearchPet.addTextChangedListener(new TextWatcher() {
@Override public void beforeTextChanged(CharSequence s, int start, int count, int after) {} @Override public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
@Override public void onTextChanged(CharSequence s, int start, int before, int count) { @Override public void onTextChanged(CharSequence s, int start, int before, int count) {
filterPets(); loadPetData();
} }
@Override public void afterTextChanged(Editable s) {} @Override public void afterTextChanged(Editable s) {}
}); });
} }
//Setup the status filter spinner /**
private void setupStatusFilter(View view) { * Configures the status filter spinner.
spinnerStatus = view.findViewById(R.id.spinnerStatus); */
String[] statuses = {"All Statuses", "Available", "Adopted"}; private void setupStatusFilter() {
BlackTextArrayAdapter<String> adapter = new BlackTextArrayAdapter<>(requireContext(), android.R.layout.simple_spinner_item, statuses); String[] statuses = {"All Statuses", "Available", "Adopted", "Owned"};
WhiteTextArrayAdapter<String> adapter = new WhiteTextArrayAdapter<>(requireContext(), android.R.layout.simple_spinner_item, statuses);
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
spinnerStatus.setAdapter(adapter); binding.spinnerStatus.setAdapter(adapter);
spinnerStatus.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { binding.spinnerStatus.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
@Override @Override
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) { public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
filterPets(); loadPetData();
} }
@Override public void onNothingSelected(AdapterView<?> parent) {}
});
}
/**
* Configures the species filter spinner with species.
*/
private void setupSpeciesFilter() {
String[] species = {"All Species", "Dog", "Cat", "Bird", "Rabbit", "Fish", "Hamster"};
WhiteTextArrayAdapter<String> adapter = new WhiteTextArrayAdapter<>(requireContext(), android.R.layout.simple_spinner_item, species);
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
binding.spinnerSpecies.setAdapter(adapter);
binding.spinnerSpecies.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
@Override @Override
public void onNothingSelected(AdapterView<?> parent) {} public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
}); loadPetData();
}
// Helper function to filter pets based on search and status filter
private void filterPets() {
String query = etSearch.getText().toString().toLowerCase();
String selectedStatus = spinnerStatus.getSelectedItem().toString();
filteredList.clear();
for (PetDTO p : petList) {
boolean matchesSearch = query.isEmpty() ||
p.getPetName().toLowerCase().contains(query) ||
p.getPetSpecies().toLowerCase().contains(query) ||
p.getPetBreed().toLowerCase().contains(query);
boolean matchesStatus = selectedStatus.equals("All Statuses") ||
p.getPetStatus().equalsIgnoreCase(selectedStatus);
if (matchesSearch && matchesStatus) {
filteredList.add(p);
} }
} @Override public void onNothingSelected(AdapterView<?> parent) {}
adapter.notifyDataSetChanged();
}
private void setupSwipeRefresh(View view) {
swipeRefreshLayout = view.findViewById(R.id.swipeRefreshPet);
swipeRefreshLayout.setOnRefreshListener(() -> {
loadPetData();
}); });
} }
//Open pet profile /**
* Configures the store filter spinner.
*/
private void setupStoreFilter() {
binding.spinnerStore.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
@Override
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
loadPetData();
}
@Override public void onNothingSelected(AdapterView<?> parent) {}
});
}
/**
* 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() {
binding.swipeRefreshPet.setOnRefreshListener(this::loadPetData);
}
/**
* Navigates to the pet profile screen.
*/
private void openPetProfile(int position) { private void openPetProfile(int position) {
PetProfileFragment profileFragment = new PetProfileFragment();
//Make a bundle to pass data to the profile fragment
Bundle args = new Bundle(); Bundle args = new Bundle();
PetDTO pet = filteredList.get(position); PetDTO pet = petList.get(position);
args.putInt("petId", pet.getPetId().intValue()); args.putLong("petId", pet.getPetId());
args.putString("petName", pet.getPetName()); NavHostFragment.findNavController(this).navigate(R.id.nav_pet_profile, args);
args.putString("petSpecies", pet.getPetSpecies());
args.putString("petBreed", pet.getPetBreed());
args.putInt("petAge", pet.getPetAge());
args.putString("petStatus", pet.getPetStatus());
try {
args.putDouble("petPrice", Double.parseDouble(pet.getPetPrice()));
} catch (Exception e) {
args.putDouble("petPrice", 0.0);
}
//send the bundle to the profile fragment to display
profileFragment.setArguments(args);
//get ListFragment to load the the pet profile view
ListFragment listFragment = (ListFragment) getParentFragment();
if (listFragment != null) {
listFragment.loadFragment(profileFragment);
}
} }
//Open the pet detail view for adding /**
private void openPetDetails(int position) { * Navigates to the pet detail screen.
PetDetailFragment detailFragment = new PetDetailFragment(); */
private void openPetDetails() {
//get ListFragment to load the detail view NavHostFragment.findNavController(this).navigate(R.id.nav_pet_detail);
ListFragment listFragment = (ListFragment) getParentFragment();
if (listFragment != null) {
listFragment.loadFragment(detailFragment);
}
} }
// Called by PetAdapter when a row is clicked to open the details view
@Override @Override
public void onPetClick(int position) { public void onPetClick(int position) {
openPetProfile(position); openPetProfile(position);
} }
// Helper function to get a list of all pets from the backend /**
* Fetches pet data from the server with all active filters.
*/
private void loadPetData() { private void loadPetData() {
if (swipeRefreshLayout != null) { String query = binding.etSearchPet.getText().toString().trim();
swipeRefreshLayout.setRefreshing(true); 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();
} }
api.getAllPets(0, 100).enqueue(new Callback<PageResponse<PetDTO>>() {
@Override
public void onResponse(Call<PageResponse<PetDTO>> call, Response<PageResponse<PetDTO>> response) {
if (swipeRefreshLayout != null) {
swipeRefreshLayout.setRefreshing(false);
}
if (response.isSuccessful() && response.body() != null) {
petList.clear();
petList.addAll(response.body().getContent());
filterPets();
} else { if (status.equals("All Statuses")) status = null;
Log.e("onResponse: ", response.message()); if (species.equals("All Species")) species = null;
}
}
@Override viewModel.getAllPets(0, 100, query, status, species, storeId, "petName").observe(getViewLifecycleOwner(), resource -> {
public void onFailure(Call<PageResponse<PetDTO>> call, Throwable t) { if (resource == null) return;
if (swipeRefreshLayout != null) {
swipeRefreshLayout.setRefreshing(false); switch (resource.status) {
} case LOADING:
Toast.makeText(getContext(), binding.swipeRefreshPet.setRefreshing(true);
"Failed to load pets", Toast.LENGTH_SHORT).show(); break;
Log.e("onFailure: ", t.getMessage()); 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;
} }
}); });
} }
//set up the recyclerview and adapter /**
private void setupRecyclerView(View view) { * Initializes the RecyclerView.
RecyclerView recyclerView = view.findViewById(R.id.recyclerViewPets); */
adapter = new PetAdapter(filteredList, this); private void setupRecyclerView() {
recyclerView.setLayoutManager(new LinearLayoutManager(getContext())); adapter = new PetAdapter(petList, this);
recyclerView.setAdapter(adapter); adapter.setBaseUrl(baseUrl);
binding.recyclerViewPets.setLayoutManager(new LinearLayoutManager(getContext()));
binding.recyclerViewPets.setAdapter(adapter);
} }
} }

View File

@@ -1,135 +1,233 @@
package com.example.petstoremobile.fragments.listfragments; package com.example.petstoremobile.fragments.listfragments;
import android.os.Bundle; import android.os.Bundle;
import android.text.*;
import android.util.Log; import androidx.annotation.NonNull;
import android.view.*; import androidx.annotation.Nullable;
import android.widget.*;
import androidx.fragment.app.Fragment; import androidx.fragment.app.Fragment;
import androidx.lifecycle.ViewModelProvider;
import androidx.navigation.fragment.NavHostFragment;
import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; import android.text.Editable;
import android.text.TextWatcher;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
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.api.RetrofitClient; import com.example.petstoremobile.databinding.FragmentProductBinding;
import com.example.petstoremobile.dtos.PageResponse; import com.example.petstoremobile.dtos.CategoryDTO;
import com.example.petstoremobile.dtos.ProductDTO; import com.example.petstoremobile.dtos.ProductDTO;
import com.example.petstoremobile.fragments.ListFragment; import com.example.petstoremobile.fragments.ListFragment;
import com.example.petstoremobile.fragments.listfragments.detailfragments.ProductDetailFragment; import com.example.petstoremobile.utils.Resource;
import com.google.android.material.floatingactionbutton.FloatingActionButton; import com.example.petstoremobile.utils.SpinnerUtils;
import java.util.*; import com.example.petstoremobile.viewmodels.ProductViewModel;
import retrofit2.*;
import java.util.ArrayList;
import java.util.List;
import javax.inject.Inject;
import javax.inject.Named;
import dagger.hilt.android.AndroidEntryPoint;
@AndroidEntryPoint
public class ProductFragment extends Fragment implements ProductAdapter.OnProductClickListener { public class ProductFragment extends Fragment implements ProductAdapter.OnProductClickListener {
private FragmentProductBinding binding;
private List<ProductDTO> productList = new ArrayList<>(); private List<ProductDTO> productList = new ArrayList<>();
private List<ProductDTO> filteredList = new ArrayList<>(); private List<CategoryDTO> categoryList = new ArrayList<>();
private ProductAdapter adapter; private ProductAdapter adapter;
private SwipeRefreshLayout swipeRefresh; private ProductViewModel viewModel;
private EditText etSearch;
@Inject @Named("baseUrl") String baseUrl;
/**
* Initializes the fragment and its associated ProductViewModel.
*/
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
viewModel = new ViewModelProvider(this).get(ProductViewModel.class);
}
/**
* Sets up the fragment's UI components, including RecyclerView, search, and swipe-to-refresh.
*/
@Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
binding = FragmentProductBinding.inflate(inflater, container, false);
setupRecyclerView();
setupSearch();
setupCategoryFilter();
setupSwipeRefresh();
setupFilterToggle();
binding.fabAddProduct.setOnClickListener(v -> openProductDetails(-1));
binding.btnHamburgerProduct.setOnClickListener(v -> {
Fragment parent = getParentFragment();
if (parent != null) {
Fragment grandParent = parent.getParentFragment();
if (grandParent instanceof ListFragment) {
((ListFragment) grandParent).openDrawer();
}
}
});
return binding.getRoot();
}
@Override @Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, public void onDestroyView() {
Bundle savedInstanceState) { super.onDestroyView();
View view = inflater.inflate(R.layout.fragment_product, container, false); binding = null;
setupRecyclerView(view);
setupSearch(view);
setupSwipeRefresh(view);
loadProducts();
FloatingActionButton fab = view.findViewById(R.id.fabAddProduct);
fab.setOnClickListener(v -> openDetail(-1));
ImageButton hamburger = view.findViewById(R.id.btnHamburgerProduct);
hamburger.setOnClickListener(v -> {
ListFragment lf = (ListFragment) getParentFragment();
if (lf != null) lf.openDrawer();
});
return view;
} }
private void setupRecyclerView(View view) { /**
RecyclerView rv = view.findViewById(R.id.recyclerViewProducts); * Reloads data every time the fragment becomes visible.
adapter = new ProductAdapter(filteredList, this); */
rv.setLayoutManager(new LinearLayoutManager(getContext())); @Override
rv.setAdapter(adapter); public void onResume() {
super.onResume();
loadProductData();
loadCategoryData();
} }
private void setupSearch(View view) { /**
etSearch = view.findViewById(R.id.etSearchProduct); * Sets up the filter toggle button to show/hide the filter layout.
etSearch.addTextChangedListener(new TextWatcher() { */
public void beforeTextChanged(CharSequence s, int a, int b, int c) {} private void setupFilterToggle() {
public void afterTextChanged(Editable s) {} binding.btnToggleFilter.setOnClickListener(v -> {
public void onTextChanged(CharSequence s, int a, int b, int c) { if (binding.layoutFilter.getVisibility() == View.GONE) {
filter(); binding.layoutFilter.setVisibility(View.VISIBLE);
binding.btnToggleFilter.setImageResource(android.R.drawable.ic_menu_close_clear_cancel);
} else {
binding.layoutFilter.setVisibility(View.GONE);
binding.btnToggleFilter.setImageResource(android.R.drawable.ic_menu_search);
// Reset filters when closing
binding.etSearchProduct.setText("");
binding.spinnerCategory.setSelection(0);
} }
}); });
} }
private void setupSwipeRefresh(View view) { /**
swipeRefresh = view.findViewById(R.id.swipeRefreshProduct); * Configures the search bar for triggering data load from backend.
swipeRefresh.setOnRefreshListener(this::loadProducts); */
} private void setupSearch() {
binding.etSearchProduct.addTextChangedListener(new TextWatcher() {
private void filter() { @Override public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
String query = etSearch.getText().toString().toLowerCase(); @Override public void onTextChanged(CharSequence s, int start, int before, int count) {
loadProductData();
filteredList.clear();
for (ProductDTO p : productList) {
boolean matchesSearch = query.isEmpty() ||
(p.getProdName() != null && p.getProdName().toLowerCase().contains(query)) ||
(p.getCategoryName() != null && p.getCategoryName().toLowerCase().contains(query)) ||
(p.getProdDesc() != null && p.getProdDesc().toLowerCase().contains(query));
if (matchesSearch) {
filteredList.add(p);
} }
} @Override public void afterTextChanged(Editable s) {}
adapter.notifyDataSetChanged(); });
} }
private void loadProducts() { /**
if (swipeRefresh != null) swipeRefresh.setRefreshing(true); * Configures the category filter spinner.
RetrofitClient.getProductApi(requireContext()).getAllProducts(null, 0, 100) */
.enqueue(new Callback<PageResponse<ProductDTO>>() { private void setupCategoryFilter() {
public void onResponse(Call<PageResponse<ProductDTO>> c, binding.spinnerCategory.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
Response<PageResponse<ProductDTO>> r) { @Override
if (swipeRefresh != null) swipeRefresh.setRefreshing(false); public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
if (r.isSuccessful() && r.body() != null) { loadProductData();
productList.clear(); }
productList.addAll(r.body().getContent()); @Override public void onNothingSelected(AdapterView<?> parent) {}
filter(); });
} else {
Toast.makeText(getContext(), "Failed to load products",
Toast.LENGTH_SHORT).show();
}
}
public void onFailure(Call<PageResponse<ProductDTO>> c, Throwable t) {
if (swipeRefresh != null) swipeRefresh.setRefreshing(false);
Log.e("ProductFragment", t.getMessage());
}
});
} }
private void openDetail(int position) { /**
ProductDetailFragment detail = new ProductDetailFragment(); * 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() {
binding.swipeRefreshProduct.setOnRefreshListener(this::loadProductData);
}
/**
* Navigates to the product detail screen.
*/
private void openProductDetails(int position) {
Bundle args = new Bundle(); Bundle args = new Bundle();
if (position != -1) { if (position != -1) {
ProductDTO p = filteredList.get(position); ProductDTO product = productList.get(position);
args.putLong("prodId", p.getProdId()); args.putLong("productId", product.getProdId());
args.putString("prodName", p.getProdName());
args.putString("prodDesc", p.getProdDesc() != null ? p.getProdDesc() : "");
args.putString("prodPrice", p.getProdPrice() != null ? p.getProdPrice().toString() : "");
args.putLong("categoryId", p.getCategoryId() != null ? p.getCategoryId() : -1);
} }
detail.setArguments(args); NavHostFragment.findNavController(this).navigate(R.id.nav_product_detail, args);
ListFragment lf = (ListFragment) getParentFragment();
if (lf != null) lf.loadFragment(detail);
} }
@Override @Override
public void onProductClick(int position) { openDetail(position); } public void onProductClick(int position) {
} openProductDetails(position);
}
/**
* Fetches product data from the server with search query, category, and sorting.
*/
private void loadProductData() {
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,134 +1,268 @@
package com.example.petstoremobile.fragments.listfragments; package com.example.petstoremobile.fragments.listfragments;
import android.os.Bundle; import android.os.Bundle;
import android.text.*; import android.text.Editable;
import android.text.TextWatcher;
import android.util.Log; import android.util.Log;
import android.view.*; import android.view.LayoutInflater;
import android.widget.*; import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment; import androidx.fragment.app.Fragment;
import androidx.lifecycle.ViewModelProvider;
import androidx.navigation.fragment.NavHostFragment;
import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
import com.example.petstoremobile.R; import com.example.petstoremobile.R;
import com.example.petstoremobile.adapters.ProductSupplierAdapter; import com.example.petstoremobile.adapters.ProductSupplierAdapter;
import com.example.petstoremobile.api.RetrofitClient; import com.example.petstoremobile.databinding.FragmentProductSupplierBinding;
import com.example.petstoremobile.dtos.PageResponse; 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.fragments.ListFragment; import com.example.petstoremobile.fragments.ListFragment;
import com.example.petstoremobile.fragments.listfragments.detailfragments.ProductSupplierDetailFragment; import com.example.petstoremobile.utils.Resource;
import com.google.android.material.floatingactionbutton.FloatingActionButton; import com.example.petstoremobile.utils.SpinnerUtils;
import java.util.*; import com.example.petstoremobile.viewmodels.ProductSupplierViewModel;
import retrofit2.*; import com.example.petstoremobile.viewmodels.ProductViewModel;
import com.example.petstoremobile.viewmodels.SupplierViewModel;
import java.util.ArrayList;
import java.util.List;
import dagger.hilt.android.AndroidEntryPoint;
@AndroidEntryPoint
public class ProductSupplierFragment extends Fragment public class ProductSupplierFragment extends Fragment
implements ProductSupplierAdapter.OnProductSupplierClickListener { implements ProductSupplierAdapter.OnProductSupplierClickListener {
private FragmentProductSupplierBinding binding;
private List<ProductSupplierDTO> psList = new ArrayList<>(); private List<ProductSupplierDTO> psList = new ArrayList<>();
private List<ProductSupplierDTO> filteredList = new ArrayList<>(); private List<ProductDTO> productList = new ArrayList<>();
private List<SupplierDTO> supplierList = new ArrayList<>();
private ProductSupplierAdapter adapter; private ProductSupplierAdapter adapter;
private SwipeRefreshLayout swipeRefresh; private ProductSupplierViewModel viewModel;
private EditText etSearch; private ProductViewModel productViewModel;
private SupplierViewModel supplierViewModel;
/**
* Initializes the fragment and its associated ViewModels.
*/
@Override @Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
viewModel = new ViewModelProvider(this).get(ProductSupplierViewModel.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
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) { Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_product_supplier, container, false); binding = FragmentProductSupplierBinding.inflate(inflater, container, false);
setupRecyclerView(view); setupRecyclerView();
setupSearch(view); setupSearch();
setupSwipeRefresh(view); setupProductFilter();
loadData(); setupSupplierFilter();
setupSwipeRefresh();
setupFilterToggle();
FloatingActionButton fab = view.findViewById(R.id.fabAddPS); binding.fabAddPS.setOnClickListener(v -> openDetail(-1));
fab.setOnClickListener(v -> openDetail(-1));
ImageButton hamburger = view.findViewById(R.id.btnHamburgerPS); binding.btnHamburgerPS.setOnClickListener(v -> {
hamburger.setOnClickListener(v -> { Fragment parent = getParentFragment();
ListFragment lf = (ListFragment) getParentFragment(); if (parent != null) {
if (lf != null) lf.openDrawer(); Fragment grandParent = parent.getParentFragment();
}); if (grandParent instanceof ListFragment) {
((ListFragment) grandParent).openDrawer();
return view;
}
private void setupRecyclerView(View view) {
RecyclerView rv = view.findViewById(R.id.recyclerViewPS);
adapter = new ProductSupplierAdapter(filteredList, this);
rv.setLayoutManager(new LinearLayoutManager(getContext()));
rv.setAdapter(adapter);
}
private void setupSearch(View view) {
etSearch = view.findViewById(R.id.etSearchPS);
etSearch.addTextChangedListener(new TextWatcher() {
public void beforeTextChanged(CharSequence s, int a, int b, int c) {}
public void afterTextChanged(Editable s) {}
public void onTextChanged(CharSequence s, int a, int b, int c) {
filter(s.toString());
}
});
}
private void setupSwipeRefresh(View view) {
swipeRefresh = view.findViewById(R.id.swipeRefreshPS);
swipeRefresh.setOnRefreshListener(this::loadData);
}
private void filter(String query) {
filteredList.clear();
if (query.isEmpty()) {
filteredList.addAll(psList);
} else {
String lower = query.toLowerCase();
for (ProductSupplierDTO ps : psList) {
if ((ps.getProductName() != null && ps.getProductName().toLowerCase().contains(lower))
|| (ps.getSupplierName() != null && ps.getSupplierName().toLowerCase().contains(lower))) {
filteredList.add(ps);
} }
} }
} });
adapter.notifyDataSetChanged();
}
private void loadData() { return binding.getRoot();
if (swipeRefresh != null) swipeRefresh.setRefreshing(true);
RetrofitClient.getProductSupplierApi(requireContext()).getAllProductSuppliers(0, 100)
.enqueue(new Callback<PageResponse<ProductSupplierDTO>>() {
public void onResponse(Call<PageResponse<ProductSupplierDTO>> c,
Response<PageResponse<ProductSupplierDTO>> r) {
if (swipeRefresh != null) swipeRefresh.setRefreshing(false);
if (r.isSuccessful() && r.body() != null) {
psList.clear();
psList.addAll(r.body().getContent());
filter(etSearch != null ? etSearch.getText().toString() : "");
} else {
Toast.makeText(getContext(), "Failed to load",
Toast.LENGTH_SHORT).show();
}
}
public void onFailure(Call<PageResponse<ProductSupplierDTO>> c, Throwable t) {
if (swipeRefresh != null) swipeRefresh.setRefreshing(false);
Log.e("PSFragment", t.getMessage());
}
});
}
private void openDetail(int position) {
ProductSupplierDetailFragment detail = new ProductSupplierDetailFragment();
Bundle args = new Bundle();
if (position != -1) {
ProductSupplierDTO ps = filteredList.get(position);
args.putLong("productId", ps.getProductId());
args.putLong("supplierId", ps.getSupplierId());
args.putString("productName", ps.getProductName());
args.putString("supplierName", ps.getSupplierName());
args.putString("cost", ps.getCost() != null ? ps.getCost().toString() : "");
}
detail.setArguments(args);
ListFragment lf = (ListFragment) getParentFragment();
if (lf != null) lf.loadFragment(detail);
} }
@Override
public void onDestroyView() {
super.onDestroyView();
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() {
binding.btnToggleFilter.setOnClickListener(v -> {
if (binding.layoutFilter.getVisibility() == View.GONE) {
binding.layoutFilter.setVisibility(View.VISIBLE);
binding.btnToggleFilter.setImageResource(android.R.drawable.ic_menu_close_clear_cancel);
} else {
binding.layoutFilter.setVisibility(View.GONE);
binding.btnToggleFilter.setImageResource(android.R.drawable.ic_menu_search);
// Reset filters when closing
binding.etSearchPS.setText("");
binding.spinnerProduct.setSelection(0);
binding.spinnerSupplier.setSelection(0);
}
});
}
/**
* Initializes the RecyclerView with a layout manager and adapter for product-supplier data.
*/
private void setupRecyclerView() {
adapter = new ProductSupplierAdapter(psList, this);
binding.recyclerViewPS.setLayoutManager(new LinearLayoutManager(getContext()));
binding.recyclerViewPS.setAdapter(adapter);
}
/**
* Configures the search bar for filtering.
*/
private void setupSearch() {
binding.etSearchPS.addTextChangedListener(new TextWatcher() {
@Override public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
@Override public void onTextChanged(CharSequence s, int start, int before, int count) {
loadData();
}
@Override public void afterTextChanged(Editable s) {}
});
}
/**
* Configures the product filter spinner.
*/
private void setupProductFilter() {
binding.spinnerProduct.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
@Override
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
loadData();
}
@Override public void onNothingSelected(AdapterView<?> parent) {}
});
}
/**
* Configures the supplier filter spinner.
*/
private void setupSupplierFilter() {
binding.spinnerSupplier.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
@Override
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
loadData();
}
@Override public void onNothingSelected(AdapterView<?> parent) {}
});
}
/**
* 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() {
binding.swipeRefreshPS.setOnRefreshListener(this::loadData);
}
/**
* Fetches product-supplier data from the server through the ViewModel with search query and filters.
*/
private void loadData() {
String query = binding.etSearchPS.getText().toString().trim();
if (query.isEmpty()) query = null;
Long productId = null;
if (binding.spinnerProduct.getSelectedItemPosition() > 0 && !productList.isEmpty()) {
productId = productList.get(binding.spinnerProduct.getSelectedItemPosition() - 1).getProdId();
}
Long supplierId = null;
if (binding.spinnerSupplier.getSelectedItemPosition() > 0 && !supplierList.isEmpty()) {
supplierId = supplierList.get(binding.spinnerSupplier.getSelectedItemPosition() - 1).getSupId();
}
viewModel.getAllProductSuppliers(0, 100, query, productId, supplierId, "productName").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.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) {
Bundle args = new Bundle();
if (position != -1) {
ProductSupplierDTO ps = psList.get(position);
args.putLong("productId", ps.getProductId());
args.putLong("supplierId", ps.getSupplierId());
}
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,139 +1,231 @@
package com.example.petstoremobile.fragments.listfragments; package com.example.petstoremobile.fragments.listfragments;
import android.os.Bundle; import android.os.Bundle;
import android.text.*; import android.text.Editable;
import android.text.TextWatcher;
import android.util.Log; import android.util.Log;
import android.view.*; import android.view.LayoutInflater;
import android.widget.*; import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment; import androidx.fragment.app.Fragment;
import androidx.lifecycle.ViewModelProvider;
import androidx.navigation.fragment.NavHostFragment;
import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
import com.example.petstoremobile.R; import com.example.petstoremobile.R;
import com.example.petstoremobile.adapters.PurchaseOrderAdapter; import com.example.petstoremobile.adapters.PurchaseOrderAdapter;
import com.example.petstoremobile.api.RetrofitClient; import com.example.petstoremobile.databinding.FragmentPurchaseOrderBinding;
import com.example.petstoremobile.dtos.PageResponse;
import com.example.petstoremobile.dtos.PurchaseOrderDTO; import com.example.petstoremobile.dtos.PurchaseOrderDTO;
import com.example.petstoremobile.dtos.StoreDTO;
import com.example.petstoremobile.fragments.ListFragment; import com.example.petstoremobile.fragments.ListFragment;
import com.example.petstoremobile.fragments.listfragments.detailfragments.PurchaseOrderDetailFragment; import com.example.petstoremobile.utils.Resource;
import java.util.*; import com.example.petstoremobile.utils.SpinnerUtils;
import retrofit2.*; import com.example.petstoremobile.viewmodels.PurchaseOrderViewModel;
import com.example.petstoremobile.viewmodels.StoreViewModel;
import java.util.ArrayList;
import java.util.List;
import dagger.hilt.android.AndroidEntryPoint;
@AndroidEntryPoint
public class PurchaseOrderFragment extends Fragment public class PurchaseOrderFragment extends Fragment
implements PurchaseOrderAdapter.OnPurchaseOrderClickListener { implements PurchaseOrderAdapter.OnPurchaseOrderClickListener {
private FragmentPurchaseOrderBinding binding;
private List<PurchaseOrderDTO> poList = new ArrayList<>(); private List<PurchaseOrderDTO> poList = new ArrayList<>();
private List<PurchaseOrderDTO> filteredList = new ArrayList<>(); private List<StoreDTO> storeList = new ArrayList<>();
private PurchaseOrderAdapter adapter; private PurchaseOrderAdapter adapter;
private SwipeRefreshLayout swipeRefresh; private PurchaseOrderViewModel viewModel;
private EditText etSearch; private StoreViewModel storeViewModel;
/**
* Initializes the fragment and its associated ViewModels.
*/
@Override @Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
viewModel = new ViewModelProvider(this).get(PurchaseOrderViewModel.class);
storeViewModel = new ViewModelProvider(this).get(StoreViewModel.class);
}
/**
* Sets up the fragment's UI components, including RecyclerView, filters, and swipe-to-refresh.
*/
@Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) { Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_purchase_order, container, false); binding = FragmentPurchaseOrderBinding.inflate(inflater, container, false);
setupRecyclerView(view); setupRecyclerView();
setupSearch(view); setupSearch();
setupSwipeRefresh(view); setupStoreFilter();
loadData(); setupSwipeRefresh();
setupFilterToggle();
ImageButton hamburger = view.findViewById(R.id.btnHamburgerPO); binding.btnHamburgerPO.setOnClickListener(v -> {
hamburger.setOnClickListener(v -> { Fragment parent = getParentFragment();
ListFragment lf = (ListFragment) getParentFragment(); if (parent != null) {
if (lf != null) Fragment grandParent = parent.getParentFragment();
lf.openDrawer(); if (grandParent instanceof ListFragment) {
}); ((ListFragment) grandParent).openDrawer();
return view;
}
private void setupRecyclerView(View view) {
RecyclerView rv = view.findViewById(R.id.recyclerViewPO);
adapter = new PurchaseOrderAdapter(filteredList, this);
rv.setLayoutManager(new LinearLayoutManager(getContext()));
rv.setAdapter(adapter);
}
private void setupSearch(View view) {
etSearch = view.findViewById(R.id.etSearchPO);
etSearch.addTextChangedListener(new TextWatcher() {
public void beforeTextChanged(CharSequence s, int a, int b, int c) {
}
public void afterTextChanged(Editable s) {
}
public void onTextChanged(CharSequence s, int a, int b, int c) {
filter(s.toString());
}
});
}
private void setupSwipeRefresh(View view) {
swipeRefresh = view.findViewById(R.id.swipeRefreshPO);
swipeRefresh.setOnRefreshListener(this::loadData);
}
private void filter(String query) {
filteredList.clear();
if (query.isEmpty()) {
filteredList.addAll(poList);
} else {
String lower = query.toLowerCase();
for (PurchaseOrderDTO po : poList) {
if ((po.getSupplierName() != null && po.getSupplierName().toLowerCase().contains(lower))
|| (po.getStatus() != null && po.getStatus().toLowerCase().contains(lower))) {
filteredList.add(po);
} }
} }
} });
adapter.notifyDataSetChanged();
return binding.getRoot();
} }
@Override
public void onDestroyView() {
super.onDestroyView();
binding = null;
}
/**
* Reloads data every time the fragment becomes visible.
*/
@Override
public void onResume() {
super.onResume();
loadData();
loadStoreData();
}
/**
* Sets up the filter toggle button to show/hide the filter layout.
*/
private void setupFilterToggle() {
binding.btnToggleFilter.setOnClickListener(v -> {
if (binding.layoutFilter.getVisibility() == View.GONE) {
binding.layoutFilter.setVisibility(View.VISIBLE);
binding.btnToggleFilter.setImageResource(android.R.drawable.ic_menu_close_clear_cancel);
} else {
binding.layoutFilter.setVisibility(View.GONE);
binding.btnToggleFilter.setImageResource(android.R.drawable.ic_menu_search);
// Reset filters when closing
binding.etSearchPO.setText("");
binding.spinnerStore.setSelection(0);
}
});
}
/**
* Configures the search bar for filtering.
*/
private void setupSearch() {
binding.etSearchPO.addTextChangedListener(new TextWatcher() {
@Override public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
@Override public void onTextChanged(CharSequence s, int start, int before, int count) {
loadData();
}
@Override public void afterTextChanged(Editable s) {}
});
}
/**
* Configures the store filter spinner.
*/
private void setupStoreFilter() {
binding.spinnerStore.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
@Override
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
loadData();
}
@Override public void onNothingSelected(AdapterView<?> parent) {}
});
}
/**
* 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() {
adapter = new PurchaseOrderAdapter(poList, this);
binding.recyclerViewPO.setLayoutManager(new LinearLayoutManager(getContext()));
binding.recyclerViewPO.setAdapter(adapter);
}
/**
* Sets up the SwipeRefreshLayout to allow manual reloading of purchase order data.
*/
private void setupSwipeRefresh() {
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() {
if (swipeRefresh != null) String query = binding.etSearchPO != null ? binding.etSearchPO.getText().toString().trim() : "";
swipeRefresh.setRefreshing(true); if (query.isEmpty()) query = null;
RetrofitClient.getPurchaseOrderApi(requireContext()).getAllPurchaseOrders(0, 100)
.enqueue(new Callback<PageResponse<PurchaseOrderDTO>>() {
public void onResponse(Call<PageResponse<PurchaseOrderDTO>> c,
Response<PageResponse<PurchaseOrderDTO>> r) {
if (swipeRefresh != null)
swipeRefresh.setRefreshing(false);
if (r.isSuccessful() && r.body() != null) {
poList.clear();
poList.addAll(r.body().getContent());
filter(etSearch != null ? etSearch.getText().toString() : "");
} else {
Toast.makeText(getContext(), "Failed to load purchase orders",
Toast.LENGTH_SHORT).show();
}
}
public void onFailure(Call<PageResponse<PurchaseOrderDTO>> c, Throwable t) { Long storeId = null;
if (swipeRefresh != null) if (binding.spinnerStore.getSelectedItemPosition() > 0 && !storeList.isEmpty()) {
swipeRefresh.setRefreshing(false); storeId = storeList.get(binding.spinnerStore.getSelectedItemPosition() - 1).getStoreId();
Log.e("POFragment", t.getMessage()); }
viewModel.getAllPurchaseOrders(0, 100, query, storeId, "purchaseOrderId,desc").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.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) {
PurchaseOrderDetailFragment detail = new PurchaseOrderDetailFragment();
Bundle args = new Bundle(); Bundle args = new Bundle();
PurchaseOrderDTO po = filteredList.get(position); PurchaseOrderDTO po = poList.get(position);
args.putLong("purchaseOrderId", po.getPurchaseOrderId()); args.putLong("purchaseOrderId", po.getPurchaseOrderId());
args.putString("supplierName", po.getSupplierName()); NavHostFragment.findNavController(this).navigate(R.id.nav_purchase_order_detail, args);
args.putString("orderDate", po.getOrderDate());
args.putString("status", po.getStatus());
detail.setArguments(args);
ListFragment lf = (ListFragment) getParentFragment();
if (lf != null)
lf.loadFragment(detail);
} }
/**
* Handles item click in the purchase order list.
*/
@Override @Override
public void onPurchaseOrderClick(int position) { public void onPurchaseOrderClick(int position) {
openDetail(position); openDetail(position);
} }
} }

View File

@@ -1,63 +1,71 @@
package com.example.petstoremobile.fragments.listfragments; package com.example.petstoremobile.fragments.listfragments;
import android.os.Bundle; import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment; import androidx.fragment.app.Fragment;
import androidx.navigation.fragment.NavHostFragment;
import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
import android.text.Editable; import android.text.Editable;
import android.text.TextWatcher; import android.text.TextWatcher;
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.EditText;
import android.widget.ImageButton;
import com.example.petstoremobile.R; import com.example.petstoremobile.R;
import com.example.petstoremobile.adapters.SaleAdapter; import com.example.petstoremobile.adapters.SaleAdapter;
import com.example.petstoremobile.api.SaleApi;
import com.example.petstoremobile.databinding.FragmentSaleBinding;
import com.example.petstoremobile.fragments.ListFragment; import com.example.petstoremobile.fragments.ListFragment;
import com.example.petstoremobile.fragments.listfragments.detailfragments.RefundDetailFragment;
import com.example.petstoremobile.models.Sale; import com.example.petstoremobile.models.Sale;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import javax.inject.Inject;
import dagger.hilt.android.AndroidEntryPoint;
@AndroidEntryPoint
public class SaleFragment extends Fragment implements SaleAdapter.OnSaleClickListener { public class SaleFragment extends Fragment implements SaleAdapter.OnSaleClickListener {
private FragmentSaleBinding binding;
private List<Sale> saleList = new ArrayList<>(); private List<Sale> saleList = new ArrayList<>();
private List<Sale> filteredList = new ArrayList<>(); private List<Sale> filteredList = new ArrayList<>();
private SaleAdapter adapter; private SaleAdapter adapter;
private SwipeRefreshLayout swipeRefreshLayout;
private EditText etSearch; @Inject SaleApi api;
private ImageButton btnHamburger;
@Override @Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) { Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_sale, container, false); binding = FragmentSaleBinding.inflate(inflater, container, false);
btnHamburger = view.findViewById(R.id.btnHamburger); setupRecyclerView();
setupRecyclerView(view);
loadSaleData(); loadSaleData();
setupSearch(view); setupSearch();
setupSwipeRefresh(view); setupSwipeRefresh();
// Make the hamburger button open the drawer from listFragment // Make the hamburger button open the drawer from listFragment
if (btnHamburger != null) { binding.btnHamburger.setOnClickListener(v -> {
btnHamburger.setOnClickListener(v -> { Fragment parent = getParentFragment();
ListFragment listFragment = (ListFragment) getParentFragment(); if (parent != null) {
if (listFragment != null) { Fragment grandParent = parent.getParentFragment();
listFragment.openDrawer(); if (grandParent instanceof ListFragment) {
((ListFragment) grandParent).openDrawer();
} }
}); }
} });
return view; return binding.getRoot();
} }
private void setupSearch(View view) { @Override
etSearch = view.findViewById(R.id.etSearchSale); public void onDestroyView() {
etSearch.addTextChangedListener(new TextWatcher() { super.onDestroyView();
binding = null;
}
private void setupSearch() {
binding.etSearchSale.addTextChangedListener(new TextWatcher() {
@Override public void beforeTextChanged(CharSequence s, int start, int count, int after) { @Override public void beforeTextChanged(CharSequence s, int start, int count, int after) {
} }
@@ -91,11 +99,10 @@ public class SaleFragment extends Fragment implements SaleAdapter.OnSaleClickLis
adapter.notifyDataSetChanged(); adapter.notifyDataSetChanged();
} }
private void setupSwipeRefresh(View view) { private void setupSwipeRefresh() {
swipeRefreshLayout = view.findViewById(R.id.swipeRefreshSale); binding.swipeRefreshSale.setOnRefreshListener(() -> {
swipeRefreshLayout.setOnRefreshListener(() -> {
loadSaleData(); loadSaleData();
swipeRefreshLayout.setRefreshing(false); binding.swipeRefreshSale.setRefreshing(false);
}); });
} }
@@ -103,19 +110,14 @@ public class SaleFragment extends Fragment implements SaleAdapter.OnSaleClickLis
@Override @Override
public void onSaleClick(int position) { public void onSaleClick(int position) {
Sale sale = filteredList.get(position); Sale sale = filteredList.get(position);
RefundDetailFragment refundFragment = new RefundDetailFragment();
Bundle args = new Bundle(); Bundle args = new Bundle();
args.putInt("saleId", sale.getSaleId()); args.putInt("saleId", sale.getSaleId());
args.putString("saleDate", sale.getSaleDate()); args.putString("saleDate", sale.getSaleDate());
args.putString("employeeName", sale.getEmployeeName()); args.putString("employeeName", sale.getEmployeeName());
args.putDouble("total", sale.getTotal()); args.putDouble("total", sale.getTotal());
args.putString("paymentMethod", sale.getPaymentMethod()); args.putString("paymentMethod", sale.getPaymentMethod());
refundFragment.setArguments(args);
refundFragment.setSaleFragment(this); NavHostFragment.findNavController(this).navigate(R.id.nav_refund_detail, args);
ListFragment listFragment = (ListFragment) getParentFragment();
if (listFragment != null)
listFragment.loadFragment(refundFragment);
} }
public void reloadSales() { public void reloadSales() {
@@ -135,10 +137,9 @@ public class SaleFragment extends Fragment implements SaleAdapter.OnSaleClickLis
adapter.notifyDataSetChanged(); adapter.notifyDataSetChanged();
} }
private void setupRecyclerView(View view) { private void setupRecyclerView() {
RecyclerView recyclerView = view.findViewById(R.id.recyclerViewSales);
adapter = new SaleAdapter(filteredList, this); adapter = new SaleAdapter(filteredList, this);
recyclerView.setLayoutManager(new LinearLayoutManager(getContext())); binding.recyclerViewSales.setLayoutManager(new LinearLayoutManager(getContext()));
recyclerView.setAdapter(adapter); binding.recyclerViewSales.setAdapter(adapter);
} }
} }

View File

@@ -2,10 +2,12 @@ package com.example.petstoremobile.fragments.listfragments;
import android.os.Bundle; import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment; import androidx.fragment.app.Fragment;
import androidx.lifecycle.ViewModelProvider;
import androidx.navigation.fragment.NavHostFragment;
import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
import android.text.Editable; import android.text.Editable;
import android.text.TextWatcher; import android.text.TextWatcher;
@@ -13,177 +15,180 @@ 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.EditText;
import android.widget.ImageButton;
import android.widget.Toast; import android.widget.Toast;
import com.example.petstoremobile.R; import com.example.petstoremobile.R;
import com.example.petstoremobile.adapters.ServiceAdapter; import com.example.petstoremobile.adapters.ServiceAdapter;
import com.example.petstoremobile.api.RetrofitClient; import com.example.petstoremobile.databinding.FragmentServiceBinding;
import com.example.petstoremobile.api.ServiceApi;
import com.example.petstoremobile.dtos.PageResponse;
import com.example.petstoremobile.dtos.ServiceDTO; import com.example.petstoremobile.dtos.ServiceDTO;
import com.example.petstoremobile.fragments.ListFragment; import com.example.petstoremobile.fragments.ListFragment;
import com.example.petstoremobile.fragments.listfragments.detailfragments.ServiceDetailFragment; import com.example.petstoremobile.viewmodels.ServiceViewModel;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import retrofit2.Call; import dagger.hilt.android.AndroidEntryPoint;
import retrofit2.Callback;
import retrofit2.Response;
@AndroidEntryPoint
public class ServiceFragment extends Fragment implements ServiceAdapter.OnServiceClickListener { public class ServiceFragment extends Fragment implements ServiceAdapter.OnServiceClickListener {
private FragmentServiceBinding binding;
private List<ServiceDTO> serviceList = new ArrayList<>(); private List<ServiceDTO> serviceList = new ArrayList<>();
private List<ServiceDTO> filteredList = new ArrayList<>();
private ServiceAdapter adapter; private ServiceAdapter adapter;
private ImageButton hamburger; private ServiceViewModel viewModel;
private ServiceApi api;
private SwipeRefreshLayout swipeRefreshLayout;
private EditText etSearch;
//load service view /**
* Initializes the fragment and its associated ServiceViewModel.
*/
@Override @Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
viewModel = new ViewModelProvider(this).get(ServiceViewModel.class);
}
/**
* Sets up the fragment's UI components, including RecyclerView, search, and swipe-to-refresh.
*/
@Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) { Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_service, container, false); binding = FragmentServiceBinding.inflate(inflater, container, false);
api = RetrofitClient.getServiceApi(requireContext()); setupRecyclerView();
hamburger = view.findViewById(R.id.btnHamburger); setupSearch();
setupSwipeRefresh();
setupRecyclerView(view); setupFilterToggle();
setupSearch(view);
setupSwipeRefresh(view);
loadServiceData(); loadServiceData();
//Add button to opens the add dialog //Add button to opens the add dialog
FloatingActionButton fabAddService = view.findViewById(R.id.fabAddService); binding.fabAddService.setOnClickListener(v -> openServiceDetails(-1));
fabAddService.setOnClickListener(v -> openServiceDetails(-1));
//Make the hamburger button open the drawer from listFragment //Make the hamburger button open the drawer from listFragment
hamburger.setOnClickListener(v -> { binding.btnHamburger.setOnClickListener(v -> {
ListFragment listFragment = (ListFragment) getParentFragment(); Fragment parent = getParentFragment();
//if list fragment is found then use its helper function to open the drawer if (parent != null) {
if (listFragment != null) { Fragment grandParent = parent.getParentFragment();
listFragment.openDrawer(); if (grandParent instanceof ListFragment) {
((ListFragment) grandParent).openDrawer();
}
} }
}); });
return view; return binding.getRoot();
} }
private void setupSearch(View view) { @Override
etSearch = view.findViewById(R.id.etSearchService); public void onDestroyView() {
etSearch.addTextChangedListener(new TextWatcher() { super.onDestroyView();
binding = null;
}
/**
* Sets up the filter toggle button to show/hide the filter layout.
*/
private void setupFilterToggle() {
binding.btnToggleFilter.setOnClickListener(v -> {
if (binding.layoutFilter.getVisibility() == View.GONE) {
binding.layoutFilter.setVisibility(View.VISIBLE);
binding.btnToggleFilter.setImageResource(android.R.drawable.ic_menu_close_clear_cancel);
} else {
binding.layoutFilter.setVisibility(View.GONE);
binding.btnToggleFilter.setImageResource(android.R.drawable.ic_menu_search);
// Reset search when closing
binding.etSearchService.setText("");
}
});
}
/**
* Configures the search bar for filtering.
*/
private void setupSearch() {
binding.etSearchService.addTextChangedListener(new TextWatcher() {
@Override public void beforeTextChanged(CharSequence s, int start, int count, int after) {} @Override public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
@Override public void onTextChanged(CharSequence s, int start, int before, int count) { @Override public void onTextChanged(CharSequence s, int start, int before, int count) {
filterServices(s.toString()); loadServiceData();
} }
@Override public void afterTextChanged(Editable s) {} @Override public void afterTextChanged(Editable s) {}
}); });
} }
private void filterServices(String query) { /**
filteredList.clear(); * Sets up the SwipeRefreshLayout to allow manual reloading of service data.
if (query.isEmpty()) { */
filteredList.addAll(serviceList); private void setupSwipeRefresh() {
} else { binding.swipeRefreshService.setOnRefreshListener(this::loadServiceData);
String lower = query.toLowerCase();
for (ServiceDTO s : serviceList) {
if (s.getServiceName().toLowerCase().contains(lower)
|| s.getServiceDesc().toLowerCase().contains(lower)) {
filteredList.add(s);
}
}
}
adapter.notifyDataSetChanged();
} }
private void setupSwipeRefresh(View view) { /**
swipeRefreshLayout = view.findViewById(R.id.swipeRefreshService); * Navigates to the service detail screen for editing an existing service or adding a new one.
swipeRefreshLayout.setOnRefreshListener(() -> { */
loadServiceData();
});
}
//Open the service detail view depending on the mode
private void openServiceDetails(int position) { private void openServiceDetails(int position) {
ServiceDetailFragment detailFragment = new ServiceDetailFragment();
//Make a bundle to pass data to the detail fragment //Make a bundle to pass data to the detail fragment
Bundle args = new Bundle(); Bundle args = new Bundle();
args.putInt("position", position);
//if editing a service, add the service data to the bundle //if editing a service, add the service id to the bundle
if (position != -1) { if (position != -1) {
ServiceDTO service = filteredList.get(position); ServiceDTO service = serviceList.get(position);
args.putInt("serviceId", service.getServiceId().intValue()); args.putLong("serviceId", service.getServiceId());
args.putString("serviceName", service.getServiceName());
args.putString("serviceDesc", service.getServiceDesc());
args.putInt("serviceDuration", service.getServiceDuration());
args.putDouble("servicePrice", service.getServicePrice());
} }
//send the bundle to the detail fragment to display NavHostFragment.findNavController(this).navigate(R.id.nav_service_detail, args);
detailFragment.setArguments(args);
//set the service fragment to the parent so we refer back to service view when save or delete is done
detailFragment.setServiceFragment(this);
//get ListFragment to load the the detail view
ListFragment listFragment = (ListFragment) getParentFragment();
if (listFragment != null) {
listFragment.loadFragment(detailFragment);
}
} }
// Called by ServiceAdapter when a row is clicked to open the details view /**
* Handles item click in the service list.
*/
@Override @Override
public void onServiceClick(int position) { public void onServiceClick(int position) {
openServiceDetails(position); openServiceDetails(position);
} }
// Helper function to get a list of all services from the backend /**
* Fetches all service data from the server through the ViewModel and updates the UI.
*/
private void loadServiceData() { private void loadServiceData() {
if (swipeRefreshLayout != null) { String query = binding.etSearchService != null ? binding.etSearchService.getText().toString().trim() : "";
swipeRefreshLayout.setRefreshing(true); if (query.isEmpty()) query = null;
}
api.getAllServices(0, 100).enqueue(new Callback<PageResponse<ServiceDTO>>() {
@Override
public void onResponse(Call<PageResponse<ServiceDTO>> call, Response<PageResponse<ServiceDTO>> response) {
if (swipeRefreshLayout != null) {
swipeRefreshLayout.setRefreshing(false);
}
if (response.isSuccessful() && response.body() != null) {
serviceList.clear();
serviceList.addAll(response.body().getContent());
filterServices(etSearch.getText().toString());
} else { //Load services from the backend with query and default sort
Log.e("onResponse: ", response.message()); viewModel.getAllServices(0, 100, query, "serviceName").observe(getViewLifecycleOwner(), resource -> {
} if (resource == null) return;
}
@Override // Check the status to see if the resource is loaded and display the data
public void onFailure(Call<PageResponse<ServiceDTO>> call, Throwable t) { switch (resource.status) {
if (swipeRefreshLayout != null) { case LOADING:
swipeRefreshLayout.setRefreshing(false); // Show loading indicator
} binding.swipeRefreshService.setRefreshing(true);
if (getContext() != null) { break;
Toast.makeText(getContext(), case SUCCESS:
"Failed to load services", Toast.LENGTH_SHORT).show(); // Hide loading indicator and display data
} binding.swipeRefreshService.setRefreshing(false);
Log.e("onFailure: ", t.getMessage()); if (resource.data != null) {
serviceList.clear();
serviceList.addAll(resource.data.getContent());
adapter.notifyDataSetChanged();
}
break;
case ERROR:
// Hide loading indicator and toast error message
binding.swipeRefreshService.setRefreshing(false);
if (getContext() != null) {
Toast.makeText(getContext(), "Failed to load services: " + resource.message, Toast.LENGTH_SHORT).show();
}
Log.e("ServiceFragment", "Error loading services: " + resource.message);
break;
} }
}); });
} }
//set up the recyclerview and adapter /**
private void setupRecyclerView(View view) { * Initializes the RecyclerView with a layout manager and adapter for services.
RecyclerView recyclerView = view.findViewById(R.id.recyclerViewServices); */
adapter = new ServiceAdapter(filteredList, this); private void setupRecyclerView() {
recyclerView.setLayoutManager(new LinearLayoutManager(getContext())); adapter = new ServiceAdapter(serviceList, this);
recyclerView.setAdapter(adapter); binding.recyclerViewServices.setLayoutManager(new LinearLayoutManager(getContext()));
binding.recyclerViewServices.setAdapter(adapter);
} }
} }

View File

@@ -2,10 +2,12 @@ package com.example.petstoremobile.fragments.listfragments;
import android.os.Bundle; import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment; import androidx.fragment.app.Fragment;
import androidx.lifecycle.ViewModelProvider;
import androidx.navigation.fragment.NavHostFragment;
import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
import android.text.Editable; import android.text.Editable;
import android.text.TextWatcher; import android.text.TextWatcher;
@@ -13,180 +15,181 @@ 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.EditText;
import android.widget.ImageButton;
import android.widget.Toast; 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.api.RetrofitClient; import com.example.petstoremobile.databinding.FragmentSupplierBinding;
import com.example.petstoremobile.api.SupplierApi;
import com.example.petstoremobile.dtos.PageResponse;
import com.example.petstoremobile.dtos.SupplierDTO; import com.example.petstoremobile.dtos.SupplierDTO;
import com.example.petstoremobile.fragments.ListFragment; import com.example.petstoremobile.fragments.ListFragment;
import com.example.petstoremobile.fragments.listfragments.detailfragments.SupplierDetailFragment; import com.example.petstoremobile.viewmodels.SupplierViewModel;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import retrofit2.Call; import dagger.hilt.android.AndroidEntryPoint;
import retrofit2.Callback;
import retrofit2.Response;
@AndroidEntryPoint
public class SupplierFragment extends Fragment implements SupplierAdapter.OnSupplierClickListener { public class SupplierFragment extends Fragment implements SupplierAdapter.OnSupplierClickListener {
private FragmentSupplierBinding binding;
private List<SupplierDTO> supplierList = new ArrayList<>(); private List<SupplierDTO> supplierList = new ArrayList<>();
private List<SupplierDTO> filteredList = new ArrayList<>();
private SupplierAdapter adapter; private SupplierAdapter adapter;
private ImageButton hamburger; private SupplierViewModel viewModel;
private SupplierApi api;
private SwipeRefreshLayout swipeRefreshLayout;
private EditText etSearch;
//load supplier view /**
* Initializes the fragment and its associated SupplierViewModel.
*/
@Override @Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
viewModel = new ViewModelProvider(this).get(SupplierViewModel.class);
}
/**
* Sets up the fragment's UI components, including RecyclerView, search, and swipe-to-refresh.
*/
@Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) { Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_supplier, container, false); binding = FragmentSupplierBinding.inflate(inflater, container, false);
api = RetrofitClient.getSupplierApi(requireContext()); setupRecyclerView();
hamburger = view.findViewById(R.id.btnHamburger); setupSearch();
setupSwipeRefresh();
setupRecyclerView(view); setupFilterToggle();
setupSearch(view);
setupSwipeRefresh(view);
loadSupplierData(); loadSupplierData();
//Add button to opens the add dialog //Add button to opens the add dialog
FloatingActionButton fabAddSupplier = view.findViewById(R.id.fabAddSupplier); binding.fabAddSupplier.setOnClickListener(v -> openSupplierDetails(-1));
fabAddSupplier.setOnClickListener(v -> openSupplierDetails(-1));
//Make the hamburger button open the drawer from listFragment //Make the hamburger button open the drawer from listFragment
hamburger.setOnClickListener(v -> { binding.btnHamburger.setOnClickListener(v -> {
ListFragment listFragment = (ListFragment) getParentFragment(); Fragment parent = getParentFragment();
//if list fragment is found then use its helper function to open the drawer if (parent != null) {
if (listFragment != null) { Fragment grandParent = parent.getParentFragment();
listFragment.openDrawer(); if (grandParent instanceof ListFragment) {
((ListFragment) grandParent).openDrawer();
}
} }
}); });
return view; return binding.getRoot();
} }
private void setupSearch(View view) { @Override
etSearch = view.findViewById(R.id.etSearchSupplier); public void onDestroyView() {
etSearch.addTextChangedListener(new TextWatcher() { super.onDestroyView();
binding = null;
}
/**
* Sets up the filter toggle button to show/hide the filter layout.
*/
private void setupFilterToggle() {
binding.btnToggleFilter.setOnClickListener(v -> {
if (binding.layoutFilter.getVisibility() == View.GONE) {
binding.layoutFilter.setVisibility(View.VISIBLE);
binding.btnToggleFilter.setImageResource(android.R.drawable.ic_menu_close_clear_cancel);
} else {
binding.layoutFilter.setVisibility(View.GONE);
binding.btnToggleFilter.setImageResource(android.R.drawable.ic_menu_search);
// Reset search when closing
binding.etSearchSupplier.setText("");
}
});
}
/**
* Configures the search bar for filtering.
*/
private void setupSearch() {
binding.etSearchSupplier.addTextChangedListener(new TextWatcher() {
@Override public void beforeTextChanged(CharSequence s, int start, int count, int after) {} @Override public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
@Override public void onTextChanged(CharSequence s, int start, int before, int count) { @Override public void onTextChanged(CharSequence s, int start, int before, int count) {
filterSuppliers(s.toString()); loadSupplierData();
} }
@Override public void afterTextChanged(Editable s) {} @Override public void afterTextChanged(Editable s) {}
}); });
} }
private void filterSuppliers(String query) { /**
filteredList.clear(); * Sets up the SwipeRefreshLayout to allow manual reloading of supplier data.
if (query.isEmpty()) { */
filteredList.addAll(supplierList); private void setupSwipeRefresh() {
} else { binding.swipeRefreshSupplier.setOnRefreshListener(this::loadSupplierData);
String lower = query.toLowerCase();
for (SupplierDTO s : supplierList) {
if (s.getSupCompany().toLowerCase().contains(lower)
|| s.getSupContactFirstName().toLowerCase().contains(lower)
|| s.getSupContactLastName().toLowerCase().contains(lower)) {
filteredList.add(s);
}
}
}
adapter.notifyDataSetChanged();
} }
private void setupSwipeRefresh(View view) { /**
swipeRefreshLayout = view.findViewById(R.id.swipeRefreshSupplier); * Navigates to the supplier detail screen for editing an existing record or adding a new one.
swipeRefreshLayout.setOnRefreshListener(() -> { */
loadSupplierData();
});
}
//Open the supplier detail view depending on the mode
private void openSupplierDetails(int position) { private void openSupplierDetails(int position) {
SupplierDetailFragment detailFragment = new SupplierDetailFragment();
//Make a bundle to pass data to the detail fragment //Make a bundle to pass data to the detail fragment
Bundle args = new Bundle(); Bundle args = new Bundle();
args.putInt("position", position);
//if editing a supplier, add the supplier data to the bundle //if editing a supplier, add the supplier id to the bundle
if (position != -1) { if (position != -1) {
SupplierDTO supplier = filteredList.get(position); SupplierDTO supplier = supplierList.get(position);
args.putInt("supId", supplier.getSupId().intValue()); args.putLong("supId", supplier.getSupId());
args.putString("supCompany", supplier.getSupCompany());
args.putString("supContactFirstName", supplier.getSupContactFirstName());
args.putString("supContactLastName", supplier.getSupContactLastName());
args.putString("supEmail", supplier.getSupEmail());
args.putString("supPhone", supplier.getSupPhone());
} }
//send the bundle to the detail fragment to display NavHostFragment.findNavController(this).navigate(R.id.nav_supplier_detail, args);
detailFragment.setArguments(args);
//set the supplier fragment to the parent so we refer back to supplier view when save or delete is done
detailFragment.setSupplierFragment(this);
//get ListFragment to load the the detail view
ListFragment listFragment = (ListFragment) getParentFragment();
if (listFragment != null) {
listFragment.loadFragment(detailFragment);
}
} }
// Called by SupplierAdapter when a row is clicked to open the details view /**
* Handles item click in the supplier list.
*/
@Override @Override
public void onSupplierClick(int position) { public void onSupplierClick(int position) {
openSupplierDetails(position); openSupplierDetails(position);
} }
// Helper function to get a list of all suppliers from the backend /**
* Fetches all supplier data from the server through the ViewModel and updates the UI.
*/
private void loadSupplierData() { private void loadSupplierData() {
if (swipeRefreshLayout != null) { String query = binding.etSearchSupplier != null ? binding.etSearchSupplier.getText().toString().trim() : "";
swipeRefreshLayout.setRefreshing(true); if (query.isEmpty()) query = null;
}
api.getAllSuppliers(0, 100).enqueue(new Callback<PageResponse<SupplierDTO>>() {
@Override
public void onResponse(Call<PageResponse<SupplierDTO>> call, Response<PageResponse<SupplierDTO>> response) {
if (swipeRefreshLayout != null) {
swipeRefreshLayout.setRefreshing(false);
}
if (response.isSuccessful() && response.body() != null) {
supplierList.clear();
supplierList.addAll(response.body().getContent());
filterSuppliers(etSearch.getText().toString());
} else { //Load suppliers from the backend with query and default sort
Log.e("onResponse: ", response.message()); viewModel.getAllSuppliers(0, 100, query, "supCompany").observe(getViewLifecycleOwner(), resource -> {
} if (resource == null) return;
}
@Override // Check the status to see if the resource is loaded and display the data
public void onFailure(Call<PageResponse<SupplierDTO>> call, Throwable t) { switch (resource.status) {
if (swipeRefreshLayout != null) { case LOADING:
swipeRefreshLayout.setRefreshing(false); // Show loading indicator
} binding.swipeRefreshSupplier.setRefreshing(true);
if (getContext() != null) { break;
Toast.makeText(getContext(), case SUCCESS:
"Failed to load suppliers", Toast.LENGTH_SHORT).show(); // Hide loading indicator and display data
} binding.swipeRefreshSupplier.setRefreshing(false);
Log.e("onFailure: ", t.getMessage()); 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;
} }
}); });
} }
//set up the recyclerview and adapter /**
private void setupRecyclerView(View view) { * Initializes the RecyclerView with a layout manager and adapter for displaying suppliers.
RecyclerView recyclerView = view.findViewById(R.id.recyclerViewSuppliers); */
adapter = new SupplierAdapter(filteredList, this); private void setupRecyclerView() {
recyclerView.setLayoutManager(new LinearLayoutManager(getContext())); adapter = new SupplierAdapter(supplierList, this);
recyclerView.setAdapter(adapter); binding.recyclerViewSuppliers.setLayoutManager(new LinearLayoutManager(getContext()));
binding.recyclerViewSuppliers.setAdapter(adapter);
} }
} }

View File

@@ -2,26 +2,34 @@ package com.example.petstoremobile.fragments.listfragments.detailfragments;
import android.app.DatePickerDialog; import android.app.DatePickerDialog;
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.annotation.Nullable;
import androidx.fragment.app.Fragment; import androidx.fragment.app.Fragment;
import com.example.petstoremobile.R; import androidx.lifecycle.ViewModelProvider;
import com.example.petstoremobile.adapters.BlackTextArrayAdapter; import androidx.navigation.fragment.NavHostFragment;
import com.example.petstoremobile.api.*;
import com.example.petstoremobile.dtos.*;
import com.example.petstoremobile.fragments.ListFragment;
import java.util.*;
import retrofit2.*;
import com.example.petstoremobile.databinding.FragmentAdoptionDetailBinding;
import com.example.petstoremobile.dtos.*;
import com.example.petstoremobile.utils.DialogUtils;
import com.example.petstoremobile.utils.Resource;
import com.example.petstoremobile.utils.SpinnerUtils;
import com.example.petstoremobile.viewmodels.AdoptionViewModel;
import com.example.petstoremobile.viewmodels.CustomerViewModel;
import com.example.petstoremobile.viewmodels.PetViewModel;
import java.util.*;
import dagger.hilt.android.AndroidEntryPoint;
/**
* Fragment for displaying and editing adoption request details.
*/
@AndroidEntryPoint
public class AdoptionDetailFragment extends Fragment { public class AdoptionDetailFragment extends Fragment {
private TextView tvMode, tvAdoptionId; private FragmentAdoptionDetailBinding binding;
private EditText etAdoptionDate;
private Spinner spinnerPet, spinnerCustomer, spinnerStatus;
private Button btnSave, btnDelete, btnBack;
private long adoptionId = -1; private long adoptionId = -1;
private boolean isEditing = false; private boolean isEditing = false;
@@ -33,44 +41,59 @@ public class AdoptionDetailFragment extends Fragment {
private final String[] STATUSES = {"Pending", "Approved", "Rejected"}; private final String[] STATUSES = {"Pending", "Approved", "Rejected"};
private AdoptionViewModel adoptionViewModel;
private PetViewModel petViewModel;
private CustomerViewModel customerViewModel;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
adoptionViewModel = new ViewModelProvider(this).get(AdoptionViewModel.class);
petViewModel = new ViewModelProvider(this).get(PetViewModel.class);
customerViewModel = new ViewModelProvider(this).get(CustomerViewModel.class);
}
@Override @Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) { Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_adoption_detail, container, false); binding = FragmentAdoptionDetailBinding.inflate(inflater, container, false);
initViews(view); return binding.getRoot();
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
setupSpinners(); setupSpinners();
setupDatePicker(); setupDatePicker();
loadData(); loadSpinnersData();
handleArguments(); handleArguments();
btnBack.setOnClickListener(v -> navigateBack()); binding.btnAdoptionBack.setOnClickListener(v -> navigateBack());
btnSave.setOnClickListener(v -> saveAdoption()); binding.btnSaveAdoption.setOnClickListener(v -> saveAdoption());
btnDelete.setOnClickListener(v -> confirmDelete()); binding.btnDeleteAdoption.setOnClickListener(v -> confirmDelete());
return view;
} }
private void initViews(View v) { @Override
tvMode = v.findViewById(R.id.tvAdoptionMode); public void onDestroyView() {
tvAdoptionId = v.findViewById(R.id.tvAdoptionId); super.onDestroyView();
etAdoptionDate = v.findViewById(R.id.etAdoptionDate); binding = null;
spinnerPet = v.findViewById(R.id.spinnerAdoptionPet);
spinnerCustomer= v.findViewById(R.id.spinnerAdoptionCustomer);
spinnerStatus = v.findViewById(R.id.spinnerAdoptionStatus);
btnSave = v.findViewById(R.id.btnSaveAdoption);
btnDelete = v.findViewById(R.id.btnDeleteAdoption);
btnBack = v.findViewById(R.id.btnAdoptionBack);
} }
/**
* Configures the spinner for adoption status.
*/
private void setupSpinners() { private void setupSpinners() {
spinnerStatus.setAdapter(new BlackTextArrayAdapter<>(requireContext(), SpinnerUtils.setupStringSpinner(requireContext(), binding.spinnerAdoptionStatus, STATUSES);
android.R.layout.simple_spinner_item, STATUSES));
} }
/**
* Configures the date picker dialog for the adoption date field.
*/
private void setupDatePicker() { private void setupDatePicker() {
etAdoptionDate.setOnClickListener(v -> { binding.etAdoptionDate.setOnClickListener(v -> {
Calendar c = Calendar.getInstance(); Calendar c = Calendar.getInstance();
new DatePickerDialog(requireContext(), new DatePickerDialog(requireContext(),
(dp, y, m, d) -> etAdoptionDate.setText( (dp, y, m, d) -> binding.etAdoptionDate.setText(
String.format("%04d-%02d-%02d", y, m + 1, d)), String.format("%04d-%02d-%02d", y, m + 1, d)),
c.get(Calendar.YEAR), c.get(Calendar.YEAR),
c.get(Calendar.MONTH), c.get(Calendar.MONTH),
@@ -78,117 +101,116 @@ public class AdoptionDetailFragment extends Fragment {
}); });
} }
private void loadData() { /**
* Fetches required data for spinners from the backend.
*/
private void loadSpinnersData() {
loadPets(); loadPets();
loadCustomers(); loadCustomers();
} }
/**
* Loads the list of pets from the API.
*/
private void loadPets() { private void loadPets() {
RetrofitClient.getPetApi(requireContext()).getAllPets(0, 200) petViewModel.getAllPets(0, 200, null, null, null, null, "petName").observe(getViewLifecycleOwner(), resource -> {
.enqueue(new Callback<PageResponse<PetDTO>>() { if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
public void onResponse(Call<PageResponse<PetDTO>> c, petList = resource.data.getContent();
Response<PageResponse<PetDTO>> r) { refreshPetSpinner();
if (r.isSuccessful() && r.body() != null) {
petList = r.body().getContent();
populatePetSpinner();
}
}
public void onFailure(Call<PageResponse<PetDTO>> c, Throwable t) {
Log.e("ADOPTION", "Pet load failed: " + t.getMessage());
}
});
}
private void populatePetSpinner() {
List<String> names = new ArrayList<>();
names.add("-- Select Pet --");
for (PetDTO p : petList) names.add(p.getPetName());
spinnerPet.setAdapter(new BlackTextArrayAdapter<>(requireContext(),
android.R.layout.simple_spinner_item, names));
if (preselectedPetId != -1) {
for (int i = 0; i < petList.size(); i++) {
if (petList.get(i).getPetId().equals(preselectedPetId)) {
spinnerPet.setSelection(i + 1); break;
}
} }
} });
} }
/**
* Populates the pet selection spinner with data.
*/
private void refreshPetSpinner() {
SpinnerUtils.populateSpinner(requireContext(), binding.spinnerAdoptionPet, petList,
PetDTO::getPetName, "-- Select Pet --",
preselectedPetId, PetDTO::getPetId);
}
/**
* Loads the list of customers from the API.
*/
private void loadCustomers() { private void loadCustomers() {
RetrofitClient.getCustomerApi(requireContext()).getAllCustomers(0, 200) customerViewModel.getAllCustomers(0, 200).observe(getViewLifecycleOwner(), resource -> {
.enqueue(new Callback<PageResponse<CustomerDTO>>() { if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
public void onResponse(Call<PageResponse<CustomerDTO>> c, customerList = resource.data.getContent();
Response<PageResponse<CustomerDTO>> r) { refreshCustomerSpinner();
if (r.isSuccessful() && r.body() != null) {
customerList = r.body().getContent();
populateCustomerSpinner();
}
}
public void onFailure(Call<PageResponse<CustomerDTO>> c, Throwable t) {
Log.e("ADOPTION", "Customer load failed: " + t.getMessage());
}
});
}
private void populateCustomerSpinner() {
List<String> names = new ArrayList<>();
names.add("-- Select Customer --");
for (CustomerDTO c : customerList)
names.add(c.getFirstName() + " " + c.getLastName());
spinnerCustomer.setAdapter(new BlackTextArrayAdapter<>(requireContext(),
android.R.layout.simple_spinner_item, names));
if (preselectedCustomerId != -1) {
for (int i = 0; i < customerList.size(); i++) {
if (customerList.get(i).getCustomerId().equals(preselectedCustomerId)) {
spinnerCustomer.setSelection(i + 1); break;
}
} }
} });
} }
/**
* Populates the customer selection spinner with data.
*/
private void refreshCustomerSpinner() {
SpinnerUtils.populateSpinner(requireContext(), binding.spinnerAdoptionCustomer, customerList,
item -> item.getFirstName() + " " + item.getLastName(),
"-- Select Customer --",
preselectedCustomerId, CustomerDTO::getCustomerId);
}
/**
* Handles arguments to determine if the fragment is in edit or add mode.
*/
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; isEditing = true;
adoptionId = a.getLong("adoptionId"); adoptionId = a.getLong("adoptionId");
preselectedPetId = a.getLong("petId", -1); binding.tvAdoptionMode.setText("Edit Adoption");
preselectedCustomerId = a.getLong("customerId", -1); binding.tvAdoptionId.setText("ID: " + adoptionId);
binding.tvAdoptionId.setVisibility(View.VISIBLE);
tvMode.setText("Edit Adoption"); binding.btnDeleteAdoption.setVisibility(View.VISIBLE);
tvAdoptionId.setText("ID: " + adoptionId); loadAdoptionData();
tvAdoptionId.setVisibility(View.VISIBLE);
etAdoptionDate.setText(a.getString("adoptionDate"));
btnDelete.setVisibility(View.VISIBLE);
// Pre-fill status
String status = a.getString("adoptionStatus", "Pending");
for (int i = 0; i < STATUSES.length; i++) {
if (STATUSES[i].equals(status)) {
spinnerStatus.setSelection(i); break;
}
}
} else { } else {
tvMode.setText("Add Adoption"); binding.tvAdoptionMode.setText("Add Adoption");
btnDelete.setVisibility(View.GONE); binding.btnDeleteAdoption.setVisibility(View.GONE);
tvAdoptionId.setVisibility(View.GONE); binding.tvAdoptionId.setVisibility(View.GONE);
} }
} }
/**
* Fetches specific adoption details from the backend using the ID.
*/
private void loadAdoptionData() {
adoptionViewModel.getAdoptionById(adoptionId).observe(getViewLifecycleOwner(), resource -> {
if (resource == null) return;
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
AdoptionDTO a = resource.data;
preselectedPetId = a.getPetId() != null ? a.getPetId() : -1;
preselectedCustomerId = a.getCustomerId() != null ? a.getCustomerId() : -1;
binding.etAdoptionDate.setText(a.getAdoptionDate());
SpinnerUtils.setSelectionByValue(binding.spinnerAdoptionStatus, a.getAdoptionStatus());
refreshPetSpinner();
refreshCustomerSpinner();
} else if (resource.status == Resource.Status.ERROR) {
Toast.makeText(getContext(), "Failed to load adoption: " + resource.message, Toast.LENGTH_SHORT).show();
}
});
}
/**
* Validates input and saves the adoption request to the backend.
*/
private void saveAdoption() { private void saveAdoption() {
if (spinnerCustomer.getSelectedItemPosition() == 0) { if (binding.spinnerAdoptionCustomer.getSelectedItemPosition() == 0) {
Toast.makeText(getContext(), "Select a customer", Toast.LENGTH_SHORT).show(); return; Toast.makeText(getContext(), "Select a customer", Toast.LENGTH_SHORT).show(); return;
} }
if (spinnerPet.getSelectedItemPosition() == 0) { if (binding.spinnerAdoptionPet.getSelectedItemPosition() == 0) {
Toast.makeText(getContext(), "Select a pet", Toast.LENGTH_SHORT).show(); return; Toast.makeText(getContext(), "Select a pet", Toast.LENGTH_SHORT).show(); return;
} }
String date = etAdoptionDate.getText().toString().trim(); String date = binding.etAdoptionDate.getText().toString().trim();
if (date.isEmpty()) { if (date.isEmpty()) {
Toast.makeText(getContext(), "Select a date", Toast.LENGTH_SHORT).show(); return; Toast.makeText(getContext(), "Select a date", Toast.LENGTH_SHORT).show(); return;
} }
CustomerDTO customer = customerList.get(spinnerCustomer.getSelectedItemPosition() - 1); CustomerDTO customer = customerList.get(binding.spinnerAdoptionCustomer.getSelectedItemPosition() - 1);
PetDTO pet = petList.get(spinnerPet.getSelectedItemPosition() - 1); PetDTO pet = petList.get(binding.spinnerAdoptionPet.getSelectedItemPosition() - 1);
String status = STATUSES[spinnerStatus.getSelectedItemPosition()]; String status = STATUSES[binding.spinnerAdoptionStatus.getSelectedItemPosition()];
AdoptionDTO dto = new AdoptionDTO( AdoptionDTO dto = new AdoptionDTO(
pet.getPetId(), pet.getPetId(),
@@ -197,62 +219,46 @@ public class AdoptionDetailFragment extends Fragment {
status status
); );
Log.d("ADOPTION_SAVE", "petId=" + pet.getPetId()
+ " customerId=" + customer.getCustomerId()
+ " date=" + date + " status=" + status);
AdoptionApi api = RetrofitClient.getAdoptionApi(requireContext());
if (isEditing) { if (isEditing) {
api.updateAdoption(adoptionId, dto).enqueue(simpleCallback("Updated")); adoptionViewModel.updateAdoption(adoptionId, dto).observe(getViewLifecycleOwner(), resource -> {
if (resource.status == Resource.Status.SUCCESS) {
Toast.makeText(getContext(), "Updated", Toast.LENGTH_SHORT).show();
navigateBack();
} else if (resource.status == Resource.Status.ERROR) {
Toast.makeText(getContext(), "Error: " + resource.message, Toast.LENGTH_SHORT).show();
}
});
} else { } else {
api.createAdoption(dto).enqueue(simpleCallback("Saved")); 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();
}
});
} }
} }
private Callback<AdoptionDTO> simpleCallback(String msg) { /**
return new Callback<>() { * Shows a confirmation dialog before deleting an adoption request.
public void onResponse(Call<AdoptionDTO> c, Response<AdoptionDTO> r) { */
Log.d("ADOPTION_SAVE", "Response: " + r.code());
if (r.isSuccessful()) {
Toast.makeText(getContext(), msg, Toast.LENGTH_SHORT).show();
navigateBack();
} else {
try {
String err = r.errorBody().string();
Log.e("ADOPTION_SAVE", "Error: " + err);
Toast.makeText(getContext(), "Error " + r.code(), Toast.LENGTH_SHORT).show();
} catch (Exception e) {
Log.e("ADOPTION_SAVE", "Failed to read error");
}
}
}
public void onFailure(Call<AdoptionDTO> c, Throwable t) {
Log.e("ADOPTION_SAVE", "Failure: " + t.getMessage());
Toast.makeText(getContext(), "Network error", Toast.LENGTH_SHORT).show();
}
};
}
private void confirmDelete() { private void confirmDelete() {
new AlertDialog.Builder(requireContext()) DialogUtils.showDeleteConfirmDialog(requireContext(), "Adoption", () ->
.setTitle("Delete Adoption?") adoptionViewModel.deleteAdoption(adoptionId).observe(getViewLifecycleOwner(), resource -> {
.setPositiveButton("Yes", (d, w) -> if (resource.status == Resource.Status.SUCCESS) {
RetrofitClient.getAdoptionApi(requireContext()) Toast.makeText(getContext(), "Deleted", Toast.LENGTH_SHORT).show();
.deleteAdoption(adoptionId) navigateBack();
.enqueue(new Callback<Void>() { } else if (resource.status == Resource.Status.ERROR) {
public void onResponse(Call<Void> c, Response<Void> r) { Toast.makeText(getContext(), "Delete failed: " + resource.message, Toast.LENGTH_SHORT).show();
navigateBack(); }
} }));
public void onFailure(Call<Void> c, Throwable t) {
Toast.makeText(getContext(), "Delete failed",
Toast.LENGTH_SHORT).show();
}
}))
.setNegativeButton("No", null).show();
} }
/**
* Navigates back to the previous fragment.
*/
private void navigateBack() { private void navigateBack() {
ListFragment lf = (ListFragment) getParentFragment(); NavHostFragment.findNavController(this).popBackStack();
if (lf != null) lf.getChildFragmentManager().popBackStack();
} }
} }

View File

@@ -6,23 +6,33 @@ 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.annotation.Nullable;
import androidx.fragment.app.Fragment; import androidx.fragment.app.Fragment;
import com.example.petstoremobile.R; import androidx.lifecycle.ViewModelProvider;
import com.example.petstoremobile.adapters.BlackTextArrayAdapter; import androidx.navigation.fragment.NavHostFragment;
import com.example.petstoremobile.api.*;
import com.example.petstoremobile.dtos.*;
import com.example.petstoremobile.fragments.ListFragment;
import java.util.*;
import retrofit2.*;
import com.example.petstoremobile.databinding.FragmentAppointmentDetailBinding;
import com.example.petstoremobile.dtos.*;
import com.example.petstoremobile.utils.DialogUtils;
import com.example.petstoremobile.utils.Resource;
import com.example.petstoremobile.utils.SpinnerUtils;
import com.example.petstoremobile.viewmodels.AppointmentViewModel;
import com.example.petstoremobile.viewmodels.CustomerViewModel;
import com.example.petstoremobile.viewmodels.PetViewModel;
import com.example.petstoremobile.viewmodels.ServiceViewModel;
import com.example.petstoremobile.viewmodels.StoreViewModel;
import java.util.*;
import dagger.hilt.android.AndroidEntryPoint;
/**
* Fragment for displaying and editing appointment details.
*/
@AndroidEntryPoint
public class AppointmentDetailFragment extends Fragment { public class AppointmentDetailFragment extends Fragment {
private TextView tvMode, tvAppointmentId; private FragmentAppointmentDetailBinding binding;
private EditText etAppointmentDate;
private Spinner spinnerPet, spinnerService, spinnerStatus, spinnerHour, spinnerMinute;
private Spinner spinnerCustomer, spinnerStore;
private Button btnSave, btnDelete, btnBack;
private long appointmentId = -1; private long appointmentId = -1;
private boolean isEditing = false; private boolean isEditing = false;
@@ -35,61 +45,73 @@ public class AppointmentDetailFragment extends Fragment {
private List<ServiceDTO> serviceList = new ArrayList<>(); private List<ServiceDTO> serviceList = new ArrayList<>();
private List<CustomerDTO> customerList = new ArrayList<>(); private List<CustomerDTO> customerList = new ArrayList<>();
private List<StoreDTO> storeList = new ArrayList<>(); private List<StoreDTO> storeList = new ArrayList<>();
private List<AppointmentDTO> allAppointments = new ArrayList<>();
private final Integer[] HOURS = {9,10,11,12,13,14,15,16,17}; private final Integer[] HOURS = {9,10,11,12,13,14,15,16,17};
private final Integer[] MINUTES = {0,15,30,45}; private final Integer[] MINUTES = {0,15,30,45};
private final String[] STATUSES = {"Booked","Completed","Cancelled"};
private AppointmentViewModel appointmentViewModel;
private PetViewModel petViewModel;
private ServiceViewModel serviceViewModel;
private StoreViewModel storeViewModel;
private CustomerViewModel customerViewModel;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
appointmentViewModel = new ViewModelProvider(this).get(AppointmentViewModel.class);
petViewModel = new ViewModelProvider(this).get(PetViewModel.class);
serviceViewModel = new ViewModelProvider(this).get(ServiceViewModel.class);
storeViewModel = new ViewModelProvider(this).get(StoreViewModel.class);
customerViewModel = new ViewModelProvider(this).get(CustomerViewModel.class);
}
@Override @Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_appointment_detail, container, false); binding = FragmentAppointmentDetailBinding.inflate(inflater, container, false);
initViews(view); return binding.getRoot();
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
setupSpinners(); setupSpinners();
setupDatePicker(); setupDatePicker();
loadData(); loadSpinnersData();
handleArguments(); handleArguments();
btnBack.setOnClickListener(v -> navigateBack()); binding.btnApptBack.setOnClickListener(v -> navigateBack());
btnSave.setOnClickListener(v -> saveAppointment()); binding.btnSaveAppointment.setOnClickListener(v -> saveAppointment());
btnDelete.setOnClickListener(v -> confirmDelete()); binding.btnDeleteAppointment.setOnClickListener(v -> confirmDelete());
return view;
} }
private void initViews(View v) { @Override
tvMode = v.findViewById(R.id.tvApptMode); public void onDestroyView() {
tvAppointmentId = v.findViewById(R.id.tvAppointmentId); super.onDestroyView();
etAppointmentDate= v.findViewById(R.id.etAppointmentDate); binding = null;
spinnerPet = v.findViewById(R.id.spinnerPet);
spinnerService = v.findViewById(R.id.spinnerService);
spinnerStatus = v.findViewById(R.id.spinnerAppointmentStatus);
spinnerHour = v.findViewById(R.id.spinnerHour);
spinnerMinute = v.findViewById(R.id.spinnerMinute);
spinnerCustomer = v.findViewById(R.id.spinnerCustomer);
spinnerStore = v.findViewById(R.id.spinnerStore);
btnSave = v.findViewById(R.id.btnSaveAppointment);
btnDelete = v.findViewById(R.id.btnDeleteAppointment);
btnBack = v.findViewById(R.id.btnApptBack);
} }
/**
* Configures the adapters for spinners.
*/
private void setupSpinners() { private void setupSpinners() {
spinnerStatus.setAdapter(new BlackTextArrayAdapter<>(requireContext(), SpinnerUtils.setupStringSpinner(requireContext(), binding.spinnerAppointmentStatus,
android.R.layout.simple_spinner_item, STATUSES)); 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] = String.format("%02d:00", HOURS[i]);
spinnerHour.setAdapter(new BlackTextArrayAdapter<>(requireContext(), SpinnerUtils.setupStringSpinner(requireContext(), binding.spinnerHour, hours);
android.R.layout.simple_spinner_item, hours)); SpinnerUtils.setupStringSpinner(requireContext(), binding.spinnerMinute, new String[]{"00","15","30","45"});
spinnerMinute.setAdapter(new BlackTextArrayAdapter<>(requireContext(),
android.R.layout.simple_spinner_item, new String[]{"00","15","30","45"}));
} }
/**
* Configures the date picker dialog for the appointment date field.
*/
private void setupDatePicker() { private void setupDatePicker() {
etAppointmentDate.setOnClickListener(v -> { binding.etAppointmentDate.setOnClickListener(v -> {
Calendar c = Calendar.getInstance(); Calendar c = Calendar.getInstance();
DatePickerDialog d = new DatePickerDialog(requireContext(), DatePickerDialog d = new DatePickerDialog(requireContext(),
(dp,y,m,d1) -> etAppointmentDate.setText( (dp,y,m,d1) -> binding.etAppointmentDate.setText(
String.format("%04d-%02d-%02d", y, m+1, d1)), String.format("%04d-%02d-%02d", y, m+1, d1)),
c.get(Calendar.YEAR), c.get(Calendar.MONTH), c.get(Calendar.YEAR), c.get(Calendar.MONTH),
c.get(Calendar.DAY_OF_MONTH)); c.get(Calendar.DAY_OF_MONTH));
@@ -98,218 +120,204 @@ public class AppointmentDetailFragment extends Fragment {
}); });
} }
private void loadData() { /**
* Fetches all required data for spinners from the backend.
*/
private void loadSpinnersData() {
loadPets(); loadPets();
loadServices(); loadServices();
loadCustomers(); loadCustomers();
loadStores(); loadStores();
loadAllAppointments();
} }
/**
* Loads the list of pets from the ViewModel.
*/
private void loadPets() { private void loadPets() {
RetrofitClient.getPetApi(requireContext()).getAllPets(0, 200) petViewModel.getAllPets(0, 200, null, null, null, null, "petName").observe(getViewLifecycleOwner(), resource -> {
.enqueue(new Callback<PageResponse<PetDTO>>() { if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
public void onResponse(Call<PageResponse<PetDTO>> c, Response<PageResponse<PetDTO>> r) { petList = resource.data.getContent();
if (r.isSuccessful() && r.body() != null) { refreshPetSpinner();
petList = r.body().getContent();
populatePetSpinner();
}
}
public void onFailure(Call<PageResponse<PetDTO>> c, Throwable t) {
Log.e("APPT", "Pet load failed: " + t.getMessage());
}
});
}
private void populatePetSpinner() {
List<String> names = new ArrayList<>();
names.add("-- Select Pet --");
for (PetDTO p : petList) names.add(p.getPetName());
spinnerPet.setAdapter(new BlackTextArrayAdapter<>(requireContext(),
android.R.layout.simple_spinner_item, names));
if (preselectedPetId != -1) {
for (int i = 0; i < petList.size(); i++) {
if (petList.get(i).getPetId().equals(preselectedPetId)) {
spinnerPet.setSelection(i + 1); break;
}
} }
} });
} }
/**
* Populates the pet selection spinner.
*/
private void refreshPetSpinner() {
SpinnerUtils.populateSpinner(requireContext(), binding.spinnerPet, petList,
PetDTO::getPetName, "-- Select Pet --",
preselectedPetId, PetDTO::getPetId);
}
/**
* Loads the list of services from the API.
*/
private void loadServices() { private void loadServices() {
RetrofitClient.getServiceApi(requireContext()).getAllServices(0, 200) serviceViewModel.getAllServices(0, 200, null, "serviceName").observe(getViewLifecycleOwner(), resource -> {
.enqueue(new Callback<PageResponse<ServiceDTO>>() { if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
public void onResponse(Call<PageResponse<ServiceDTO>> c, Response<PageResponse<ServiceDTO>> r) { serviceList = resource.data.getContent();
if (r.isSuccessful() && r.body() != null) { refreshServiceSpinner();
serviceList = r.body().getContent();
populateServiceSpinner();
}
}
public void onFailure(Call<PageResponse<ServiceDTO>> c, Throwable t) {
Log.e("APPT", "Service load failed: " + t.getMessage());
}
});
}
private void populateServiceSpinner() {
List<String> names = new ArrayList<>();
names.add("-- Select Service --");
for (ServiceDTO s : serviceList) names.add(s.getServiceName());
spinnerService.setAdapter(new BlackTextArrayAdapter<>(requireContext(),
android.R.layout.simple_spinner_item, names));
if (preselectedServiceId != -1) {
for (int i = 0; i < serviceList.size(); i++) {
if (serviceList.get(i).getServiceId().equals(preselectedServiceId)) {
spinnerService.setSelection(i + 1); break;
}
} }
} });
} }
/**
* 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() { private void loadCustomers() {
RetrofitClient.getCustomerApi(requireContext()).getAllCustomers(0, 200) customerViewModel.getAllCustomers(0, 200).observe(getViewLifecycleOwner(), resource -> {
.enqueue(new Callback<PageResponse<CustomerDTO>>() { if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
public void onResponse(Call<PageResponse<CustomerDTO>> c, Response<PageResponse<CustomerDTO>> r) { customerList = resource.data.getContent();
if (r.isSuccessful() && r.body() != null) { refreshCustomerSpinner();
customerList = r.body().getContent();
populateCustomerSpinner();
}
}
public void onFailure(Call<PageResponse<CustomerDTO>> c, Throwable t) {
Log.e("APPT", "Customer load failed: " + t.getMessage());
}
});
}
private void populateCustomerSpinner() {
List<String> names = new ArrayList<>();
names.add("-- Select Customer --");
for (CustomerDTO c : customerList)
names.add(c.getFirstName() + " " + c.getLastName());
spinnerCustomer.setAdapter(new BlackTextArrayAdapter<>(requireContext(),
android.R.layout.simple_spinner_item, names));
if (preselectedCustomerId != -1) {
for (int i = 0; i < customerList.size(); i++) {
if (customerList.get(i).getCustomerId().equals(preselectedCustomerId)) {
spinnerCustomer.setSelection(i + 1); break;
}
} }
} });
} }
/**
* 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() { private void loadStores() {
RetrofitClient.getStoreApi(requireContext()).getAllStores(0, 50) storeViewModel.getAllStores(0, 50).observe(getViewLifecycleOwner(), resource -> {
.enqueue(new Callback<PageResponse<StoreDTO>>() { if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
public void onResponse(Call<PageResponse<StoreDTO>> c, Response<PageResponse<StoreDTO>> r) { storeList = resource.data.getContent();
if (r.isSuccessful() && r.body() != null) { refreshStoreSpinner();
storeList = r.body().getContent();
populateStoreSpinner();
}
}
public void onFailure(Call<PageResponse<StoreDTO>> c, Throwable t) {
Log.e("APPT", "Store load failed: " + t.getMessage());
}
});
}
private void populateStoreSpinner() {
List<String> names = new ArrayList<>();
names.add("-- Select Store --");
for (StoreDTO s : storeList) names.add(s.getStoreName());
spinnerStore.setAdapter(new BlackTextArrayAdapter<>(requireContext(),
android.R.layout.simple_spinner_item, names));
if (preselectedStoreId != -1) {
for (int i = 0; i < storeList.size(); i++) {
if (storeList.get(i).getStoreId().equals(preselectedStoreId)) {
spinnerStore.setSelection(i + 1); break;
}
} }
} });
} }
private void loadAllAppointments() { /**
RetrofitClient.getAppointmentApi(requireContext()).getAllAppointments(0, 500) * Populates the store selection spinner.
.enqueue(new Callback<PageResponse<AppointmentDTO>>() { */
public void onResponse(Call<PageResponse<AppointmentDTO>> c, Response<PageResponse<AppointmentDTO>> r) { private void refreshStoreSpinner() {
if (r.isSuccessful() && r.body() != null) SpinnerUtils.populateSpinner(requireContext(), binding.spinnerStore, storeList,
allAppointments = r.body().getContent(); StoreDTO::getStoreName, "-- Select Store --",
} preselectedStoreId, StoreDTO::getStoreId);
public void onFailure(Call<PageResponse<AppointmentDTO>> c, Throwable t) {}
});
} }
/**
* 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; isEditing = true;
appointmentId = a.getLong("appointmentId"); appointmentId = a.getLong("appointmentId");
preselectedPetId = a.getLong("petId", -1); binding.tvApptMode.setText("Edit Appointment");
preselectedServiceId= a.getLong("serviceId", -1); binding.tvAppointmentId.setText("ID: " + appointmentId);
preselectedCustomerId = a.getLong("customerId", -1); binding.tvAppointmentId.setVisibility(View.VISIBLE);
preselectedStoreId = a.getLong("storeId", -1); binding.btnDeleteAppointment.setVisibility(View.VISIBLE);
loadAppointmentData();
tvMode.setText("Edit Appointment");
tvAppointmentId.setText("ID: " + appointmentId);
tvAppointmentId.setVisibility(View.VISIBLE);
etAppointmentDate.setText(a.getString("appointmentDate"));
btnDelete.setVisibility(View.VISIBLE);
// Pre-fill time spinners
String time = a.getString("appointmentTime", "09:00");
if (time.length() > 5) time = time.substring(0, 5);
String[] parts = time.split(":");
if (parts.length == 2) {
int hour = Integer.parseInt(parts[0]);
int min = Integer.parseInt(parts[1]);
for (int i = 0; i < HOURS.length; i++)
if (HOURS[i] == hour) { spinnerHour.setSelection(i); break; }
for (int i = 0; i < MINUTES.length; i++)
if (MINUTES[i] == min) { spinnerMinute.setSelection(i); break; }
}
// Pre-fill status
String status = a.getString("appointmentStatus", "Booked");
for (int i = 0; i < STATUSES.length; i++)
if (STATUSES[i].equals(status)) { spinnerStatus.setSelection(i); break; }
} else { } else {
tvMode.setText("Add Appointment"); binding.tvApptMode.setText("Add Appointment");
btnDelete.setVisibility(View.GONE); binding.btnDeleteAppointment.setVisibility(View.GONE);
tvAppointmentId.setVisibility(View.GONE); binding.tvAppointmentId.setVisibility(View.GONE);
} }
} }
/**
* Fetches specific appointment details from the backend using the ID.
*/
private void loadAppointmentData() {
appointmentViewModel.getAppointmentById(appointmentId).observe(getViewLifecycleOwner(), resource -> {
if (resource == null) return;
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
AppointmentDTO a = resource.data;
preselectedPetId = (a.getPetId() != null) ? a.getPetId() : -1;
preselectedServiceId = (a.getServiceId() != null) ? a.getServiceId() : -1;
preselectedCustomerId = (a.getCustomerId() != null) ? a.getCustomerId() : -1;
preselectedStoreId = (a.getStoreId() != null) ? a.getStoreId() : -1;
binding.etAppointmentDate.setText(a.getAppointmentDate());
// Pre-fill time spinners
String time = a.getAppointmentTime() != null ? a.getAppointmentTime() : "09:00";
if (time.length() > 5) time = time.substring(0, 5);
String[] parts = time.split(":");
if (parts.length == 2) {
try {
int hour = Integer.parseInt(parts[0]);
int min = Integer.parseInt(parts[1]);
for (int i = 0; i < HOURS.length; i++)
if (HOURS[i] == hour) { binding.spinnerHour.setSelection(i); break; }
for (int i = 0; i < MINUTES.length; i++)
if (MINUTES[i] == min) { binding.spinnerMinute.setSelection(i); break; }
} catch (NumberFormatException ignored) {}
}
// Match Title labels with backend values
String status = a.getAppointmentStatus();
if (status != null && !status.isEmpty()) {
String formattedStatus = status.substring(0, 1).toUpperCase() + status.substring(1).toLowerCase();
SpinnerUtils.setSelectionByValue(binding.spinnerAppointmentStatus, formattedStatus);
}
refreshPetSpinner();
refreshServiceSpinner();
refreshCustomerSpinner();
refreshStoreSpinner();
} else if (resource.status == Resource.Status.ERROR) {
Toast.makeText(getContext(), "Failed to load appointment: " + resource.message, Toast.LENGTH_SHORT).show();
}
});
}
/**
* Validates input and saves the appointment to the backend.
*/
private void saveAppointment() { private void saveAppointment() {
if (spinnerCustomer.getSelectedItemPosition() == 0) { if (binding.spinnerCustomer.getSelectedItemPosition() == 0) {
Toast.makeText(getContext(), "Select a customer", Toast.LENGTH_SHORT).show(); return; Toast.makeText(getContext(), "Select a customer", Toast.LENGTH_SHORT).show(); return;
} }
if (spinnerStore.getSelectedItemPosition() == 0) { if (binding.spinnerStore.getSelectedItemPosition() == 0) {
Toast.makeText(getContext(), "Select a store", Toast.LENGTH_SHORT).show(); return; Toast.makeText(getContext(), "Select a store", Toast.LENGTH_SHORT).show(); return;
} }
if (spinnerPet.getSelectedItemPosition() == 0) { if (binding.spinnerPet.getSelectedItemPosition() == 0) {
Toast.makeText(getContext(), "Select a pet", Toast.LENGTH_SHORT).show(); return; Toast.makeText(getContext(), "Select a pet", Toast.LENGTH_SHORT).show(); return;
} }
if (spinnerService.getSelectedItemPosition() == 0) { if (binding.spinnerService.getSelectedItemPosition() == 0) {
Toast.makeText(getContext(), "Select a service", Toast.LENGTH_SHORT).show(); return; Toast.makeText(getContext(), "Select a service", Toast.LENGTH_SHORT).show(); return;
} }
String date = etAppointmentDate.getText().toString().trim(); String date = binding.etAppointmentDate.getText().toString().trim();
if (date.isEmpty()) { if (date.isEmpty()) {
Toast.makeText(getContext(), "Select a date", Toast.LENGTH_SHORT).show(); return; Toast.makeText(getContext(), "Select a date", Toast.LENGTH_SHORT).show(); return;
} }
CustomerDTO customer = customerList.get(spinnerCustomer.getSelectedItemPosition() - 1); CustomerDTO customer = customerList.get(binding.spinnerCustomer.getSelectedItemPosition() - 1);
StoreDTO store = storeList.get(spinnerStore.getSelectedItemPosition() - 1); StoreDTO store = storeList.get(binding.spinnerStore.getSelectedItemPosition() - 1);
PetDTO pet = petList.get(spinnerPet.getSelectedItemPosition() - 1); PetDTO pet = petList.get(binding.spinnerPet.getSelectedItemPosition() - 1);
ServiceDTO service = serviceList.get(spinnerService.getSelectedItemPosition() - 1); ServiceDTO service = serviceList.get(binding.spinnerService.getSelectedItemPosition() - 1);
String time = String.format("%02d:%02d", String time = String.format("%02d:%02d",
HOURS[spinnerHour.getSelectedItemPosition()], HOURS[binding.spinnerHour.getSelectedItemPosition()],
MINUTES[spinnerMinute.getSelectedItemPosition()]); MINUTES[binding.spinnerMinute.getSelectedItemPosition()]);
String status = STATUSES[spinnerStatus.getSelectedItemPosition()];
// Get status and convert to uppercase for backend
String status = binding.spinnerAppointmentStatus.getSelectedItem().toString().toUpperCase();
// Validate future date+time if status is Booked // Validate future date+time if status is BOOKED
if ("Booked".equalsIgnoreCase(status)) { if ("BOOKED".equalsIgnoreCase(status)) {
try { try {
String[] dateParts = date.split("-"); String[] dateParts = date.split("-");
String[] timeParts = time.split(":"); String[] timeParts = time.split(":");
@@ -323,7 +331,7 @@ public class AppointmentDetailFragment extends Fragment {
0 0
); );
if (selected.before(Calendar.getInstance())) { if (selected.before(Calendar.getInstance())) {
showErrorDialog("Invalid Time", DialogUtils.showInfoDialog(requireContext(), "Invalid Time",
"Booked appointments must be in the future. " + "Booked appointments must be in the future. " +
"Please select a future date and time."); "Please select a future date and time.");
return; return;
@@ -341,104 +349,76 @@ public class AppointmentDetailFragment extends Fragment {
date, date,
time, time,
status, status,
Collections.singletonList(pet.getPetId()) pet.getPetId()
); );
Log.d("APPT_SAVE", "customerId=" + customer.getCustomerId() androidx.lifecycle.Observer<Resource<AppointmentDTO>> observer = resource -> {
+ " storeId=" + store.getStoreId() if (resource.status == Resource.Status.SUCCESS) {
+ " serviceId=" + service.getServiceId() Toast.makeText(getContext(), isEditing ? "Updated" : "Saved", Toast.LENGTH_SHORT).show();
+ " petId=" + pet.getPetId() navigateBack();
+ " date=" + date + " time=" + time); } else if (resource.status == Resource.Status.ERROR) {
handleSaveError(resource.message);
}
};
AppointmentApi api = RetrofitClient.getAppointmentApi(requireContext());
if (isEditing) { if (isEditing) {
api.updateAppointment(appointmentId, dto).enqueue(simpleCallback("Updated")); appointmentViewModel.updateAppointment(appointmentId, dto).observe(getViewLifecycleOwner(), observer);
} else { } else {
api.createAppointment(dto).enqueue(simpleCallback("Saved")); appointmentViewModel.createAppointment(dto).observe(getViewLifecycleOwner(), observer);
} }
} }
private Callback<AppointmentDTO> simpleCallback(String msg) { /**
return new Callback<>() { * Handles errors that occur during the saving process.
public void onResponse(Call<AppointmentDTO> c, Response<AppointmentDTO> r) { */
Log.d("APPT_SAVE", "Response: " + r.code()); private void handleSaveError(String errorMessage) {
if (r.isSuccessful()) { if (errorMessage != null) {
Toast.makeText(getContext(), msg, Toast.LENGTH_SHORT).show(); Log.e("APPT_SAVE", "Error: " + errorMessage);
navigateBack(); if (errorMessage.toLowerCase().contains("future")) {
} else { DialogUtils.showInfoDialog(requireContext(), "Invalid Date/Time",
try { "Booked appointments must be scheduled in the future.");
String errorBody = r.errorBody().string(); } else if (errorMessage.toLowerCase().contains("not available")) {
Log.e("APPT_SAVE", "Error: " + errorBody); showNoAvailabilityDialog();
} else {
// Show proper dialog based on error type Toast.makeText(getContext(), "Operation failed", Toast.LENGTH_SHORT).show();
if (errorBody.toLowerCase().contains("future")) {
showErrorDialog("Invalid Date/Time",
"Booked appointments must be scheduled in the future. " +
"Please select a future date and time.");
//------------------------------------------
} else if (errorBody.toLowerCase().contains("not available") ||
errorBody.toLowerCase().contains("time is not available")) {
showNoAvailabilityDialog();
} else if (r.code() == 404) {
showErrorDialog("Not Found",
"The selected pet, customer or service was not found.");
} else if (r.code() == 403) {
showErrorDialog("Access Denied",
"You don't have permission to perform this action.");
} else if (r.code() == 400) {
showErrorDialog("Invalid Request", errorBody);
} else {
showErrorDialog("Error", "Something went wrong. Please try again.");
}
//-----------------------------
} catch (Exception e) {
Log.e("APPT_SAVE", "Failed to read error body");
showErrorDialog("Error", "Something went wrong. Please try again.");
}
}
} }
} else {
public void onFailure(Call<AppointmentDTO> c, Throwable t) { Toast.makeText(getContext(), "Something went wrong", Toast.LENGTH_SHORT).show();
Log.e("APPT_SAVE", "Failure: " + t.getMessage()); }
Toast.makeText(getContext(), "Network error", Toast.LENGTH_SHORT).show();
}
};
} }
/**
* Shows a specialized dialog when a time slot is not available.
*/
private void showNoAvailabilityDialog() { private void showNoAvailabilityDialog() {
new AlertDialog.Builder(requireContext()) new androidx.appcompat.app.AlertDialog.Builder(requireContext())
.setTitle("No Availability") .setTitle("No Availability")
.setMessage("This time slot is already booked for the selected service and store. Please choose a different time or date.") .setMessage("This time slot is already booked. Please choose a different time or date.")
.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())
.setCancelable(false) .setCancelable(false)
.show(); .show();
} }
private void showErrorDialog(String title, String message) { /**
new AlertDialog.Builder(requireContext()) * Shows a confirmation dialog and handles the deletion of an appointment.
.setTitle(title) */
.setMessage(message)
.setPositiveButton("OK", null)
.show();
}
private void confirmDelete() { private void confirmDelete() {
new AlertDialog.Builder(requireContext()) DialogUtils.showDeleteConfirmDialog(requireContext(), "Appointment", () ->
.setTitle("Delete Appointment?") appointmentViewModel.deleteAppointment(appointmentId).observe(getViewLifecycleOwner(), resource -> {
.setPositiveButton("Yes", (d, w) -> if (resource.status == Resource.Status.SUCCESS) {
RetrofitClient.getAppointmentApi(requireContext()) Toast.makeText(getContext(), "Deleted", Toast.LENGTH_SHORT).show();
.deleteAppointment(appointmentId) navigateBack();
.enqueue(new Callback<Void>() { } else if (resource.status == Resource.Status.ERROR) {
public void onResponse(Call<Void> c, Response<Void> r) { navigateBack(); } Toast.makeText(getContext(), "Delete failed", Toast.LENGTH_SHORT).show();
public void onFailure(Call<Void> c, Throwable t) { }
Toast.makeText(getContext(), "Delete failed", Toast.LENGTH_SHORT).show(); }));
}
}))
.setNegativeButton("No", null).show();
} }
/**
* Navigates back to the previous screen.
*/
private void navigateBack() { private void navigateBack() {
ListFragment lf = (ListFragment) getParentFragment(); NavHostFragment.findNavController(this).popBackStack();
if (lf != null) lf.getChildFragmentManager().popBackStack();
} }
} }

View File

@@ -9,43 +9,40 @@ import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.ArrayAdapter; import android.widget.ArrayAdapter;
import android.widget.AutoCompleteTextView;
import android.widget.Button;
import android.widget.TextView;
import android.widget.Toast; import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AlertDialog;
import androidx.fragment.app.Fragment; import androidx.fragment.app.Fragment;
import androidx.lifecycle.ViewModelProvider;
import androidx.navigation.fragment.NavHostFragment;
import com.example.petstoremobile.R;
import com.example.petstoremobile.adapters.BlackTextArrayAdapter; import com.example.petstoremobile.adapters.BlackTextArrayAdapter;
import com.example.petstoremobile.api.InventoryApi; import com.example.petstoremobile.databinding.FragmentInventoryDetailBinding;
import com.example.petstoremobile.api.ProductApi;
import com.example.petstoremobile.api.RetrofitClient;
import com.example.petstoremobile.dtos.InventoryDTO; import com.example.petstoremobile.dtos.InventoryDTO;
import com.example.petstoremobile.dtos.InventoryRequest; import com.example.petstoremobile.dtos.InventoryRequest;
import com.example.petstoremobile.dtos.PageResponse;
import com.example.petstoremobile.dtos.ProductDTO; import com.example.petstoremobile.dtos.ProductDTO;
import com.example.petstoremobile.fragments.ListFragment; import com.example.petstoremobile.utils.InputValidator;
import com.example.petstoremobile.fragments.listfragments.InventoryFragment; import com.example.petstoremobile.utils.Resource;
import com.example.petstoremobile.viewmodels.InventoryViewModel;
import com.example.petstoremobile.viewmodels.ProductViewModel;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import retrofit2.Call; import dagger.hilt.android.AndroidEntryPoint;
import retrofit2.Callback;
import retrofit2.Response;
/**
* Fragment for displaying and editing inventory item details.
*/
@AndroidEntryPoint
public class InventoryDetailFragment extends Fragment { public class InventoryDetailFragment extends Fragment {
private TextView tvMode, tvInventoryId, tvProductInfo; private FragmentInventoryDetailBinding binding;
private AutoCompleteTextView etProductSearch;
private android.widget.EditText etQuantity;
private Button btnSave, btnDelete, btnBack;
private InventoryApi inventoryApi; private InventoryViewModel inventoryViewModel;
private ProductApi productApi; private ProductViewModel productViewModel;
private InventoryFragment inventoryFragment;
private boolean isEditing = false; private boolean isEditing = false;
private long inventoryId = -1; private long inventoryId = -1;
@@ -61,62 +58,72 @@ public class InventoryDetailFragment extends Fragment {
private final List<ProductDTO> productSuggestions = new ArrayList<>(); private final List<ProductDTO> productSuggestions = new ArrayList<>();
private ArrayAdapter<String> dropdownAdapter; private ArrayAdapter<String> dropdownAdapter;
public void setInventoryFragment(InventoryFragment fragment) { /**
this.inventoryFragment = fragment; * Initializes the view models.
*/
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
inventoryViewModel = new ViewModelProvider(this).get(InventoryViewModel.class);
productViewModel = new ViewModelProvider(this).get(ProductViewModel.class);
} }
/**
* Inflates the layout.
*/
@Override @Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) { Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_inventory_detail, container, false); binding = FragmentInventoryDetailBinding.inflate(inflater, container, false);
return binding.getRoot();
}
inventoryApi = RetrofitClient.getInventoryApi(requireContext()); /**
productApi = RetrofitClient.getProductApi(requireContext()); * Sets up UI components after the view is created.
*/
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
initViews(view);
setupProductSearch(); setupProductSearch();
handleArguments(); handleArguments();
btnBack.setOnClickListener(v -> navigateBack()); binding.btnInventoryBack.setOnClickListener(v -> navigateBack());
btnSave.setOnClickListener(v -> saveInventory()); binding.btnSaveInventory.setOnClickListener(v -> saveInventory());
btnDelete.setOnClickListener(v -> confirmDelete()); binding.btnDeleteInventory.setOnClickListener(v -> confirmDelete());
return view;
}
private void initViews(View view) {
tvMode = view.findViewById(R.id.tvInventoryMode);
tvInventoryId = view.findViewById(R.id.tvInventoryId);
tvProductInfo = view.findViewById(R.id.tvProductInfo);
etProductSearch = view.findViewById(R.id.etProductSearch);
etQuantity = view.findViewById(R.id.etQuantity);
btnSave = view.findViewById(R.id.btnSaveInventory);
btnDelete = view.findViewById(R.id.btnDeleteInventory);
btnBack = view.findViewById(R.id.btnInventoryBack);
// Setup dropdown adapter // Setup dropdown adapter
dropdownAdapter = new BlackTextArrayAdapter<>(requireContext(), dropdownAdapter = new BlackTextArrayAdapter<>(requireContext(),
android.R.layout.simple_dropdown_item_1line, new ArrayList<>()); android.R.layout.simple_dropdown_item_1line, new ArrayList<>());
etProductSearch.setAdapter(dropdownAdapter); binding.etProductSearch.setAdapter(dropdownAdapter);
etProductSearch.setThreshold(1); // start showing after 1 character binding.etProductSearch.setThreshold(1); // start showing after 1 character
} }
// Product search dropdown @Override
public void onDestroyView() {
super.onDestroyView();
if (searchRunnable != null) {
searchHandler.removeCallbacks(searchRunnable);
}
binding = null;
}
/**
* Sets up the product search dropdown.
*/
private void setupProductSearch() { private void setupProductSearch() {
etProductSearch.addTextChangedListener(new TextWatcher() { binding.etProductSearch.addTextChangedListener(new TextWatcher() {
@Override @Override public void beforeTextChanged(CharSequence s, int i, int i1, int i2) {
public void beforeTextChanged(CharSequence s, int i, int i1, int i2) {
} }
@Override @Override public void afterTextChanged(Editable s) {
public void afterTextChanged(Editable s) {
} }
@Override @Override
public void onTextChanged(CharSequence s, int start, int before, int count) { public void onTextChanged(CharSequence s, int start, int before, int count) {
// Clear selected product when user is typing again // Clear selected product when user is typing again
selectedProduct = null; selectedProduct = null;
tvProductInfo.setVisibility(View.GONE); binding.tvProductInfo.setVisibility(View.GONE);
if (searchRunnable != null) if (searchRunnable != null)
searchHandler.removeCallbacks(searchRunnable); searchHandler.removeCallbacks(searchRunnable);
@@ -130,163 +137,142 @@ public class InventoryDetailFragment extends Fragment {
}); });
// When user picks an item from the dropdown // When user picks an item from the dropdown
etProductSearch.setOnItemClickListener((parent, view, position, id) -> { binding.etProductSearch.setOnItemClickListener((parent, view, position, id) -> {
if (position < productSuggestions.size()) { if (position < productSuggestions.size()) {
selectedProduct = productSuggestions.get(position); selectedProduct = productSuggestions.get(position);
// Show product details below the search box // Show product details below the search box
tvProductInfo.setText( binding.tvProductInfo.setText(
"ID: " + selectedProduct.getProdId() "ID: " + selectedProduct.getProdId()
+ "" + selectedProduct.getCategoryName()); + "" + selectedProduct.getCategoryName());
tvProductInfo.setVisibility(View.VISIBLE); binding.tvProductInfo.setVisibility(View.VISIBLE);
} }
}); });
} }
/**
* Searches for products matching the query from the backend.
*/
private void searchProducts(String query) { private void searchProducts(String query) {
productApi.getAllProducts(query, 0, 20).enqueue(new Callback<PageResponse<ProductDTO>>() { if (getView() == null) return;
@Override productViewModel.getAllProducts(query, null, 0, 20, "prodName").observe(getViewLifecycleOwner(), resource -> {
public void onResponse(Call<PageResponse<ProductDTO>> call, if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
Response<PageResponse<ProductDTO>> response) { productSuggestions.clear();
if (response.isSuccessful() && response.body() != null) { productSuggestions.addAll(resource.data.getContent());
productSuggestions.clear();
productSuggestions.addAll(response.body().getContent());
// Build display strings: "Product Name (ID: X)" // Build display strings: "Product Name (ID: X)"
List<String> names = new ArrayList<>(); List<String> names = new ArrayList<>();
for (ProductDTO p : productSuggestions) { for (ProductDTO p : productSuggestions) {
names.add(p.getProdName() + " (ID: " + p.getProdId() + ")"); names.add(p.getProdName() + " (ID: " + p.getProdId() + ")");
}
dropdownAdapter.clear();
dropdownAdapter.addAll(names);
dropdownAdapter.notifyDataSetChanged();
etProductSearch.showDropDown();
} }
}
@Override dropdownAdapter.clear();
public void onFailure(Call<PageResponse<ProductDTO>> call, Throwable t) { dropdownAdapter.addAll(names);
Toast.makeText(getContext(), "Failed to load products", Toast.LENGTH_SHORT).show(); dropdownAdapter.notifyDataSetChanged();
binding.etProductSearch.showDropDown();
} }
}); });
} }
// Arguments (edit mode) /**
* 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; isEditing = true;
inventoryId = args.getLong("inventoryId"); inventoryId = args.getLong("inventoryId");
tvMode.setText("Edit Inventory"); binding.tvInventoryMode.setText("Edit Inventory");
tvInventoryId.setText("Inventory ID: " + inventoryId); binding.tvInventoryId.setText("Inventory ID: " + inventoryId);
tvInventoryId.setVisibility(View.VISIBLE); binding.tvInventoryId.setVisibility(View.VISIBLE);
binding.btnDeleteInventory.setVisibility(View.VISIBLE);
binding.btnSaveInventory.setText("Save");
// Pre-fill search box with existing product name loadInventoryData();
String productName = args.getString("productName", "");
long prodId = args.getLong("prodId", -1);
etProductSearch.setText(productName);
// Show existing product info
if (prodId != -1) {
tvProductInfo.setText(
"ID: " + prodId
+ "" + args.getString("categoryName", ""));
tvProductInfo.setVisibility(View.VISIBLE);
// Build a minimal ProductDTO so selectedProduct is not null on save
selectedProduct = new ProductDTO(productName, null, null, null);
selectedProduct.setProdId(prodId);
}
etQuantity.setText(String.valueOf(args.getInt("quantity", 0)));
btnDelete.setVisibility(View.VISIBLE);
btnSave.setText("Save");
} else { } else {
isEditing = false; isEditing = false;
tvMode.setText("Add Inventory"); binding.tvInventoryMode.setText("Add Inventory");
tvInventoryId.setVisibility(View.GONE); binding.tvInventoryId.setVisibility(View.GONE);
tvProductInfo.setVisibility(View.GONE); binding.tvProductInfo.setVisibility(View.GONE);
btnDelete.setVisibility(View.GONE); binding.btnDeleteInventory.setVisibility(View.GONE);
btnSave.setText("Add"); binding.btnSaveInventory.setText("Add");
} }
} }
// Save /**
* Loads existing inventory data from the backend.
*/
private void loadInventoryData() {
inventoryViewModel.getInventoryById(inventoryId).observe(getViewLifecycleOwner(), resource -> {
if (resource == null) return;
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
InventoryDTO inv = resource.data;
binding.etProductSearch.setText(inv.getProductName());
binding.etQuantity.setText(String.valueOf(inv.getQuantity()));
if (inv.getProdId() != null) {
binding.tvProductInfo.setText(
"ID: " + inv.getProdId()
+ "" + inv.getCategoryName());
binding.tvProductInfo.setVisibility(View.VISIBLE);
selectedProduct = new ProductDTO();
selectedProduct.setProdId(inv.getProdId());
selectedProduct.setProdName(inv.getProductName());
selectedProduct.setCategoryName(inv.getCategoryName());
}
} else if (resource.status == Resource.Status.ERROR) {
Toast.makeText(getContext(), "Failed to load inventory: " + resource.message, Toast.LENGTH_SHORT).show();
}
});
}
/**
* Validates input and saves the current inventory item details to the backend.
*/
private void saveInventory() { private void saveInventory() {
if (selectedProduct == null) { if (selectedProduct == null) {
etProductSearch.setError("Please select a product from the list"); binding.etProductSearch.setError("Please select a product from the list");
etProductSearch.requestFocus(); binding.etProductSearch.requestFocus();
return; return;
} }
String quantityStr = etQuantity.getText().toString().trim(); if (!InputValidator.isNotEmpty(binding.etQuantity, "Quantity") ||
if (quantityStr.isEmpty()) { !InputValidator.isPositiveInteger(binding.etQuantity, "Quantity")) {
etQuantity.setError("Quantity is required");
etQuantity.requestFocus();
return; return;
} }
int quantity; int quantity = Integer.parseInt(binding.etQuantity.getText().toString().trim());
try {
quantity = Integer.parseInt(quantityStr);
} catch (NumberFormatException e) {
etQuantity.setError("Invalid quantity");
return;
}
if (quantity < 0) {
etQuantity.setError("Quantity must be 0 or more");
etQuantity.requestFocus();
return;
}
InventoryRequest request = new InventoryRequest(selectedProduct.getProdId(), quantity); InventoryRequest request = new InventoryRequest(selectedProduct.getProdId(), quantity);
setButtonsEnabled(false); setButtonsEnabled(false);
if (isEditing) { if (isEditing) {
inventoryApi.updateInventory(inventoryId, request).enqueue(new Callback<InventoryDTO>() { inventoryViewModel.updateInventory(inventoryId, request).observe(getViewLifecycleOwner(), resource -> {
@Override setButtonsEnabled(true);
public void onResponse(Call<InventoryDTO> call, Response<InventoryDTO> response) { if (resource.status == Resource.Status.SUCCESS) {
setButtonsEnabled(true); Toast.makeText(getContext(), "Inventory updated", Toast.LENGTH_SHORT).show();
if (response.isSuccessful()) { navigateBack();
Toast.makeText(getContext(), "Inventory updated", Toast.LENGTH_SHORT).show(); } else if (resource.status == Resource.Status.ERROR) {
notifyParentAndGoBack(); Toast.makeText(getContext(), "Error: " + resource.message, Toast.LENGTH_SHORT).show();
} else {
Toast.makeText(getContext(), "Update failed: " + response.code(), Toast.LENGTH_SHORT).show();
}
}
@Override
public void onFailure(Call<InventoryDTO> call, Throwable t) {
setButtonsEnabled(true);
Toast.makeText(getContext(), "Network error", Toast.LENGTH_SHORT).show();
} }
}); });
} else { } else {
inventoryApi.createInventory(request).enqueue(new Callback<InventoryDTO>() { inventoryViewModel.createInventory(request).observe(getViewLifecycleOwner(), resource -> {
@Override setButtonsEnabled(true);
public void onResponse(Call<InventoryDTO> call, Response<InventoryDTO> response) { if (resource.status == Resource.Status.SUCCESS) {
setButtonsEnabled(true); Toast.makeText(getContext(), "Inventory created", Toast.LENGTH_SHORT).show();
if (response.isSuccessful()) { navigateBack();
Toast.makeText(getContext(), "Inventory created", Toast.LENGTH_SHORT).show(); } else if (resource.status == Resource.Status.ERROR) {
notifyParentAndGoBack(); Toast.makeText(getContext(), "Error: " + resource.message, Toast.LENGTH_SHORT).show();
} else {
Toast.makeText(getContext(), "Create failed: " + response.code(), Toast.LENGTH_SHORT).show();
}
}
@Override
public void onFailure(Call<InventoryDTO> call, Throwable t) {
setButtonsEnabled(true);
Toast.makeText(getContext(), "Network error", Toast.LENGTH_SHORT).show();
} }
}); });
} }
} }
// Delete /**
* Shows a confirmation dialog before deleting an inventory item.
*/
private void confirmDelete() { private void confirmDelete() {
new AlertDialog.Builder(requireContext()) new AlertDialog.Builder(requireContext())
.setTitle("Delete inventory item?") .setTitle("Delete inventory item?")
@@ -296,45 +282,35 @@ public class InventoryDetailFragment extends Fragment {
.show(); .show();
} }
/**
* Sends a request to the API to delete the inventory item.
*/
private void deleteInventory() { private void deleteInventory() {
setButtonsEnabled(false); setButtonsEnabled(false);
inventoryApi.deleteInventory(inventoryId).enqueue(new Callback<Void>() { inventoryViewModel.deleteInventory(inventoryId).observe(getViewLifecycleOwner(), resource -> {
@Override setButtonsEnabled(true);
public void onResponse(Call<Void> call, Response<Void> response) { if (resource.status == Resource.Status.SUCCESS) {
setButtonsEnabled(true); Toast.makeText(getContext(), "Inventory deleted", Toast.LENGTH_SHORT).show();
if (response.isSuccessful()) { navigateBack();
Toast.makeText(getContext(), "Inventory deleted", Toast.LENGTH_SHORT).show(); } else if (resource.status == Resource.Status.ERROR) {
notifyParentAndGoBack(); Toast.makeText(getContext(), "Delete failed: " + resource.message, Toast.LENGTH_SHORT).show();
} else {
Toast.makeText(getContext(), "Delete failed: " + response.code(), Toast.LENGTH_SHORT).show();
}
}
@Override
public void onFailure(Call<Void> call, Throwable t) {
setButtonsEnabled(true);
Toast.makeText(getContext(), "Network error", Toast.LENGTH_SHORT).show();
} }
}); });
} }
// Helpers /**
* Navigates back to the previous fragment.
private void notifyParentAndGoBack() { */
if (inventoryFragment != null)
inventoryFragment.onInventoryChanged();
navigateBack();
}
private void navigateBack() { private void navigateBack() {
ListFragment lf = (ListFragment) getParentFragment(); NavHostFragment.findNavController(this).popBackStack();
if (lf != null)
lf.getChildFragmentManager().popBackStack();
} }
/**
* Enables or disables action buttons.
*/
private void setButtonsEnabled(boolean enabled) { private void setButtonsEnabled(boolean enabled) {
btnSave.setEnabled(enabled); binding.btnSaveInventory.setEnabled(enabled);
btnDelete.setEnabled(enabled); binding.btnDeleteInventory.setEnabled(enabled);
btnBack.setEnabled(enabled); binding.btnInventoryBack.setEnabled(enabled);
} }
} }

View File

@@ -4,84 +4,140 @@ import android.os.Bundle;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.core.content.ContextCompat;
import androidx.fragment.app.Fragment; import androidx.fragment.app.Fragment;
import androidx.lifecycle.ViewModelProvider;
import androidx.navigation.fragment.NavHostFragment;
import android.util.Log;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.ArrayAdapter; import android.widget.AdapterView;
import android.widget.Button;
import android.widget.EditText;
import android.widget.Spinner; import android.widget.Spinner;
import android.widget.TextView; import android.widget.TextView;
import android.widget.Toast; import android.widget.Toast;
import com.example.petstoremobile.R; import com.example.petstoremobile.R;
import com.example.petstoremobile.adapters.BlackTextArrayAdapter; import com.example.petstoremobile.databinding.FragmentPetDetailBinding;
import com.example.petstoremobile.api.PetApi; import com.example.petstoremobile.dtos.CustomerDTO;
import com.example.petstoremobile.api.RetrofitClient;
import com.example.petstoremobile.dtos.PetDTO; import com.example.petstoremobile.dtos.PetDTO;
import com.example.petstoremobile.fragments.ListFragment; import com.example.petstoremobile.dtos.StoreDTO;
import com.example.petstoremobile.fragments.listfragments.PetFragment;
import com.example.petstoremobile.utils.ActivityLogger; import com.example.petstoremobile.utils.ActivityLogger;
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.SpinnerUtils;
import com.example.petstoremobile.viewmodels.CustomerViewModel;
import com.example.petstoremobile.viewmodels.PetViewModel;
import com.example.petstoremobile.viewmodels.StoreViewModel;
import retrofit2.Call; import java.util.ArrayList;
import retrofit2.Callback; import java.util.List;
import retrofit2.Response; import java.util.Locale;
import dagger.hilt.android.AndroidEntryPoint;
/**
* Fragment for displaying and editing pet details.
*/
@AndroidEntryPoint
public class PetDetailFragment extends Fragment { public class PetDetailFragment extends Fragment {
private TextView tvMode, tvPetId; private FragmentPetDetailBinding binding;
private EditText etPetName, etPetSpecies, etPetBreed, etPetAge, etPetPrice; private long petId;
private Spinner spinnerPetStatus;
private Button btnSavePet, btnDeletePet, btnBack;
private int petId;
private boolean isEditing = false; private boolean isEditing = false;
private PetFragment petFragment;
//set the pet fragment to the parent so we refer back to pet view when save or delete is done private PetViewModel viewModel;
public void setPetFragment(PetFragment fragment) { private CustomerViewModel customerViewModel;
this.petFragment = fragment; private StoreViewModel storeViewModel;
private List<CustomerDTO> customerList = new ArrayList<>();
private List<StoreDTO> storeList = new ArrayList<>();
private Long selectedCustomerId = null;
private Long selectedStoreId = null;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
viewModel = new ViewModelProvider(this).get(PetViewModel.class);
customerViewModel = new ViewModelProvider(this).get(CustomerViewModel.class);
storeViewModel = new ViewModelProvider(this).get(StoreViewModel.class);
} }
@Override @Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) { Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_pet_detail, container, false); binding = FragmentPetDetailBinding.inflate(inflater, container, false);
return binding.getRoot();
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
//set up spinner and get controls from layout and display the view depending on the mode
initViews(view);
setupSpinner(); setupSpinner();
loadCustomers();
loadStores();
handleArguments(); handleArguments();
//set button click listeners //set button click listeners
btnBack.setOnClickListener(v -> navigateBack()); binding.btnBack.setOnClickListener(v -> navigateBack());
btnSavePet.setOnClickListener(v -> savePet()); binding.btnSavePet.setOnClickListener(v -> savePet());
btnDeletePet.setOnClickListener(v -> deletePet()); binding.btnDeletePet.setOnClickListener(v -> deletePet());
return view;
} }
//Method to Update or Add a pet @Override
public void onDestroyView() {
super.onDestroyView();
binding = null;
}
/**
* Handles the saving of pet data (adding/updating).
*/
private void savePet() { private void savePet() {
// Validates all fields using InputValidator // Validates all fields using InputValidator
if (!InputValidator.isNotEmpty(etPetName, "Pet Name")) return; if (!InputValidator.isNotEmpty(binding.etPetName, "Pet Name")) return;
if (!InputValidator.isNotEmpty(etPetSpecies, "Species")) return; if (!InputValidator.isNotEmpty(binding.etPetSpecies, "Species")) return;
if (!InputValidator.isNotEmpty(etPetBreed, "Breed")) return; if (!InputValidator.isNotEmpty(binding.etPetBreed, "Breed")) return;
if (!InputValidator.isPositiveInteger(etPetAge, "Age")) return; if (!InputValidator.isPositiveInteger(binding.etPetAge, "Age")) return;
if (!InputValidator.isPositiveDecimal(etPetPrice, "Price")) return; if (!InputValidator.isPositiveDecimal(binding.etPetPrice, "Price")) return;
//get all the values from the fields //get all the values from the fields
String name = etPetName.getText().toString().trim(); String name = binding.etPetName.getText().toString().trim();
String species = etPetSpecies.getText().toString().trim(); String species = binding.etPetSpecies.getText().toString().trim();
String breed = etPetBreed.getText().toString().trim(); String breed = binding.etPetBreed.getText().toString().trim();
int age = Integer.parseInt(etPetAge.getText().toString().trim()); int age = Integer.parseInt(binding.etPetAge.getText().toString().trim());
String priceStr = etPetPrice.getText().toString().trim(); double price = Double.parseDouble(binding.etPetPrice.getText().toString().trim());
String status = spinnerPetStatus.getSelectedItem().toString(); String status = binding.spinnerPetStatus.getSelectedItem().toString();
// Get selected customer
Long customerId = null;
int customerPos = binding.spinnerCustomer.getSelectedItemPosition();
if (customerPos > 0) { // 0 means no customer for pet
customerId = customerList.get(customerPos - 1).getCustomerId();
}
// Get selected store
Long storeId = null;
int storePos = binding.spinnerStore.getSelectedItemPosition();
if (storePos > 0) {
storeId = storeList.get(storePos - 1).getStoreId();
}
// Validation: If status is Available, a store must be selected
if ("Available".equalsIgnoreCase(status)) {
if (!InputValidator.isSpinnerSelected(binding.spinnerStore, "Store")) return;
}
// Validation: If status is Owned, an owner must be selected
if ("Owned".equalsIgnoreCase(status)) {
if (!InputValidator.isSpinnerSelected(binding.spinnerCustomer, "Owner")) return;
}
// Validation: If status is Adopted, an owner and store must be selected
if ("Adopted".equalsIgnoreCase(status)) {
if (!InputValidator.isSpinnerSelected(binding.spinnerCustomer, "Owner")) return;
if (!InputValidator.isSpinnerSelected(binding.spinnerStore, "Store")) return;
}
//create a pet object to send to the API //create a pet object to send to the API
PetDTO petDTO = new PetDTO(); PetDTO petDTO = new PetDTO();
@@ -89,157 +145,221 @@ public class PetDetailFragment extends Fragment {
petDTO.setPetSpecies(species); petDTO.setPetSpecies(species);
petDTO.setPetBreed(breed); petDTO.setPetBreed(breed);
petDTO.setPetAge(age); petDTO.setPetAge(age);
petDTO.setPetPrice(priceStr); petDTO.setPetPrice(price);
petDTO.setPetStatus(status); petDTO.setPetStatus(status);
petDTO.setCustomerId(customerId);
PetApi petApi = RetrofitClient.getPetApi(requireContext()); petDTO.setStoreId(storeId);
//check if the pet is being edited or added //check if the pet is being edited or added
if (isEditing) { if (isEditing) {
// Update existing pet // Update existing pet
petDTO.setPetId((long) petId); petDTO.setPetId(petId);
petApi.updatePet((long) petId, petDTO).enqueue(new Callback<PetDTO>() { viewModel.updatePet(petId, petDTO).observe(getViewLifecycleOwner(), resource -> {
@Override if (resource.status == Resource.Status.SUCCESS) {
public void onResponse(Call<PetDTO> call, Response<PetDTO> response) { ActivityLogger.logChange(requireContext(), "Pet", "UPDATED", (int) petId);
if (response.isSuccessful()) { Toast.makeText(getContext(), "Pet updated successfully!", Toast.LENGTH_SHORT).show();
ActivityLogger.logChange(requireContext(), "Pet", "UPDATED", petId); navigateToPetList();
Toast.makeText(getContext(), "Pet updated successfully!", Toast.LENGTH_SHORT).show(); } else if (resource.status == Resource.Status.ERROR) {
navigateBack(); Toast.makeText(getContext(), "Error: " + resource.message, Toast.LENGTH_SHORT).show();
} else {
Toast.makeText(getContext(), "Failed to update pet: " + response.code(), Toast.LENGTH_SHORT).show();
}
}
@Override
public void onFailure(Call<PetDTO> call, Throwable t) {
ActivityLogger.logException(requireContext(), "PetDetailFragment.updatePet", new Exception(t));
Log.e("PetDetailFragment", "Error updating pet", t);
Toast.makeText(getContext(), "Error: " + t.getMessage(), Toast.LENGTH_SHORT).show();
} }
}); });
} else { } else {
// Add new pet // Add new pet
petApi.createPet(petDTO).enqueue(new Callback<PetDTO>() { viewModel.createPet(petDTO).observe(getViewLifecycleOwner(), resource -> {
@Override if (resource.status == Resource.Status.SUCCESS) {
public void onResponse(Call<PetDTO> call, Response<PetDTO> response) { ActivityLogger.log(requireContext(), "Added new Pet: " + name);
if (response.isSuccessful()) { Toast.makeText(getContext(), "Pet added successfully!", Toast.LENGTH_SHORT).show();
ActivityLogger.log(requireContext(), "Added new Pet: " + name); navigateToPetList();
Toast.makeText(getContext(), "Pet added successfully!", Toast.LENGTH_SHORT).show(); } else if (resource.status == Resource.Status.ERROR) {
navigateBack(); Toast.makeText(getContext(), "Error: " + resource.message, Toast.LENGTH_SHORT).show();
} else {
Toast.makeText(getContext(), "Failed to add pet: " + response.code(), Toast.LENGTH_SHORT).show();
}
}
@Override
public void onFailure(Call<PetDTO> call, Throwable t) {
ActivityLogger.logException(requireContext(), "PetDetailFragment.createPet", new Exception(t));
Log.e("PetDetailFragment", "Error adding pet", t);
Toast.makeText(getContext(), "Error: " + t.getMessage(), Toast.LENGTH_SHORT).show();
} }
}); });
} }
} }
//Method to Delete a pet /**
* Displays a confirmation dialog and handles the deletion of a pet.
*/
private void deletePet() { private void deletePet() {
//Alert the user to confirm the delete DialogUtils.showDeleteConfirmDialog(requireContext(), "Pet", () ->
new AlertDialog.Builder(requireContext()) viewModel.deletePet(petId).observe(getViewLifecycleOwner(), resource -> {
.setTitle("Delete Pet") if (resource.status == Resource.Status.SUCCESS) {
.setMessage("Are you sure you want to delete " + etPetName.getText().toString() + "?") ActivityLogger.logChange(requireContext(), "Pet", "DELETED", (int) petId);
.setPositiveButton("Delete", (dialog, which) -> { Toast.makeText(getContext(), "Pet deleted successfully!", Toast.LENGTH_SHORT).show();
PetApi petApi = RetrofitClient.getPetApi(requireContext()); navigateToPetList();
//if they say yes then delete the pet } else if (resource.status == Resource.Status.ERROR) {
petApi.deletePet((long) petId).enqueue(new Callback<Void>() { Toast.makeText(getContext(), "Delete failed: " + resource.message, Toast.LENGTH_SHORT).show();
@Override }
public void onResponse(Call<Void> call, Response<Void> response) { }));
if (response.isSuccessful()) {
ActivityLogger.logChange(requireContext(), "Pet", "DELETED", petId);
Toast.makeText(getContext(), "Pet deleted successfully!", Toast.LENGTH_SHORT).show();
navigateBack();
} else {
Toast.makeText(getContext(), "Failed to delete pet: " + response.code(), Toast.LENGTH_SHORT).show();
}
}
@Override
public void onFailure(Call<Void> call, Throwable t) {
ActivityLogger.logException(requireContext(), "PetDetailFragment.deletePet", new Exception(t));
Log.e("PetDetailFragment", "Error deleting pet", t);
Toast.makeText(getContext(), "Error: " + t.getMessage(), Toast.LENGTH_SHORT).show();
}
});
})
.setNegativeButton("Cancel", null)
.show();
} }
//Helper method to navigate back to the list /**
* Navigates back to the pet list screen.
*/
private void navigateToPetList() {
NavHostFragment.findNavController(this).popBackStack(R.id.nav_pet, false);
}
/**
* Navigates back to the previous screen.
*/
private void navigateBack() { private void navigateBack() {
ListFragment listFragment = (ListFragment) getParentFragment(); NavHostFragment.findNavController(this).popBackStack();
if (listFragment != null) {
// If editing pop back twice to get back to PetDetail Fragment instead of PetProfileFragment
if (isEditing) {
listFragment.getChildFragmentManager().popBackStack();
}
listFragment.getChildFragmentManager().popBackStack();
}
} }
//helper function to check if pet is being edited or added and show the view accordingly /**
* Handles arguments passed to the fragment to determine if it's in edit or add mode.
*/
private void handleArguments() { private void handleArguments() {
// Pet is being edited if the bundle contains a petId // 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 // Get pet data from arguments and populate fields
isEditing = true; isEditing = true;
petId = getArguments().getInt("petId"); petId = getArguments().getLong("petId");
tvMode.setText("Edit Pet"); binding.tvMode.setText("Edit Pet");
tvPetId.setText("ID: " + petId); binding.tvPetId.setText("ID: " + petId);
etPetName.setText(getArguments().getString("petName")); binding.tvPetId.setVisibility(View.VISIBLE);
etPetSpecies.setText(getArguments().getString("petSpecies")); binding.btnDeletePet.setVisibility(View.VISIBLE);
etPetBreed.setText(getArguments().getString("petBreed")); loadPetData();
etPetAge.setText(String.valueOf(getArguments().getInt("petAge")));
etPetPrice.setText(String.valueOf(getArguments().getDouble("petPrice")));
String status = getArguments().getString("petStatus");
if ("Available".equals(status)) {
spinnerPetStatus.setSelection(0);
} else {
spinnerPetStatus.setSelection(1);
}
btnDeletePet.setVisibility(View.VISIBLE);
} else { } else {
// Pet is being added // Pet is being added
// Set default values for add a new pet // Set default values for add a new pet
isEditing = false; isEditing = false;
tvMode.setText("Add Pet"); binding.tvMode.setText("Add Pet");
tvPetId.setVisibility(View.GONE); binding.tvPetId.setVisibility(View.GONE);
btnDeletePet.setVisibility(View.GONE); binding.btnDeletePet.setVisibility(View.GONE);
btnSavePet.setText("Add"); binding.btnSavePet.setText("Add");
} }
} }
//helper function to get controls from layout /**
private void initViews(View view) { * Fetches specific pet details from the backend using the ID.
tvMode = view.findViewById(R.id.tvMode); */
tvPetId = view.findViewById(R.id.tvPetId); private void loadPetData() {
etPetName = view.findViewById(R.id.etPetName); viewModel.getPetById(petId).observe(getViewLifecycleOwner(), resource -> {
etPetSpecies = view.findViewById(R.id.etPetSpecies); if (resource == null) return;
etPetBreed = view.findViewById(R.id.etPetBreed); if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
etPetAge = view.findViewById(R.id.etPetAge); PetDTO p = resource.data;
etPetPrice = view.findViewById(R.id.etPetPrice); binding.etPetName.setText(p.getPetName());
spinnerPetStatus = view.findViewById(R.id.spinnerPetStatus); binding.etPetSpecies.setText(p.getPetSpecies());
btnSavePet = view.findViewById(R.id.btnSavePet); binding.etPetBreed.setText(p.getPetBreed());
btnDeletePet = view.findViewById(R.id.btnDeletePet); binding.etPetAge.setText(String.valueOf(p.getPetAge()));
btnBack = view.findViewById(R.id.btnBack); if (p.getPetPrice() != null) {
binding.etPetPrice.setText(String.format(Locale.getDefault(), "%.2f", p.getPetPrice()));
}
SpinnerUtils.setSelectionByValue(binding.spinnerPetStatus, p.getPetStatus());
selectedCustomerId = p.getCustomerId();
updateCustomerSpinnerSelection();
selectedStoreId = p.getStoreId();
updateStoreSpinnerSelection();
} else if (resource.status == Resource.Status.ERROR) {
Toast.makeText(getContext(), "Failed to load pet: " + resource.message, Toast.LENGTH_SHORT).show();
}
});
} }
//helper function to set up the spinner menu for pet status /**
* 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() {
SpinnerUtils.populateSpinner(
requireContext(),
binding.spinnerCustomer,
customerList,
CustomerDTO::getFullName,
"No Owner",
selectedCustomerId,
CustomerDTO::getCustomerId
);
}
/**
* Updates the store spinner with the current list and sets the selection if needed.
*/
private void updateStoreSpinnerSelection() {
SpinnerUtils.populateSpinner(
requireContext(),
binding.spinnerStore,
storeList,
StoreDTO::getStoreName,
"None",
selectedStoreId,
StoreDTO::getStoreId
);
}
/**
* Initializes the spinner for pet status selection.
*/
private void setupSpinner() { private void setupSpinner() {
BlackTextArrayAdapter<String> adapter = new BlackTextArrayAdapter<>(requireContext(), SpinnerUtils.setupStringSpinner(requireContext(), binding.spinnerPetStatus,
android.R.layout.simple_spinner_item, new String[]{"Available", "Adopted", "Owned"});
new String[]{"Available", "Adopted"});
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); binding.spinnerPetStatus.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
spinnerPetStatus.setAdapter(adapter); @Override
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
String status = parent.getItemAtPosition(position).toString();
// Clear any existing error icons when status changes
clearSpinnerError(binding.spinnerCustomer);
clearSpinnerError(binding.spinnerStore);
//Disable the customer spinner if the status is "Available"
if ("Available".equalsIgnoreCase(status)) {
binding.spinnerCustomer.setSelection(0);
binding.spinnerCustomer.setEnabled(false);
} else {
binding.spinnerCustomer.setEnabled(true);
}
//Disable the store spinner if the status is "Owned"
if ("Owned".equalsIgnoreCase(status)) {
binding.spinnerStore.setSelection(0);
binding.spinnerStore.setEnabled(false);
} else {
binding.spinnerStore.setEnabled(true);
}
}
@Override
public void onNothingSelected(AdapterView<?> parent) {
}
});
} }
/**
* Clears error messages from a Spinner's selected view.
*/
private void clearSpinnerError(Spinner spinner) {
View selectedView = spinner.getSelectedView();
if (selectedView instanceof TextView) {
((TextView) selectedView).setError(null);
}
}
} }

View File

@@ -1,396 +1,321 @@
package com.example.petstoremobile.fragments.listfragments.detailfragments; package com.example.petstoremobile.fragments.listfragments.detailfragments;
import android.Manifest;
import android.app.Activity;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.net.Uri; import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import android.provider.MediaStore;
import android.util.Log;
import android.view.*; import android.view.*;
import android.widget.*; import android.widget.*;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog; import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat;
import androidx.core.content.FileProvider;
import androidx.fragment.app.Fragment; import androidx.fragment.app.Fragment;
import androidx.lifecycle.ViewModelProvider;
import androidx.navigation.fragment.NavHostFragment;
import com.bumptech.glide.Glide; import com.bumptech.glide.Glide;
import com.bumptech.glide.load.engine.DiskCacheStrategy;
import com.example.petstoremobile.R; import com.example.petstoremobile.R;
import com.example.petstoremobile.adapters.BlackTextArrayAdapter;
import com.example.petstoremobile.api.*; import com.example.petstoremobile.api.*;
import com.example.petstoremobile.api.auth.TokenManager;
import com.example.petstoremobile.databinding.FragmentProductDetailBinding;
import com.example.petstoremobile.dtos.*; import com.example.petstoremobile.dtos.*;
import com.example.petstoremobile.fragments.ListFragment; import com.example.petstoremobile.viewmodels.ProductViewModel;
import com.example.petstoremobile.utils.DialogUtils;
import com.example.petstoremobile.utils.FileUtils;
import com.example.petstoremobile.utils.GlideUtils;
import com.example.petstoremobile.utils.ImagePickerHelper;
import com.example.petstoremobile.utils.InputValidator;
import com.example.petstoremobile.utils.Resource;
import com.example.petstoremobile.utils.SpinnerUtils;
import java.io.File; import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.util.*; import java.util.*;
import javax.inject.Inject;
import javax.inject.Named;
import dagger.hilt.android.AndroidEntryPoint;
import okhttp3.MediaType; import okhttp3.MediaType;
import okhttp3.MultipartBody; import okhttp3.MultipartBody;
import okhttp3.RequestBody; import okhttp3.RequestBody;
import retrofit2.*;
/**
* Fragment for displaying and editing product details, including image selection.
*/
@AndroidEntryPoint
public class ProductDetailFragment extends Fragment { public class ProductDetailFragment extends Fragment {
private TextView tvMode, tvProductId; private FragmentProductDetailBinding binding;
private EditText etProductName, etProductDesc, etProductPrice;
private Spinner spinnerCategory;
private Button btnSave, btnDelete, btnBack;
private ImageView ivProductImage;
private long prodId = -1; private long prodId = -1;
private boolean isEditing = false; 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 isImageRemoved = false;
private List<CategoryDTO> categoryList = new ArrayList<>(); private List<CategoryDTO> categoryList = new ArrayList<>();
private Uri photoUri; private Uri photoUri;
private ProductViewModel viewModel;
private ImagePickerHelper imagePickerHelper;
private ActivityResultLauncher<Intent> galleryLauncher; @Inject @Named("baseUrl") String baseUrl;
private ActivityResultLauncher<Uri> cameraLauncher; @Inject TokenManager tokenManager;
private ActivityResultLauncher<String> permissionLauncher;
/**
* Initializes activity launchers and the ImagePickerHelper.
*/
@Override @Override
public void onCreate(Bundle savedInstanceState) { public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
galleryLauncher = registerForActivityResult( viewModel = new ViewModelProvider(this).get(ProductViewModel.class);
new ActivityResultContracts.StartActivityForResult(),
result -> { imagePickerHelper = new ImagePickerHelper(this, "product_photo.jpg", new ImagePickerHelper.ImagePickerListener() {
if (result.getResultCode() == Activity.RESULT_OK && result.getData() != null) { @Override
Uri selectedImage = result.getData().getData(); public void onImagePicked(Uri uri) {
if (isEditing) { photoUri = uri;
uploadProductImage(selectedImage); Glide.with(ProductDetailFragment.this).load(uri).into(binding.ivProductImage);
} else { hasImage = true;
ivProductImage.setImageURI(selectedImage); isImageChanged = true;
photoUri = selectedImage; isImageRemoved = false;
hasImage = true; }
}
} @Override
} public void onImageRemoved() {
); photoUri = null;
cameraLauncher = registerForActivityResult( hasImage = false;
new ActivityResultContracts.TakePicture(), isImageChanged = false;
success -> { isImageRemoved = true;
if (success) { binding.ivProductImage.setImageResource(R.drawable.placeholder2);
if (isEditing) { }
uploadProductImage(photoUri); });
} else {
ivProductImage.setImageURI(photoUri);
hasImage = true;
}
}
}
);
permissionLauncher = registerForActivityResult(
new ActivityResultContracts.RequestPermission(),
granted -> {
if (granted) launchCamera();
else Toast.makeText(getContext(), "Camera permission denied", Toast.LENGTH_SHORT).show();
}
);
} }
/**
* 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) {
View view = inflater.inflate(R.layout.fragment_product_detail, container, false); binding = FragmentProductDetailBinding.inflate(inflater, container, false);
initViews(view); return binding.getRoot();
}
/**
* Sets up UI components and listeners after the view is created.
*/
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
loadCategories(); loadCategories();
handleArguments(); handleArguments();
btnBack.setOnClickListener(v -> navigateBack()); binding.btnProductBack.setOnClickListener(v -> navigateBack());
btnSave.setOnClickListener(v -> saveProduct()); binding.btnSaveProduct.setOnClickListener(v -> saveProduct());
btnDelete.setOnClickListener(v -> confirmDelete()); binding.btnDeleteProduct.setOnClickListener(v -> confirmDelete());
ivProductImage.setOnClickListener(v -> showImagePickerDialog()); binding.ivProductImage.setOnClickListener(v -> imagePickerHelper.showImagePickerDialog("Select Product Image", hasImage));
return view;
} }
private void initViews(View v) { @Override
tvMode = v.findViewById(R.id.tvProductMode); public void onDestroyView() {
tvProductId = v.findViewById(R.id.tvProductId); super.onDestroyView();
etProductName = v.findViewById(R.id.etProductName); binding = null;
etProductDesc = v.findViewById(R.id.etProductDesc);
etProductPrice = v.findViewById(R.id.etProductPrice);
spinnerCategory = v.findViewById(R.id.spinnerProductCategory);
btnSave = v.findViewById(R.id.btnSaveProduct);
btnDelete = v.findViewById(R.id.btnDeleteProduct);
btnBack = v.findViewById(R.id.btnProductBack);
ivProductImage = v.findViewById(R.id.ivProductImage);
}
// Helper function to show the image picker dialog
private void showImagePickerDialog() {
List<String> options = new ArrayList<>();
options.add("Take Photo");
options.add("Choose from Gallery");
if (hasImage) {
options.add("Remove Photo");
}
new AlertDialog.Builder(requireContext())
.setTitle("Select Product Image")
.setItems(options.toArray(new String[0]), (dialog, which) -> {
String selectedOption = options.get(which);
if (selectedOption.equals("Take Photo")) {
if (ContextCompat.checkSelfPermission(requireContext(), Manifest.permission.CAMERA)
== PackageManager.PERMISSION_GRANTED) {
launchCamera();
} else {
permissionLauncher.launch(Manifest.permission.CAMERA);
}
} else if (selectedOption.equals("Choose from Gallery")) {
Intent intent = new Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI);
galleryLauncher.launch(intent);
} else if (selectedOption.equals("Remove Photo")) {
removePhoto();
}
})
.show();
}
// Helper function to remove the photo
private void removePhoto() {
if (isEditing) {
RetrofitClient.getProductApi(requireContext()).deleteProductImage(prodId)
.enqueue(new Callback<Void>() {
@Override
public void onResponse(Call<Void> call, Response<Void> response) {
if (response.isSuccessful()) {
Toast.makeText(getContext(), "Photo removed", Toast.LENGTH_SHORT).show();
ivProductImage.setImageResource(R.drawable.placeholder2);
hasImage = false;
} else {
Toast.makeText(getContext(), "Failed to remove photo", Toast.LENGTH_SHORT).show();
}
}
@Override
public void onFailure(Call<Void> call, Throwable t) {
Toast.makeText(getContext(), "Network error", Toast.LENGTH_SHORT).show();
}
});
} else {
photoUri = null;
hasImage = false;
ivProductImage.setImageResource(R.drawable.placeholder2);
}
}
// Helper function to launch the camera
private void launchCamera() {
File photoFile = new File(requireContext().getCacheDir(), "product_photo.jpg");
photoUri = FileProvider.getUriForFile(requireContext(), requireContext().getPackageName() + ".fileprovider", photoFile);
cameraLauncher.launch(photoUri);
} }
/**
* Fetches all product categories for the selection spinner.
*/
private void loadCategories() { private void loadCategories() {
RetrofitClient.getCategoryApi(requireContext()).getAllCategories(0, 100) viewModel.getAllCategories(0, 100).observe(getViewLifecycleOwner(), resource -> {
.enqueue(new Callback<PageResponse<CategoryDTO>>() { if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) {
public void onResponse(Call<PageResponse<CategoryDTO>> c, categoryList = resource.data.getContent();
Response<PageResponse<CategoryDTO>> r) { SpinnerUtils.populateSpinner(requireContext(), binding.spinnerProductCategory, categoryList,
if (r.isSuccessful() && r.body() != null) { CategoryDTO::getCategoryName, "-- Select Category --",
categoryList = r.body().getContent(); preselectedCategoryId, CategoryDTO::getCategoryId);
populateCategorySpinner();
}
}
public void onFailure(Call<PageResponse<CategoryDTO>> c, Throwable t) {
Log.e("ProductDetail", "Category load failed: " + t.getMessage());
}
});
}
private void populateCategorySpinner() {
List<String> names = new ArrayList<>();
names.add("-- Select Category --");
for (CategoryDTO c : categoryList) names.add(c.getCategoryName());
spinnerCategory.setAdapter(new BlackTextArrayAdapter<>(requireContext(),
android.R.layout.simple_spinner_item, names));
if (preselectedCategoryId != -1) {
for (int i = 0; i < categoryList.size(); i++) {
if (categoryList.get(i).getCategoryId().equals(preselectedCategoryId)) {
spinnerCategory.setSelection(i + 1); break;
}
} }
} });
} }
/**
* 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; isEditing = true;
prodId = a.getLong("prodId"); prodId = a.getLong("prodId");
preselectedCategoryId = a.getLong("categoryId", -1); binding.tvProductMode.setText("Edit Product");
hasImage = true; binding.tvProductId.setText("ID: " + prodId);
binding.tvProductId.setVisibility(View.VISIBLE);
tvMode.setText("Edit Product"); binding.btnDeleteProduct.setVisibility(View.VISIBLE);
tvProductId.setText("ID: " + prodId); loadProductData();
tvProductId.setVisibility(View.VISIBLE);
etProductName.setText(a.getString("prodName"));
etProductDesc.setText(a.getString("prodDesc"));
etProductPrice.setText(a.getString("prodPrice"));
btnDelete.setVisibility(View.VISIBLE);
loadProductImage(); loadProductImage();
} else { } else {
tvMode.setText("Add Product"); binding.tvProductMode.setText("Add Product");
btnDelete.setVisibility(View.GONE); binding.btnDeleteProduct.setVisibility(View.GONE);
tvProductId.setVisibility(View.GONE); binding.tvProductId.setVisibility(View.GONE);
hasImage = false; hasImage = false;
} }
} }
//load the product image from the backend /**
private void loadProductImage() { * Loads the product data from the backend.
String imageUrl = RetrofitClient.BASE_URL + String.format(Locale.US, ProductApi.PRODUCT_IMAGE_PATH, prodId); */
Glide.with(this) private void loadProductData() {
.load(imageUrl) viewModel.getProductById(prodId).observe(getViewLifecycleOwner(), resource -> {
.diskCacheStrategy(DiskCacheStrategy.NONE) if (resource == null) return;
.skipMemoryCache(true) if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
.placeholder(R.drawable.placeholder2) ProductDTO p = resource.data;
.error(R.drawable.placeholder2) binding.etProductName.setText(p.getProdName());
.into(ivProductImage); binding.etProductDesc.setText(p.getProdDesc());
} binding.etProductPrice.setText(p.getProdPrice() != null ? p.getProdPrice().toString() : "");
preselectedCategoryId = p.getCategoryId() != null ? p.getCategoryId() : -1;
// Function to upload the product image by calling the backend
private void uploadProductImage(Uri uri) { // Refresh spinner selection once data is loaded
try { if (!categoryList.isEmpty()) {
File file = getFileFromUri(uri); SpinnerUtils.populateSpinner(requireContext(), binding.spinnerProductCategory, categoryList,
if (file == null) return; CategoryDTO::getCategoryName, "-- Select Category --",
preselectedCategoryId, CategoryDTO::getCategoryId);
RequestBody requestFile = RequestBody.create(file, MediaType.parse(requireContext().getContentResolver().getType(uri))); }
MultipartBody.Part body = MultipartBody.Part.createFormData("image", file.getName(), requestFile); } else if (resource.status == Resource.Status.ERROR) {
Toast.makeText(getContext(), "Failed to load product: " + resource.message, Toast.LENGTH_SHORT).show();
RetrofitClient.getProductApi(requireContext()).uploadProductImage(prodId, body)
.enqueue(new Callback<Void>() {
@Override
public void onResponse(Call<Void> call, Response<Void> response) {
if (response.isSuccessful()) {
Toast.makeText(getContext(), "Image uploaded", Toast.LENGTH_SHORT).show();
hasImage = true;
loadProductImage();
} else {
Toast.makeText(getContext(), "Upload failed", Toast.LENGTH_SHORT).show();
}
}
@Override
public void onFailure(Call<Void> call, Throwable t) {
Toast.makeText(getContext(), "Network error", Toast.LENGTH_SHORT).show();
}
});
} catch (Exception e) {
Log.e("ProductDetail", "Error uploading image", e);
}
}
// Helper function to get the File from the Uri
private File getFileFromUri(Uri uri) {
try {
InputStream inputStream = requireContext().getContentResolver().openInputStream(uri);
File tempFile = new File(requireContext().getCacheDir(), "upload_product_image.jpg");
FileOutputStream outputStream = new FileOutputStream(tempFile);
byte[] buffer = new byte[1024];
int length;
while ((length = inputStream.read(buffer)) > 0) {
outputStream.write(buffer, 0, length);
} }
outputStream.close(); });
inputStream.close(); }
return tempFile;
} catch (Exception e) { /**
return null; * Loads the product image from the backend.
*/
private void loadProductImage() {
String imageUrl = baseUrl + String.format(Locale.US, ProductApi.PRODUCT_IMAGE_PATH, prodId);
String token = tokenManager.getToken();
GlideUtils.loadImageWithToken(requireContext(), binding.ivProductImage, imageUrl, token, R.drawable.placeholder2, new GlideUtils.ImageLoadListener() {
@Override
public void onResourceReady() {
hasImage = true;
}
@Override
public void onLoadFailed() {
hasImage = false;
}
});
}
/**
* Performs image related actions (upload/delete) after product details are saved.
*/
private void performPendingImageActions(String successMsg) {
if (isImageRemoved) {
viewModel.deleteProductImage(prodId).observe(getViewLifecycleOwner(), resource -> {
if (resource != null && resource.status != Resource.Status.LOADING) {
if (resource.status == Resource.Status.SUCCESS) {
Toast.makeText(getContext(), successMsg, Toast.LENGTH_SHORT).show();
} else {
Toast.makeText(getContext(), successMsg + " (but image removal failed)", Toast.LENGTH_SHORT).show();
}
navigateBack();
}
});
} else if (isImageChanged && photoUri != null) {
uploadProductImageAndNavigate(photoUri, successMsg);
} else {
Toast.makeText(getContext(), successMsg, Toast.LENGTH_SHORT).show();
navigateBack();
} }
} }
private void saveProduct() { /**
String name = etProductName.getText().toString().trim(); * Uploads the selected image file to the server.
String desc = etProductDesc.getText().toString().trim(); */
String priceStr = etProductPrice.getText().toString().trim(); private void uploadProductImageAndNavigate(Uri uri, String successMsg) {
File file = FileUtils.getFileFromUri(requireContext(), uri);
if (name.isEmpty()) { if (file == null) {
etProductName.setError("Enter product name"); return; Toast.makeText(getContext(), successMsg, Toast.LENGTH_SHORT).show();
navigateBack();
return;
} }
if (spinnerCategory.getSelectedItemPosition() == 0) {
RequestBody requestFile = RequestBody.create(file, MediaType.parse(requireContext().getContentResolver().getType(uri)));
MultipartBody.Part body = MultipartBody.Part.createFormData("image", file.getName(), requestFile);
viewModel.uploadProductImage(prodId, body).observe(getViewLifecycleOwner(), resource -> {
if (resource != null && resource.status != Resource.Status.LOADING) {
if (resource.status == Resource.Status.SUCCESS) {
Toast.makeText(getContext(), successMsg, Toast.LENGTH_SHORT).show();
} else {
Toast.makeText(getContext(), successMsg + " (but image upload failed)", Toast.LENGTH_SHORT).show();
}
navigateBack();
}
});
}
/**
* Validates input fields and saves product information to the backend.
*/
private void saveProduct() {
if (!InputValidator.isNotEmpty(binding.etProductName, "Product Name")) return;
if (binding.spinnerProductCategory.getSelectedItemPosition() == 0) {
Toast.makeText(getContext(), "Select a category", Toast.LENGTH_SHORT).show(); return; Toast.makeText(getContext(), "Select a category", Toast.LENGTH_SHORT).show(); return;
} }
if (priceStr.isEmpty()) {
etProductPrice.setError("Enter price"); return; if (!InputValidator.isNotEmpty(binding.etProductPrice, "Price") ||
!InputValidator.isPositiveDecimal(binding.etProductPrice, "Price")) {
return;
} }
CategoryDTO category = categoryList.get(spinnerCategory.getSelectedItemPosition() - 1); String name = binding.etProductName.getText().toString().trim();
BigDecimal price; String desc = binding.etProductDesc.getText().toString().trim();
try { BigDecimal price = new BigDecimal(binding.etProductPrice.getText().toString().trim());
price = new BigDecimal(priceStr);
} catch (Exception e) {
etProductPrice.setError("Invalid price"); return;
}
CategoryDTO category = categoryList.get(binding.spinnerProductCategory.getSelectedItemPosition() - 1);
ProductDTO dto = new ProductDTO(name, category.getCategoryId(), desc, price); ProductDTO dto = new ProductDTO(name, category.getCategoryId(), desc, price);
ProductApi api = RetrofitClient.getProductApi(requireContext());
if (isEditing) { if (isEditing) {
api.updateProduct(prodId, dto).enqueue(simpleCallback("Updated")); viewModel.updateProduct(prodId, dto).observe(getViewLifecycleOwner(), resource -> {
} else { if (resource != null && resource.status != Resource.Status.LOADING) {
api.createProduct(dto).enqueue(new Callback<ProductDTO>() { if (resource.status == Resource.Status.SUCCESS) {
@Override performPendingImageActions("Updated");
public void onResponse(Call<ProductDTO> call, Response<ProductDTO> response) {
if (response.isSuccessful() && response.body() != null) {
long newId = response.body().getProdId();
if (photoUri != null) {
prodId = newId;
uploadProductImage(photoUri);
}
Toast.makeText(getContext(), "Saved", Toast.LENGTH_SHORT).show();
navigateBack();
} else { } else {
Toast.makeText(getContext(), "Error saving", Toast.LENGTH_SHORT).show(); Toast.makeText(getContext(), "Error: " + resource.message, Toast.LENGTH_SHORT).show();
} }
} }
@Override });
public void onFailure(Call<ProductDTO> call, Throwable t) { } else {
Toast.makeText(getContext(), "Network error", Toast.LENGTH_SHORT).show(); 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();
}
} }
}); });
} }
} }
private Callback<ProductDTO> simpleCallback(String msg) { /**
return new Callback<>() { * Displays a confirmation dialog before deleting the product.
public void onResponse(Call<ProductDTO> c, Response<ProductDTO> r) { */
if (r.isSuccessful()) {
Toast.makeText(getContext(), msg, Toast.LENGTH_SHORT).show();
navigateBack();
} else {
Toast.makeText(getContext(), "Error " + r.code(), Toast.LENGTH_SHORT).show();
}
}
public void onFailure(Call<ProductDTO> c, Throwable t) {
Toast.makeText(getContext(), "Network error", Toast.LENGTH_SHORT).show();
}
};
}
private void confirmDelete() { private void confirmDelete() {
new AlertDialog.Builder(requireContext()) DialogUtils.showDeleteConfirmDialog(requireContext(), "Product", () ->
.setTitle("Delete Product?") viewModel.deleteProduct(prodId).observe(getViewLifecycleOwner(), resource -> {
.setPositiveButton("Yes", (d, w) -> if (resource != null && resource.status == Resource.Status.SUCCESS) {
RetrofitClient.getProductApi(requireContext()) navigateBack();
.deleteProduct(prodId) } else if (resource != null && resource.status == Resource.Status.ERROR) {
.enqueue(new Callback<Void>() { Toast.makeText(getContext(), "Delete failed: " + resource.message, Toast.LENGTH_SHORT).show();
public void onResponse(Call<Void> c, Response<Void> r) { }
navigateBack(); }));
}
public void onFailure(Call<Void> c, Throwable t) {
Toast.makeText(getContext(), "Delete failed",
Toast.LENGTH_SHORT).show();
}
}))
.setNegativeButton("No", null).show();
} }
/**
* Navigates back to the previous fragment.
*/
private void navigateBack() { private void navigateBack() {
ListFragment lf = (ListFragment) getParentFragment(); NavHostFragment.findNavController(this).popBackStack();
if (lf != null) lf.getChildFragmentManager().popBackStack();
} }
} }

View File

@@ -1,27 +1,36 @@
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.annotation.Nullable;
import androidx.fragment.app.Fragment; import androidx.fragment.app.Fragment;
import com.example.petstoremobile.R; import androidx.lifecycle.ViewModelProvider;
import com.example.petstoremobile.adapters.BlackTextArrayAdapter; import androidx.navigation.fragment.NavHostFragment;
import com.example.petstoremobile.api.*;
import com.example.petstoremobile.databinding.FragmentProductSupplierDetailBinding;
import com.example.petstoremobile.dtos.*; import com.example.petstoremobile.dtos.*;
import com.example.petstoremobile.fragments.ListFragment; import com.example.petstoremobile.utils.DialogUtils;
import com.example.petstoremobile.utils.InputValidator;
import com.example.petstoremobile.utils.Resource;
import com.example.petstoremobile.utils.SpinnerUtils;
import com.example.petstoremobile.viewmodels.ProductSupplierViewModel;
import com.example.petstoremobile.viewmodels.ProductViewModel;
import com.example.petstoremobile.viewmodels.SupplierViewModel;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.util.*; import java.util.*;
import retrofit2.*;
import dagger.hilt.android.AndroidEntryPoint;
/**
* Fragment for displaying and editing the relationship between products and suppliers.
*/
@AndroidEntryPoint
public class ProductSupplierDetailFragment extends Fragment { public class ProductSupplierDetailFragment extends Fragment {
private TextView tvMode; private FragmentProductSupplierDetailBinding binding;
private Spinner spinnerProduct, spinnerSupplier;
private EditText etCost;
private Button btnSave, btnDelete, btnBack;
private boolean isEditing = false; private boolean isEditing = false;
private long editProductId = -1; private long editProductId = -1;
@@ -32,190 +41,171 @@ public class ProductSupplierDetailFragment extends Fragment {
private List<ProductDTO> productList = new ArrayList<>(); private List<ProductDTO> productList = new ArrayList<>();
private List<SupplierDTO> supplierList = new ArrayList<>(); private List<SupplierDTO> supplierList = new ArrayList<>();
private ProductSupplierViewModel psViewModel;
private ProductViewModel productViewModel;
private SupplierViewModel supplierViewModel;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
psViewModel = new ViewModelProvider(this).get(ProductSupplierViewModel.class);
productViewModel = new ViewModelProvider(this).get(ProductViewModel.class);
supplierViewModel = new ViewModelProvider(this).get(SupplierViewModel.class);
}
@Override @Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) { Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_product_supplier_detail, container, false); binding = FragmentProductSupplierDetailBinding.inflate(inflater, container, false);
initViews(view); return binding.getRoot();
loadData(); }
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
loadSpinnersData();
handleArguments(); handleArguments();
btnBack.setOnClickListener(v -> navigateBack()); binding.btnPSBack.setOnClickListener(v -> navigateBack());
btnSave.setOnClickListener(v -> save()); binding.btnSavePS.setOnClickListener(v -> save());
btnDelete.setOnClickListener(v -> confirmDelete()); binding.btnDeletePS.setOnClickListener(v -> confirmDelete());
return view;
} }
private void initViews(View v) { @Override
tvMode = v.findViewById(R.id.tvPSMode); public void onDestroyView() {
spinnerProduct = v.findViewById(R.id.spinnerPSProduct); super.onDestroyView();
spinnerSupplier = v.findViewById(R.id.spinnerPSSupplier); binding = null;
etCost = v.findViewById(R.id.etPSCost);
btnSave = v.findViewById(R.id.btnSavePS);
btnDelete = v.findViewById(R.id.btnDeletePS);
btnBack = v.findViewById(R.id.btnPSBack);
} }
private void loadData() { /**
* Fetches products and suppliers to populate the spinners.
*/
private void loadSpinnersData() {
loadProducts(); loadProducts();
loadSuppliers(); loadSuppliers();
} }
/**
* Loads the list of products from the API.
*/
private void loadProducts() { private void loadProducts() {
RetrofitClient.getProductApi(requireContext()).getAllProducts(null, 0, 200) productViewModel.getAllProducts(null, null, 0, 200, "prodName").observe(getViewLifecycleOwner(), resource -> {
.enqueue(new Callback<PageResponse<ProductDTO>>() { if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
public void onResponse(Call<PageResponse<ProductDTO>> c, productList = resource.data.getContent();
Response<PageResponse<ProductDTO>> r) { refreshProductSpinner();
if (r.isSuccessful() && r.body() != null) {
productList = r.body().getContent();
populateProductSpinner();
}
}
public void onFailure(Call<PageResponse<ProductDTO>> c, Throwable t) {
Log.e("PSDetail", "Product load failed: " + t.getMessage());
}
});
}
private void populateProductSpinner() {
List<String> names = new ArrayList<>();
names.add("-- Select Product --");
for (ProductDTO p : productList) names.add(p.getProdName());
spinnerProduct.setAdapter(new BlackTextArrayAdapter<>(requireContext(),
android.R.layout.simple_spinner_item, names));
if (preselectedProductId != -1) {
for (int i = 0; i < productList.size(); i++) {
if (productList.get(i).getProdId().equals(preselectedProductId)) {
spinnerProduct.setSelection(i + 1); break;
}
} }
} });
} }
private void refreshProductSpinner() {
SpinnerUtils.populateSpinner(requireContext(), binding.spinnerPSProduct, productList,
ProductDTO::getProdName, "-- Select Product --",
preselectedProductId, ProductDTO::getProdId);
}
/**
* Loads the list of suppliers from the API.
*/
private void loadSuppliers() { private void loadSuppliers() {
RetrofitClient.getSupplierApi(requireContext()).getAllSuppliers(0, 200) supplierViewModel.getAllSuppliers(0, 200, null, "supCompany").observe(getViewLifecycleOwner(), resource -> {
.enqueue(new Callback<PageResponse<SupplierDTO>>() { if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
public void onResponse(Call<PageResponse<SupplierDTO>> c, supplierList = resource.data.getContent();
Response<PageResponse<SupplierDTO>> r) { refreshSupplierSpinner();
if (r.isSuccessful() && r.body() != null) {
supplierList = r.body().getContent();
populateSupplierSpinner();
}
}
public void onFailure(Call<PageResponse<SupplierDTO>> c, Throwable t) {
Log.e("PSDetail", "Supplier load failed: " + t.getMessage());
}
});
}
private void populateSupplierSpinner() {
List<String> names = new ArrayList<>();
names.add("-- Select Supplier --");
for (SupplierDTO s : supplierList) names.add(s.getSupCompany());
spinnerSupplier.setAdapter(new BlackTextArrayAdapter<>(requireContext(),
android.R.layout.simple_spinner_item, names));
if (preselectedSupplierId != -1) {
for (int i = 0; i < supplierList.size(); i++) {
if (supplierList.get(i).getSupId().equals(preselectedSupplierId)) {
spinnerSupplier.setSelection(i + 1); break;
}
} }
} });
} }
private void refreshSupplierSpinner() {
SpinnerUtils.populateSpinner(requireContext(), binding.spinnerPSSupplier, supplierList,
SupplierDTO::getSupCompany, "-- Select Supplier --",
preselectedSupplierId, SupplierDTO::getSupId);
}
/**
* Handles arguments to determine if the fragment is in edit or add mode.
*/
private void handleArguments() { private void handleArguments() {
Bundle a = getArguments(); Bundle a = getArguments();
if (a != null && a.containsKey("productId")) { if (a != null && a.containsKey("productId") && a.containsKey("supplierId")) {
isEditing = true; isEditing = true;
editProductId = a.getLong("productId"); editProductId = a.getLong("productId");
editSupplierId = a.getLong("supplierId"); editSupplierId = a.getLong("supplierId");
preselectedProductId = editProductId; preselectedProductId = editProductId;
preselectedSupplierId = editSupplierId; preselectedSupplierId = editSupplierId;
etCost.setText(a.getString("cost"));
tvMode.setText("Edit Product Supplier"); binding.tvPSMode.setText("Edit Product Supplier");
btnDelete.setVisibility(View.VISIBLE); binding.btnDeletePS.setVisibility(View.VISIBLE);
} else { } else {
tvMode.setText("Add Product Supplier"); binding.tvPSMode.setText("Add Product Supplier");
btnDelete.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 (spinnerProduct.getSelectedItemPosition() == 0) { if (binding.spinnerPSProduct.getSelectedItemPosition() == 0) {
Toast.makeText(getContext(), "Select a product", Toast.LENGTH_SHORT).show(); return; Toast.makeText(getContext(), "Select a product", Toast.LENGTH_SHORT).show(); return;
} }
if (spinnerSupplier.getSelectedItemPosition() == 0) { if (binding.spinnerPSSupplier.getSelectedItemPosition() == 0) {
Toast.makeText(getContext(), "Select a supplier", Toast.LENGTH_SHORT).show(); return; Toast.makeText(getContext(), "Select a supplier", Toast.LENGTH_SHORT).show(); return;
} }
String costStr = etCost.getText().toString().trim();
if (costStr.isEmpty()) { if (!InputValidator.isNotEmpty(binding.etPSCost, "Cost") ||
etCost.setError("Enter cost"); return; !InputValidator.isPositiveDecimal(binding.etPSCost, "Cost")) {
return;
} }
ProductDTO product = productList.get(spinnerProduct.getSelectedItemPosition() - 1); ProductDTO product = productList.get(binding.spinnerPSProduct.getSelectedItemPosition() - 1);
SupplierDTO supplier = supplierList.get(spinnerSupplier.getSelectedItemPosition() - 1); SupplierDTO supplier = supplierList.get(binding.spinnerPSSupplier.getSelectedItemPosition() - 1);
BigDecimal cost; BigDecimal cost = new BigDecimal(binding.etPSCost.getText().toString().trim());
try {
cost = new BigDecimal(costStr);
} catch (Exception e) {
etCost.setError("Invalid cost"); return;
}
ProductSupplierDTO dto = new ProductSupplierDTO( ProductSupplierDTO dto = new ProductSupplierDTO(
product.getProdId(), supplier.getSupId(), cost); product.getProdId(), supplier.getSupId(), cost);
ProductSupplierApi api = RetrofitClient.getProductSupplierApi(requireContext());
if (isEditing) { if (isEditing) {
api.updateProductSupplier(editProductId, editSupplierId, dto) psViewModel.updateProductSupplier(editProductId, editSupplierId, dto).observe(getViewLifecycleOwner(), resource -> {
.enqueue(simpleCallback("Updated")); if (resource.status == Resource.Status.SUCCESS) {
Toast.makeText(getContext(), "Updated", Toast.LENGTH_SHORT).show();
navigateBack();
} else if (resource.status == Resource.Status.ERROR) {
Toast.makeText(getContext(), "Error: " + resource.message, Toast.LENGTH_SHORT).show();
}
});
} else { } else {
api.createProductSupplier(dto).enqueue(simpleCallback("Saved")); 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();
}
});
} }
} }
private Callback<ProductSupplierDTO> simpleCallback(String msg) { /**
return new Callback<>() { * Shows a confirmation dialog before deleting a product-supplier relationship.
public void onResponse(Call<ProductSupplierDTO> c, Response<ProductSupplierDTO> r) { */
if (r.isSuccessful()) {
Toast.makeText(getContext(), msg, Toast.LENGTH_SHORT).show();
navigateBack();
} else {
try {
String err = r.errorBody().string();
Log.e("PS_SAVE", "Error: " + err);
Toast.makeText(getContext(), "Error " + r.code(), Toast.LENGTH_SHORT).show();
} catch (Exception e) {
Log.e("PS_SAVE", "Failed to read error");
}
}
}
public void onFailure(Call<ProductSupplierDTO> c, Throwable t) {
Log.e("PS_SAVE", "Failure: " + t.getMessage());
Toast.makeText(getContext(), "Network error", Toast.LENGTH_SHORT).show();
}
};
}
private void confirmDelete() { private void confirmDelete() {
new AlertDialog.Builder(requireContext()) DialogUtils.showDeleteConfirmDialog(requireContext(), "Product Supplier", () ->
.setTitle("Delete?") psViewModel.deleteProductSupplier(editProductId, editSupplierId).observe(getViewLifecycleOwner(), resource -> {
.setPositiveButton("Yes", (d, w) -> if (resource.status == Resource.Status.SUCCESS) {
RetrofitClient.getProductSupplierApi(requireContext()) Toast.makeText(getContext(), "Deleted", Toast.LENGTH_SHORT).show();
.deleteProductSupplier(editProductId, editSupplierId) navigateBack();
.enqueue(new Callback<Void>() { } else if (resource.status == Resource.Status.ERROR) {
public void onResponse(Call<Void> c, Response<Void> r) { Toast.makeText(getContext(), "Delete failed: " + resource.message, Toast.LENGTH_SHORT).show();
navigateBack(); }
} }));
public void onFailure(Call<Void> c, Throwable t) {
Toast.makeText(getContext(), "Delete failed",
Toast.LENGTH_SHORT).show();
}
}))
.setNegativeButton("No", null).show();
} }
/**
* Navigates back to the previous screen.
*/
private void navigateBack() { private void navigateBack() {
ListFragment lf = (ListFragment) getParentFragment(); NavHostFragment.findNavController(this).popBackStack();
if (lf != null) lf.getChildFragmentManager().popBackStack();
} }
} }

View File

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

View File

@@ -1,54 +1,66 @@
package com.example.petstoremobile.fragments.listfragments.detailfragments; package com.example.petstoremobile.fragments.listfragments.detailfragments;
import android.os.Bundle; import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment; import androidx.fragment.app.Fragment;
import androidx.navigation.fragment.NavHostFragment;
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.ArrayAdapter;
import android.widget.Button;
import android.widget.EditText;
import android.widget.Spinner;
import android.widget.TextView;
import android.widget.Toast; import android.widget.Toast;
import com.example.petstoremobile.R;
import com.example.petstoremobile.adapters.BlackTextArrayAdapter; import com.example.petstoremobile.api.SaleApi;
import com.example.petstoremobile.fragments.ListFragment; import com.example.petstoremobile.databinding.FragmentRefundDetailBinding;
import com.example.petstoremobile.fragments.listfragments.SaleFragment; import com.example.petstoremobile.fragments.listfragments.SaleFragment;
import com.example.petstoremobile.utils.ActivityLogger; import com.example.petstoremobile.utils.ActivityLogger;
import com.example.petstoremobile.utils.InputValidator; import com.example.petstoremobile.utils.InputValidator;
import com.example.petstoremobile.utils.SpinnerUtils;
import javax.inject.Inject;
import dagger.hilt.android.AndroidEntryPoint;
@AndroidEntryPoint
public class RefundDetailFragment extends Fragment { public class RefundDetailFragment extends Fragment {
private EditText etRefundSaleId, etRefundReason; private FragmentRefundDetailBinding binding;
private TextView tvSaleInfo;
private Spinner spinnerRefundPayment;
private Button btnLoadSale, btnProcessRefund, btnBack;
private int saleId; private int saleId;
private SaleFragment saleFragment; private SaleFragment saleFragment;
@Inject SaleApi saleApi;
public void setSaleFragment(SaleFragment fragment) { public void setSaleFragment(SaleFragment fragment) {
this.saleFragment = fragment; this.saleFragment = fragment;
} }
@Override @Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) { Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_refund_detail, container, false); binding = FragmentRefundDetailBinding.inflate(inflater, container, false);
return binding.getRoot();
}
initViews(view); @Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
setupSpinner(); setupSpinner();
handleArguments(); handleArguments();
btnBack.setOnClickListener(v -> goBack()); binding.btnRefundBack.setOnClickListener(v -> goBack());
btnLoadSale.setOnClickListener(v -> loadSaleDetails()); binding.btnLoadSale.setOnClickListener(v -> loadSaleDetails());
btnProcessRefund.setOnClickListener(v -> processRefund()); binding.btnProcessRefund.setOnClickListener(v -> processRefund());
}
return view; @Override
public void onDestroyView() {
super.onDestroyView();
binding = null;
} }
private void loadSaleDetails() { private void loadSaleDetails() {
String idText = etRefundSaleId.getText().toString().trim(); String idText = binding.etRefundSaleId.getText().toString().trim();
if (idText.isEmpty()) { if (idText.isEmpty()) {
Toast.makeText(getContext(), "Enter a Sale ID", Toast.LENGTH_SHORT).show(); Toast.makeText(getContext(), "Enter a Sale ID", Toast.LENGTH_SHORT).show();
return; return;
@@ -58,22 +70,21 @@ public class RefundDetailFragment extends Fragment {
int id = Integer.parseInt(idText); int id = Integer.parseInt(idText);
// TODO: Replace with actual API call - GET v1/sales/{id} // TODO: Replace with actual API call - GET v1/sales/{id}
// For now show placeholder info // For now show placeholder info
tvSaleInfo.setText("Sale ID: " + id + " loaded. Enter reason and payment method to process refund."); binding.tvSaleInfo.setText("Sale ID: " + id + " loaded. Enter reason and payment method to process refund.");
tvSaleInfo.setTextColor(getResources().getColor(android.R.color.holo_green_dark)); binding.tvSaleInfo.setTextColor(getResources().getColor(android.R.color.holo_green_dark));
} catch (NumberFormatException e) { } catch (NumberFormatException e) {
Toast.makeText(getContext(), "Invalid Sale ID", Toast.LENGTH_SHORT).show(); Toast.makeText(getContext(), "Invalid Sale ID", Toast.LENGTH_SHORT).show();
} }
} }
private void processRefund() { private void processRefund() {
if (!InputValidator.isNotEmpty(etRefundSaleId, "Sale ID")) if (!InputValidator.isNotEmpty(binding.etRefundSaleId, "Sale ID"))
return; return;
if (!InputValidator.isNotEmpty(etRefundReason, "Refund Reason")) if (!InputValidator.isNotEmpty(binding.etRefundReason, "Refund Reason"))
return; return;
String idText = etRefundSaleId.getText().toString().trim(); String idText = binding.etRefundSaleId.getText().toString().trim();
String reason = etRefundReason.getText().toString().trim(); String reason = binding.etRefundReason.getText().toString().trim();
String payment = spinnerRefundPayment.getSelectedItem().toString();
try { try {
int id = Integer.parseInt(idText); int id = Integer.parseInt(idText);
@@ -91,46 +102,25 @@ public class RefundDetailFragment extends Fragment {
private void handleArguments() { private void handleArguments() {
if (getArguments() != null && getArguments().containsKey("saleId")) { if (getArguments() != null && getArguments().containsKey("saleId")) {
saleId = getArguments().getInt("saleId"); saleId = getArguments().getInt("saleId");
etRefundSaleId.setText(String.valueOf(saleId)); binding.etRefundSaleId.setText(String.valueOf(saleId));
String info = "Sale Date: " + getArguments().getString("saleDate") String info = "Sale Date: " + getArguments().getString("saleDate")
+ " | Employee: " + getArguments().getString("employeeName") + " | Employee: " + getArguments().getString("employeeName")
+ " | Total: $" + String.format("%.2f", getArguments().getDouble("total")) + " | Total: $" + String.format("%.2f", getArguments().getDouble("total"))
+ " | Payment: " + getArguments().getString("paymentMethod"); + " | Payment: " + getArguments().getString("paymentMethod");
tvSaleInfo.setText(info); binding.tvSaleInfo.setText(info);
tvSaleInfo.setTextColor(getResources().getColor(android.R.color.holo_green_dark)); binding.tvSaleInfo.setTextColor(getResources().getColor(android.R.color.holo_green_dark));
// Pre-select payment method // Pre-select payment method
String payment = getArguments().getString("paymentMethod"); SpinnerUtils.setSelectionByValue(binding.spinnerRefundPayment, getArguments().getString("paymentMethod"));
ArrayAdapter<String> adapter = (ArrayAdapter<String>) spinnerRefundPayment.getAdapter();
if (adapter != null && payment != null) {
int pos = adapter.getPosition(payment);
if (pos >= 0)
spinnerRefundPayment.setSelection(pos);
}
} }
} }
private void goBack() { private void goBack() {
ListFragment listFragment = (ListFragment) getParentFragment(); NavHostFragment.findNavController(this).popBackStack();
if (listFragment != null)
listFragment.getChildFragmentManager().popBackStack();
}
private void initViews(View view) {
etRefundSaleId = view.findViewById(R.id.etRefundSaleId);
etRefundReason = view.findViewById(R.id.etRefundReason);
tvSaleInfo = view.findViewById(R.id.tvSaleInfo);
spinnerRefundPayment = view.findViewById(R.id.spinnerRefundPayment);
btnLoadSale = view.findViewById(R.id.btnLoadSale);
btnProcessRefund = view.findViewById(R.id.btnProcessRefund);
btnBack = view.findViewById(R.id.btnRefundBack);
} }
private void setupSpinner() { private void setupSpinner() {
BlackTextArrayAdapter<String> adapter = new BlackTextArrayAdapter<>(requireContext(), SpinnerUtils.setupStringSpinner(requireContext(), binding.spinnerRefundPayment,
android.R.layout.simple_spinner_item,
new String[] { "Cash", "Card", "Debit" }); new String[] { "Cash", "Card", "Debit" });
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
spinnerRefundPayment.setAdapter(adapter);
} }
} }

View File

@@ -2,75 +2,87 @@ package com.example.petstoremobile.fragments.listfragments.detailfragments;
import android.os.Bundle; import android.os.Bundle;
import androidx.appcompat.app.AlertDialog; import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment; import androidx.fragment.app.Fragment;
import androidx.lifecycle.ViewModelProvider;
import androidx.navigation.fragment.NavHostFragment;
import android.util.Log;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;
import android.widget.Toast; import android.widget.Toast;
import com.example.petstoremobile.R; import com.example.petstoremobile.R;
import com.example.petstoremobile.api.RetrofitClient; import com.example.petstoremobile.databinding.FragmentServiceDetailBinding;
import com.example.petstoremobile.api.ServiceApi;
import com.example.petstoremobile.dtos.ServiceDTO; import com.example.petstoremobile.dtos.ServiceDTO;
import com.example.petstoremobile.fragments.ListFragment;
import com.example.petstoremobile.fragments.listfragments.ServiceFragment;
import com.example.petstoremobile.utils.ActivityLogger; import com.example.petstoremobile.utils.ActivityLogger;
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.viewmodels.ServiceViewModel;
import retrofit2.Call; import dagger.hilt.android.AndroidEntryPoint;
import retrofit2.Callback;
import retrofit2.Response;
/**
* Fragment for displaying and editing service details.
*/
@AndroidEntryPoint
public class ServiceDetailFragment extends Fragment { public class ServiceDetailFragment extends Fragment {
private TextView tvMode, tvServiceId; private FragmentServiceDetailBinding binding;
private EditText etServiceName, etServiceDesc, etServiceDuration, etServicePrice; private long serviceId;
private Button btnSaveService, btnDeleteService, btnBack;
private int serviceId;
private boolean isEditing = false; private boolean isEditing = false;
private ServiceFragment serviceFragment;
//set the service fragment to the parent so we refer back to service view when save or delete is done private ServiceViewModel viewModel;
public void setServiceFragment(ServiceFragment fragment) {
this.serviceFragment = fragment; @Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
viewModel = new ViewModelProvider(this).get(ServiceViewModel.class);
} }
@Override @Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) { Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_service_detail, container, false); binding = FragmentServiceDetailBinding.inflate(inflater, container, false);
return binding.getRoot();
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
//get controls from layout and display the view depending on the mode //get controls from layout and display the view depending on the mode
initViews(view);
handleArguments(); handleArguments();
//set button click listeners //set button click listeners
btnBack.setOnClickListener(v -> navigateBack()); binding.btnBack.setOnClickListener(v -> navigateBack());
btnSaveService.setOnClickListener(v -> saveService()); binding.btnSaveService.setOnClickListener(v -> saveService());
btnDeleteService.setOnClickListener(v -> deleteService()); binding.btnDeleteService.setOnClickListener(v -> deleteService());
return view;
} }
//Method to Update or Add a service @Override
public void onDestroyView() {
super.onDestroyView();
binding = null;
}
/**
* Handles the saving of service data (adding or updating).
*/
private void saveService() { private void saveService() {
// Validates all fields using InputValidator // Validates all fields using InputValidator
if (!InputValidator.isNotEmpty(etServiceName, "Service Name")) return; if (!InputValidator.isNotEmpty(binding.etServiceName, "Service Name")) return;
if (!InputValidator.isNotEmpty(etServiceDesc, "Description")) return; if (!InputValidator.isNotEmpty(binding.etServiceDesc, "Description")) return;
if (!InputValidator.isPositiveInteger(etServiceDuration, "Duration")) return; if (!InputValidator.isPositiveInteger(binding.etServiceDuration, "Duration")) return;
if (!InputValidator.isPositiveDecimal(etServicePrice, "Price")) return; if (!InputValidator.isPositiveDecimal(binding.etServicePrice, "Price")) return;
//get all the values from the fields //get all the values from the fields
String name = etServiceName.getText().toString().trim(); String name = binding.etServiceName.getText().toString().trim();
String desc = etServiceDesc.getText().toString().trim(); String desc = binding.etServiceDesc.getText().toString().trim();
int duration = Integer.parseInt(etServiceDuration.getText().toString().trim()); int duration = Integer.parseInt(binding.etServiceDuration.getText().toString().trim());
double price = Double.parseDouble(etServicePrice.getText().toString().trim()); double price = Double.parseDouble(binding.etServicePrice.getText().toString().trim());
//create a service object to send to the API //create a service object to send to the API
ServiceDTO serviceDTO = new ServiceDTO(); ServiceDTO serviceDTO = new ServiceDTO();
@@ -79,130 +91,94 @@ public class ServiceDetailFragment extends Fragment {
serviceDTO.setServiceDuration(duration); serviceDTO.setServiceDuration(duration);
serviceDTO.setServicePrice(price); serviceDTO.setServicePrice(price);
ServiceApi serviceApi = RetrofitClient.getServiceApi(requireContext());
//check if the service is being edited or added //check if the service is being edited or added
if (isEditing) { if (isEditing) {
// Update existing service // Update existing service
serviceDTO.setServiceId((long) serviceId); serviceDTO.setServiceId(serviceId);
serviceApi.updateService((long) serviceId, serviceDTO).enqueue(new Callback<ServiceDTO>() { viewModel.updateService(serviceId, serviceDTO).observe(getViewLifecycleOwner(), resource -> {
@Override if (resource.status == Resource.Status.SUCCESS) {
public void onResponse(Call<ServiceDTO> call, Response<ServiceDTO> response) { ActivityLogger.logChange(requireContext(), "Service", "UPDATED", (int) serviceId);
if (response.isSuccessful()) { Toast.makeText(getContext(), "Service updated successfully!", Toast.LENGTH_SHORT).show();
ActivityLogger.logChange(requireContext(), "Service", "UPDATED", serviceId); navigateBack();
Toast.makeText(getContext(), "Service updated successfully!", Toast.LENGTH_SHORT).show(); } else if (resource.status == Resource.Status.ERROR) {
navigateBack(); Toast.makeText(getContext(), "Error: " + resource.message, Toast.LENGTH_SHORT).show();
} else {
Toast.makeText(getContext(), "Failed to update service: " + response.code(), Toast.LENGTH_SHORT).show();
}
}
@Override
public void onFailure(Call<ServiceDTO> call, Throwable t) {
ActivityLogger.logException(requireContext(), "ServiceDetailFragment.updateService", new Exception(t));
Log.e("ServiceDetailFragment", "Error updating service", t);
Toast.makeText(getContext(), "Error: " + t.getMessage(), Toast.LENGTH_SHORT).show();
} }
}); });
} else { } else {
// Add new service viewModel.createService(serviceDTO).observe(getViewLifecycleOwner(), resource -> {
serviceApi.createService(serviceDTO).enqueue(new Callback<ServiceDTO>() { if (resource.status == Resource.Status.SUCCESS) {
@Override ActivityLogger.log(requireContext(), "Added new Service: " + name);
public void onResponse(Call<ServiceDTO> call, Response<ServiceDTO> response) { Toast.makeText(getContext(), "Service added successfully!", Toast.LENGTH_SHORT).show();
if (response.isSuccessful()) { navigateBack();
ActivityLogger.log(requireContext(), "Added new Service: " + name); } else if (resource.status == Resource.Status.ERROR) {
Toast.makeText(getContext(), "Service added successfully!", Toast.LENGTH_SHORT).show(); Toast.makeText(getContext(), "Error: " + resource.message, Toast.LENGTH_SHORT).show();
navigateBack();
} else {
Toast.makeText(getContext(), "Failed to add service: " + response.code(), Toast.LENGTH_SHORT).show();
}
}
@Override
public void onFailure(Call<ServiceDTO> call, Throwable t) {
ActivityLogger.logException(requireContext(), "ServiceDetailFragment.createService", new Exception(t));
Log.e("ServiceDetailFragment", "Error adding service", t);
Toast.makeText(getContext(), "Error: " + t.getMessage(), Toast.LENGTH_SHORT).show();
} }
}); });
} }
} }
//Method to Delete a service /**
* Displays a confirmation dialog and handles the deletion of a service.
*/
private void deleteService() { private void deleteService() {
//Alert the user to confirm the delete DialogUtils.showDeleteConfirmDialog(requireContext(), "Service", () ->
new AlertDialog.Builder(requireContext()) viewModel.deleteService(serviceId).observe(getViewLifecycleOwner(), resource -> {
.setTitle("Delete Service") if (resource.status == Resource.Status.SUCCESS) {
.setMessage("Are you sure you want to delete " + etServiceName.getText().toString() + "?") ActivityLogger.logChange(requireContext(), "Service", "DELETED", (int) serviceId);
.setPositiveButton("Delete", (dialog, which) -> { Toast.makeText(getContext(), "Service deleted successfully!", Toast.LENGTH_SHORT).show();
ServiceApi serviceApi = RetrofitClient.getServiceApi(requireContext()); navigateBack();
serviceApi.deleteService((long) serviceId).enqueue(new Callback<Void>() { } else if (resource.status == Resource.Status.ERROR) {
@Override Toast.makeText(getContext(), "Delete failed: " + resource.message, Toast.LENGTH_SHORT).show();
public void onResponse(Call<Void> call, Response<Void> response) { }
if (response.isSuccessful()) { }));
ActivityLogger.logChange(requireContext(), "Service", "DELETED", serviceId);
Toast.makeText(getContext(), "Service deleted successfully!", Toast.LENGTH_SHORT).show();
navigateBack();
} else {
Toast.makeText(getContext(), "Failed to delete service: " + response.code(), Toast.LENGTH_SHORT).show();
}
}
@Override
public void onFailure(Call<Void> call, Throwable t) {
ActivityLogger.logException(requireContext(), "ServiceDetailFragment.deleteService", new Exception(t));
Log.e("ServiceDetailFragment", "Error deleting service", t);
Toast.makeText(getContext(), "Error: " + t.getMessage(), Toast.LENGTH_SHORT).show();
}
});
})
.setNegativeButton("Cancel", null)
.show();
} }
//Helper method to navigate back to the list /**
* Navigates back to the previous screen.
*/
private void navigateBack() { private void navigateBack() {
ListFragment listFragment = (ListFragment) getParentFragment(); NavHostFragment.findNavController(this).popBackStack();
if (listFragment != null) {
listFragment.getChildFragmentManager().popBackStack();
}
} }
//helper function to check if service is being edited or added and show the view accordingly /**
* Handles arguments passed to the fragment to determine if it's in edit or add mode.
*/
private void handleArguments() { private void handleArguments() {
// Service is being edited if the bundle contains a serviceId // 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 // Get service data from arguments and populate fields
isEditing = true; isEditing = true;
serviceId = getArguments().getInt("serviceId"); serviceId = getArguments().getLong("serviceId");
tvMode.setText("Edit Service"); binding.tvMode.setText("Edit Service");
tvServiceId.setText("ID: " + serviceId); binding.tvServiceId.setText("ID: " + serviceId);
etServiceName.setText(getArguments().getString("serviceName")); binding.btnDeleteService.setVisibility(View.VISIBLE);
etServiceDesc.setText(getArguments().getString("serviceDesc")); loadServiceData();
etServiceDuration.setText(String.valueOf(getArguments().getInt("serviceDuration")));
etServicePrice.setText(String.valueOf(getArguments().getDouble("servicePrice")));
btnDeleteService.setVisibility(View.VISIBLE);
} else { } else {
// Service is being added // Service is being added
// Set default values for add a new service // Set default values for add a new service
isEditing = false; isEditing = false;
tvMode.setText("Add Service"); binding.tvMode.setText("Add Service");
tvServiceId.setVisibility(View.GONE); binding.tvServiceId.setVisibility(View.GONE);
btnDeleteService.setVisibility(View.GONE); binding.btnDeleteService.setVisibility(View.GONE);
btnSaveService.setText("Add"); binding.btnSaveService.setText("Add");
} }
} }
//helper function to get controls from layout /**
private void initViews(View view) { * Fetches specific service details from the backend using the ID.
tvMode = view.findViewById(R.id.tvMode); */
tvServiceId = view.findViewById(R.id.tvServiceId); private void loadServiceData() {
etServiceName = view.findViewById(R.id.etServiceName); viewModel.getServiceById(serviceId).observe(getViewLifecycleOwner(), resource -> {
etServiceDesc = view.findViewById(R.id.etServiceDesc); if (resource == null) return;
etServiceDuration = view.findViewById(R.id.etServiceDuration); if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
etServicePrice = view.findViewById(R.id.etServicePrice); ServiceDTO s = resource.data;
btnSaveService = view.findViewById(R.id.btnSaveService); binding.etServiceName.setText(s.getServiceName());
btnDeleteService = view.findViewById(R.id.btnDeleteService); binding.etServiceDesc.setText(s.getServiceDesc());
btnBack = view.findViewById(R.id.btnBack); binding.etServiceDuration.setText(String.valueOf(s.getServiceDuration()));
binding.etServicePrice.setText(String.valueOf(s.getServicePrice()));
} else if (resource.status == Resource.Status.ERROR) {
Toast.makeText(getContext(), "Failed to load service: " + resource.message, Toast.LENGTH_SHORT).show();
}
});
} }
} }

View File

@@ -2,77 +2,91 @@ package com.example.petstoremobile.fragments.listfragments.detailfragments;
import android.os.Bundle; import android.os.Bundle;
import androidx.appcompat.app.AlertDialog; import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment; import androidx.fragment.app.Fragment;
import androidx.lifecycle.ViewModelProvider;
import androidx.navigation.fragment.NavHostFragment;
import android.util.Log;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;
import android.widget.Toast; import android.widget.Toast;
import com.example.petstoremobile.R; import com.example.petstoremobile.databinding.FragmentSupplierDetailBinding;
import com.example.petstoremobile.api.RetrofitClient;
import com.example.petstoremobile.api.SupplierApi;
import com.example.petstoremobile.dtos.SupplierDTO; import com.example.petstoremobile.dtos.SupplierDTO;
import com.example.petstoremobile.fragments.ListFragment;
import com.example.petstoremobile.fragments.listfragments.SupplierFragment;
import com.example.petstoremobile.utils.ActivityLogger; import com.example.petstoremobile.utils.ActivityLogger;
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.UIUtils;
import com.example.petstoremobile.viewmodels.SupplierViewModel;
import retrofit2.Call; import dagger.hilt.android.AndroidEntryPoint;
import retrofit2.Callback;
import retrofit2.Response;
/**
* Fragment for displaying and editing supplier details.
*/
@AndroidEntryPoint
public class SupplierDetailFragment extends Fragment { public class SupplierDetailFragment extends Fragment {
private TextView tvMode, tvSupId; private FragmentSupplierDetailBinding binding;
private EditText etSupCompany, etSupContactFirstName, etSupContactLastName, etSupEmail, etSupPhone; private long supId;
private Button btnSaveSupplier, btnDeleteSupplier, btnBack;
private int supId;
private boolean isEditing = false; private boolean isEditing = false;
private SupplierFragment supplierFragment;
//set the supplier fragment to the parent so we refer back to supplier view when save or delete is done private SupplierViewModel viewModel;
public void setSupplierFragment(SupplierFragment fragment) {
this.supplierFragment = fragment; @Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
viewModel = new ViewModelProvider(this).get(SupplierViewModel.class);
} }
@Override @Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) { Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_supplier_detail, container, false); binding = FragmentSupplierDetailBinding.inflate(inflater, container, false);
return binding.getRoot();
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
// Add phone number formatting (CA) and limit length to 14 characters
UIUtils.formatPhoneInput(binding.etSupPhone);
//get controls from layout and display the view depending on the mode
initViews(view);
handleArguments(); handleArguments();
//set button click listeners //set button click listeners
btnBack.setOnClickListener(v -> navigateBack()); binding.btnBack.setOnClickListener(v -> navigateBack());
btnSaveSupplier.setOnClickListener(v -> saveSupplier()); binding.btnSaveSupplier.setOnClickListener(v -> saveSupplier());
btnDeleteSupplier.setOnClickListener(v -> deleteSupplier()); binding.btnDeleteSupplier.setOnClickListener(v -> deleteSupplier());
return view;
} }
//Method to Update or Add a supplier @Override
public void onDestroyView() {
super.onDestroyView();
binding = null;
}
/**
* Handles the saving of supplier data (adding or updating).
*/
private void saveSupplier() { private void saveSupplier() {
// Validates all fields using InputValidator // Validates all fields using InputValidator
if (!InputValidator.isNotEmpty(etSupCompany, "Company Name")) return; if (!InputValidator.isNotEmpty(binding.etSupCompany, "Company Name")) return;
if (!InputValidator.isNotEmpty(etSupContactFirstName, "First Name")) return; if (!InputValidator.isNotEmpty(binding.etSupContactFirstName, "First Name")) return;
if (!InputValidator.isNotEmpty(etSupContactLastName, "Last Name")) return; if (!InputValidator.isNotEmpty(binding.etSupContactLastName, "Last Name")) return;
if (!InputValidator.isValidEmail(etSupEmail)) return; if (!InputValidator.isValidEmail(binding.etSupEmail)) return;
if (!InputValidator.isValidPhone(etSupPhone)) return; if (!InputValidator.isValidPhone(binding.etSupPhone)) return;
//get all the values from the fields //get all the values from the fields
String company = etSupCompany.getText().toString().trim(); String company = binding.etSupCompany.getText().toString().trim();
String firstName = etSupContactFirstName.getText().toString().trim(); String firstName = binding.etSupContactFirstName.getText().toString().trim();
String lastName = etSupContactLastName.getText().toString().trim(); String lastName = binding.etSupContactLastName.getText().toString().trim();
String email = etSupEmail.getText().toString().trim(); String email = binding.etSupEmail.getText().toString().trim();
String phone = etSupPhone.getText().toString().trim(); String phone = binding.etSupPhone.getText().toString().trim();
//create a supplier object to send to the API //create a supplier object to send to the API
SupplierDTO supplierDTO = new SupplierDTO(); SupplierDTO supplierDTO = new SupplierDTO();
@@ -82,137 +96,97 @@ public class SupplierDetailFragment extends Fragment {
supplierDTO.setSupEmail(email); supplierDTO.setSupEmail(email);
supplierDTO.setSupPhone(phone); supplierDTO.setSupPhone(phone);
SupplierApi supplierApi = RetrofitClient.getSupplierApi(requireContext());
//check if the supplier is being edited or added //check if the supplier is being edited or added
if (isEditing) { if (isEditing) {
// Update existing supplier // Update existing supplier
supplierDTO.setSupId((long) supId); supplierDTO.setSupId(supId);
supplierApi.updateSupplier((long) supId, supplierDTO).enqueue(new Callback<SupplierDTO>() { viewModel.updateSupplier(supId, supplierDTO).observe(getViewLifecycleOwner(), resource -> {
@Override if (resource.status == Resource.Status.SUCCESS) {
public void onResponse(Call<SupplierDTO> call, Response<SupplierDTO> response) { ActivityLogger.logChange(requireContext(), "Supplier", "UPDATED", (int) supId);
if (response.isSuccessful()) { Toast.makeText(getContext(), "Supplier updated successfully!", Toast.LENGTH_SHORT).show();
ActivityLogger.logChange(requireContext(), "Supplier", "UPDATED", supId); navigateBack();
Toast.makeText(getContext(), "Supplier updated successfully!", Toast.LENGTH_SHORT).show(); } else if (resource.status == Resource.Status.ERROR) {
navigateBack(); Toast.makeText(getContext(), "Error: " + resource.message, Toast.LENGTH_SHORT).show();
} else {
Toast.makeText(getContext(), "Failed to update supplier: " + response.code(), Toast.LENGTH_SHORT).show();
}
}
@Override
public void onFailure(Call<SupplierDTO> call, Throwable t) {
ActivityLogger.logException(requireContext(), "SupplierDetailFragment.updateSupplier", new Exception(t));
Log.e("SupplierDetailFragment", "Error updating supplier", t);
Toast.makeText(getContext(), "Error: " + t.getMessage(), Toast.LENGTH_SHORT).show();
} }
}); });
} else { } else {
// Add new supplier // Add new supplier
supplierApi.createSupplier(supplierDTO).enqueue(new Callback<SupplierDTO>() { viewModel.createSupplier(supplierDTO).observe(getViewLifecycleOwner(), resource -> {
@Override if (resource.status == Resource.Status.SUCCESS) {
public void onResponse(Call<SupplierDTO> call, Response<SupplierDTO> response) { ActivityLogger.log(requireContext(), "Added new Supplier: " + company);
if (response.isSuccessful()) { Toast.makeText(getContext(), "Supplier added successfully!", Toast.LENGTH_SHORT).show();
ActivityLogger.log(requireContext(), "Added new Supplier: " + company); navigateBack();
Toast.makeText(getContext(), "Supplier added successfully!", Toast.LENGTH_SHORT).show(); } else if (resource.status == Resource.Status.ERROR) {
navigateBack(); Toast.makeText(getContext(), "Error: " + resource.message, Toast.LENGTH_SHORT).show();
} else {
Toast.makeText(getContext(), "Failed to add supplier: " + response.code(), Toast.LENGTH_SHORT).show();
}
}
@Override
public void onFailure(Call<SupplierDTO> call, Throwable t) {
ActivityLogger.logException(requireContext(), "SupplierDetailFragment.createSupplier", new Exception(t));
Log.e("SupplierDetailFragment", "Error adding supplier", t);
Toast.makeText(getContext(), "Error: " + t.getMessage(), Toast.LENGTH_SHORT).show();
} }
}); });
} }
} }
//Method to Delete a supplier /**
* Displays a confirmation dialog and handles the deletion of a supplier.
*/
private void deleteSupplier() { private void deleteSupplier() {
//Alert the user to confirm the delete DialogUtils.showDeleteConfirmDialog(requireContext(), "Supplier", () ->
new AlertDialog.Builder(requireContext()) viewModel.deleteSupplier(supId).observe(getViewLifecycleOwner(), resource -> {
.setTitle("Delete Supplier") if (resource.status == Resource.Status.SUCCESS) {
.setMessage("Are you sure you want to delete " + etSupCompany.getText().toString() + "?") ActivityLogger.logChange(requireContext(), "Supplier", "DELETED", (int) supId);
.setPositiveButton("Delete", (dialog, which) -> { Toast.makeText(getContext(), "Supplier deleted successfully!", Toast.LENGTH_SHORT).show();
SupplierApi supplierApi = RetrofitClient.getSupplierApi(requireContext()); navigateBack();
supplierApi.deleteSupplier((long) supId).enqueue(new Callback<Void>() { } else if (resource.status == Resource.Status.ERROR) {
@Override Toast.makeText(getContext(), "Delete failed: " + resource.message, Toast.LENGTH_SHORT).show();
public void onResponse(Call<Void> call, Response<Void> response) { }
if (response.isSuccessful()) { }));
ActivityLogger.logChange(requireContext(), "Supplier", "DELETED", supId);
Toast.makeText(getContext(), "Supplier deleted successfully!", Toast.LENGTH_SHORT).show();
navigateBack();
} else {
Toast.makeText(getContext(), "Failed to delete supplier: " + response.code(), Toast.LENGTH_SHORT).show();
}
}
@Override
public void onFailure(Call<Void> call, Throwable t) {
ActivityLogger.logException(requireContext(), "SupplierDetailFragment.deleteSupplier", new Exception(t));
Log.e("SupplierDetailFragment", "Error deleting supplier", t);
Toast.makeText(getContext(), "Error: " + t.getMessage(), Toast.LENGTH_SHORT).show();
}
});
})
.setNegativeButton("Cancel", null)
.show();
} }
//Helper method to navigate back to the list /**
* Navigates back to the previous screen.
*/
private void navigateBack() { private void navigateBack() {
ListFragment listFragment = (ListFragment) getParentFragment(); NavHostFragment.findNavController(this).popBackStack();
if (listFragment != null) {
listFragment.getChildFragmentManager().popBackStack();
}
} }
//helper function to check if supplier is being edited or added and show the view accordingly /**
* Handles arguments passed to the fragment to determine if it's in edit or add mode.
*/
private void handleArguments() { private void handleArguments() {
// Supplier is being edited if the bundle contains a supId // 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 // Get supplier data from arguments and populate fields
isEditing = true; isEditing = true;
supId = getArguments().getInt("supId"); supId = getArguments().getLong("supId");
tvMode.setText("Edit Supplier"); binding.tvMode.setText("Edit Supplier");
tvSupId.setText("ID: " + supId); binding.tvSupId.setText("ID: " + supId);
etSupCompany.setText(getArguments().getString("supCompany")); binding.tvSupId.setVisibility(View.VISIBLE);
etSupContactFirstName.setText(getArguments().getString("supContactFirstName")); binding.btnDeleteSupplier.setVisibility(View.VISIBLE);
etSupContactLastName.setText(getArguments().getString("supContactLastName")); loadSupplierData();
etSupEmail.setText(getArguments().getString("supEmail"));
etSupPhone.setText(getArguments().getString("supPhone"));
btnDeleteSupplier.setVisibility(View.VISIBLE);
} else { } else {
// Supplier is being added // Supplier is being added
// Set default values for add a new supplier // Set default values for add a new supplier
isEditing = false; isEditing = false;
tvMode.setText("Add Supplier"); binding.tvMode.setText("Add Supplier");
tvSupId.setVisibility(View.GONE); binding.tvSupId.setVisibility(View.GONE);
btnDeleteSupplier.setVisibility(View.GONE); binding.btnDeleteSupplier.setVisibility(View.GONE);
btnSaveSupplier.setText("Add"); binding.btnSaveSupplier.setText("Add");
} }
} }
//helper function to get controls from layout /**
private void initViews(View view) { * Fetches specific supplier details from the backend using the ID.
tvMode = view.findViewById(R.id.tvMode); */
tvSupId = view.findViewById(R.id.tvSupId); private void loadSupplierData() {
etSupCompany = view.findViewById(R.id.etSupCompany); viewModel.getSupplierById(supId).observe(getViewLifecycleOwner(), resource -> {
etSupContactFirstName = view.findViewById(R.id.etSupContactFirstName); if (resource == null) return;
etSupContactLastName = view.findViewById(R.id.etSupContactLastName); if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
etSupEmail = view.findViewById(R.id.etSupEmail); SupplierDTO s = resource.data;
etSupPhone = view.findViewById(R.id.etSupPhone); binding.etSupCompany.setText(s.getSupCompany());
binding.etSupContactFirstName.setText(s.getSupContactFirstName());
// Add phone number formatting (CA) and limit length to 14 characters binding.etSupContactLastName.setText(s.getSupContactLastName());
etSupPhone.addTextChangedListener(new android.telephony.PhoneNumberFormattingTextWatcher("CA")); binding.etSupEmail.setText(s.getSupEmail());
etSupPhone.setFilters(new android.text.InputFilter[]{new android.text.InputFilter.LengthFilter(14)}); binding.etSupPhone.setText(s.getSupPhone());
} else if (resource.status == Resource.Status.ERROR) {
btnSaveSupplier = view.findViewById(R.id.btnSaveSupplier); Toast.makeText(getContext(), "Failed to load supplier: " + resource.message, Toast.LENGTH_SHORT).show();
btnDeleteSupplier = view.findViewById(R.id.btnDeleteSupplier); }
btnBack = view.findViewById(R.id.btnBack); });
} }
} }

View File

@@ -1,310 +1,210 @@
package com.example.petstoremobile.fragments.listfragments.listprofilefragments; package com.example.petstoremobile.fragments.listfragments.listprofilefragments;
import android.Manifest;
import android.app.Activity;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.graphics.Color;
import android.net.Uri; import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import androidx.activity.result.ActivityResultLauncher; import androidx.annotation.NonNull;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.appcompat.app.AlertDialog;
import androidx.core.content.ContextCompat;
import androidx.core.content.FileProvider;
import androidx.fragment.app.Fragment; import androidx.fragment.app.Fragment;
import androidx.lifecycle.ViewModelProvider;
import androidx.navigation.fragment.NavHostFragment;
import android.provider.MediaStore;
import android.util.Log; 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.Button;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast; import android.widget.Toast;
import com.bumptech.glide.Glide;
import com.bumptech.glide.load.engine.DiskCacheStrategy;
import com.example.petstoremobile.R; import com.example.petstoremobile.R;
import com.example.petstoremobile.api.PetApi; import com.example.petstoremobile.api.PetApi;
import com.example.petstoremobile.api.RetrofitClient; import com.example.petstoremobile.api.auth.TokenManager;
import com.example.petstoremobile.fragments.ListFragment; import com.example.petstoremobile.databinding.FragmentPetProfileBinding;
import com.example.petstoremobile.fragments.listfragments.detailfragments.PetDetailFragment; import com.example.petstoremobile.dtos.PetDTO;
import com.example.petstoremobile.utils.FileUtils;
import com.example.petstoremobile.utils.GlideUtils;
import com.example.petstoremobile.utils.ImagePickerHelper;
import com.example.petstoremobile.utils.Resource;
import com.example.petstoremobile.viewmodels.PetViewModel;
import java.io.File; import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale; import java.util.Locale;
import javax.inject.Inject;
import javax.inject.Named;
import dagger.hilt.android.AndroidEntryPoint;
import okhttp3.MediaType; import okhttp3.MediaType;
import okhttp3.MultipartBody; import okhttp3.MultipartBody;
import okhttp3.RequestBody; import okhttp3.RequestBody;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
@AndroidEntryPoint
public class PetProfileFragment extends Fragment { public class PetProfileFragment extends Fragment {
private TextView tvPetName, tvPetSpecies, tvPetBreed, tvPetAge, tvPetPrice; private FragmentPetProfileBinding binding;
private Button btnBack, btnEditPet, btnChangePhoto; private long petId;
private ImageView imgPet;
private Uri photoUri;
private int petId;
private boolean hasImage = false; private boolean hasImage = false;
// launchers for camera and gallery @Inject @Named("baseUrl") String baseUrl;
private ActivityResultLauncher<Intent> galleryLauncher; @Inject TokenManager tokenManager;
private ActivityResultLauncher<Uri> cameraLauncher;
private ActivityResultLauncher<String> permissionLauncher; private PetViewModel viewModel;
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);
// Launcher to open gallery to select image imagePickerHelper = new ImagePickerHelper(this, "pet_photo.jpg", new ImagePickerHelper.ImagePickerListener() {
galleryLauncher = registerForActivityResult( @Override
new ActivityResultContracts.StartActivityForResult(), public void onImagePicked(Uri uri) {
result -> { uploadPetImage(uri);
if (result.getResultCode() == Activity.RESULT_OK && result.getData() != null) { }
Uri selectedImage = result.getData().getData();
uploadPetImage(selectedImage);
}
}
);
// Launcher for camera to open and capture image @Override
cameraLauncher = registerForActivityResult( public void onImageRemoved() {
new ActivityResultContracts.TakePicture(), deletePetImage();
success -> { }
if (success) { });
uploadPetImage(photoUri);
}
}
);
// Launcher to request camera permission
permissionLauncher = registerForActivityResult(
new ActivityResultContracts.RequestPermission(),
granted -> {
if (granted) {
launchCamera();
} else {
new AlertDialog.Builder(requireContext())
.setTitle("Permission Required")
.setMessage("Please grant camera permission to use this feature")
.setPositiveButton("Open Settings", (dialog, which) -> {
Intent intent = new Intent(android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
intent.setData(Uri.fromParts("package", requireContext().getPackageName(), null));
startActivity(intent);
})
.setNegativeButton("Cancel", null)
.show();
}
}
);
} }
/**
* Inflates the layout using view binding, initializes views, and sets up click listeners.
*/
@Override @Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) { Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_pet_profile, container, false); binding = FragmentPetProfileBinding.inflate(inflater, container, false);
// Initialize views
tvPetName = view.findViewById(R.id.tvPetName);
tvPetSpecies = view.findViewById(R.id.tvPetSpecies);
tvPetBreed = view.findViewById(R.id.tvPetBreed);
tvPetAge = view.findViewById(R.id.tvPetAge);
tvPetPrice = view.findViewById(R.id.tvPetPrice);
btnBack = view.findViewById(R.id.btnBack);
btnEditPet = view.findViewById(R.id.btnEditPet);
btnChangePhoto = view.findViewById(R.id.btnChangePhoto);
imgPet = view.findViewById(R.id.imgPet);
// Set pet details to display // Set pet details to display
if (getArguments() != null) { if (getArguments() != null) {
petId = getArguments().getInt("petId"); petId = getArguments().getLong("petId");
tvPetName.setText(getArguments().getString("petName")); loadPetData();
tvPetSpecies.setText(getArguments().getString("petSpecies")); loadPetImage((int) petId);
tvPetBreed.setText(getArguments().getString("petBreed"));
tvPetAge.setText(String.format(Locale.getDefault(), "%d yr(s)", getArguments().getInt("petAge")));
tvPetPrice.setText(String.format(Locale.getDefault(), "$%.2f", getArguments().getDouble("petPrice")));
// Load pet image from backend
loadPetImage(petId);
} }
//set button click listeners //set button click listeners
btnBack.setOnClickListener(v -> { binding.btnBack.setOnClickListener(v -> {
//get the list fragment and pop the back stack to return to the previous view (PetFragment) NavHostFragment.findNavController(this).popBackStack();
ListFragment listFragment = (ListFragment) getParentFragment();
if (listFragment != null) {
listFragment.getChildFragmentManager().popBackStack();
}
}); });
//Make the edit button go to the pet detail view //Make the edit button go to the pet detail view
btnEditPet.setOnClickListener(v -> { binding.btnEditPet.setOnClickListener(v -> {
if (getArguments() == null) return; Bundle args = new Bundle();
args.putLong("petId", petId);
PetDetailFragment detailFragment = new PetDetailFragment(); NavHostFragment.findNavController(this).navigate(R.id.nav_pet_detail, args);
//send the bundle to the pet detail fragment
detailFragment.setArguments(getArguments());
//get ListFragment to load the the detail view
ListFragment listFragment = (ListFragment) getParentFragment();
if (listFragment != null) {
listFragment.loadFragment(detailFragment);
}
}); });
//Make change photo button ask user to select a new photo //Make change photo button ask user to select a new photo
btnChangePhoto.setOnClickListener(v -> { binding.btnChangePhoto.setOnClickListener(v -> {
List<String> options = new ArrayList<>(); imagePickerHelper.showImagePickerDialog("Change Pet Photo", hasImage);
options.add("Take Photo");
options.add("Choose from Gallery");
if (hasImage) {
options.add("Remove Photo");
}
new AlertDialog.Builder(requireContext())
.setTitle("Change Pet Photo")
.setItems(options.toArray(new String[0]), (dialog, which) -> {
String selected = options.get(which);
if (selected.equals("Take Photo")) {
// Choose Camera
//Checks if the user has granted the camera permission already
if (ContextCompat.checkSelfPermission(requireContext(), Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED) {
//if the permission is already granted then launch the camera
launchCamera();
} else {
//otherwise request the permission
permissionLauncher.launch(Manifest.permission.CAMERA);
}
} else if (selected.equals("Choose from Gallery")) {
Intent intent = new Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI);
galleryLauncher.launch(intent);
} else if (selected.equals("Remove Photo")) {
deletePetImage();
}
})
.show();
}); });
return view; return binding.getRoot();
} }
// Helper function to load pet image from backend @Override
public void onDestroyView() {
super.onDestroyView();
binding = null;
}
/**
* Fetches current pet data from the backend and updates the UI.
*/
private void loadPetData() {
viewModel.getPetById(petId).observe(getViewLifecycleOwner(), resource -> {
if (resource == null) return;
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
PetDTO pet = resource.data;
binding.tvPetName.setText(pet.getPetName());
binding.tvPetSpecies.setText(pet.getPetSpecies());
binding.tvPetBreed.setText(pet.getPetBreed());
binding.tvPetAge.setText(String.format(Locale.getDefault(), "%d yr(s)", pet.getPetAge()));
if (pet.getPetPrice() != null) {
binding.tvPetPrice.setText(String.format(Locale.getDefault(), "$%.2f", pet.getPetPrice()));
} else {
binding.tvPetPrice.setText("$0.00");
}
// Display owner name if available, otherwise show No Owner
if (pet.getCustomerName() != null && !pet.getCustomerName().isEmpty()) {
binding.tvPetOwner.setText(pet.getCustomerName());
} else {
binding.tvPetOwner.setText("No Owner");
}
} else if (resource.status == Resource.Status.ERROR) {
Toast.makeText(getContext(), "Failed to load pet data: " + resource.message, Toast.LENGTH_SHORT).show();
}
});
}
/**
* Fetches and displays the pet\'s image from the server.
*/
private void loadPetImage(int petId) { private void loadPetImage(int petId) {
String imageUrl = RetrofitClient.BASE_URL + 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();
Glide.with(this) GlideUtils.loadImageWithToken(requireContext(), binding.imgPet, imageUrl, token, R.drawable.placeholder, new GlideUtils.ImageLoadListener() {
.load(imageUrl) @Override
.diskCacheStrategy(DiskCacheStrategy.NONE) public void onResourceReady() {
.skipMemoryCache(true) hasImage = true;
.placeholder(R.drawable.placeholder) }
.error(R.drawable.placeholder)
.listener(new com.bumptech.glide.request.RequestListener<android.graphics.drawable.Drawable>() {
@Override
public boolean onLoadFailed(@androidx.annotation.Nullable com.bumptech.glide.load.engine.GlideException e, Object model, com.bumptech.glide.request.target.Target<android.graphics.drawable.Drawable> target, boolean isFirstResource) {
hasImage = false;
return false;
}
@Override @Override
public boolean onResourceReady(android.graphics.drawable.Drawable resource, Object model, com.bumptech.glide.request.target.Target<android.graphics.drawable.Drawable> target, com.bumptech.glide.load.DataSource dataSource, boolean isFirstResource) { public void onLoadFailed() {
hasImage = true; hasImage = false;
return false; }
} });
})
.into(imgPet);
} }
// Helper function to upload pet image to backend /**
* 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 = getFileFromUri(uri); File file = FileUtils.getFileFromUri(requireContext(), uri);
if (file == null) return; if (file == null) return;
// Create RequestBody for file upload // 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);
// Call the backend to upload the image // Use ViewModel to upload image
PetApi petApi = RetrofitClient.getPetApi(requireContext()); viewModel.uploadPetImage(petId, body).observe(getViewLifecycleOwner(), resource -> {
petApi.uploadPetImage((long) petId, body).enqueue(new Callback<Void>() { if (resource != null && resource.status != Resource.Status.LOADING) {
@Override if (resource.status == Resource.Status.SUCCESS) {
public void onResponse(Call<Void> call, Response<Void> response) { Toast.makeText(getContext(), "Pet photo updated successfully", Toast.LENGTH_SHORT).show();
if (response.isSuccessful()) { loadPetImage((int) petId);
Toast.makeText(requireContext(), "Pet photo updated successfully", Toast.LENGTH_SHORT).show();
// Reload image after successful upload
loadPetImage(petId);
} else { } else {
Toast.makeText(requireContext(), "Failed to upload pet photo", Toast.LENGTH_SHORT).show(); Toast.makeText(getContext(), "Upload failed: " + resource.message, Toast.LENGTH_SHORT).show();
} }
} }
@Override
public void onFailure(Call<Void> call, Throwable t) {
Log.e("UPLOAD_PET_IMAGE", "Failure: " + t.getMessage());
Toast.makeText(requireContext(), "Network error", Toast.LENGTH_SHORT).show();
}
}); });
} catch (Exception e) { } catch (Exception e) {
Log.e("UPLOAD_PET_IMAGE", "Error: " + e.getMessage()); Log.e("UPLOAD_PET_IMAGE", "Error: " + e.getMessage());
} }
} }
/**
* Sends a request to the ViewModel to remove the current pet photo.
*/
private void deletePetImage() { private void deletePetImage() {
PetApi petApi = RetrofitClient.getPetApi(requireContext()); viewModel.deletePetImage(petId).observe(getViewLifecycleOwner(), resource -> {
petApi.deletePetImage((long) petId).enqueue(new Callback<Void>() { if (resource != null && resource.status != Resource.Status.LOADING) {
@Override if (resource.status == Resource.Status.SUCCESS) {
public void onResponse(Call<Void> call, Response<Void> response) { Toast.makeText(getContext(), "Pet photo removed", Toast.LENGTH_SHORT).show();
if (response.isSuccessful()) {
Toast.makeText(requireContext(), "Pet photo removed", Toast.LENGTH_SHORT).show();
hasImage = false; hasImage = false;
imgPet.setImageResource(R.drawable.placeholder); binding.imgPet.setImageResource(R.drawable.placeholder);
} else { } else {
Toast.makeText(requireContext(), "Failed to remove pet photo", Toast.LENGTH_SHORT).show(); Toast.makeText(getContext(), "Delete failed: " + resource.message, Toast.LENGTH_SHORT).show();
} }
} }
@Override
public void onFailure(Call<Void> call, Throwable t) {
Log.e("DELETE_PET_IMAGE", "Failure: " + t.getMessage());
Toast.makeText(requireContext(), "Network error", Toast.LENGTH_SHORT).show();
}
}); });
} }
}
// Helper function to create a temporary File object from a Uri for uploading
private File getFileFromUri(Uri uri) {
try {
InputStream inputStream = requireContext().getContentResolver().openInputStream(uri);
File tempFile = new File(requireContext().getCacheDir(), "upload_pet_image.jpg");
FileOutputStream outputStream = new FileOutputStream(tempFile);
byte[] buffer = new byte[1024];
int length;
while ((length = inputStream.read(buffer)) > 0) {
outputStream.write(buffer, 0, length);
}
outputStream.close();
inputStream.close();
return tempFile;
} catch (Exception e) {
Log.e("FILE_UTILS", "Error creating temp file", e);
return null;
}
}
private void launchCamera() {
File photoFile = new File(requireContext().getCacheDir(), "pet_photo.jpg");
photoUri = FileProvider.getUriForFile(requireContext(), requireContext().getPackageName() + ".fileprovider", photoFile);
cameraLauncher.launch(photoUri);
}
}

View File

@@ -1,79 +0,0 @@
package com.example.petstoremobile.models;
public class Adoption {
private int adoptionId;
private String adopterName;
private String adopterEmail;
private String adopterPhone;
private String petName;
private String adoptionDate;
private String status;
// Constructor
public Adoption(int adoptionId, String adopterName, String adopterEmail, String adopterPhone, String petName, String adoptionDate, String status) {
this.adoptionId = adoptionId;
this.adopterName = adopterName;
this.adopterEmail = adopterEmail;
this.adopterPhone = adopterPhone;
this.petName = petName;
this.adoptionDate = adoptionDate;
this.status = status;
}
// Getters and setters
public int getAdoptionId() {
return adoptionId;
}
public void setAdoptionId(int adoptionId) {
this.adoptionId = adoptionId;
}
public String getAdopterName() {
return adopterName;
}
public void setAdopterName(String adopterName) {
this.adopterName = adopterName;
}
public String getAdopterEmail() {
return adopterEmail;
}
public void setAdopterEmail(String adopterEmail) {
this.adopterEmail = adopterEmail;
}
public String getAdopterPhone() {
return adopterPhone;
}
public void setAdopterPhone(String adopterPhone) {
this.adopterPhone = adopterPhone;
}
public String getPetName() {
return petName;
}
public void setPetName(String petName) {
this.petName = petName;
}
public String getAdoptionDate() {
return adoptionDate;
}
public void setAdoptionDate(String adoptionDate) {
this.adoptionDate = adoptionDate;
}
public String getStatus() {
return status;
}
public void setStatus(String status) {
this.status = status;
}
}

View File

@@ -1,76 +0,0 @@
package com.example.petstoremobile.models;
public class Appointment {
private int appointmentId;
private String customerName;
private String petName;
private String serviceType;
private String appointmentDate;
private String appointmentTime;
private String status;
// Constructor
public Appointment(int appointmentId, String customerName, String petName, String serviceType, String appointmentDate, String appointmentTime, String status) {
this.appointmentId = appointmentId;
this.customerName = customerName;
this.petName = petName;
this.serviceType = serviceType;
this.appointmentDate = appointmentDate;
this.appointmentTime = appointmentTime;
this.status = status;
}
// Getters and setters
public int getAppointmentId() {
return appointmentId;
}
public String getCustomerName() {
return customerName;
}
public void setCustomerName(String customerName) {
this.customerName = customerName;
}
public String getPetName() {
return petName;
}
public void setPetName(String petName) {
this.petName = petName;
}
public String getServiceType() {
return serviceType;
}
public void setServiceType(String serviceType) {
this.serviceType = serviceType;
}
public String getAppointmentDate() {
return appointmentDate;
}
public void setAppointmentDate(String appointmentDate) {
this.appointmentDate = appointmentDate;
}
public String getAppointmentTime() {
return appointmentTime;
}
public void setAppointmentTime(String appointmentTime) {
this.appointmentTime = appointmentTime;
}
public String getStatus() {
return status;
}
public void setStatus(String status) {
this.status = status;
}
}

View File

@@ -1,66 +0,0 @@
package com.example.petstoremobile.models;
public class Inventory {
private int inventoryId;
private String itemName;
private String category;
private int quantity;
private double unitPrice;
private String supplier;
// Constructor
public Inventory(int inventoryId, String itemName, String category, int quantity, double unitPrice, String supplier) {
this.inventoryId = inventoryId;
this.itemName = itemName;
this.category = category;
this.quantity = quantity;
this.unitPrice = unitPrice;
this.supplier = supplier;
}
// Getters and setters
public int getInventoryId() {
return inventoryId;
}
public String getItemName() {
return itemName;
}
public void setItemName(String itemName) {
this.itemName = itemName;
}
public String getCategory() {
return category;
}
public void setCategory(String category) {
this.category = category;
}
public int getQuantity() {
return quantity;
}
public void setQuantity(int quantity) {
this.quantity = quantity;
}
public double getUnitPrice() {
return unitPrice;
}
public void setUnitPrice(double unitPrice) {
this.unitPrice = unitPrice;
}
public String getSupplier() {
return supplier;
}
public void setSupplier(String supplier) {
this.supplier = supplier;
}
}

View File

@@ -1,66 +0,0 @@
package com.example.petstoremobile.models;
public class Product {
private int productId;
private String productName;
private String productDesc;
private String category;
private double productPrice;
private int stockQuantity;
// Constructor
public Product(int productId, String productName, String productDesc, String category, double productPrice, int stockQuantity) {
this.productId = productId;
this.productName = productName;
this.productDesc = productDesc;
this.category = category;
this.productPrice = productPrice;
this.stockQuantity = stockQuantity;
}
// Getters and setters
public int getProductId() {
return productId;
}
public String getProductName() {
return productName;
}
public void setProductName(String productName) {
this.productName = productName;
}
public String getProductDesc() {
return productDesc;
}
public void setProductDesc(String productDesc) {
this.productDesc = productDesc;
}
public String getCategory() {
return category;
}
public void setCategory(String category) {
this.category = category;
}
public double getProductPrice() {
return productPrice;
}
public void setProductPrice(double productPrice) {
this.productPrice = productPrice;
}
public int getStockQuantity() {
return stockQuantity;
}
public void setStockQuantity(int stockQuantity) {
this.stockQuantity = stockQuantity;
}
}

View File

@@ -1,49 +0,0 @@
package com.example.petstoremobile.models;
public class ProductSupplier {
private int supId;
private int prodId;
private String supCompany;
private String prodName;
private double cost;
public ProductSupplier(int supId, int prodId, String supCompany, String prodName, double cost) {
this.supId = supId;
this.prodId = prodId;
this.supCompany = supCompany;
this.prodName = prodName;
this.cost = cost;
}
public int getSupId() {
return supId;
}
public int getProdId() {
return prodId;
}
public String getSupCompany() {
return supCompany;
}
public String getProdName() {
return prodName;
}
public double getCost() {
return cost;
}
public void setSupCompany(String supCompany) {
this.supCompany = supCompany;
}
public void setProdName(String prodName) {
this.prodName = prodName;
}
public void setCost(double cost) {
this.cost = cost;
}
}

View File

@@ -1,31 +0,0 @@
package com.example.petstoremobile.models;
public class PurchaseOrder {
private int purchaseOrderId;
private String supplierName;
private String orderDate;
private String status;
public PurchaseOrder(int purchaseOrderId, String supplierName, String orderDate, String status) {
this.purchaseOrderId = purchaseOrderId;
this.supplierName = supplierName;
this.orderDate = orderDate;
this.status = status;
}
public int getPurchaseOrderId() {
return purchaseOrderId;
}
public String getSupplierName() {
return supplierName;
}
public String getOrderDate() {
return orderDate;
}
public String getStatus() {
return status;
}
}

View File

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

View File

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

View File

@@ -0,0 +1,107 @@
package com.example.petstoremobile.repositories;
import androidx.annotation.NonNull;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import com.example.petstoremobile.api.auth.AuthApi;
import com.example.petstoremobile.api.auth.TokenManager;
import com.example.petstoremobile.dtos.AuthDTO;
import com.example.petstoremobile.dtos.UserDTO;
import com.example.petstoremobile.utils.ErrorUtils;
import com.example.petstoremobile.utils.Resource;
import java.util.Map;
import javax.inject.Inject;
import javax.inject.Singleton;
import okhttp3.MultipartBody;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
@Singleton
public class AuthRepository extends BaseRepository {
private final AuthApi authApi;
private final TokenManager tokenManager;
@Inject
public AuthRepository(AuthApi authApi, TokenManager tokenManager) {
super("AuthRepository");
this.authApi = authApi;
this.tokenManager = tokenManager;
}
/**
* Authenticates the user and saves login data (token, username, role) upon success.
*/
public LiveData<Resource<AuthDTO.LoginResponse>> login(AuthDTO.LoginRequest loginRequest) {
MutableLiveData<Resource<AuthDTO.LoginResponse>> data = new MutableLiveData<>();
data.setValue(Resource.loading(null));
authApi.login(loginRequest).enqueue(new Callback<AuthDTO.LoginResponse>() {
@Override
public void onResponse(@NonNull Call<AuthDTO.LoginResponse> call, @NonNull Response<AuthDTO.LoginResponse> response) {
if (response.isSuccessful()) {
AuthDTO.LoginResponse result = response.body();
if (result != null && result.getToken() != null) {
tokenManager.saveLoginData(result.getToken(), result.getUsername(), result.getRole());
data.setValue(Resource.success(result));
} else {
data.setValue(Resource.error("Login failed: Invalid response", null));
}
} else {
String errorMsg = ErrorUtils.getErrorMessage(response, "Login failed");
data.setValue(Resource.error(errorMsg, null));
}
}
@Override
public void onFailure(@NonNull Call<AuthDTO.LoginResponse> call, @NonNull Throwable t) {
data.setValue(Resource.error(ErrorUtils.getFailureMessage(t), null));
}
});
return data;
}
/**
* Retrieves the current user's profile information from the API.
*/
public LiveData<Resource<UserDTO>> getMe() {
return executeCall(authApi.getMe());
}
/**
* Updates the current user's profile details.
*/
public LiveData<Resource<UserDTO>> updateMe(Map<String, String> updates) {
return executeCall(authApi.updateMe(updates));
}
/**
* Uploads a multipart image to be used as the current user's avatar.
*/
public LiveData<Resource<UserDTO>> uploadAvatar(MultipartBody.Part avatar) {
return executeCall(authApi.uploadAvatar(avatar));
}
/**
* Sends a request to the API to remove the current user's avatar.
*/
public LiveData<Resource<Void>> deleteAvatar() {
return executeCall(authApi.deleteAvatar());
}
/**
* Clears all authentication and login data from storage.
*/
public void logout() {
tokenManager.clearLoginData();
}
public boolean isLoggedIn() {
return tokenManager.getToken() != null;
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,73 @@
package com.example.petstoremobile.repositories;
import androidx.lifecycle.LiveData;
import com.example.petstoremobile.api.PetApi;
import com.example.petstoremobile.dtos.PageResponse;
import com.example.petstoremobile.dtos.PetDTO;
import com.example.petstoremobile.utils.Resource;
import javax.inject.Inject;
import javax.inject.Singleton;
import okhttp3.MultipartBody;
@Singleton
public class PetRepository extends BaseRepository {
private final PetApi petApi;
@Inject
public PetRepository(PetApi petApi) {
super("PetRepository");
this.petApi = petApi;
}
/**
* 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) {
return executeCall(petApi.getAllPets(page, size, query, status, species, storeId, sort));
}
/**
* Retrieves a specific pet by its ID from the API.
*/
public LiveData<Resource<PetDTO>> getPetById(Long id) {
return executeCall(petApi.getPetById(id));
}
/**
* Sends a request to the API to create a new pet record.
*/
public LiveData<Resource<PetDTO>> createPet(PetDTO pet) {
return executeCall(petApi.createPet(pet));
}
/**
* Sends a request to the API to update an existing pet record.
*/
public LiveData<Resource<PetDTO>> updatePet(Long id, PetDTO pet) {
return executeCall(petApi.updatePet(id, pet));
}
/**
* Sends a request to the API to delete a specific pet record.
*/
public LiveData<Resource<Void>> deletePet(Long id) {
return executeCall(petApi.deletePet(id));
}
/**
* Uploads an image file for a specific pet via the API.
*/
public LiveData<Resource<Void>> uploadPetImage(Long id, MultipartBody.Part image) {
return executeCall(petApi.uploadPetImage(id, image));
}
/**
* Sends a request to the API to delete the image of a specific pet.
*/
public LiveData<Resource<Void>> deletePetImage(Long id) {
return executeCall(petApi.deletePetImage(id));
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -8,24 +8,30 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import com.example.petstoremobile.api.ChatApi; import com.example.petstoremobile.api.ChatApi;
import com.example.petstoremobile.api.CustomerApi; import com.example.petstoremobile.api.CustomerApi;
import com.example.petstoremobile.api.RetrofitClient;
import com.example.petstoremobile.api.auth.TokenManager; import com.example.petstoremobile.api.auth.TokenManager;
import com.example.petstoremobile.dtos.ConversationDTO; import com.example.petstoremobile.dtos.ConversationDTO;
import com.example.petstoremobile.dtos.CustomerDTO; import com.example.petstoremobile.dtos.CustomerDTO;
import com.example.petstoremobile.dtos.MessageDTO; import com.example.petstoremobile.dtos.MessageDTO;
import com.example.petstoremobile.dtos.PageResponse; import com.example.petstoremobile.dtos.PageResponse;
import com.example.petstoremobile.utils.NotificationHelper; import com.example.petstoremobile.utils.NotificationHelper;
import com.example.petstoremobile.utils.RetrofitUtils;
import com.example.petstoremobile.websocket.StompChatManager; import com.example.petstoremobile.websocket.StompChatManager;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import javax.inject.Inject;
import javax.inject.Named;
import dagger.hilt.android.AndroidEntryPoint;
import retrofit2.Call; import retrofit2.Call;
import retrofit2.Callback; import retrofit2.Callback;
import retrofit2.Response; import retrofit2.Response;
// Service to receive notifications when a new conversation is created // Service to receive notifications when a new conversation is created
@AndroidEntryPoint
public class ChatNotificationService extends Service { public class ChatNotificationService extends Service {
private static final String TAG = "ChatNotificationService"; private static final String TAG = "ChatNotificationService";
@@ -37,6 +43,11 @@ public class ChatNotificationService extends Service {
private final Map<Long, String> customerIdToName = new HashMap<>(); private final Map<Long, String> customerIdToName = new HashMap<>();
private Long currentUserId; private Long currentUserId;
@Inject CustomerApi customerApi;
@Inject ChatApi chatApi;
@Inject TokenManager tokenManager;
@Inject @Named("baseUrl") String baseUrl;
//When the service starts, connect to the websocket //When the service starts, connect to the websocket
@Override @Override
public int onStartCommand(Intent intent, int flags, int startId) { public int onStartCommand(Intent intent, int flags, int startId) {
@@ -48,69 +59,42 @@ public class ChatNotificationService extends Service {
// helper function to connect to the websocket // helper function to connect to the websocket
private void connectWebSocket() { private void connectWebSocket() {
//get the token and role from the shared preferences //get the token and role from the shared preferences
TokenManager tm = TokenManager.getInstance(this); String token = tokenManager.getToken();
String token = tm.getToken(); String role = tokenManager.getRole();
String role = tm.getRole(); currentUserId = tokenManager.getUserId();
currentUserId = tm.getUserId();
if (token != null && stompChatManager == null) { if (token != null && stompChatManager == null) {
//load customers to have names associated with customer ids customerApi.getAllCustomers(0, 1000).enqueue(RetrofitUtils.createSilentCallback(TAG, result -> {
CustomerApi customerApi = RetrofitClient.getCustomerApi(this); for (CustomerDTO customer : result.getContent()) {
customerApi.getAllCustomers(0, 1000).enqueue(new Callback<PageResponse<CustomerDTO>>() { customerIdToName.put(customer.getCustomerId(), customer.getFullName());
@Override
public void onResponse(@NonNull Call<PageResponse<CustomerDTO>> call, @NonNull Response<PageResponse<CustomerDTO>> response) {
if (response.isSuccessful() && response.body() != null) {
for (CustomerDTO customer : response.body().getContent()) {
customerIdToName.put(customer.getCustomerId(), customer.getFullName());
}
}
loadConversationsAndStartStomp(token, role);
} }
loadConversationsAndStartStomp(token, role);
@Override }));
public void onFailure(@NonNull Call<PageResponse<CustomerDTO>> call, @NonNull Throwable t) {
Log.e(TAG, "Failed to load customers", t);
loadConversationsAndStartStomp(token, role);
}
});
} }
} }
private void loadConversationsAndStartStomp(String token, String role) { private void loadConversationsAndStartStomp(String token, String role) {
// Fetch existing conversations // Fetch existing conversations
ChatApi chatApi = RetrofitClient.getChatApi(this); chatApi.getAllConversations().enqueue(RetrofitUtils.createSilentCallback(TAG, result -> {
chatApi.getAllConversations().enqueue(new Callback<List<ConversationDTO>>() { for (ConversationDTO conversation : result) {
@Override if (conversation.getId() != null) {
public void onResponse(@NonNull Call<List<ConversationDTO>> call, @NonNull Response<List<ConversationDTO>> response) { knownConversationIds.add(conversation.getId());
if (response.isSuccessful() && response.body() != null) { conversationToCustomerId.put(conversation.getId(), conversation.getCustomerId());
for (ConversationDTO conversation : response.body()) { // subscribe to existing conversations to get message notifications
if (conversation.getId() != null) { if (stompChatManager != null) {
knownConversationIds.add(conversation.getId()); stompChatManager.subscribeToConversation(conversation.getId());
conversationToCustomerId.put(conversation.getId(), conversation.getCustomerId());
// subscribe to existing conversations to get message notifications
if (stompChatManager != null) {
stompChatManager.subscribeToConversation(conversation.getId());
}
}
} }
Log.d(TAG, "Loaded " + knownConversationIds.size() + " existing conversations");
} }
startStomp(token, role);
} }
Log.d(TAG, "Loaded " + knownConversationIds.size() + " existing conversations");
@Override startStomp(token, role);
public void onFailure(@NonNull Call<List<ConversationDTO>> call, @NonNull Throwable t) { }));
Log.e(TAG, "Failed to load existing conversations", t);
//tries to connect if loading fails
startStomp(token, role);
}
});
} }
private void startStomp(String token, String role) { private void startStomp(String token, String role) {
if (stompChatManager != null) return; if (stompChatManager != null) return;
stompChatManager = new StompChatManager(token, role); stompChatManager = new StompChatManager(token, role, baseUrl);
// Listen for messages in existing conversations // Listen for messages in existing conversations
stompChatManager.setMessageListener(message -> { stompChatManager.setMessageListener(message -> {
@@ -193,20 +177,9 @@ public class ChatNotificationService extends Service {
// Helper function to fetch customer name for a conversation // Helper function to fetch customer name for a conversation
private void fetchCustomerName(Long customerId) { private void fetchCustomerName(Long customerId) {
CustomerApi customerApi = RetrofitClient.getCustomerApi(this); customerApi.getCustomerById(customerId).enqueue(RetrofitUtils.createSilentCallback(TAG, result -> {
customerApi.getCustomerById(customerId).enqueue(new Callback<CustomerDTO>() { customerIdToName.put(customerId, result.getFullName());
@Override }));
public void onResponse(@NonNull Call<CustomerDTO> call, @NonNull Response<CustomerDTO> response) {
if (response.isSuccessful() && response.body() != null) {
customerIdToName.put(customerId, response.body().getFullName());
}
}
@Override
public void onFailure(@NonNull Call<CustomerDTO> call, @NonNull Throwable t) {
Log.e(TAG, "Failed to fetch customer name", t);
}
});
} }
//When the service is destroyed, disconnect from the websocket //When the service is destroyed, disconnect from the websocket

View File

@@ -0,0 +1,47 @@
package com.example.petstoremobile.utils;
import android.content.Context;
import androidx.appcompat.app.AlertDialog;
/**
* Utility class for creating and displaying common dialogs.
*/
public class DialogUtils {
/**
* Interface for handling dialog button clicks.
*/
public interface DialogCallback {
void onConfirm();
}
/**
* Shows a confirmation dialog with "Yes" and "No" buttons.
*/
public static void showConfirmDialog(Context context, String title, String message, DialogCallback callback) {
new AlertDialog.Builder(context)
.setTitle(title)
.setMessage(message)
.setPositiveButton("Yes", (dialog, which) -> callback.onConfirm())
.setNegativeButton("No", null)
.show();
}
/**
* Shows a delete confirmation dialog.
*/
public static void showDeleteConfirmDialog(Context context, String itemName, DialogCallback callback) {
showConfirmDialog(context, "Delete " + itemName + "?", "Are you sure you want to delete this " + itemName.toLowerCase() + "? This action cannot be undone.", callback);
}
/**
* Shows a simple information or error dialog with an "OK" button.
*/
public static void showInfoDialog(Context context, String title, String message) {
new AlertDialog.Builder(context)
.setTitle(title)
.setMessage(message)
.setPositiveButton("OK", null)
.show();
}
}

View File

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

View File

@@ -0,0 +1,27 @@
package com.example.petstoremobile.utils;
import android.content.Context;
import android.net.Uri;
import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStream;
public class FileUtils {
public static File getFileFromUri(Context context, Uri uri) {
try {
InputStream inputStream = context.getContentResolver().openInputStream(uri);
File tempFile = new File(context.getCacheDir(), "upload_image_" + System.currentTimeMillis() + ".jpg");
FileOutputStream outputStream = new FileOutputStream(tempFile);
byte[] buffer = new byte[1024];
int length;
while ((length = inputStream.read(buffer)) > 0) {
outputStream.write(buffer, 0, length);
}
outputStream.close();
inputStream.close();
return tempFile;
} catch (Exception e) {
return null;
}
}
}

View File

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

View File

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

View File

@@ -1,6 +1,9 @@
package com.example.petstoremobile.utils; package com.example.petstoremobile.utils;
import android.view.View;
import android.widget.EditText; import android.widget.EditText;
import android.widget.Spinner;
import android.widget.TextView;
public class InputValidator { public class InputValidator {
@@ -94,4 +97,21 @@ public class InputValidator {
} }
return true; return true;
} }
/**
* Checks if a selection has been made in a Spinner.
* Assumes position 0 is a placeholder like "None" or "Select".
*/
public static boolean isSpinnerSelected(Spinner spinner, String fieldName) {
if (spinner.getSelectedItemPosition() <= 0) {
View selectedView = spinner.getSelectedView();
if (selectedView instanceof TextView) {
TextView tv = (TextView) selectedView;
tv.setError(fieldName + " is required");
spinner.requestFocus();
}
return false;
}
return true;
}
} }

View File

@@ -0,0 +1,30 @@
package com.example.petstoremobile.utils;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
public class Resource<T> {
public enum Status { SUCCESS, ERROR, LOADING }
public final Status status;
public final T data;
public final String message;
private Resource(Status status, @Nullable T data, @Nullable String message) {
this.status = status;
this.data = data;
this.message = message;
}
public static <T> Resource<T> success(@Nullable T data) {
return new Resource<>(Status.SUCCESS, data, null);
}
public static <T> Resource<T> error(String msg, @Nullable T data) {
return new Resource<>(Status.ERROR, data, msg);
}
public static <T> Resource<T> loading(@Nullable T data) {
return new Resource<>(Status.LOADING, data, null);
}
}

View File

@@ -0,0 +1,105 @@
package com.example.petstoremobile.utils;
import android.content.Context;
import android.util.Log;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.lifecycle.MutableLiveData;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
/**
* Utility class for common Retrofit operations and standardized callbacks.
*/
public class RetrofitUtils {
/**
* Interface for handling successful API responses.
* @param <T> The type of the response body.
*/
public interface SuccessCallback<T> {
void onSuccess(T result);
}
/**
* Enqueues a Retrofit call and updates the provided MutableLiveData with Resource states.
*/
public static <T> void enqueue(@NonNull Call<T> call, @NonNull MutableLiveData<Resource<T>> data, String tag) {
data.setValue(Resource.loading(null));
call.enqueue(new Callback<T>() {
@Override
public void onResponse(@NonNull Call<T> call, @NonNull Response<T> response) {
if (response.isSuccessful()) {
data.setValue(Resource.success(response.body()));
} else {
String errorMsg = ErrorUtils.getErrorMessage(response, "API Error: " + response.code());
Log.e(tag, errorMsg);
data.setValue(Resource.error(errorMsg, null));
}
}
@Override
public void onFailure(@NonNull Call<T> call, @NonNull Throwable t) {
String errorMsg = ErrorUtils.getFailureMessage(t);
Log.e(tag, "Network Error: " + t.getMessage(), t);
data.setValue(Resource.error(errorMsg, null));
}
});
}
/**
* Creates a callback for Retrofit calls that handles errors and logging.
* @deprecated Use {@link #enqueue(Call, MutableLiveData, String)} for LiveData-based architecture.
*/
@Deprecated
public static <T> Callback<T> createCallback(Context context, String tag, String successMsg, SuccessCallback<T> successCallback) {
return new Callback<T>() {
@Override
public void onResponse(@NonNull Call<T> call, @NonNull Response<T> response) {
if (response.isSuccessful()) {
if (successMsg != null) {
Toast.makeText(context, successMsg, Toast.LENGTH_SHORT).show();
}
if (successCallback != null) {
successCallback.onSuccess(response.body());
}
} else {
ErrorUtils.showErrorMessage(context, response, "Operation failed");
Log.e(tag, "API Error: " + response.code());
}
}
@Override
public void onFailure(@NonNull Call<T> call, @NonNull Throwable t) {
String errorMsg = ErrorUtils.getFailureMessage(t);
Log.e(tag, "Network Error: " + t.getMessage(), t);
Toast.makeText(context, errorMsg, Toast.LENGTH_SHORT).show();
}
};
}
/**
* Creates a callback that doesn't show toasts
*/
@Deprecated
public static <T> Callback<T> createSilentCallback(String tag, SuccessCallback<T> successCallback) {
return new Callback<T>() {
@Override
public void onResponse(@NonNull Call<T> call, @NonNull Response<T> response) {
if (response.isSuccessful() && successCallback != null) {
successCallback.onSuccess(response.body());
} else {
Log.e(tag, "API Error: " + response.code());
}
}
@Override
public void onFailure(@NonNull Call<T> call, @NonNull Throwable t) {
Log.e(tag, "Network Error: " + t.getMessage(), t);
}
};
}
}

View File

@@ -0,0 +1,94 @@
package com.example.petstoremobile.utils;
import android.content.Context;
import android.widget.ArrayAdapter;
import android.widget.Spinner;
import com.example.petstoremobile.adapters.BlackTextArrayAdapter;
import com.example.petstoremobile.adapters.WhiteTextArrayAdapter;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.function.Function;
/**
* Utility class for Spinners.
*/
public class SpinnerUtils {
/**
* Populates a spinner with a list of items and handles pre-selection.
*/
public static <T> void populateSpinner(Context context, Spinner spinner, List<T> data,
Function<T, String> nameExtractor, String defaultText,
Long preselectedId, Function<T, Long> idExtractor) {
populateSpinnerWithAdapter(context, spinner, data, nameExtractor, defaultText, preselectedId, idExtractor, false);
}
/**
* Populates a spinner with white text (for dark backgrounds).
*/
public static <T> void populateWhiteSpinner(Context context, Spinner spinner, List<T> data,
Function<T, String> nameExtractor, String defaultText,
Long preselectedId, Function<T, Long> idExtractor) {
populateSpinnerWithAdapter(context, spinner, data, nameExtractor, defaultText, preselectedId, idExtractor, true);
}
private static <T> void populateSpinnerWithAdapter(Context context, Spinner spinner, List<T> data,
Function<T, String> nameExtractor, String defaultText,
Long preselectedId, Function<T, Long> idExtractor,
boolean useWhiteText) {
List<String> names = new ArrayList<>();
if (defaultText != null) {
names.add(defaultText);
}
for (T item : data) {
names.add(nameExtractor.apply(item));
}
ArrayAdapter<String> adapter;
if (useWhiteText) {
adapter = new WhiteTextArrayAdapter<>(context, android.R.layout.simple_spinner_item, names);
} else {
adapter = new BlackTextArrayAdapter<>(context, android.R.layout.simple_spinner_item, names);
}
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
spinner.setAdapter(adapter);
if (preselectedId != null && preselectedId != -1) {
int offset = (defaultText != null) ? 1 : 0;
for (int i = 0; i < data.size(); i++) {
Long currentId = idExtractor.apply(data.get(i));
if (Objects.equals(currentId, preselectedId)) {
spinner.setSelection(i + offset);
break;
}
}
}
}
/**
* Sets the selection of a spinner based on a string value.
*/
public static void setSelectionByValue(Spinner spinner, String value) {
if (value == null || spinner.getAdapter() == null) return;
ArrayAdapter<String> adapter = (ArrayAdapter<String>) spinner.getAdapter();
int pos = adapter.getPosition(value);
if (pos >= 0) {
spinner.setSelection(pos);
}
}
/**
* Configures a simple string array spinner.
*/
public static void setupStringSpinner(Context context, Spinner spinner, String[] items) {
BlackTextArrayAdapter<String> adapter = new BlackTextArrayAdapter<>(context,
android.R.layout.simple_spinner_item, items);
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
spinner.setAdapter(adapter);
}
}

View File

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

View File

@@ -0,0 +1,58 @@
package com.example.petstoremobile.viewmodels;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.ViewModel;
import com.example.petstoremobile.dtos.AdoptionDTO;
import com.example.petstoremobile.dtos.PageResponse;
import com.example.petstoremobile.repositories.AdoptionRepository;
import com.example.petstoremobile.utils.Resource;
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.
*/
public LiveData<Resource<PageResponse<AdoptionDTO>>> getAllAdoptions(int page, int size) {
return repository.getAllAdoptions(page, size);
}
/**
* 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);
}
}

View File

@@ -0,0 +1,58 @@
package com.example.petstoremobile.viewmodels;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.ViewModel;
import com.example.petstoremobile.dtos.AppointmentDTO;
import com.example.petstoremobile.dtos.PageResponse;
import com.example.petstoremobile.repositories.AppointmentRepository;
import com.example.petstoremobile.utils.Resource;
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);
}
}

View File

@@ -0,0 +1,68 @@
package com.example.petstoremobile.viewmodels;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.ViewModel;
import com.example.petstoremobile.dtos.AuthDTO;
import com.example.petstoremobile.dtos.UserDTO;
import com.example.petstoremobile.repositories.AuthRepository;
import com.example.petstoremobile.utils.Resource;
import java.util.Map;
import javax.inject.Inject;
import dagger.hilt.android.lifecycle.HiltViewModel;
import okhttp3.MultipartBody;
@HiltViewModel
public class AuthViewModel extends ViewModel {
private final AuthRepository repository;
@Inject
public AuthViewModel(AuthRepository repository) {
this.repository = repository;
}
/**
* Authenticates a user with username and password.
*/
public LiveData<Resource<AuthDTO.LoginResponse>> login(String username, String password) {
return repository.login(new AuthDTO.LoginRequest(username, password));
}
/**
* Retrieves the profile information of the currently authenticated user.
*/
public LiveData<Resource<UserDTO>> getMe() {
return repository.getMe();
}
/**
* Updates the profile information of the current user.
*/
public LiveData<Resource<UserDTO>> updateMe(Map<String, String> updates) {
return repository.updateMe(updates);
}
/**
* Uploads a new avatar image for the current user.
*/
public LiveData<Resource<UserDTO>> uploadAvatar(MultipartBody.Part avatar) {
return repository.uploadAvatar(avatar);
}
/**
* Deletes the avatar image of the current user.
*/
public LiveData<Resource<Void>> deleteAvatar() {
return repository.deleteAvatar();
}
/**
* Logs out the current user by clearing stored credentials.
*/
public void logout() {
repository.logout();
}
}

View File

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

View File

@@ -0,0 +1,37 @@
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

@@ -0,0 +1,91 @@
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.InventoryRequest;
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, String category, Long storeId, int page, int size, String sort) {
return inventoryRepository.getAllInventory(query, category, 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(InventoryRequest request) {
return inventoryRepository.createInventory(request);
}
/**
* Updates an existing inventory record by ID.
*/
public LiveData<Resource<InventoryDTO>> updateInventory(Long id, InventoryRequest 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<Long> 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,73 @@
package com.example.petstoremobile.viewmodels;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.ViewModel;
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 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);
}
/**
* 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,51 @@
package com.example.petstoremobile.viewmodels;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.ViewModel;
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 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);
}
}

View File

@@ -0,0 +1,84 @@
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,37 @@
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,58 @@
package com.example.petstoremobile.viewmodels;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.ViewModel;
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 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);
}
}

View File

@@ -0,0 +1,30 @@
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,58 @@
package com.example.petstoremobile.viewmodels;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.ViewModel;
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 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);
}
}

View File

@@ -3,7 +3,6 @@ package com.example.petstoremobile.websocket;
import android.os.Handler; import android.os.Handler;
import android.os.Looper; import android.os.Looper;
import android.util.Log; import android.util.Log;
import com.example.petstoremobile.api.RetrofitClient;
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.google.gson.Gson; import com.google.gson.Gson;
@@ -54,14 +53,16 @@ public class StompChatManager {
private ConnectionListener connectionListener; private ConnectionListener connectionListener;
private final String authToken; private final String authToken;
private final String role; private final String role;
private final String baseUrl;
private boolean isConnected; private boolean isConnected;
private boolean isConnecting; private boolean isConnecting;
private boolean manualDisconnect; private boolean manualDisconnect;
private Long pendingConversationId; private Long pendingConversationId;
public StompChatManager(String authToken, String role) { public StompChatManager(String authToken, String role, String baseUrl) {
this.authToken = authToken; this.authToken = authToken;
this.role = role == null ? "" : role.trim().toUpperCase(Locale.ROOT); this.role = role == null ? "" : role.trim().toUpperCase(Locale.ROOT);
this.baseUrl = baseUrl;
} }
public void setMessageListener(MessageListener listener) { public void setMessageListener(MessageListener listener) {
@@ -267,16 +268,16 @@ public class StompChatManager {
// Make the URL for the websocket connection // Make the URL for the websocket connection
private String buildWebSocketUrl() { private String buildWebSocketUrl() {
String baseUrl = RetrofitClient.BASE_URL.endsWith("/") String cleanBaseUrl = baseUrl.endsWith("/")
? RetrofitClient.BASE_URL.substring(0, RetrofitClient.BASE_URL.length() - 1) ? baseUrl.substring(0, baseUrl.length() - 1)
: RetrofitClient.BASE_URL; : baseUrl;
if (baseUrl.startsWith("https://")) { if (cleanBaseUrl.startsWith("https://")) {
return "wss://" + baseUrl.substring("https://".length()) + "/ws/chat"; return "wss://" + cleanBaseUrl.substring("https://".length()) + "/ws/chat";
} }
if (baseUrl.startsWith("http://")) { if (cleanBaseUrl.startsWith("http://")) {
return "ws://" + baseUrl.substring("http://".length()) + "/ws/chat"; return "ws://" + cleanBaseUrl.substring("http://".length()) + "/ws/chat";
} }
return baseUrl + "/ws/chat"; return cleanBaseUrl + "/ws/chat";
} }
// Helper to check if the current user is a customer // Helper to check if the current user is a customer
@@ -292,4 +293,4 @@ public class StompChatManager {
reconnectHandler.removeCallbacksAndMessages(null); reconnectHandler.removeCallbacksAndMessages(null);
reconnectHandler.postDelayed(this::connect, 1000); reconnectHandler.postDelayed(this::connect, 1000);
} }
} }

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="@color/white"/>
<corners android:radius="22dp"/>
</shape>

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="#1AFFFFFF"/>
<stroke android:width="1dp" android:color="#33FFFFFF"/>
<corners android:radius="8dp"/>
</shape>

View File

@@ -7,12 +7,14 @@
android:orientation="vertical" android:orientation="vertical"
android:background="@color/primary_dark"> android:background="@color/primary_dark">
<FrameLayout <androidx.fragment.app.FragmentContainerView
android:id="@+id/fragment_container" android:id="@+id/nav_host_fragment"
android:name="androidx.navigation.fragment.NavHostFragment"
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:background="@color/background_grey"/> app:defaultNavHost="true"
app:navGraph="@navigation/nav_graph" />
<com.google.android.material.bottomnavigation.BottomNavigationView <com.google.android.material.bottomnavigation.BottomNavigationView
android:id="@+id/bottom_navigation" android:id="@+id/bottom_navigation"

View File

@@ -96,6 +96,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:hint="Enter password" android:hint="Enter password"
android:inputType="textPassword" android:inputType="textPassword"
android:imeOptions="actionDone"
android:layout_marginBottom="24dp" android:layout_marginBottom="24dp"
android:textColor="@color/text_dark"/> android:textColor="@color/text_dark"/>

View File

@@ -28,15 +28,35 @@
android:contentDescription="Open menu"/> android:contentDescription="Open menu"/>
<TextView <TextView
android:layout_width="match_parent" android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="Adoptions" android:text="Adoptions"
android:textColor="@color/white" android:textColor="@color/white"
android:textSize="20sp" android:textSize="20sp"
android:textStyle="bold"/> android:textStyle="bold"/>
<ImageButton
android:id="@+id/btnToggleCalendarModeAdoption"
android:layout_width="48dp"
android:layout_height="48dp"
android:src="@android:drawable/ic_menu_today"
android:background="?attr/selectableItemBackgroundBorderless"
app:tint="@color/white"
android:contentDescription="Toggle Calendar Mode"/>
</LinearLayout> </LinearLayout>
<com.prolificinteractive.materialcalendarview.MaterialCalendarView
android:id="@+id/calendarViewAdoption"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@android:color/white"
app:mcv_showOtherDates="all"
app:mcv_selectionColor="@color/accent_blue"
app:mcv_calendarMode="week"
app:mcv_tileHeight="40dp" />
<EditText <EditText
android:id="@+id/etSearchAdoption" android:id="@+id/etSearchAdoption"
android:layout_width="match_parent" android:layout_width="match_parent"

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