merge origin/main into morefiles, resolve all conflicts
This commit is contained in:
@@ -1,7 +1,25 @@
|
||||
import java.util.Properties
|
||||
|
||||
plugins {
|
||||
alias(libs.plugins.android.application)
|
||||
alias(libs.plugins.hilt)
|
||||
alias(libs.plugins.navigation.safeargs)
|
||||
}
|
||||
|
||||
val localProperties = Properties().apply {
|
||||
val file = rootProject.file("local.properties")
|
||||
if (file.exists()) {
|
||||
file.inputStream().use { load(it) }
|
||||
}
|
||||
}
|
||||
|
||||
fun quoted(value: String): String = "\"$value\""
|
||||
|
||||
val emulatorBackendUrl =
|
||||
(localProperties.getProperty("petstore.backend.emulatorUrl") ?: "http://10.0.2.2:8080/").trim()
|
||||
val deviceBackendUrl =
|
||||
(localProperties.getProperty("petstore.backend.deviceUrl") ?: "http://10.0.0.200:8080/").trim()
|
||||
|
||||
android {
|
||||
namespace = "com.example.petstoremobile"
|
||||
compileSdk = 36
|
||||
@@ -14,6 +32,14 @@ android {
|
||||
versionName = "1.0"
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
|
||||
buildConfigField("String", "EMULATOR_BACKEND_URL", quoted(emulatorBackendUrl))
|
||||
buildConfigField("String", "DEVICE_BACKEND_URL", quoted(deviceBackendUrl))
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
buildConfig = true
|
||||
viewBinding = true
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
@@ -32,34 +58,46 @@ android {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
// Core AndroidX & UI
|
||||
implementation(libs.appcompat)
|
||||
implementation(libs.material)
|
||||
implementation(libs.activity)
|
||||
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.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("io.reactivex.rxjava2:rxjava:2.2.21")
|
||||
implementation("io.reactivex.rxjava2:rxandroid:2.1.1")
|
||||
implementation("com.github.prolificinteractive:material-calendarview:2.0.1")
|
||||
|
||||
implementation("com.github.bumptech.glide:glide:4.16.0")
|
||||
annotationProcessor("com.github.bumptech.glide:compiler:4.16.0")
|
||||
|
||||
// Testing
|
||||
testImplementation(libs.junit)
|
||||
androidTestImplementation(libs.ext.junit)
|
||||
androidTestImplementation(libs.espresso.core)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
package com.example.petstoremobile;
|
||||
|
||||
import android.app.Application;
|
||||
import com.example.petstoremobile.api.auth.TokenManager;
|
||||
import dagger.hilt.android.HiltAndroidApp;
|
||||
|
||||
@HiltAndroidApp
|
||||
public class PetStoreApplication extends Application {
|
||||
@Override
|
||||
public void onCreate() {
|
||||
|
||||
@@ -10,23 +10,25 @@ import android.util.Log;
|
||||
import androidx.activity.EdgeToEdge;
|
||||
import androidx.activity.result.ActivityResultLauncher;
|
||||
import androidx.activity.result.contract.ActivityResultContracts;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.core.graphics.Insets;
|
||||
import androidx.core.view.ViewCompat;
|
||||
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.fragments.ChatFragment;
|
||||
import com.example.petstoremobile.fragments.ListFragment;
|
||||
import com.example.petstoremobile.fragments.ProfileFragment;
|
||||
import com.example.petstoremobile.databinding.ActivityHomeBinding;
|
||||
import com.example.petstoremobile.services.ChatNotificationService;
|
||||
import com.google.android.material.bottomnavigation.BottomNavigationView;
|
||||
|
||||
import dagger.hilt.android.AndroidEntryPoint;
|
||||
|
||||
@AndroidEntryPoint
|
||||
public class HomeActivity extends AppCompatActivity {
|
||||
private BottomNavigationView bottomNav;
|
||||
private ActivityHomeBinding binding;
|
||||
private NavController navController;
|
||||
|
||||
// Launcher to ask for notification permission
|
||||
private final ActivityResultLauncher<String> requestPermissionLauncher =
|
||||
@@ -36,81 +38,73 @@ public class HomeActivity extends AppCompatActivity {
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Sets up the home screen, initializes bottom navigation, and handles incoming navigation intents.
|
||||
*/
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
EdgeToEdge.enable(this);
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_home);
|
||||
binding = ActivityHomeBinding.inflate(getLayoutInflater());
|
||||
setContentView(binding.getRoot());
|
||||
|
||||
ViewCompat.setOnApplyWindowInsetsListener(binding.main, (v, insets) -> {
|
||||
Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
|
||||
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom);
|
||||
return insets;
|
||||
});
|
||||
|
||||
// Initialize Navigation Component
|
||||
NavHostFragment navHostFragment = (NavHostFragment) getSupportFragmentManager()
|
||||
.findFragmentById(R.id.nav_host_fragment);
|
||||
if (navHostFragment != null) {
|
||||
navController = navHostFragment.getNavController();
|
||||
NavigationUI.setupWithNavController(binding.bottomNavigation, navController);
|
||||
}
|
||||
|
||||
//get the bottom navbar from the layout
|
||||
bottomNav = findViewById(R.id.bottom_navigation);
|
||||
|
||||
//load the list fragment by default if it's a fresh start
|
||||
if (savedInstanceState == null) {
|
||||
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
|
||||
startNotificationService();
|
||||
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
|
||||
protected void onNewIntent(Intent intent) {
|
||||
super.onNewIntent(intent);
|
||||
setIntent(intent); // Set the new intent so fragments can access updated extras
|
||||
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) {
|
||||
if (intent != null && "chat".equals(intent.getStringExtra("navigate_to"))) {
|
||||
ChatFragment chatFragment = new ChatFragment();
|
||||
if (intent.hasExtra("conversation_id")) {
|
||||
Bundle args = new Bundle();
|
||||
args.putLong("conversation_id", intent.getLongExtra("conversation_id", -1));
|
||||
chatFragment.setArguments(args);
|
||||
if (binding.bottomNavigation != null) {
|
||||
// Navigate by selecting the bottom nav item.
|
||||
binding.bottomNavigation.setSelectedItemId(R.id.nav_chat);
|
||||
}
|
||||
loadFragment(chatFragment);
|
||||
bottomNav.setSelectedItemId(R.id.nav_chat);
|
||||
} else {
|
||||
new android.os.Handler().postDelayed(() -> {
|
||||
loadFragment(new ListFragment());
|
||||
bottomNav.setSelectedItemId(R.id.nav_list);
|
||||
}, 100);
|
||||
}
|
||||
}
|
||||
|
||||
// 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() {
|
||||
new Thread(() -> {
|
||||
try {
|
||||
Intent serviceIntent = new Intent(this, ChatNotificationService.class);
|
||||
startService(serviceIntent);
|
||||
} catch (Exception e) {
|
||||
Log.e("HomeActivity", "Failed to start notification service: " + e.getMessage());
|
||||
}
|
||||
}).start();
|
||||
Intent serviceIntent = new Intent(this, ChatNotificationService.class);
|
||||
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() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
if (ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) {
|
||||
@@ -118,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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,10 +2,7 @@ package com.example.petstoremobile.activities;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
import android.widget.Button;
|
||||
import android.widget.EditText;
|
||||
import android.widget.TextView;
|
||||
import android.view.inputmethod.EditorInfo;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.activity.EdgeToEdge;
|
||||
@@ -13,157 +10,134 @@ import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.core.graphics.Insets;
|
||||
import androidx.core.view.ViewCompat;
|
||||
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.RetrofitClient;
|
||||
import com.example.petstoremobile.dtos.AuthDTO;
|
||||
import com.example.petstoremobile.dtos.UserDTO;
|
||||
import com.example.petstoremobile.databinding.ActivityMainBinding;
|
||||
import com.example.petstoremobile.viewmodels.AuthViewModel;
|
||||
import com.example.petstoremobile.utils.Resource;
|
||||
|
||||
import retrofit2.Call;
|
||||
import retrofit2.Callback;
|
||||
import retrofit2.Response;
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Named;
|
||||
|
||||
import dagger.hilt.android.AndroidEntryPoint;
|
||||
|
||||
//The login screen activity
|
||||
@AndroidEntryPoint
|
||||
public class MainActivity extends AppCompatActivity {
|
||||
|
||||
private EditText etUser;
|
||||
private EditText etPassword;
|
||||
private Button btnLogin;
|
||||
private TextView tvLoginStatus;
|
||||
private ActivityMainBinding binding;
|
||||
private AuthViewModel viewModel;
|
||||
|
||||
@Inject TokenManager tokenManager;
|
||||
@Inject @Named("baseUrl") String baseUrl;
|
||||
|
||||
/**
|
||||
* Initializes the activity, sets up the UI, and checks for an existing login session.
|
||||
*/
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
EdgeToEdge.enable(this);
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
// Check if user is already logged in
|
||||
TokenManager tokenManager = TokenManager.getInstance(this);
|
||||
if (tokenManager.isLoggedIn()) {
|
||||
if ("CUSTOMER".equalsIgnoreCase(tokenManager.getRole())) {
|
||||
// If a customer somehow remained logged in, clear them out
|
||||
tokenManager.clearLoginData();
|
||||
} else {
|
||||
Intent intent = new Intent(this, HomeActivity.class);
|
||||
startActivity(intent);
|
||||
startActivity(new Intent(this, HomeActivity.class));
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
setContentView(R.layout.activity_main);
|
||||
binding = ActivityMainBinding.inflate(getLayoutInflater());
|
||||
setContentView(binding.getRoot());
|
||||
viewModel = new ViewModelProvider(this).get(AuthViewModel.class);
|
||||
|
||||
ViewCompat.setOnApplyWindowInsetsListener(binding.main, (v, insets) -> {
|
||||
Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
|
||||
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom);
|
||||
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
|
||||
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
|
||||
btnLogin.setOnClickListener(v -> {
|
||||
binding.btnLogin.setOnClickListener(v -> {
|
||||
//Get username and password from text fields
|
||||
String username = etUser.getText().toString();
|
||||
String password = etPassword.getText().toString();
|
||||
String username = binding.etUser.getText().toString();
|
||||
String password = binding.etPassword.getText().toString();
|
||||
|
||||
//check if fields are empty
|
||||
if (username.isEmpty() || password.isEmpty()) {
|
||||
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;
|
||||
}
|
||||
|
||||
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>() {
|
||||
@Override
|
||||
public void onResponse(Call<AuthDTO.LoginResponse> call, Response<AuthDTO.LoginResponse> response) {
|
||||
if (response.isSuccessful() && response.body() != null) {
|
||||
String role = response.body().getRole();
|
||||
/**
|
||||
* Executes the login process using the AuthViewModel and handles the authentication response.
|
||||
*/
|
||||
private void performLogin(String username, String password) {
|
||||
viewModel.login(username, password).observe(this, resource -> {
|
||||
if (resource == null) return;
|
||||
|
||||
// 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)) {
|
||||
Toast.makeText(MainActivity.this, "Access denied: Customers are not allowed to log in.", Toast.LENGTH_LONG).show();
|
||||
tvLoginStatus.setText("Customers are not allowed to log in");
|
||||
return;
|
||||
binding.btnLogin.setEnabled(true);
|
||||
binding.tvLoginStatus.setText("Customers are not allowed to log in");
|
||||
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());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(Call<AuthDTO.LoginResponse> call, Throwable t) {
|
||||
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);
|
||||
}
|
||||
});
|
||||
Toast.makeText(this, "Login successful", Toast.LENGTH_SHORT).show();
|
||||
startActivity(new Intent(this, HomeActivity.class));
|
||||
finish();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,81 +1,128 @@
|
||||
package com.example.petstoremobile.adapters;
|
||||
|
||||
import android.graphics.Color;
|
||||
import android.view.*;
|
||||
import android.widget.TextView;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import com.example.petstoremobile.R;
|
||||
import com.example.petstoremobile.databinding.ItemAdoptionBinding;
|
||||
import com.example.petstoremobile.dtos.AdoptionDTO;
|
||||
import com.example.petstoremobile.utils.BulkDeleteHandler;
|
||||
import com.example.petstoremobile.utils.SelectionHelper;
|
||||
import java.util.List;
|
||||
|
||||
public class AdoptionAdapter extends RecyclerView.Adapter<AdoptionAdapter.AdoptionViewHolder> {
|
||||
public class AdoptionAdapter extends RecyclerView.Adapter<AdoptionAdapter.AdoptionViewHolder> implements BulkDeleteHandler.SelectableAdapter {
|
||||
|
||||
private List<AdoptionDTO> adoptionList;
|
||||
private OnAdoptionClickListener listener;
|
||||
private final SelectionHelper selectionHelper;
|
||||
|
||||
public interface OnAdoptionClickListener {
|
||||
void onAdoptionClick(int position);
|
||||
void onSelectionChanged(int count);
|
||||
}
|
||||
|
||||
public AdoptionAdapter(List<AdoptionDTO> adoptionList, OnAdoptionClickListener listener) {
|
||||
this.adoptionList = adoptionList;
|
||||
this.listener = listener;
|
||||
this.selectionHelper = new SelectionHelper(new SelectionHelper.SelectionListener() {
|
||||
@Override
|
||||
public void onSelectionChanged(int count) {
|
||||
listener.onSelectionChanged(count);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSelectionModeToggle(boolean selectionMode) {
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getSelectedKeys() {
|
||||
return selectionHelper.getSelectedKeys();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearSelection() {
|
||||
selectionHelper.clearSelection();
|
||||
}
|
||||
|
||||
public static class AdoptionViewHolder extends RecyclerView.ViewHolder {
|
||||
TextView tvCustomerName, tvPetName, tvDate, tvEmployee, tvFee, tvStatus;
|
||||
final ItemAdoptionBinding binding;
|
||||
|
||||
public AdoptionViewHolder(@NonNull View v) {
|
||||
super(v);
|
||||
tvCustomerName = v.findViewById(R.id.tvAdoptionCustomerName);
|
||||
tvPetName = v.findViewById(R.id.tvAdoptionPetName);
|
||||
tvDate = v.findViewById(R.id.tvAdoptionDate);
|
||||
tvEmployee = v.findViewById(R.id.tvAdoptionEmployee);
|
||||
tvFee = v.findViewById(R.id.tvAdoptionFee);
|
||||
tvStatus = v.findViewById(R.id.tvAdoptionStatus);
|
||||
public AdoptionViewHolder(@NonNull ItemAdoptionBinding binding) {
|
||||
super(binding.getRoot());
|
||||
this.binding = binding;
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public AdoptionViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
View v = LayoutInflater.from(parent.getContext())
|
||||
.inflate(R.layout.item_adoption, parent, false);
|
||||
return new AdoptionViewHolder(v);
|
||||
ItemAdoptionBinding binding = ItemAdoptionBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false);
|
||||
return new AdoptionViewHolder(binding);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull AdoptionViewHolder holder, int position) {
|
||||
AdoptionDTO a = adoptionList.get(position);
|
||||
ItemAdoptionBinding binding = holder.binding;
|
||||
|
||||
binding.tvAdoptionCustomerName.setText(a.getCustomerName() != null ? a.getCustomerName() : "");
|
||||
binding.tvAdoptionPetName.setText("Pet: " + (a.getPetName() != null ? a.getPetName() : ""));
|
||||
binding.tvAdoptionStaffName.setText("Staff: " + (a.getEmployeeName() != null ? a.getEmployeeName() : "N/A"));
|
||||
binding.tvAdoptionDate.setText("Date: " + (a.getAdoptionDate() != null ? a.getAdoptionDate() : ""));
|
||||
binding.tvAdoptionFee.setText(a.getAdoptionFee() != null ? "$" + a.getAdoptionFee() : "");
|
||||
|
||||
|
||||
holder.tvCustomerName.setText(a.getCustomerName() != null ? a.getCustomerName() : "");
|
||||
holder.tvPetName.setText("Pet: " + (a.getPetName() != null ? a.getPetName() : ""));
|
||||
holder.tvDate.setText("Date: " + (a.getAdoptionDate() != null ? a.getAdoptionDate() : ""));
|
||||
holder.tvFee.setText(a.getAdoptionFee() != null ? "$" + a.getAdoptionFee() : "");
|
||||
holder.tvEmployee.setText("Staff: " +
|
||||
(a.getEmployeeName() != null ? a.getEmployeeName() : "Unassigned"));
|
||||
String status = a.getAdoptionStatus() != null ? a.getAdoptionStatus() : "";
|
||||
holder.tvStatus.setText(status);
|
||||
binding.tvAdoptionStatus.setText(status);
|
||||
|
||||
switch (status) {
|
||||
case "Approved":
|
||||
holder.tvStatus.setBackgroundColor(Color.parseColor("#4CAF50"));
|
||||
case "Completed":
|
||||
binding.tvAdoptionStatus.setBackgroundColor(Color.parseColor("#4CAF50"));
|
||||
break;
|
||||
case "Pending":
|
||||
holder.tvStatus.setBackgroundColor(Color.parseColor("#FF9800"));
|
||||
binding.tvAdoptionStatus.setBackgroundColor(Color.parseColor("#FF9800"));
|
||||
break;
|
||||
case "Rejected":
|
||||
holder.tvStatus.setBackgroundColor(Color.parseColor("#F44336"));
|
||||
case "Cancelled":
|
||||
binding.tvAdoptionStatus.setBackgroundColor(Color.parseColor("#F44336"));
|
||||
break;
|
||||
default:
|
||||
holder.tvStatus.setBackgroundColor(Color.parseColor("#9E9E9E"));
|
||||
binding.tvAdoptionStatus.setBackgroundColor(Color.parseColor("#9E9E9E"));
|
||||
break;
|
||||
}
|
||||
|
||||
holder.itemView.setOnClickListener(v -> listener.onAdoptionClick(position));
|
||||
String key = String.valueOf(a.getAdoptionId());
|
||||
|
||||
// Bulk delete selection mode
|
||||
if (selectionHelper.isInSelectionMode()) {
|
||||
binding.cbSelectAdoption.setVisibility(View.VISIBLE);
|
||||
binding.cbSelectAdoption.setChecked(selectionHelper.isSelected(key));
|
||||
} else {
|
||||
binding.cbSelectAdoption.setVisibility(View.GONE);
|
||||
binding.cbSelectAdoption.setChecked(false);
|
||||
}
|
||||
|
||||
holder.itemView.setOnClickListener(v -> {
|
||||
if (selectionHelper.isInSelectionMode()) {
|
||||
selectionHelper.toggleSelection(key);
|
||||
notifyItemChanged(position);
|
||||
} else {
|
||||
listener.onAdoptionClick(position);
|
||||
}
|
||||
});
|
||||
|
||||
holder.itemView.setOnLongClickListener(v -> {
|
||||
if (!selectionHelper.isInSelectionMode()) {
|
||||
selectionHelper.startSelection(key);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() { return adoptionList.size(); }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,79 +4,124 @@ import android.graphics.Color;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import com.example.petstoremobile.R;
|
||||
import com.example.petstoremobile.databinding.ItemAppointmentBinding;
|
||||
import com.example.petstoremobile.dtos.AppointmentDTO;
|
||||
import com.example.petstoremobile.utils.BulkDeleteHandler;
|
||||
import com.example.petstoremobile.utils.SelectionHelper;
|
||||
import java.util.List;
|
||||
|
||||
public class AppointmentAdapter extends RecyclerView.Adapter<AppointmentAdapter.AppointmentViewHolder> {
|
||||
public class AppointmentAdapter extends RecyclerView.Adapter<AppointmentAdapter.AppointmentViewHolder> implements BulkDeleteHandler.SelectableAdapter {
|
||||
|
||||
private List<AppointmentDTO> appointmentList;
|
||||
private OnAppointmentClickListener appointmentClickListener;
|
||||
private final SelectionHelper selectionHelper;
|
||||
|
||||
public interface OnAppointmentClickListener {
|
||||
void onAppointmentClick(int position);
|
||||
void onSelectionChanged(int count);
|
||||
}
|
||||
|
||||
public AppointmentAdapter(List<AppointmentDTO> appointmentList,
|
||||
OnAppointmentClickListener appointmentClickListener) {
|
||||
this.appointmentList = appointmentList;
|
||||
this.appointmentClickListener = appointmentClickListener;
|
||||
this.selectionHelper = new SelectionHelper(new SelectionHelper.SelectionListener() {
|
||||
@Override
|
||||
public void onSelectionChanged(int count) {
|
||||
appointmentClickListener.onSelectionChanged(count);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSelectionModeToggle(boolean selectionMode) {
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getSelectedKeys() {
|
||||
return selectionHelper.getSelectedKeys();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearSelection() {
|
||||
selectionHelper.clearSelection();
|
||||
}
|
||||
|
||||
public static class AppointmentViewHolder extends RecyclerView.ViewHolder {
|
||||
TextView tvCustomerName, tvPetName, tvServiceType, tvDateTime,tvEmployee, tvAppointmentStatus;
|
||||
private final ItemAppointmentBinding binding;
|
||||
|
||||
public AppointmentViewHolder(@NonNull View v) {
|
||||
super(v);
|
||||
tvCustomerName = v.findViewById(R.id.tvCustomerName);
|
||||
tvPetName = v.findViewById(R.id.tvApptPetName);
|
||||
tvServiceType = v.findViewById(R.id.tvServiceType);
|
||||
tvDateTime = v.findViewById(R.id.tvDateTime);
|
||||
|
||||
tvAppointmentStatus = v.findViewById(R.id.tvAppointmentStatus);
|
||||
public AppointmentViewHolder(@NonNull ItemAppointmentBinding binding) {
|
||||
super(binding.getRoot());
|
||||
this.binding = binding;
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public AppointmentViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_appointment, parent, false);
|
||||
return new AppointmentViewHolder(v);
|
||||
ItemAppointmentBinding binding = ItemAppointmentBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false);
|
||||
return new AppointmentViewHolder(binding);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull AppointmentViewHolder holder, int position) {
|
||||
AppointmentDTO a = appointmentList.get(position);
|
||||
ItemAppointmentBinding binding = holder.binding;
|
||||
|
||||
holder.tvCustomerName.setText(a.getCustomerName() != null ? a.getCustomerName() : "");
|
||||
holder.tvPetName.setText("Pet: " + (a.getPetName() != null ? a.getPetName() : ""));
|
||||
holder.tvServiceType.setText(a.getServiceType() != null ? a.getServiceType() : "");
|
||||
holder.tvDateTime.setText((a.getAppointmentDate() != null ? a.getAppointmentDate() : "") +
|
||||
binding.tvCustomerName.setText(a.getCustomerName() != null ? a.getCustomerName() : "");
|
||||
binding.tvApptPetName.setText("Pet: " + (a.getPetName() != null ? a.getPetName() : ""));
|
||||
binding.tvServiceType.setText(a.getServiceType() != null ? a.getServiceType() : "");
|
||||
binding.tvStaffName.setText("Staff: " + (a.getEmployeeName() != null ? a.getEmployeeName() : "Unassigned"));
|
||||
binding.tvDateTime.setText((a.getAppointmentDate() != null ? a.getAppointmentDate() : "") +
|
||||
" at " + (a.getAppointmentTime() != null ? a.getAppointmentTime() : ""));
|
||||
holder.tvEmployee.setText("Staff: " + (a.getEmployeeName() != null ? a.getEmployeeName() : ""));
|
||||
|
||||
String status = a.getStatus() != null ? a.getStatus() : "";
|
||||
holder.tvAppointmentStatus.setText(status);
|
||||
binding.tvAppointmentStatus.setText(status);
|
||||
|
||||
switch (status) {
|
||||
case "Booked":
|
||||
holder.tvAppointmentStatus.setBackgroundColor(Color.parseColor("#2196F3")); // blue
|
||||
switch (status.toUpperCase()) {
|
||||
case "BOOKED":
|
||||
binding.tvAppointmentStatus.setBackgroundColor(Color.parseColor("#2196F3")); // blue
|
||||
break;
|
||||
case "Completed":
|
||||
holder.tvAppointmentStatus.setBackgroundColor(Color.parseColor("#4CAF50")); // green
|
||||
case "COMPLETED":
|
||||
binding.tvAppointmentStatus.setBackgroundColor(Color.parseColor("#4CAF50")); // green
|
||||
break;
|
||||
case "Cancelled":
|
||||
holder.tvAppointmentStatus.setBackgroundColor(Color.parseColor("#F44336")); // red
|
||||
case "CANCELLED":
|
||||
binding.tvAppointmentStatus.setBackgroundColor(Color.parseColor("#F44336")); // red
|
||||
break;
|
||||
default:
|
||||
holder.tvAppointmentStatus.setBackgroundColor(Color.parseColor("#9E9E9E")); // gray
|
||||
binding.tvAppointmentStatus.setBackgroundColor(Color.parseColor("#9E9E9E")); // gray
|
||||
break;
|
||||
}
|
||||
|
||||
holder.itemView.setOnClickListener(v -> appointmentClickListener.onAppointmentClick(position));
|
||||
String key = String.valueOf(a.getAppointmentId());
|
||||
|
||||
// Bulk delete selection mode
|
||||
if (selectionHelper.isInSelectionMode()) {
|
||||
binding.cbSelectAppointment.setVisibility(View.VISIBLE);
|
||||
binding.cbSelectAppointment.setChecked(selectionHelper.isSelected(key));
|
||||
} else {
|
||||
binding.cbSelectAppointment.setVisibility(View.GONE);
|
||||
binding.cbSelectAppointment.setChecked(false);
|
||||
}
|
||||
|
||||
holder.itemView.setOnClickListener(v -> {
|
||||
if (selectionHelper.isInSelectionMode()) {
|
||||
selectionHelper.toggleSelection(key);
|
||||
notifyItemChanged(position);
|
||||
} else {
|
||||
appointmentClickListener.onAppointmentClick(position);
|
||||
}
|
||||
});
|
||||
|
||||
holder.itemView.setOnLongClickListener(v -> {
|
||||
if (!selectionHelper.isInSelectionMode()) {
|
||||
selectionHelper.startSelection(key);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
package com.example.petstoremobile.adapters;
|
||||
|
||||
import android.content.Context;
|
||||
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 changes based on theme
|
||||
public class BlackTextArrayAdapter<T> extends ArrayAdapter<T> {
|
||||
public BlackTextArrayAdapter(@NonNull Context context, int resource, @NonNull T[] objects) {
|
||||
super(context, resource, objects);
|
||||
}
|
||||
|
||||
public BlackTextArrayAdapter(@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(ContextCompat.getColor(getContext(), R.color.white));
|
||||
if (view instanceof TextView) {
|
||||
((TextView) view).setTextColor(ContextCompat.getColor(getContext(), R.color.spinner_text));
|
||||
}
|
||||
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.white));
|
||||
if (view instanceof TextView) {
|
||||
((TextView) view).setTextColor(ContextCompat.getColor(getContext(), R.color.spinner_text));
|
||||
}
|
||||
return view;
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,12 @@
|
||||
package com.example.petstoremobile.adapters;
|
||||
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.example.petstoremobile.R;
|
||||
import com.example.petstoremobile.databinding.ItemChatBinding;
|
||||
import com.example.petstoremobile.models.Chat;
|
||||
|
||||
import java.util.List;
|
||||
@@ -30,15 +28,15 @@ public class ChatAdapter extends RecyclerView.Adapter<ChatAdapter.ChatViewHolder
|
||||
@NonNull
|
||||
@Override
|
||||
public ChatViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_chat, parent, false);
|
||||
return new ChatViewHolder(view);
|
||||
ItemChatBinding binding = ItemChatBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false);
|
||||
return new ChatViewHolder(binding);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull ChatViewHolder holder, int position) {
|
||||
Chat chat = chatList.get(position);
|
||||
holder.tvCustomerName.setText(chat.getCustomerName());
|
||||
holder.tvLastMessage.setText(chat.getLastMessage());
|
||||
holder.binding.tvCustomerName.setText(chat.getCustomerName());
|
||||
holder.binding.tvLastMessage.setText(chat.getLastMessage());
|
||||
holder.itemView.setOnClickListener(v -> listener.onChatClick(chat));
|
||||
}
|
||||
|
||||
@@ -48,12 +46,11 @@ public class ChatAdapter extends RecyclerView.Adapter<ChatAdapter.ChatViewHolder
|
||||
}
|
||||
|
||||
public static class ChatViewHolder extends RecyclerView.ViewHolder {
|
||||
TextView tvCustomerName, tvLastMessage;
|
||||
final ItemChatBinding binding;
|
||||
|
||||
public ChatViewHolder(@NonNull View itemView) {
|
||||
super(itemView);
|
||||
tvCustomerName = itemView.findViewById(R.id.tvCustomerName);
|
||||
tvLastMessage = itemView.findViewById(R.id.tvLastMessage);
|
||||
public ChatViewHolder(@NonNull ItemChatBinding binding) {
|
||||
super(binding.getRoot());
|
||||
this.binding = binding;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,24 +4,22 @@ import android.graphics.Color;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.CheckBox;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.example.petstoremobile.R;
|
||||
import com.example.petstoremobile.databinding.ItemInventoryBinding;
|
||||
import com.example.petstoremobile.dtos.InventoryDTO;
|
||||
import com.example.petstoremobile.utils.BulkDeleteHandler;
|
||||
import com.example.petstoremobile.utils.SelectionHelper;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class InventoryAdapter extends RecyclerView.Adapter<InventoryAdapter.InventoryViewHolder> {
|
||||
public class InventoryAdapter extends RecyclerView.Adapter<InventoryAdapter.InventoryViewHolder> implements BulkDeleteHandler.SelectableAdapter {
|
||||
|
||||
private final List<InventoryDTO> inventoryList;
|
||||
private final OnInventoryClickListener clickListener;
|
||||
private final List<Long> selectedIds = new ArrayList<>();
|
||||
private boolean selectionMode = false;
|
||||
private final SelectionHelper selectionHelper;
|
||||
|
||||
public interface OnInventoryClickListener {
|
||||
void onInventoryClick(int position);
|
||||
@@ -32,117 +30,97 @@ public class InventoryAdapter extends RecyclerView.Adapter<InventoryAdapter.Inve
|
||||
public InventoryAdapter(List<InventoryDTO> inventoryList, OnInventoryClickListener clickListener) {
|
||||
this.inventoryList = inventoryList;
|
||||
this.clickListener = clickListener;
|
||||
this.selectionHelper = new SelectionHelper(new SelectionHelper.SelectionListener() {
|
||||
@Override
|
||||
public void onSelectionChanged(int count) {
|
||||
clickListener.onSelectionChanged(count);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSelectionModeToggle(boolean selectionMode) {
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getSelectedKeys() {
|
||||
return selectionHelper.getSelectedKeys();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearSelection() {
|
||||
selectionHelper.clearSelection();
|
||||
}
|
||||
|
||||
public static class InventoryViewHolder extends RecyclerView.ViewHolder {
|
||||
// Matches desktop table columns: Inventory ID, Product ID, Product Name,
|
||||
// Quantity
|
||||
TextView tvInventoryId, tvProdId, tvProductName, tvQuantity;
|
||||
CheckBox checkBox;
|
||||
final ItemInventoryBinding binding;
|
||||
|
||||
public InventoryViewHolder(@NonNull View v) {
|
||||
super(v);
|
||||
tvInventoryId = v.findViewById(R.id.tvInventoryId);
|
||||
tvProdId = v.findViewById(R.id.tvProdId);
|
||||
tvProductName = v.findViewById(R.id.tvProductName);
|
||||
tvQuantity = v.findViewById(R.id.tvQuantity);
|
||||
checkBox = v.findViewById(R.id.cbSelectInventory);
|
||||
public InventoryViewHolder(@NonNull ItemInventoryBinding binding) {
|
||||
super(binding.getRoot());
|
||||
this.binding = binding;
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public InventoryViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
View v = LayoutInflater.from(parent.getContext())
|
||||
.inflate(R.layout.item_inventory, parent, false);
|
||||
return new InventoryViewHolder(v);
|
||||
ItemInventoryBinding binding = ItemInventoryBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false);
|
||||
return new InventoryViewHolder(binding);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull InventoryViewHolder holder, int position) {
|
||||
InventoryDTO inv = inventoryList.get(position);
|
||||
|
||||
// Column: Inventory ID
|
||||
holder.tvInventoryId.setText(String.valueOf(inv.getInventoryId() != null ? inv.getInventoryId() : "—"));
|
||||
|
||||
// Column: Product ID
|
||||
holder.tvProdId.setText(String.valueOf(inv.getProdId() != null ? inv.getProdId() : "—"));
|
||||
ItemInventoryBinding binding = holder.binding;
|
||||
|
||||
// Column: Product Name
|
||||
holder.tvProductName.setText(inv.getProductName() != null ? inv.getProductName() : "—");
|
||||
binding.tvProductName.setText(inv.getProductName() != null ? inv.getProductName() : "—");
|
||||
|
||||
// Column: Store Name
|
||||
binding.tvInventoryStore.setText("Store: " + (inv.getStoreName() != null ? inv.getStoreName() : "—"));
|
||||
|
||||
// Column: Quantity
|
||||
int qty = inv.getQuantity() != null ? inv.getQuantity() : 0;
|
||||
holder.tvQuantity.setText(String.valueOf(qty));
|
||||
binding.tvQuantity.setText("Stock: " + qty);
|
||||
|
||||
// Low stock = red, normal = green (like desktop reorder concept)
|
||||
if (qty <= 5) {
|
||||
holder.tvQuantity.setTextColor(Color.parseColor("#F44336"));
|
||||
binding.tvQuantity.setTextColor(Color.parseColor("#F44336"));
|
||||
} else {
|
||||
holder.tvQuantity.setTextColor(Color.parseColor("#4CAF50"));
|
||||
binding.tvQuantity.setTextColor(Color.parseColor("#4CAF50"));
|
||||
}
|
||||
|
||||
String key = String.valueOf(inv.getInventoryId());
|
||||
|
||||
// Bulk delete selection mode
|
||||
if (selectionMode) {
|
||||
holder.checkBox.setVisibility(View.VISIBLE);
|
||||
holder.checkBox.setChecked(inv.getInventoryId() != null
|
||||
&& selectedIds.contains(inv.getInventoryId()));
|
||||
if (selectionHelper.isInSelectionMode()) {
|
||||
binding.cbSelectInventory.setVisibility(View.VISIBLE);
|
||||
binding.cbSelectInventory.setChecked(selectionHelper.isSelected(key));
|
||||
} else {
|
||||
holder.checkBox.setVisibility(View.GONE);
|
||||
holder.checkBox.setChecked(false);
|
||||
binding.cbSelectInventory.setVisibility(View.GONE);
|
||||
binding.cbSelectInventory.setChecked(false);
|
||||
}
|
||||
|
||||
holder.itemView.setOnClickListener(v -> {
|
||||
if (selectionMode) {
|
||||
toggleSelection(inv.getInventoryId(), holder.checkBox);
|
||||
if (selectionHelper.isInSelectionMode()) {
|
||||
selectionHelper.toggleSelection(key);
|
||||
notifyItemChanged(position);
|
||||
} else {
|
||||
clickListener.onInventoryClick(holder.getAdapterPosition());
|
||||
}
|
||||
});
|
||||
|
||||
holder.itemView.setOnLongClickListener(v -> {
|
||||
if (!selectionMode) {
|
||||
selectionMode = true;
|
||||
toggleSelection(inv.getInventoryId(), holder.checkBox);
|
||||
notifyDataSetChanged();
|
||||
if (!selectionHelper.isInSelectionMode()) {
|
||||
selectionHelper.startSelection(key);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
private void toggleSelection(Long id, CheckBox checkBox) {
|
||||
if (id == null)
|
||||
return;
|
||||
if (selectedIds.contains(id)) {
|
||||
selectedIds.remove(id);
|
||||
checkBox.setChecked(false);
|
||||
} else {
|
||||
selectedIds.add(id);
|
||||
checkBox.setChecked(true);
|
||||
}
|
||||
clickListener.onSelectionChanged(selectedIds.size());
|
||||
if (selectedIds.isEmpty()) {
|
||||
selectionMode = false;
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public List<Long> getSelectedIds() {
|
||||
return new ArrayList<>(selectedIds);
|
||||
}
|
||||
|
||||
public void clearSelection() {
|
||||
selectedIds.clear();
|
||||
selectionMode = false;
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
public boolean isInSelectionMode() {
|
||||
return selectionMode;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return inventoryList.size();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,10 +3,17 @@ package com.example.petstoremobile.adapters;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import com.bumptech.glide.Glide;
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy;
|
||||
import com.bumptech.glide.load.model.GlideUrl;
|
||||
import com.bumptech.glide.load.model.LazyHeaders;
|
||||
import com.example.petstoremobile.R;
|
||||
import com.example.petstoremobile.databinding.ItemMessageReceivedBinding;
|
||||
import com.example.petstoremobile.databinding.ItemMessageSentBinding;
|
||||
import com.example.petstoremobile.models.Message;
|
||||
import java.util.List;
|
||||
|
||||
@@ -17,6 +24,7 @@ public class MessageAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder
|
||||
|
||||
private final List<Message> messages;
|
||||
private Long currentUserId;
|
||||
private String token;
|
||||
|
||||
public MessageAdapter(List<Message> messages, Long currentUserId) {
|
||||
this.messages = messages;
|
||||
@@ -28,6 +36,10 @@ public class MessageAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
public void setToken(String token) {
|
||||
this.token = token;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemViewType(int position) {
|
||||
Message m = messages.get(position);
|
||||
@@ -41,38 +53,75 @@ public class MessageAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder
|
||||
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
LayoutInflater inf = LayoutInflater.from(parent.getContext());
|
||||
if (viewType == TYPE_SENT) {
|
||||
View v = inf.inflate(R.layout.item_message_sent, parent, false);
|
||||
return new SentHolder(v);
|
||||
ItemMessageSentBinding binding = ItemMessageSentBinding.inflate(inf, parent, false);
|
||||
return new SentHolder(binding);
|
||||
} else {
|
||||
View v = inf.inflate(R.layout.item_message_received, parent, false);
|
||||
return new ReceivedHolder(v);
|
||||
ItemMessageReceivedBinding binding = ItemMessageReceivedBinding.inflate(inf, parent, false);
|
||||
return new ReceivedHolder(binding);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
|
||||
Message m = messages.get(position);
|
||||
if (holder instanceof SentHolder) ((SentHolder) holder).bind(m);
|
||||
if (holder instanceof ReceivedHolder) ((ReceivedHolder) holder).bind(m);
|
||||
if (holder instanceof SentHolder) ((SentHolder) holder).bind(m, token);
|
||||
if (holder instanceof ReceivedHolder) ((ReceivedHolder) holder).bind(m, token);
|
||||
}
|
||||
|
||||
@Override public int getItemCount() { return messages.size(); }
|
||||
|
||||
static class SentHolder extends RecyclerView.ViewHolder {
|
||||
TextView tvMessage;
|
||||
SentHolder(View v) {
|
||||
super(v);
|
||||
tvMessage = v.findViewById(R.id.tvMessageContent); // updated
|
||||
final ItemMessageSentBinding binding;
|
||||
SentHolder(ItemMessageSentBinding binding) {
|
||||
super(binding.getRoot());
|
||||
this.binding = binding;
|
||||
}
|
||||
void bind(Message m, String token) {
|
||||
binding.tvMessageContent.setText(m.getContent());
|
||||
displayAttachment(m, binding.ivAttachment, binding.tvAttachmentName, token);
|
||||
}
|
||||
void bind(Message m) { tvMessage.setText(m.getContent()); }
|
||||
}
|
||||
|
||||
static class ReceivedHolder extends RecyclerView.ViewHolder {
|
||||
TextView tvMessage;
|
||||
ReceivedHolder(View v) {
|
||||
super(v);
|
||||
tvMessage = v.findViewById(R.id.tvMessageContent); // updated
|
||||
final ItemMessageReceivedBinding binding;
|
||||
ReceivedHolder(ItemMessageReceivedBinding binding) {
|
||||
super(binding.getRoot());
|
||||
this.binding = binding;
|
||||
}
|
||||
void bind(Message m, String token) {
|
||||
binding.tvMessageContent.setText(m.getContent());
|
||||
displayAttachment(m, binding.ivAttachment, binding.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()); }
|
||||
}
|
||||
}
|
||||
@@ -4,40 +4,75 @@ import android.graphics.Color;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.example.petstoremobile.R;
|
||||
import com.example.petstoremobile.api.PetApi;
|
||||
import com.example.petstoremobile.databinding.ItemPetBinding;
|
||||
import com.example.petstoremobile.dtos.PetDTO;
|
||||
import com.example.petstoremobile.utils.BulkDeleteHandler;
|
||||
import com.example.petstoremobile.utils.GlideUtils;
|
||||
import com.example.petstoremobile.utils.SelectionHelper;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class PetAdapter extends RecyclerView.Adapter<PetAdapter.PetViewHolder> {
|
||||
public class PetAdapter extends RecyclerView.Adapter<PetAdapter.PetViewHolder> implements BulkDeleteHandler.SelectableAdapter {
|
||||
|
||||
private List<PetDTO> petList;
|
||||
private OnPetClickListener petClickListener;
|
||||
private String baseUrl;
|
||||
private String token;
|
||||
private final SelectionHelper selectionHelper;
|
||||
|
||||
// Interface for pet click on recycler view
|
||||
public interface OnPetClickListener {
|
||||
void onPetClick(int position);
|
||||
void onSelectionChanged(int selectedCount);
|
||||
}
|
||||
|
||||
//Constructor
|
||||
public PetAdapter(List<PetDTO> petList, OnPetClickListener petClickListener) {
|
||||
this.petList = petList;
|
||||
this.petClickListener = petClickListener;
|
||||
this.selectionHelper = new SelectionHelper(new SelectionHelper.SelectionListener() {
|
||||
@Override
|
||||
public void onSelectionChanged(int count) {
|
||||
petClickListener.onSelectionChanged(count);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSelectionModeToggle(boolean selectionMode) {
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void setBaseUrl(String baseUrl) {
|
||||
this.baseUrl = baseUrl;
|
||||
}
|
||||
|
||||
public void setToken(String token) {
|
||||
this.token = token;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getSelectedKeys() {
|
||||
return selectionHelper.getSelectedKeys();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearSelection() {
|
||||
selectionHelper.clearSelection();
|
||||
}
|
||||
|
||||
// Get the controls of each row in recycler view
|
||||
public static class PetViewHolder extends RecyclerView.ViewHolder {
|
||||
TextView tvPetName, tvPetSpeciesBreed, tvPetAge, tvPetPrice, tvPetStatus;
|
||||
private final ItemPetBinding binding;
|
||||
|
||||
public PetViewHolder(@NonNull View v) {
|
||||
super(v);
|
||||
tvPetName = v.findViewById(R.id.tvPetName);
|
||||
tvPetSpeciesBreed = v.findViewById(R.id.tvPetSpeciesBreed);
|
||||
tvPetAge = v.findViewById(R.id.tvPetAge);
|
||||
tvPetPrice = v.findViewById(R.id.tvPetPrice);
|
||||
tvPetStatus = v.findViewById(R.id.tvPetStatus);
|
||||
public PetViewHolder(@NonNull ItemPetBinding binding) {
|
||||
super(binding.getRoot());
|
||||
this.binding = binding;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,41 +80,75 @@ public class PetAdapter extends RecyclerView.Adapter<PetAdapter.PetViewHolder> {
|
||||
@NonNull
|
||||
@Override
|
||||
public PetViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_pet, parent, false);
|
||||
return new PetViewHolder(v);
|
||||
ItemPetBinding binding = ItemPetBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false);
|
||||
return new PetViewHolder(binding);
|
||||
}
|
||||
|
||||
//populate the row with pet data
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull PetViewHolder holder, int position) {
|
||||
PetDTO pet = petList.get(position);
|
||||
ItemPetBinding binding = holder.binding;
|
||||
|
||||
holder.tvPetName.setText(pet.getPetName());
|
||||
holder.tvPetSpeciesBreed.setText(pet.getPetSpecies() + " - " + pet.getPetBreed());
|
||||
holder.tvPetAge.setText("Age: " + pet.getPetAge() + " yr(s)");
|
||||
binding.tvPetName.setText(pet.getPetName());
|
||||
binding.tvPetSpeciesBreed.setText(pet.getPetSpecies() + " - " + pet.getPetBreed());
|
||||
binding.tvPetAge.setText("Age: " + pet.getPetAge() + " yr(s)");
|
||||
|
||||
try {
|
||||
double price = Double.parseDouble(pet.getPetPrice());
|
||||
holder.tvPetPrice.setText("$" + String.format("%.2f", price));
|
||||
} catch (Exception e) {
|
||||
holder.tvPetPrice.setText("$" + pet.getPetPrice());
|
||||
Double price = pet.getPetPrice();
|
||||
if (price != null) {
|
||||
binding.tvPetPrice.setText("$" + String.format("%.2f", price));
|
||||
} else {
|
||||
binding.tvPetPrice.setText("$0.00");
|
||||
}
|
||||
|
||||
holder.tvPetStatus.setText(pet.getPetStatus());
|
||||
binding.tvPetStatus.setText(pet.getPetStatus());
|
||||
|
||||
//Set the status color depending on availability. If available, green, otherwise red
|
||||
if (pet.getPetStatus() != null && pet.getPetStatus().equals("Available")) {
|
||||
holder.tvPetStatus.setBackgroundColor(Color.parseColor("#4CAF50"));
|
||||
binding.tvPetStatus.setBackgroundColor(Color.parseColor("#4CAF50"));
|
||||
} else {
|
||||
holder.tvPetStatus.setBackgroundColor(Color.parseColor("#F44336"));
|
||||
binding.tvPetStatus.setBackgroundColor(Color.parseColor("#F44336"));
|
||||
}
|
||||
|
||||
// Load pet image using Glide
|
||||
if (baseUrl != null) {
|
||||
String imageUrl = baseUrl + String.format(PetApi.PET_IMAGE_PATH, pet.getPetId());
|
||||
GlideUtils.loadImageWithTokenCircle(holder.itemView.getContext(), binding.ivPetProfile, imageUrl, token, R.drawable.placeholder);
|
||||
} else {
|
||||
binding.ivPetProfile.setImageResource(R.drawable.placeholder);
|
||||
}
|
||||
|
||||
String key = String.valueOf(pet.getPetId());
|
||||
|
||||
// Bulk delete selection mode
|
||||
if (selectionHelper.isInSelectionMode()) {
|
||||
binding.cbSelectPet.setVisibility(View.VISIBLE);
|
||||
binding.cbSelectPet.setChecked(selectionHelper.isSelected(key));
|
||||
} else {
|
||||
binding.cbSelectPet.setVisibility(View.GONE);
|
||||
binding.cbSelectPet.setChecked(false);
|
||||
}
|
||||
|
||||
//when a row is clicked, open the detail view
|
||||
holder.itemView.setOnClickListener(v -> petClickListener.onPetClick(position));
|
||||
holder.itemView.setOnClickListener(v -> {
|
||||
if (selectionHelper.isInSelectionMode()) {
|
||||
selectionHelper.toggleSelection(key);
|
||||
notifyItemChanged(position);
|
||||
} else {
|
||||
petClickListener.onPetClick(position);
|
||||
}
|
||||
});
|
||||
|
||||
holder.itemView.setOnLongClickListener(v -> {
|
||||
if (!selectionHelper.isInSelectionMode()) {
|
||||
selectionHelper.startSelection(key);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return petList.size();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,22 @@
|
||||
package com.example.petstoremobile.adapters;
|
||||
|
||||
import android.view.*;
|
||||
import android.widget.TextView;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.example.petstoremobile.R;
|
||||
import com.example.petstoremobile.api.ProductApi;
|
||||
import com.example.petstoremobile.databinding.ItemProductBinding;
|
||||
import com.example.petstoremobile.dtos.ProductDTO;
|
||||
import com.example.petstoremobile.utils.GlideUtils;
|
||||
import java.util.List;
|
||||
|
||||
public class ProductAdapter extends RecyclerView.Adapter<ProductAdapter.ProductViewHolder> {
|
||||
|
||||
private List<ProductDTO> productList;
|
||||
private OnProductClickListener listener;
|
||||
private String baseUrl;
|
||||
private String token;
|
||||
|
||||
public interface OnProductClickListener {
|
||||
void onProductClick(int position);
|
||||
@@ -22,33 +27,48 @@ public class ProductAdapter extends RecyclerView.Adapter<ProductAdapter.ProductV
|
||||
this.listener = listener;
|
||||
}
|
||||
|
||||
public static class ProductViewHolder extends RecyclerView.ViewHolder {
|
||||
TextView tvName, tvCategory, tvDesc, tvPrice;
|
||||
public void setBaseUrl(String baseUrl) {
|
||||
this.baseUrl = baseUrl;
|
||||
}
|
||||
|
||||
public ProductViewHolder(@NonNull View v) {
|
||||
super(v);
|
||||
tvName = v.findViewById(R.id.tvProductName);
|
||||
tvCategory = v.findViewById(R.id.tvProductCategory);
|
||||
tvDesc = v.findViewById(R.id.tvProductDesc);
|
||||
tvPrice = v.findViewById(R.id.tvProductPrice);
|
||||
public void setToken(String token) {
|
||||
this.token = token;
|
||||
}
|
||||
|
||||
public static class ProductViewHolder extends RecyclerView.ViewHolder {
|
||||
final ItemProductBinding binding;
|
||||
|
||||
public ProductViewHolder(@NonNull ItemProductBinding binding) {
|
||||
super(binding.getRoot());
|
||||
this.binding = binding;
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public ProductViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
View v = LayoutInflater.from(parent.getContext())
|
||||
.inflate(R.layout.item_product, parent, false);
|
||||
return new ProductViewHolder(v);
|
||||
ItemProductBinding binding = ItemProductBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false);
|
||||
return new ProductViewHolder(binding);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull ProductViewHolder holder, int position) {
|
||||
ProductDTO p = productList.get(position);
|
||||
holder.tvName.setText(p.getProdName() != null ? p.getProdName() : "");
|
||||
holder.tvCategory.setText("Category: " + (p.getCategoryName() != null ? p.getCategoryName() : ""));
|
||||
holder.tvDesc.setText(p.getProdDesc() != null ? p.getProdDesc() : "");
|
||||
holder.tvPrice.setText(p.getProdPrice() != null ? "$" + p.getProdPrice() : "");
|
||||
ItemProductBinding binding = holder.binding;
|
||||
|
||||
binding.tvProductName.setText(p.getProdName() != null ? p.getProdName() : "");
|
||||
binding.tvProductCategory.setText("Category: " + (p.getCategoryName() != null ? p.getCategoryName() : ""));
|
||||
binding.tvProductDesc.setText(p.getProdDesc() != null ? p.getProdDesc() : "");
|
||||
binding.tvProductPrice.setText(p.getProdPrice() != null ? "$" + p.getProdPrice() : "");
|
||||
|
||||
// Load product image using Glide
|
||||
if (baseUrl != null) {
|
||||
String imageUrl = baseUrl + String.format(ProductApi.PRODUCT_IMAGE_PATH, p.getProdId());
|
||||
GlideUtils.loadImageWithTokenCircle(holder.itemView.getContext(), binding.ivProductImage, imageUrl, token, R.drawable.placeholder);
|
||||
} else {
|
||||
binding.ivProductImage.setImageResource(R.drawable.placeholder);
|
||||
}
|
||||
|
||||
holder.itemView.setOnClickListener(v -> listener.onProductClick(position));
|
||||
}
|
||||
|
||||
|
||||
@@ -1,55 +1,109 @@
|
||||
package com.example.petstoremobile.adapters;
|
||||
|
||||
import android.view.*;
|
||||
import android.widget.TextView;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import com.example.petstoremobile.R;
|
||||
|
||||
import com.example.petstoremobile.databinding.ItemProductSupplierBinding;
|
||||
import com.example.petstoremobile.dtos.ProductSupplierDTO;
|
||||
import com.example.petstoremobile.utils.BulkDeleteHandler;
|
||||
import com.example.petstoremobile.utils.SelectionHelper;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class ProductSupplierAdapter extends RecyclerView.Adapter<ProductSupplierAdapter.PSViewHolder> {
|
||||
public class ProductSupplierAdapter extends RecyclerView.Adapter<ProductSupplierAdapter.PSViewHolder> implements BulkDeleteHandler.SelectableAdapter {
|
||||
|
||||
private List<ProductSupplierDTO> list;
|
||||
private OnProductSupplierClickListener listener;
|
||||
private final List<ProductSupplierDTO> list;
|
||||
private final OnProductSupplierClickListener listener;
|
||||
private final SelectionHelper selectionHelper;
|
||||
|
||||
public interface OnProductSupplierClickListener {
|
||||
void onProductSupplierClick(int position);
|
||||
void onSelectionChanged(int count);
|
||||
}
|
||||
|
||||
public ProductSupplierAdapter(List<ProductSupplierDTO> list, OnProductSupplierClickListener listener) {
|
||||
this.list = list;
|
||||
this.listener = listener;
|
||||
this.selectionHelper = new SelectionHelper(new SelectionHelper.SelectionListener() {
|
||||
@Override
|
||||
public void onSelectionChanged(int count) {
|
||||
listener.onSelectionChanged(count);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSelectionModeToggle(boolean selectionMode) {
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getSelectedKeys() {
|
||||
return selectionHelper.getSelectedKeys();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearSelection() {
|
||||
selectionHelper.clearSelection();
|
||||
}
|
||||
|
||||
public static class PSViewHolder extends RecyclerView.ViewHolder {
|
||||
TextView tvProductName, tvSupplierName, tvCost;
|
||||
final ItemProductSupplierBinding binding;
|
||||
|
||||
public PSViewHolder(@NonNull View v) {
|
||||
super(v);
|
||||
tvProductName = v.findViewById(R.id.tvPSProductName);
|
||||
tvSupplierName = v.findViewById(R.id.tvPSSupplierName);
|
||||
tvCost = v.findViewById(R.id.tvPSCost);
|
||||
public PSViewHolder(@NonNull ItemProductSupplierBinding binding) {
|
||||
super(binding.getRoot());
|
||||
this.binding = binding;
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public PSViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
View v = LayoutInflater.from(parent.getContext())
|
||||
.inflate(R.layout.item_product_supplier, parent, false);
|
||||
return new PSViewHolder(v);
|
||||
ItemProductSupplierBinding binding = ItemProductSupplierBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false);
|
||||
return new PSViewHolder(binding);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull PSViewHolder holder, int position) {
|
||||
ProductSupplierDTO ps = list.get(position);
|
||||
holder.tvProductName.setText(ps.getProductName() != null ? ps.getProductName() : "");
|
||||
holder.tvSupplierName.setText("Supplier: " + (ps.getSupplierName() != null ? ps.getSupplierName() : ""));
|
||||
holder.tvCost.setText(ps.getCost() != null ? "Cost: $" + ps.getCost() : "");
|
||||
holder.itemView.setOnClickListener(v -> listener.onProductSupplierClick(position));
|
||||
ItemProductSupplierBinding binding = holder.binding;
|
||||
|
||||
binding.tvPSProductName.setText(ps.getProductName() != null ? ps.getProductName() : "");
|
||||
binding.tvPSSupplierName.setText("Supplier: " + (ps.getSupplierName() != null ? ps.getSupplierName() : ""));
|
||||
binding.tvPSCost.setText(ps.getCost() != null ? "Cost: $" + ps.getCost() : "");
|
||||
|
||||
String key = ps.getProductId() + "-" + ps.getSupplierId();
|
||||
|
||||
// Bulk delete selection mode
|
||||
if (selectionHelper.isInSelectionMode()) {
|
||||
binding.cbSelectProductSupplier.setVisibility(View.VISIBLE);
|
||||
binding.cbSelectProductSupplier.setChecked(selectionHelper.isSelected(key));
|
||||
} else {
|
||||
binding.cbSelectProductSupplier.setVisibility(View.GONE);
|
||||
binding.cbSelectProductSupplier.setChecked(false);
|
||||
}
|
||||
|
||||
holder.itemView.setOnClickListener(v -> {
|
||||
if (selectionHelper.isInSelectionMode()) {
|
||||
selectionHelper.toggleSelection(key);
|
||||
notifyItemChanged(position);
|
||||
} else {
|
||||
listener.onProductSupplierClick(position);
|
||||
}
|
||||
});
|
||||
|
||||
holder.itemView.setOnLongClickListener(v -> {
|
||||
if (!selectionHelper.isInSelectionMode()) {
|
||||
selectionHelper.startSelection(key);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() { return list.size(); }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
package com.example.petstoremobile.adapters;
|
||||
|
||||
import android.graphics.Color;
|
||||
import android.view.*;
|
||||
import android.widget.TextView;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.ViewGroup;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import com.example.petstoremobile.R;
|
||||
import com.example.petstoremobile.databinding.ItemPurchaseOrderBinding;
|
||||
import com.example.petstoremobile.dtos.PurchaseOrderDTO;
|
||||
import java.util.List;
|
||||
|
||||
@@ -24,47 +24,49 @@ public class PurchaseOrderAdapter extends RecyclerView.Adapter<PurchaseOrderAdap
|
||||
}
|
||||
|
||||
public static class POViewHolder extends RecyclerView.ViewHolder {
|
||||
TextView tvId, tvSupplier, tvDate, tvStatus;
|
||||
final ItemPurchaseOrderBinding binding;
|
||||
|
||||
public POViewHolder(@NonNull View v) {
|
||||
super(v);
|
||||
tvId = v.findViewById(R.id.tvPOId);
|
||||
tvSupplier = v.findViewById(R.id.tvPOSupplier);
|
||||
tvDate = v.findViewById(R.id.tvPODate);
|
||||
tvStatus = v.findViewById(R.id.tvPOStatus);
|
||||
public POViewHolder(@NonNull ItemPurchaseOrderBinding binding) {
|
||||
super(binding.getRoot());
|
||||
this.binding = binding;
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public POViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
View v = LayoutInflater.from(parent.getContext())
|
||||
.inflate(R.layout.item_purchase_order, parent, false);
|
||||
return new POViewHolder(v);
|
||||
ItemPurchaseOrderBinding binding = ItemPurchaseOrderBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false);
|
||||
return new POViewHolder(binding);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull POViewHolder holder, int position) {
|
||||
PurchaseOrderDTO po = list.get(position);
|
||||
holder.tvId.setText("PO #" + (po.getPurchaseOrderId() != null ? po.getPurchaseOrderId() : ""));
|
||||
holder.tvSupplier.setText("Supplier: " + (po.getSupplierName() != null ? po.getSupplierName() : ""));
|
||||
holder.tvDate.setText("Date: " + (po.getOrderDate() != null ? po.getOrderDate() : ""));
|
||||
ItemPurchaseOrderBinding binding = holder.binding;
|
||||
|
||||
binding.tvPOId.setText("PO #" + (po.getPurchaseOrderId() != null ? po.getPurchaseOrderId() : ""));
|
||||
binding.tvPOSupplier.setText("Supplier: " + (po.getSupplierName() != null ? po.getSupplierName() : ""));
|
||||
binding.tvPOStore.setText("Store: " + (po.getStoreName() != null ? po.getStoreName() : ""));
|
||||
binding.tvPODate.setText("Date: " + (po.getOrderDate() != null ? po.getOrderDate() : ""));
|
||||
|
||||
String status = po.getStatus() != null ? po.getStatus() : "";
|
||||
holder.tvStatus.setText(status);
|
||||
binding.tvPOStatus.setText(status);
|
||||
|
||||
switch (status) {
|
||||
case "Completed":
|
||||
holder.tvStatus.setBackgroundColor(Color.parseColor("#4CAF50"));
|
||||
switch (status.toUpperCase()) {
|
||||
case "RECEIVED":
|
||||
binding.tvPOStatus.setBackgroundColor(Color.parseColor("#4CAF50"));
|
||||
break;
|
||||
case "Pending":
|
||||
holder.tvStatus.setBackgroundColor(Color.parseColor("#FF9800"));
|
||||
case "PLACED":
|
||||
binding.tvPOStatus.setBackgroundColor(Color.parseColor("#2196F3"));
|
||||
break;
|
||||
case "Cancelled":
|
||||
holder.tvStatus.setBackgroundColor(Color.parseColor("#F44336"));
|
||||
case "PENDING":
|
||||
binding.tvPOStatus.setBackgroundColor(Color.parseColor("#FF9800"));
|
||||
break;
|
||||
case "CANCELLED":
|
||||
binding.tvPOStatus.setBackgroundColor(Color.parseColor("#F44336"));
|
||||
break;
|
||||
default:
|
||||
holder.tvStatus.setBackgroundColor(Color.parseColor("#9E9E9E"));
|
||||
binding.tvPOStatus.setBackgroundColor(Color.parseColor("#9E9E9E"));
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
package com.example.petstoremobile.adapters;
|
||||
|
||||
import android.view.*;
|
||||
import android.widget.TextView;
|
||||
import android.graphics.Color;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import com.example.petstoremobile.R;
|
||||
import com.example.petstoremobile.databinding.ItemSaleBinding;
|
||||
import com.example.petstoremobile.dtos.SaleDTO;
|
||||
import java.util.List;
|
||||
|
||||
@@ -23,41 +25,39 @@ public class SaleAdapter extends RecyclerView.Adapter<SaleAdapter.SaleViewHolder
|
||||
}
|
||||
|
||||
public static class SaleViewHolder extends RecyclerView.ViewHolder {
|
||||
TextView tvId, tvEmployee, tvDate, tvPayment, tvTotal, tvRefundBadge;
|
||||
final ItemSaleBinding binding;
|
||||
|
||||
public SaleViewHolder(@NonNull View v) {
|
||||
super(v);
|
||||
tvId = v.findViewById(R.id.tvSaleId);
|
||||
tvEmployee = v.findViewById(R.id.tvSaleEmployee);
|
||||
tvDate = v.findViewById(R.id.tvSaleDate);
|
||||
tvPayment = v.findViewById(R.id.tvSalePayment);
|
||||
tvTotal = v.findViewById(R.id.tvSaleTotal);
|
||||
tvRefundBadge = v.findViewById(R.id.tvSaleRefundBadge);
|
||||
public SaleViewHolder(@NonNull ItemSaleBinding binding) {
|
||||
super(binding.getRoot());
|
||||
this.binding = binding;
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public SaleViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
View v = LayoutInflater.from(parent.getContext())
|
||||
.inflate(R.layout.item_sale, parent, false);
|
||||
return new SaleViewHolder(v);
|
||||
ItemSaleBinding binding = ItemSaleBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false);
|
||||
return new SaleViewHolder(binding);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull SaleViewHolder holder, int position) {
|
||||
SaleDTO s = saleList.get(position);
|
||||
holder.tvId.setText("Sale #" + (s.getSaleId() != null ? s.getSaleId() : ""));
|
||||
holder.tvEmployee.setText("By: " + (s.getEmployeeName() != null ? s.getEmployeeName() : ""));
|
||||
holder.tvDate.setText(s.getSaleDate() != null ? s.getSaleDate().substring(0, 10) : "");
|
||||
holder.tvPayment.setText(s.getPaymentMethod() != null ? s.getPaymentMethod() : "");
|
||||
holder.tvTotal.setText(s.getTotalAmount() != null ? "$" + s.getTotalAmount() : "");
|
||||
ItemSaleBinding binding = holder.binding;
|
||||
|
||||
binding.tvSaleId.setText("Sale #" + (s.getSaleId() != null ? s.getSaleId() : ""));
|
||||
binding.tvSaleEmployee.setText("By: " + (s.getEmployeeName() != null ? s.getEmployeeName() : ""));
|
||||
binding.tvSaleDate.setText(s.getSaleDate() != null ? s.getSaleDate().substring(0, Math.min(10, s.getSaleDate().length())) : "");
|
||||
binding.tvSalePayment.setText(s.getPaymentMethod() != null ? s.getPaymentMethod() : "");
|
||||
binding.tvSaleTotal.setText(s.getTotalAmount() != null ? "$" + s.getTotalAmount() : "");
|
||||
|
||||
// Show refund badge
|
||||
if (Boolean.TRUE.equals(s.getIsRefund())) {
|
||||
holder.tvRefundBadge.setVisibility(View.VISIBLE);
|
||||
binding.tvSaleRefundBadge.setVisibility(View.VISIBLE);
|
||||
binding.tvSaleRefundBadge.setBackgroundColor(Color.parseColor("#F44336"));
|
||||
binding.tvSaleTotal.setTextColor(Color.parseColor("#F44336"));
|
||||
} else {
|
||||
holder.tvRefundBadge.setVisibility(View.GONE);
|
||||
binding.tvSaleRefundBadge.setVisibility(View.GONE);
|
||||
binding.tvSaleTotal.setTextColor(Color.parseColor("#4CAF50"));
|
||||
}
|
||||
|
||||
holder.itemView.setOnClickListener(v -> listener.onSaleClick(position));
|
||||
@@ -67,4 +67,4 @@ public class SaleAdapter extends RecyclerView.Adapter<SaleAdapter.SaleViewHolder
|
||||
public int getItemCount() {
|
||||
return saleList.size();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,39 +3,69 @@ package com.example.petstoremobile.adapters;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import com.example.petstoremobile.R;
|
||||
|
||||
import com.example.petstoremobile.databinding.ItemServiceBinding;
|
||||
import com.example.petstoremobile.dtos.ServiceDTO;
|
||||
import com.example.petstoremobile.utils.BulkDeleteHandler;
|
||||
import com.example.petstoremobile.utils.SelectionHelper;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class ServiceAdapter extends RecyclerView.Adapter<ServiceAdapter.ServiceViewHolder> {
|
||||
/**
|
||||
* Adapter class for displaying a list of services in a RecyclerView.
|
||||
*/
|
||||
public class ServiceAdapter extends RecyclerView.Adapter<ServiceAdapter.ServiceViewHolder> implements BulkDeleteHandler.SelectableAdapter {
|
||||
|
||||
private List<ServiceDTO> serviceList;
|
||||
private OnServiceClickListener serviceClickListener;
|
||||
private final List<ServiceDTO> serviceList;
|
||||
private final OnServiceClickListener clickListener;
|
||||
private final SelectionHelper selectionHelper;
|
||||
|
||||
// Interface for service click on recycler view
|
||||
/**
|
||||
* Interface for handling clicks on service items.
|
||||
*/
|
||||
public interface OnServiceClickListener {
|
||||
void onServiceClick(int position);
|
||||
void onSelectionChanged(int count);
|
||||
}
|
||||
|
||||
//Constructor
|
||||
public ServiceAdapter(List<ServiceDTO> serviceList, OnServiceClickListener serviceClickListener) {
|
||||
this.serviceList = serviceList;
|
||||
this.serviceClickListener = serviceClickListener;
|
||||
public ServiceAdapter(List<ServiceDTO> serviceList, OnServiceClickListener clickListener) {
|
||||
this.serviceList = serviceList;
|
||||
this.clickListener = clickListener;
|
||||
this.selectionHelper = new SelectionHelper(new SelectionHelper.SelectionListener() {
|
||||
@Override
|
||||
public void onSelectionChanged(int count) {
|
||||
clickListener.onSelectionChanged(count);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSelectionModeToggle(boolean selectionMode) {
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Get the controls of each row in recycler view
|
||||
@Override
|
||||
public List<String> getSelectedKeys() {
|
||||
return selectionHelper.getSelectedKeys();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearSelection() {
|
||||
selectionHelper.clearSelection();
|
||||
}
|
||||
|
||||
/**
|
||||
* ViewHolder class for service items.
|
||||
*/
|
||||
public static class ServiceViewHolder extends RecyclerView.ViewHolder {
|
||||
TextView tvServiceName, tvServiceDesc, tvServiceDuration, tvServicePrice;
|
||||
final ItemServiceBinding binding;
|
||||
|
||||
public ServiceViewHolder(@NonNull View v) {
|
||||
super(v);
|
||||
tvServiceName = v.findViewById(R.id.tvServiceName);
|
||||
tvServiceDesc = v.findViewById(R.id.tvServiceDesc);
|
||||
tvServiceDuration = v.findViewById(R.id.tvServiceDuration);
|
||||
tvServicePrice = v.findViewById(R.id.tvServicePrice);
|
||||
public ServiceViewHolder(@NonNull ItemServiceBinding binding) {
|
||||
super(binding.getRoot());
|
||||
this.binding = binding;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,26 +73,51 @@ public class ServiceAdapter extends RecyclerView.Adapter<ServiceAdapter.ServiceV
|
||||
@NonNull
|
||||
@Override
|
||||
public ServiceViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_service, parent, false);
|
||||
return new ServiceViewHolder(v);
|
||||
ItemServiceBinding binding = ItemServiceBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false);
|
||||
return new ServiceViewHolder(binding);
|
||||
}
|
||||
|
||||
//populate the row with service data
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull ServiceViewHolder holder, int position) {
|
||||
ServiceDTO service = serviceList.get(position);
|
||||
ItemServiceBinding binding = holder.binding;
|
||||
|
||||
holder.tvServiceName.setText(service.getServiceName());
|
||||
holder.tvServiceDesc.setText(service.getServiceDesc());
|
||||
holder.tvServiceDuration.setText("Duration: " + service.getServiceDuration() + " min");
|
||||
holder.tvServicePrice.setText("$" + String.format("%.2f", service.getServicePrice()));
|
||||
binding.tvServiceName.setText(service.getServiceName());
|
||||
binding.tvServiceDesc.setText(service.getServiceDesc());
|
||||
binding.tvServiceDuration.setText(service.getServiceDuration() != null ? service.getServiceDuration() + " mins" : "0 mins");
|
||||
binding.tvServicePrice.setText(service.getServicePrice() != null ? "$" + String.format("%.2f", service.getServicePrice()) : "$0.00");
|
||||
|
||||
//when a row is clicked, open the detail view
|
||||
holder.itemView.setOnClickListener(v -> serviceClickListener.onServiceClick(position));
|
||||
String key = String.valueOf(service.getServiceId());
|
||||
|
||||
// Bulk delete selection mode
|
||||
if (selectionHelper.isInSelectionMode()) {
|
||||
binding.cbSelectService.setVisibility(View.VISIBLE);
|
||||
binding.cbSelectService.setChecked(selectionHelper.isSelected(key));
|
||||
} else {
|
||||
binding.cbSelectService.setVisibility(View.GONE);
|
||||
binding.cbSelectService.setChecked(false);
|
||||
}
|
||||
|
||||
holder.itemView.setOnClickListener(v -> {
|
||||
if (selectionHelper.isInSelectionMode()) {
|
||||
selectionHelper.toggleSelection(key);
|
||||
notifyItemChanged(position);
|
||||
} else {
|
||||
clickListener.onServiceClick(position);
|
||||
}
|
||||
});
|
||||
|
||||
holder.itemView.setOnLongClickListener(v -> {
|
||||
if (!selectionHelper.isInSelectionMode()) {
|
||||
selectionHelper.startSelection(key);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return serviceList.size();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,39 +3,63 @@ package com.example.petstoremobile.adapters;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import com.example.petstoremobile.R;
|
||||
|
||||
import com.example.petstoremobile.databinding.ItemSupplierBinding;
|
||||
import com.example.petstoremobile.dtos.SupplierDTO;
|
||||
import com.example.petstoremobile.utils.BulkDeleteHandler;
|
||||
import com.example.petstoremobile.utils.SelectionHelper;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class SupplierAdapter extends RecyclerView.Adapter<SupplierAdapter.SupplierViewHolder> {
|
||||
public class SupplierAdapter extends RecyclerView.Adapter<SupplierAdapter.SupplierViewHolder> implements BulkDeleteHandler.SelectableAdapter {
|
||||
|
||||
private List<SupplierDTO> supplierList;
|
||||
private OnSupplierClickListener supplierClickListener;
|
||||
private final List<SupplierDTO> supplierList;
|
||||
private final OnSupplierClickListener supplierClickListener;
|
||||
private final SelectionHelper selectionHelper;
|
||||
|
||||
// Interface for supplier click on recycler view
|
||||
public interface OnSupplierClickListener {
|
||||
void onSupplierClick(int position);
|
||||
void onSelectionChanged(int count);
|
||||
}
|
||||
|
||||
//Constructor
|
||||
public SupplierAdapter(List<SupplierDTO> supplierList, OnSupplierClickListener supplierClickListener) {
|
||||
this.supplierList = supplierList;
|
||||
this.supplierClickListener = supplierClickListener;
|
||||
this.selectionHelper = new SelectionHelper(new SelectionHelper.SelectionListener() {
|
||||
@Override
|
||||
public void onSelectionChanged(int count) {
|
||||
supplierClickListener.onSelectionChanged(count);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSelectionModeToggle(boolean selectionMode) {
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getSelectedKeys() {
|
||||
return selectionHelper.getSelectedKeys();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearSelection() {
|
||||
selectionHelper.clearSelection();
|
||||
}
|
||||
|
||||
// Get the controls of each row in recycler view
|
||||
public static class SupplierViewHolder extends RecyclerView.ViewHolder {
|
||||
TextView tvSupCompany, tvSupContactName, tvSupEmail, tvSupPhone;
|
||||
final ItemSupplierBinding binding;
|
||||
|
||||
public SupplierViewHolder(@NonNull View v) {
|
||||
super(v);
|
||||
tvSupCompany = v.findViewById(R.id.tvSupCompany);
|
||||
tvSupContactName = v.findViewById(R.id.tvSupContactName);
|
||||
tvSupEmail = v.findViewById(R.id.tvSupEmail);
|
||||
tvSupPhone = v.findViewById(R.id.tvSupPhone);
|
||||
public SupplierViewHolder(@NonNull ItemSupplierBinding binding) {
|
||||
super(binding.getRoot());
|
||||
this.binding = binding;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,26 +67,52 @@ public class SupplierAdapter extends RecyclerView.Adapter<SupplierAdapter.Suppli
|
||||
@NonNull
|
||||
@Override
|
||||
public SupplierViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_supplier, parent, false);
|
||||
return new SupplierViewHolder(v);
|
||||
ItemSupplierBinding binding = ItemSupplierBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false);
|
||||
return new SupplierViewHolder(binding);
|
||||
}
|
||||
|
||||
//populate the row with supplier data
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull SupplierViewHolder holder, int position) {
|
||||
SupplierDTO supplier = supplierList.get(position);
|
||||
ItemSupplierBinding binding = holder.binding;
|
||||
|
||||
holder.tvSupCompany.setText(supplier.getSupCompany());
|
||||
holder.tvSupContactName.setText(supplier.getSupContactFirstName() + " " + supplier.getSupContactLastName());
|
||||
holder.tvSupEmail.setText(supplier.getSupEmail());
|
||||
holder.tvSupPhone.setText(supplier.getSupPhone());
|
||||
binding.tvSupCompany.setText(supplier.getSupCompany());
|
||||
binding.tvSupContactName.setText(supplier.getSupContactFirstName() + " " + supplier.getSupContactLastName());
|
||||
binding.tvSupEmail.setText(supplier.getSupEmail());
|
||||
binding.tvSupPhone.setText(supplier.getSupPhone());
|
||||
|
||||
String key = String.valueOf(supplier.getSupId());
|
||||
|
||||
// Bulk delete selection mode
|
||||
if (selectionHelper.isInSelectionMode()) {
|
||||
binding.cbSelectSupplier.setVisibility(View.VISIBLE);
|
||||
binding.cbSelectSupplier.setChecked(selectionHelper.isSelected(key));
|
||||
} else {
|
||||
binding.cbSelectSupplier.setVisibility(View.GONE);
|
||||
binding.cbSelectSupplier.setChecked(false);
|
||||
}
|
||||
|
||||
//when a row is clicked, open the detail view
|
||||
holder.itemView.setOnClickListener(v -> supplierClickListener.onSupplierClick(position));
|
||||
holder.itemView.setOnClickListener(v -> {
|
||||
if (selectionHelper.isInSelectionMode()) {
|
||||
selectionHelper.toggleSelection(key);
|
||||
notifyItemChanged(position);
|
||||
} else {
|
||||
supplierClickListener.onSupplierClick(position);
|
||||
}
|
||||
});
|
||||
|
||||
holder.itemView.setOnLongClickListener(v -> {
|
||||
if (!selectionHelper.isInSelectionMode()) {
|
||||
selectionHelper.startSelection(key);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return supplierList.size();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,14 @@
|
||||
package com.example.petstoremobile.api;
|
||||
|
||||
import com.example.petstoremobile.dtos.AdoptionDTO;
|
||||
import com.example.petstoremobile.dtos.BulkDeleteRequest;
|
||||
import com.example.petstoremobile.dtos.PageResponse;
|
||||
|
||||
import retrofit2.Call;
|
||||
import retrofit2.http.Body;
|
||||
import retrofit2.http.DELETE;
|
||||
import retrofit2.http.GET;
|
||||
import retrofit2.http.HTTP;
|
||||
import retrofit2.http.POST;
|
||||
import retrofit2.http.PUT;
|
||||
import retrofit2.http.Path;
|
||||
@@ -17,7 +19,12 @@ public interface AdoptionApi {
|
||||
@GET("api/v1/adoptions")
|
||||
Call<PageResponse<AdoptionDTO>> getAllAdoptions(
|
||||
@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/adoptions/{id}")
|
||||
Call<AdoptionDTO> getAdoptionById(@Path("id") Long id);
|
||||
@@ -30,5 +37,7 @@ public interface AdoptionApi {
|
||||
|
||||
@DELETE("api/v1/adoptions/{id}")
|
||||
Call<Void> deleteAdoption(@Path("id") Long id);
|
||||
}
|
||||
|
||||
@HTTP(method = "DELETE", path = "api/v1/adoptions", hasBody = true)
|
||||
Call<Void> bulkDeleteAdoptions(@Body BulkDeleteRequest request);
|
||||
}
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
package com.example.petstoremobile.api;
|
||||
|
||||
import com.example.petstoremobile.dtos.AppointmentDTO;
|
||||
import com.example.petstoremobile.dtos.BulkDeleteRequest;
|
||||
import com.example.petstoremobile.dtos.PageResponse;
|
||||
|
||||
import retrofit2.Call;
|
||||
import retrofit2.http.Body;
|
||||
import retrofit2.http.DELETE;
|
||||
import retrofit2.http.GET;
|
||||
import retrofit2.http.HTTP;
|
||||
import retrofit2.http.POST;
|
||||
import retrofit2.http.PUT;
|
||||
import retrofit2.http.Path;
|
||||
@@ -17,7 +19,12 @@ public interface AppointmentApi {
|
||||
@GET("api/v1/appointments")
|
||||
Call<PageResponse<AppointmentDTO>> getAllAppointments(
|
||||
@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}")
|
||||
Call<AppointmentDTO> getAppointmentById(@Path("id") Long id);
|
||||
@@ -30,4 +37,7 @@ public interface AppointmentApi {
|
||||
|
||||
@DELETE("api/v1/appointments/{id}")
|
||||
Call<Void> deleteAppointment(@Path("id") Long id);
|
||||
|
||||
@HTTP(method = "DELETE", path = "api/v1/appointments", hasBody = true)
|
||||
Call<Void> bulkDeleteAppointments(@Body BulkDeleteRequest request);
|
||||
}
|
||||
@@ -2,13 +2,13 @@ package com.example.petstoremobile.api;
|
||||
|
||||
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 retrofit2.Call;
|
||||
import retrofit2.http.Body;
|
||||
import retrofit2.http.DELETE;
|
||||
import retrofit2.http.GET;
|
||||
import retrofit2.http.HTTP;
|
||||
import retrofit2.http.POST;
|
||||
import retrofit2.http.PUT;
|
||||
import retrofit2.http.Path;
|
||||
@@ -16,12 +16,12 @@ import retrofit2.http.Query;
|
||||
|
||||
public interface InventoryApi {
|
||||
|
||||
// GET /api/v1/inventory?q=...&page=...&size=...
|
||||
@GET("api/v1/inventory")
|
||||
Call<PageResponse<InventoryDTO>> getAllInventory(
|
||||
@Query("q") String query,
|
||||
@Query("page") int page,
|
||||
@Query("size") int size,
|
||||
@Query("q") String query,
|
||||
@Query("storeId") Long storeId,
|
||||
@Query("sort") String sort);
|
||||
|
||||
// GET /api/v1/inventory/{id}
|
||||
@@ -30,17 +30,17 @@ public interface InventoryApi {
|
||||
|
||||
// POST /api/v1/inventory
|
||||
@POST("api/v1/inventory")
|
||||
Call<InventoryDTO> createInventory(@Body InventoryRequest request);
|
||||
Call<InventoryDTO> createInventory(@Body InventoryDTO request);
|
||||
|
||||
// PUT /api/v1/inventory/{id}
|
||||
@PUT("api/v1/inventory/{id}")
|
||||
Call<InventoryDTO> updateInventory(@Path("id") Long id, @Body InventoryRequest request);
|
||||
Call<InventoryDTO> updateInventory(@Path("id") Long id, @Body InventoryDTO request);
|
||||
|
||||
// DELETE /api/v1/inventory/{id}
|
||||
@DELETE("api/v1/inventory/{id}")
|
||||
Call<Void> deleteInventory(@Path("id") Long id);
|
||||
|
||||
// DELETE /api/v1/inventory (bulk delete)
|
||||
@DELETE("api/v1/inventory")
|
||||
@HTTP(method = "DELETE", path = "api/v1/inventory", hasBody = true)
|
||||
Call<Void> bulkDeleteInventory(@Body BulkDeleteRequest request);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.example.petstoremobile.api;
|
||||
|
||||
import com.example.petstoremobile.dtos.BulkDeleteRequest;
|
||||
import com.example.petstoremobile.dtos.PageResponse;
|
||||
import com.example.petstoremobile.dtos.PetDTO;
|
||||
|
||||
@@ -8,6 +9,7 @@ import retrofit2.Call;
|
||||
import retrofit2.http.Body;
|
||||
import retrofit2.http.DELETE;
|
||||
import retrofit2.http.GET;
|
||||
import retrofit2.http.HTTP;
|
||||
import retrofit2.http.Multipart;
|
||||
import retrofit2.http.POST;
|
||||
import retrofit2.http.PUT;
|
||||
@@ -20,11 +22,16 @@ public interface PetApi {
|
||||
// endpoint for downloading the pet's image file
|
||||
String PET_IMAGE_PATH = "api/v1/pets/%d/image";
|
||||
|
||||
// Get all pets
|
||||
// Get all pets with filters
|
||||
@GET("api/v1/pets")
|
||||
Call<PageResponse<PetDTO>> getAllPets(
|
||||
@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
|
||||
@@ -43,9 +50,17 @@ public interface PetApi {
|
||||
@DELETE("api/v1/pets/{id}")
|
||||
Call<Void> deletePet(@Path("id") Long id);
|
||||
|
||||
// Bulk delete pets
|
||||
@HTTP(method = "DELETE", path = "api/v1/pets", hasBody = true)
|
||||
Call<Void> bulkDeletePets(@Body BulkDeleteRequest request);
|
||||
|
||||
// Upload pet image
|
||||
@Multipart
|
||||
@POST("api/v1/pets/{id}/image")
|
||||
Call<Void> uploadPetImage(@Path("id") Long id, @Part MultipartBody.Part image);
|
||||
|
||||
// Delete pet image
|
||||
@DELETE("api/v1/pets/{id}/image")
|
||||
Call<Void> deletePetImage(@Path("id") Long id);
|
||||
|
||||
}
|
||||
|
||||
@@ -2,16 +2,20 @@ package com.example.petstoremobile.api;
|
||||
|
||||
import com.example.petstoremobile.dtos.PageResponse;
|
||||
import com.example.petstoremobile.dtos.ProductDTO;
|
||||
import okhttp3.MultipartBody;
|
||||
import retrofit2.Call;
|
||||
import retrofit2.http.*;
|
||||
|
||||
public interface ProductApi {
|
||||
String PRODUCT_IMAGE_PATH = "api/v1/products/%d/image";
|
||||
|
||||
@GET("api/v1/products")
|
||||
Call<PageResponse<ProductDTO>> getAllProducts(
|
||||
@Query("q") String query,
|
||||
@Query("categoryId") Long categoryId,
|
||||
@Query("page") int page,
|
||||
@Query("size") int size);
|
||||
@Query("size") int size,
|
||||
@Query("sort") String sort);
|
||||
|
||||
@GET("api/v1/products/{id}")
|
||||
Call<ProductDTO> getProductById(@Path("id") Long id);
|
||||
@@ -24,4 +28,11 @@ public interface ProductApi {
|
||||
|
||||
@DELETE("api/v1/products/{id}")
|
||||
Call<Void> deleteProduct(@Path("id") Long id);
|
||||
|
||||
@Multipart
|
||||
@POST("api/v1/products/{id}/image")
|
||||
Call<Void> uploadProductImage(@Path("id") Long id, @Part MultipartBody.Part image);
|
||||
|
||||
@DELETE("api/v1/products/{id}/image")
|
||||
Call<Void> deleteProductImage(@Path("id") Long id);
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.example.petstoremobile.api;
|
||||
|
||||
import com.example.petstoremobile.dtos.BulkDeleteRequest;
|
||||
import com.example.petstoremobile.dtos.PageResponse;
|
||||
import com.example.petstoremobile.dtos.ProductSupplierDTO;
|
||||
import retrofit2.Call;
|
||||
@@ -10,7 +11,16 @@ public interface ProductSupplierApi {
|
||||
@GET("api/v1/product-suppliers")
|
||||
Call<PageResponse<ProductSupplierDTO>> getAllProductSuppliers(
|
||||
@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")
|
||||
Call<ProductSupplierDTO> createProductSupplier(@Body ProductSupplierDTO dto);
|
||||
@@ -25,4 +35,6 @@ public interface ProductSupplierApi {
|
||||
Call<Void> deleteProductSupplier(
|
||||
@Path("productId") Long productId,
|
||||
@Path("supplierId") Long supplierId);
|
||||
@HTTP(method = "DELETE", path = "api/v1/product-suppliers", hasBody = true)
|
||||
Call<Void> bulkDeleteProductSuppliers(@Body BulkDeleteRequest request);
|
||||
}
|
||||
@@ -12,7 +12,10 @@ public interface PurchaseOrderApi {
|
||||
@GET("api/v1/purchase-orders")
|
||||
Call<PageResponse<PurchaseOrderDTO>> getAllPurchaseOrders(
|
||||
@Query("page") int page,
|
||||
@Query("size") int size);
|
||||
@Query("size") int size,
|
||||
@Query("q") String query,
|
||||
@Query("storeId") Long storeId,
|
||||
@Query("sort") String sort);
|
||||
|
||||
@GET("api/v1/purchase-orders/{id}")
|
||||
Call<PurchaseOrderDTO> getPurchaseOrderById(@Path("id") Long id);
|
||||
|
||||
@@ -2,9 +2,12 @@ package com.example.petstoremobile.api;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Build;
|
||||
import android.util.Log;
|
||||
|
||||
import com.example.petstoremobile.BuildConfig;
|
||||
import com.example.petstoremobile.api.auth.AuthApi;
|
||||
import com.example.petstoremobile.api.auth.AuthInterceptor;
|
||||
import com.example.petstoremobile.api.auth.TokenManager;
|
||||
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.logging.HttpLoggingInterceptor;
|
||||
@@ -13,29 +16,30 @@ import retrofit2.converter.gson.GsonConverterFactory;
|
||||
|
||||
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 {
|
||||
private static final String TAG = "RetrofitClient";
|
||||
public static final String BASE_URL = getBaseUrl();
|
||||
|
||||
// Helper function to determine BASE_URL based on whether we are testing on an emulator or a real device
|
||||
private static String getBaseUrl() {
|
||||
if (Build.FINGERPRINT.contains("generic")
|
||||
|| Build.FINGERPRINT.contains("unknown")
|
||||
String url = isEmulator() ? BuildConfig.EMULATOR_BACKEND_URL : BuildConfig.DEVICE_BACKEND_URL;
|
||||
Log.i(TAG, "Using backend URL: " + url + " (emulator=" + isEmulator() + ")");
|
||||
return url;
|
||||
}
|
||||
|
||||
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.BRAND.startsWith("generic") && Build.DEVICE.startsWith("generic"))
|
||||
|| "google_sdk".equals(Build.PRODUCT)) {
|
||||
return "http://10.0.2.2:8080/"; //emulator testing
|
||||
}
|
||||
else {
|
||||
return "http://10.0.0.200:8080/"; //Hardware testing
|
||||
}
|
||||
|
||||
/*else {
|
||||
return "http://192.168.1.148:8080/"; //Hardware testing
|
||||
} */
|
||||
|| 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"));
|
||||
}
|
||||
|
||||
private static Retrofit retrofit = null;
|
||||
@@ -47,7 +51,7 @@ public class RetrofitClient {
|
||||
|
||||
OkHttpClient client = new OkHttpClient.Builder()
|
||||
.addInterceptor(interceptor)
|
||||
.addInterceptor(new AuthInterceptor(context))
|
||||
.addInterceptor(new AuthInterceptor(new TokenManager(context)))
|
||||
.connectTimeout(30, TimeUnit.SECONDS)
|
||||
.readTimeout(30, TimeUnit.SECONDS)
|
||||
.writeTimeout(30, TimeUnit.SECONDS)
|
||||
@@ -131,8 +135,9 @@ public class RetrofitClient {
|
||||
public static RefundApi getRefundApi(Context context) {
|
||||
return getClient(context).create(RefundApi.class);
|
||||
}
|
||||
|
||||
public static EmployeeApi getEmployeeApi(Context context) {
|
||||
return getClient(context).create(EmployeeApi.class);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,8 +2,13 @@ package com.example.petstoremobile.api;
|
||||
|
||||
import com.example.petstoremobile.dtos.PageResponse;
|
||||
import com.example.petstoremobile.dtos.SaleDTO;
|
||||
|
||||
import retrofit2.Call;
|
||||
import retrofit2.http.*;
|
||||
import retrofit2.http.Body;
|
||||
import retrofit2.http.GET;
|
||||
import retrofit2.http.POST;
|
||||
import retrofit2.http.Path;
|
||||
import retrofit2.http.Query;
|
||||
|
||||
public interface SaleApi {
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.example.petstoremobile.api;
|
||||
|
||||
import com.example.petstoremobile.dtos.BulkDeleteRequest;
|
||||
import com.example.petstoremobile.dtos.PageResponse;
|
||||
import com.example.petstoremobile.dtos.ServiceDTO;
|
||||
|
||||
@@ -7,6 +8,7 @@ import retrofit2.Call;
|
||||
import retrofit2.http.Body;
|
||||
import retrofit2.http.DELETE;
|
||||
import retrofit2.http.GET;
|
||||
import retrofit2.http.HTTP;
|
||||
import retrofit2.http.POST;
|
||||
import retrofit2.http.PUT;
|
||||
import retrofit2.http.Path;
|
||||
@@ -18,7 +20,9 @@ public interface ServiceApi {
|
||||
@GET("api/v1/services")
|
||||
Call<PageResponse<ServiceDTO>> getAllServices(
|
||||
@Query("page") int page,
|
||||
@Query("size") int size
|
||||
@Query("size") int size,
|
||||
@Query("q") String query,
|
||||
@Query("sort") String sort
|
||||
);
|
||||
|
||||
// Get service by id
|
||||
@@ -36,4 +40,8 @@ public interface ServiceApi {
|
||||
// Delete service
|
||||
@DELETE("api/v1/services/{id}")
|
||||
Call<Void> deleteService(@Path("id") Long id);
|
||||
|
||||
// Bulk delete services
|
||||
@HTTP(method = "DELETE", path = "api/v1/services", hasBody = true)
|
||||
Call<Void> bulkDeleteServices(@Body BulkDeleteRequest request);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.example.petstoremobile.api;
|
||||
|
||||
import com.example.petstoremobile.dtos.BulkDeleteRequest;
|
||||
import com.example.petstoremobile.dtos.PageResponse;
|
||||
import com.example.petstoremobile.dtos.SupplierDTO;
|
||||
|
||||
@@ -7,6 +8,7 @@ import retrofit2.Call;
|
||||
import retrofit2.http.Body;
|
||||
import retrofit2.http.DELETE;
|
||||
import retrofit2.http.GET;
|
||||
import retrofit2.http.HTTP;
|
||||
import retrofit2.http.POST;
|
||||
import retrofit2.http.PUT;
|
||||
import retrofit2.http.Path;
|
||||
@@ -18,7 +20,9 @@ public interface SupplierApi {
|
||||
@GET("api/v1/suppliers")
|
||||
Call<PageResponse<SupplierDTO>> getAllSuppliers(
|
||||
@Query("page") int page,
|
||||
@Query("size") int size
|
||||
@Query("size") int size,
|
||||
@Query("q") String query,
|
||||
@Query("sort") String sort
|
||||
);
|
||||
|
||||
// Get supplier by id
|
||||
@@ -36,4 +40,8 @@ public interface SupplierApi {
|
||||
// Delete supplier
|
||||
@DELETE("api/v1/suppliers/{id}")
|
||||
Call<Void> deleteSupplier(@Path("id") Long id);
|
||||
|
||||
// Bulk delete suppliers
|
||||
@HTTP(method = "DELETE", path = "api/v1/suppliers", hasBody = true)
|
||||
Call<Void> bulkDeleteSuppliers(@Body BulkDeleteRequest request);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.example.petstoremobile.api;
|
||||
|
||||
import com.example.petstoremobile.dtos.PageResponse;
|
||||
import com.example.petstoremobile.dtos.UserDTO;
|
||||
|
||||
import retrofit2.Call;
|
||||
import retrofit2.http.GET;
|
||||
import retrofit2.http.Query;
|
||||
|
||||
public interface UserApi {
|
||||
@GET("api/v1/users")
|
||||
Call<PageResponse<UserDTO>> getUsers(@Query("role") String role, @Query("page") int page, @Query("size") int size);
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.example.petstoremobile.api.auth;
|
||||
|
||||
import com.example.petstoremobile.dtos.AuthDTO;
|
||||
import com.example.petstoremobile.dtos.AvatarUploadResponse;
|
||||
import com.example.petstoremobile.dtos.UserDTO;
|
||||
|
||||
import java.util.Map;
|
||||
@@ -8,6 +9,7 @@ import java.util.Map;
|
||||
import okhttp3.MultipartBody;
|
||||
import retrofit2.Call;
|
||||
import retrofit2.http.Body;
|
||||
import retrofit2.http.DELETE;
|
||||
import retrofit2.http.GET;
|
||||
import retrofit2.http.Multipart;
|
||||
import retrofit2.http.POST;
|
||||
@@ -35,6 +37,10 @@ public interface AuthApi {
|
||||
//upload avatar endpoint
|
||||
@Multipart
|
||||
@POST("api/v1/auth/me/avatar")
|
||||
Call<UserDTO> uploadAvatar(@Part MultipartBody.Part avatar);
|
||||
Call<AvatarUploadResponse> uploadAvatar(@Part MultipartBody.Part avatar);
|
||||
|
||||
//delete avatar endpoint
|
||||
@DELETE("api/v1/auth/me/avatar")
|
||||
Call<Void> deleteAvatar();
|
||||
|
||||
}
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
package com.example.petstoremobile.api.auth;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.io.IOException;
|
||||
@@ -15,8 +13,8 @@ public class AuthInterceptor implements Interceptor {
|
||||
|
||||
private final TokenManager tokenManager;
|
||||
|
||||
public AuthInterceptor(Context context) {
|
||||
this.tokenManager = TokenManager.getInstance(context);
|
||||
public AuthInterceptor(TokenManager tokenManager) {
|
||||
this.tokenManager = tokenManager;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
|
||||
@@ -3,7 +3,12 @@ package com.example.petstoremobile.api.auth;
|
||||
import android.content.Context;
|
||||
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 {
|
||||
private static final String TOKEN_KEY = "token";
|
||||
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 USER_ID_KEY = "user_id";
|
||||
|
||||
private static TokenManager instance;
|
||||
private SharedPreferences prefs;
|
||||
|
||||
private TokenManager(Context context) {
|
||||
@Inject
|
||||
public TokenManager(@ApplicationContext Context context) {
|
||||
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
|
||||
public void saveLoginData(String token, String username, String role) {
|
||||
prefs.edit()
|
||||
@@ -65,6 +63,4 @@ public class TokenManager {
|
||||
public void clearLoginData() {
|
||||
prefs.edit().clear().apply();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
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);
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
public static UserApi provideUserApi(Retrofit retrofit) {
|
||||
return retrofit.create(UserApi.class);
|
||||
}
|
||||
}
|
||||
@@ -9,88 +9,68 @@ public class AdoptionDTO {
|
||||
private String petName;
|
||||
private Long customerId;
|
||||
private String customerName;
|
||||
private Long employeeId;
|
||||
private String employeeName;
|
||||
private Long sourceStoreId;
|
||||
private String sourceStoreName;
|
||||
private String adoptionDate;
|
||||
private String adoptionStatus;
|
||||
private BigDecimal adoptionFee;
|
||||
private Long employeeId;
|
||||
private String employeeName;
|
||||
private String createdAt;
|
||||
private String updatedAt;
|
||||
|
||||
// Constructor for create/update requests
|
||||
public AdoptionDTO(Long petId, Long customerId, String adoptionDate, String adoptionStatus, Long employeeId) {
|
||||
public AdoptionDTO() {}
|
||||
|
||||
public AdoptionDTO(Long petId, Long customerId, Long employeeId, Long sourceStoreId,
|
||||
String adoptionDate, String adoptionStatus, BigDecimal adoptionFee) {
|
||||
this.petId = petId;
|
||||
this.customerId = customerId;
|
||||
this.employeeId = employeeId;
|
||||
this.sourceStoreId = sourceStoreId;
|
||||
this.adoptionDate = adoptionDate;
|
||||
this.adoptionStatus = adoptionStatus;
|
||||
this.employeeId = employeeId;
|
||||
this.adoptionFee = adoptionFee;
|
||||
}
|
||||
|
||||
public Long getAdoptionId() {
|
||||
public Long getAdoptionId() { return adoptionId; }
|
||||
public void setAdoptionId(Long adoptionId) { this.adoptionId = adoptionId; }
|
||||
|
||||
return adoptionId;
|
||||
}
|
||||
public Long getPetId() { return petId; }
|
||||
public void setPetId(Long petId) { this.petId = petId; }
|
||||
|
||||
public Long getPetId() {
|
||||
public String getPetName() { return petName; }
|
||||
public void setPetName(String petName) { this.petName = petName; }
|
||||
|
||||
return petId;
|
||||
}
|
||||
public Long getCustomerId() { return customerId; }
|
||||
public void setCustomerId(Long customerId) { this.customerId = customerId; }
|
||||
|
||||
public String getPetName() {
|
||||
public String getCustomerName() { return customerName; }
|
||||
public void setCustomerName(String customerName) { this.customerName = customerName; }
|
||||
|
||||
return petName;
|
||||
}
|
||||
public Long getEmployeeId() { return employeeId; }
|
||||
public void setEmployeeId(Long employeeId) { this.employeeId = employeeId; }
|
||||
|
||||
public Long getCustomerId() {
|
||||
public String getEmployeeName() { return employeeName; }
|
||||
public void setEmployeeName(String employeeName) { this.employeeName = employeeName; }
|
||||
|
||||
return customerId;
|
||||
}
|
||||
public Long getSourceStoreId() { return sourceStoreId; }
|
||||
public void setSourceStoreId(Long sourceStoreId) { this.sourceStoreId = sourceStoreId; }
|
||||
|
||||
public String getCustomerName() {
|
||||
public String getSourceStoreName() { return sourceStoreName; }
|
||||
public void setSourceStoreName(String sourceStoreName) { this.sourceStoreName = sourceStoreName; }
|
||||
|
||||
return customerName;
|
||||
}
|
||||
public String getAdoptionDate() { return adoptionDate; }
|
||||
public void setAdoptionDate(String adoptionDate) { this.adoptionDate = adoptionDate; }
|
||||
|
||||
public String getAdoptionDate() {
|
||||
public String getAdoptionStatus() { return adoptionStatus; }
|
||||
public void setAdoptionStatus(String adoptionStatus) { this.adoptionStatus = adoptionStatus; }
|
||||
|
||||
return adoptionDate;
|
||||
}
|
||||
public BigDecimal getAdoptionFee() { return adoptionFee; }
|
||||
public void setAdoptionFee(BigDecimal adoptionFee) { this.adoptionFee = adoptionFee; }
|
||||
|
||||
public String getEmployeeName() {
|
||||
public String getCreatedAt() { return createdAt; }
|
||||
public void setCreatedAt(String createdAt) { this.createdAt = createdAt; }
|
||||
|
||||
return employeeName;
|
||||
}
|
||||
|
||||
public Long getEmployeeId() {
|
||||
return employeeId;
|
||||
}
|
||||
|
||||
public String getAdoptionStatus() {
|
||||
|
||||
return adoptionStatus;
|
||||
}
|
||||
|
||||
public String getStatus() {
|
||||
|
||||
return adoptionStatus;
|
||||
}
|
||||
|
||||
public BigDecimal getAdoptionFee() {
|
||||
|
||||
return adoptionFee;
|
||||
}
|
||||
|
||||
public String getCreatedAt() {
|
||||
|
||||
return createdAt;
|
||||
}
|
||||
|
||||
public String getUpdatedAt() {
|
||||
|
||||
return updatedAt;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
public String getUpdatedAt() { return updatedAt; }
|
||||
public void setUpdatedAt(String updatedAt) { this.updatedAt = updatedAt; }
|
||||
}
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
package com.example.petstoremobile.dtos;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
|
||||
public class AppointmentDTO {
|
||||
// Response fields (from server)
|
||||
|
||||
private Long appointmentId;
|
||||
private Long customerId;
|
||||
private String customerName;
|
||||
@@ -12,143 +9,72 @@ public class AppointmentDTO {
|
||||
private String storeName;
|
||||
private Long serviceId;
|
||||
private String serviceName;
|
||||
|
||||
private Long employeeId;
|
||||
private String employeeName;
|
||||
private String appointmentDate;
|
||||
private String appointmentTime;
|
||||
private String appointmentStatus;
|
||||
private List<String> petNames;
|
||||
private List<Long> petIds;
|
||||
|
||||
private String petName;
|
||||
private Long petId;
|
||||
private String createdAt;
|
||||
private String updatedAt;
|
||||
|
||||
|
||||
|
||||
// Constructor for CREATE/UPDATE request body
|
||||
// Matches AppointmentRequest exactly
|
||||
public AppointmentDTO(Long customerId, Long storeId, Long serviceId,
|
||||
String appointmentDate, String appointmentTime,
|
||||
String appointmentStatus, List<Long> petIds) {
|
||||
String appointmentStatus, Long petId) {
|
||||
this(customerId, storeId, serviceId, null, appointmentDate, appointmentTime, appointmentStatus, petId);
|
||||
}
|
||||
|
||||
public AppointmentDTO(Long customerId, Long storeId, Long serviceId, Long employeeId,
|
||||
String appointmentDate, String appointmentTime,
|
||||
String appointmentStatus, Long petId) {
|
||||
this.customerId = customerId;
|
||||
this.storeId = storeId;
|
||||
this.serviceId = serviceId;
|
||||
this.employeeId = employeeId;
|
||||
this.appointmentDate = appointmentDate;
|
||||
this.appointmentTime = appointmentTime;
|
||||
this.appointmentStatus = appointmentStatus;
|
||||
this.petIds = petIds;
|
||||
this.employeeId = employeeId;
|
||||
this.petId = petId;
|
||||
}
|
||||
|
||||
// Getters
|
||||
public Long getAppointmentId() {
|
||||
public Long getAppointmentId() { return appointmentId; }
|
||||
|
||||
return appointmentId;
|
||||
}
|
||||
public Long getCustomerId() { return customerId; }
|
||||
|
||||
public Long getCustomerId() {
|
||||
public String getCustomerName() { return customerName; }
|
||||
|
||||
return customerId;
|
||||
}
|
||||
public Long getStoreId() { return storeId; }
|
||||
|
||||
public String getCustomerName() {
|
||||
public String getStoreName() { return storeName; }
|
||||
|
||||
return customerName;
|
||||
}
|
||||
public Long getServiceId() { return serviceId; }
|
||||
|
||||
public Long getStoreId() {
|
||||
public String getServiceName() { return serviceName; }
|
||||
|
||||
return storeId;
|
||||
}
|
||||
public Long getEmployeeId() { return employeeId; }
|
||||
|
||||
public String getStoreName() {
|
||||
public String getEmployeeName() { return employeeName; }
|
||||
|
||||
return storeName;
|
||||
}
|
||||
public String getAppointmentDate() { return appointmentDate; }
|
||||
|
||||
public Long getServiceId() {
|
||||
public String getAppointmentTime() { return appointmentTime; }
|
||||
|
||||
return serviceId;
|
||||
}
|
||||
public String getAppointmentStatus() { return appointmentStatus; }
|
||||
|
||||
public String getServiceName() {
|
||||
public String getPetName() { return petName; }
|
||||
|
||||
return serviceName;
|
||||
}
|
||||
public Long getPetId() { return petId; }
|
||||
|
||||
public String getAppointmentDate() {
|
||||
public String getCreatedAt() { return createdAt; }
|
||||
|
||||
return appointmentDate;
|
||||
}
|
||||
public String getUpdatedAt() { return updatedAt; }
|
||||
|
||||
public String getAppointmentTime() {
|
||||
public Long getPetID() { return petId; }
|
||||
|
||||
return appointmentTime;
|
||||
}
|
||||
|
||||
public String getAppointmentStatus() {
|
||||
|
||||
return appointmentStatus;
|
||||
}
|
||||
|
||||
public List<String> getPetNames() {
|
||||
|
||||
return petNames;
|
||||
}
|
||||
|
||||
public List<Long> getPetIds() {
|
||||
|
||||
return petIds;
|
||||
}
|
||||
|
||||
|
||||
|
||||
public String getCreatedAt() {
|
||||
|
||||
return createdAt;
|
||||
}
|
||||
|
||||
public String getUpdatedAt() {
|
||||
|
||||
return updatedAt;
|
||||
}
|
||||
|
||||
// Convenience getters for adapter/list display
|
||||
public String getPetName() {
|
||||
return (petNames != null && !petNames.isEmpty()) ? petNames.get(0) : "";
|
||||
}
|
||||
|
||||
public Long getPetID() {
|
||||
|
||||
return (petIds != null && !petIds.isEmpty()) ? petIds.get(0) : null;
|
||||
}
|
||||
|
||||
public Long getPetId() {
|
||||
|
||||
return getPetID();
|
||||
}
|
||||
|
||||
// Keep old name so adapter doesn't break
|
||||
public String getServiceType() {
|
||||
return serviceName;
|
||||
}
|
||||
|
||||
public Long getServiceID() {
|
||||
|
||||
return serviceId;
|
||||
}
|
||||
|
||||
public String getEmployeeName() {
|
||||
|
||||
return employeeName;
|
||||
}
|
||||
|
||||
// Status alias
|
||||
public String getStatus() {
|
||||
|
||||
return appointmentStatus;
|
||||
}
|
||||
public String getServiceType() { return serviceName; }
|
||||
|
||||
public Long getServiceID() { return serviceId; }
|
||||
|
||||
public String getStatus() { return appointmentStatus; }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.example.petstoremobile.dtos;
|
||||
|
||||
public class AvatarUploadResponse {
|
||||
private String avatarUrl;
|
||||
private String message;
|
||||
|
||||
public AvatarUploadResponse() {
|
||||
}
|
||||
|
||||
public String getAvatarUrl() {
|
||||
return avatarUrl;
|
||||
}
|
||||
|
||||
public void setAvatarUrl(String avatarUrl) {
|
||||
this.avatarUrl = avatarUrl;
|
||||
}
|
||||
|
||||
public String getMessage() {
|
||||
return message;
|
||||
}
|
||||
|
||||
public void setMessage(String message) {
|
||||
this.message = message;
|
||||
}
|
||||
}
|
||||
@@ -3,20 +3,20 @@ package com.example.petstoremobile.dtos;
|
||||
import java.util.List;
|
||||
|
||||
public class BulkDeleteRequest {
|
||||
private List<Long> ids;
|
||||
private List<String> ids;
|
||||
|
||||
public BulkDeleteRequest() {
|
||||
}
|
||||
|
||||
public BulkDeleteRequest(List<Long> ids) {
|
||||
public BulkDeleteRequest(List<String> ids) {
|
||||
this.ids = ids;
|
||||
}
|
||||
|
||||
public List<Long> getIds() {
|
||||
public List<String> getIds() {
|
||||
return ids;
|
||||
}
|
||||
|
||||
public void setIds(List<Long> ids) {
|
||||
public void setIds(List<String> ids) {
|
||||
this.ids = ids;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
package com.example.petstoremobile.dtos;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
|
||||
public class CustomerDTO {
|
||||
@SerializedName("id")
|
||||
private Long customerId;
|
||||
private String firstName;
|
||||
private String lastName;
|
||||
@@ -12,18 +15,34 @@ public class CustomerDTO {
|
||||
return customerId;
|
||||
}
|
||||
|
||||
public void setCustomerId(Long customerId) {
|
||||
this.customerId = customerId;
|
||||
}
|
||||
|
||||
public String getFirstName() {
|
||||
return firstName;
|
||||
}
|
||||
|
||||
public void setFirstName(String firstName) {
|
||||
this.firstName = firstName;
|
||||
}
|
||||
|
||||
public String getLastName() {
|
||||
return lastName;
|
||||
}
|
||||
|
||||
public void setLastName(String lastName) {
|
||||
this.lastName = lastName;
|
||||
}
|
||||
|
||||
public String getEmail() {
|
||||
return email;
|
||||
}
|
||||
|
||||
public void setEmail(String email) {
|
||||
this.email = email;
|
||||
}
|
||||
|
||||
public String getFullName() {
|
||||
return firstName + " " + lastName;
|
||||
}
|
||||
@@ -32,7 +51,15 @@ public class CustomerDTO {
|
||||
return createdAt;
|
||||
}
|
||||
|
||||
public void setCreatedAt(String createdAt) {
|
||||
this.createdAt = createdAt;
|
||||
}
|
||||
|
||||
public String getUpdatedAt() {
|
||||
return updatedAt;
|
||||
}
|
||||
}
|
||||
|
||||
public void setUpdatedAt(String updatedAt) {
|
||||
this.updatedAt = updatedAt;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,8 @@ public class InventoryDTO {
|
||||
private Long prodId;
|
||||
private String productName;
|
||||
private String categoryName;
|
||||
private Long storeId;
|
||||
private String storeName;
|
||||
private Integer quantity;
|
||||
private String createdAt;
|
||||
private String updatedAt;
|
||||
@@ -14,8 +16,9 @@ public class InventoryDTO {
|
||||
}
|
||||
|
||||
// Constructor for create/update requests (matches InventoryRequest)
|
||||
public InventoryDTO(Long prodId, Integer quantity) {
|
||||
public InventoryDTO(Long prodId, Long storeId, Integer quantity) {
|
||||
this.prodId = prodId;
|
||||
this.storeId = storeId;
|
||||
this.quantity = quantity;
|
||||
}
|
||||
|
||||
@@ -51,6 +54,22 @@ public class InventoryDTO {
|
||||
this.categoryName = categoryName;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
public Integer getQuantity() {
|
||||
return quantity;
|
||||
}
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
package com.example.petstoremobile.dtos;
|
||||
|
||||
public class InventoryRequest {
|
||||
private Long prodId;
|
||||
private Integer quantity;
|
||||
|
||||
public InventoryRequest() {
|
||||
}
|
||||
|
||||
public InventoryRequest(Long prodId, Integer quantity) {
|
||||
this.prodId = prodId;
|
||||
this.quantity = quantity;
|
||||
}
|
||||
|
||||
public Long getProdId() {
|
||||
return prodId;
|
||||
}
|
||||
|
||||
public void setProdId(Long prodId) {
|
||||
this.prodId = prodId;
|
||||
}
|
||||
|
||||
public Integer getQuantity() {
|
||||
return quantity;
|
||||
}
|
||||
|
||||
public void setQuantity(Integer quantity) {
|
||||
this.quantity = quantity;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,15 @@ public class MessageDTO {
|
||||
@SerializedName("isRead")
|
||||
private Boolean isRead;
|
||||
|
||||
@SerializedName("attachmentUrl")
|
||||
private String attachmentUrl;
|
||||
|
||||
@SerializedName("attachmentName")
|
||||
private String attachmentName;
|
||||
|
||||
@SerializedName("attachmentType")
|
||||
private String attachmentType;
|
||||
|
||||
public MessageDTO() {}
|
||||
|
||||
public Long getId() { return id; }
|
||||
@@ -41,4 +50,13 @@ public class MessageDTO {
|
||||
|
||||
public Boolean getIsRead() { return isRead; }
|
||||
public void setIsRead(Boolean isRead) { this.isRead = isRead; }
|
||||
|
||||
public String getAttachmentUrl() { return attachmentUrl; }
|
||||
public void setAttachmentUrl(String attachmentUrl) { this.attachmentUrl = attachmentUrl; }
|
||||
|
||||
public String getAttachmentName() { return attachmentName; }
|
||||
public void setAttachmentName(String attachmentName) { this.attachmentName = attachmentName; }
|
||||
|
||||
public String getAttachmentType() { return attachmentType; }
|
||||
public void setAttachmentType(String attachmentType) { this.attachmentType = attachmentType; }
|
||||
}
|
||||
@@ -7,9 +7,13 @@ public class PetDTO {
|
||||
private String petBreed;
|
||||
private Integer petAge;
|
||||
private String petStatus;
|
||||
private String petPrice;
|
||||
private Double petPrice;
|
||||
private String createdAt;
|
||||
private String updatedAt;
|
||||
private Long customerId;
|
||||
private String customerName;
|
||||
private Long storeId;
|
||||
private String storeName;
|
||||
|
||||
public Long getPetId() { return petId; }
|
||||
public void setPetId(Long petId) { this.petId = petId; }
|
||||
@@ -29,12 +33,24 @@ public class PetDTO {
|
||||
public String getPetStatus() { return petStatus; }
|
||||
public void setPetStatus(String petStatus) { this.petStatus = petStatus; }
|
||||
|
||||
public String getPetPrice() { return petPrice; }
|
||||
public void setPetPrice(String petPrice) { this.petPrice = petPrice; }
|
||||
public Double getPetPrice() { return petPrice; }
|
||||
public void setPetPrice(Double petPrice) { this.petPrice = petPrice; }
|
||||
|
||||
public String getCreatedAt() { return createdAt; }
|
||||
public void setCreatedAt(String createdAt) { this.createdAt = createdAt; }
|
||||
|
||||
public String getUpdatedAt() { return 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; }
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@ public class PurchaseOrderDTO {
|
||||
private Long purchaseOrderId;
|
||||
private Long supId;
|
||||
private String supplierName;
|
||||
private Long storeId;
|
||||
private String storeName;
|
||||
private String orderDate;
|
||||
private String status;
|
||||
private String createdAt;
|
||||
@@ -21,6 +23,14 @@ public class PurchaseOrderDTO {
|
||||
return supplierName;
|
||||
}
|
||||
|
||||
public Long getStoreId() {
|
||||
return storeId;
|
||||
}
|
||||
|
||||
public String getStoreName() {
|
||||
return storeName;
|
||||
}
|
||||
|
||||
public String getOrderDate() {
|
||||
return orderDate;
|
||||
}
|
||||
|
||||
@@ -1,47 +1,53 @@
|
||||
package com.example.petstoremobile.fragments;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.provider.OpenableColumns;
|
||||
import android.util.Log;
|
||||
import android.view.*;
|
||||
import android.widget.*;
|
||||
import android.view.inputmethod.EditorInfo;
|
||||
import android.widget.Toast;
|
||||
import androidx.activity.result.ActivityResultLauncher;
|
||||
import androidx.activity.result.contract.ActivityResultContracts;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.core.view.GravityCompat;
|
||||
import androidx.drawerlayout.widget.DrawerLayout;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import com.bumptech.glide.Glide;
|
||||
import com.example.petstoremobile.R;
|
||||
import com.example.petstoremobile.adapters.ChatAdapter;
|
||||
import com.example.petstoremobile.adapters.MessageAdapter;
|
||||
import com.example.petstoremobile.api.auth.TokenManager;
|
||||
import com.example.petstoremobile.api.ChatApi;
|
||||
import com.example.petstoremobile.api.CustomerApi;
|
||||
import com.example.petstoremobile.api.MessageApi;
|
||||
import com.example.petstoremobile.api.RetrofitClient;
|
||||
import com.example.petstoremobile.databinding.FragmentChatBinding;
|
||||
import com.example.petstoremobile.dtos.ConversationDTO;
|
||||
import com.example.petstoremobile.dtos.CustomerDTO;
|
||||
import com.example.petstoremobile.dtos.MessageDTO;
|
||||
import com.example.petstoremobile.dtos.PageResponse;
|
||||
import com.example.petstoremobile.dtos.SendMessageRequest;
|
||||
import com.example.petstoremobile.models.Chat;
|
||||
import com.example.petstoremobile.models.Message;
|
||||
import com.example.petstoremobile.services.ChatNotificationService;
|
||||
import com.example.petstoremobile.utils.Resource;
|
||||
import com.example.petstoremobile.viewmodels.ChatViewModel;
|
||||
import com.example.petstoremobile.websocket.StompChatManager;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
import retrofit2.*;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Named;
|
||||
|
||||
import dagger.hilt.android.AndroidEntryPoint;
|
||||
|
||||
@AndroidEntryPoint
|
||||
public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickListener, StompChatManager.MessageListener,
|
||||
StompChatManager.ConversationListener, StompChatManager.ConnectionListener {
|
||||
|
||||
private static final String TAG = "ChatFragment";
|
||||
|
||||
// View
|
||||
private DrawerLayout drawerLayout;
|
||||
private RecyclerView rvChatList, rvMessages;
|
||||
private EditText etMessage;
|
||||
private Button btnSend;
|
||||
private TextView tvChatTitle;
|
||||
private FragmentChatBinding binding;
|
||||
private ChatViewModel viewModel;
|
||||
|
||||
// Adapters
|
||||
private ChatAdapter chatAdapter;
|
||||
@@ -51,71 +57,108 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis
|
||||
private final List<Chat> chatList = new ArrayList<>();
|
||||
private final List<Message> messageList = new ArrayList<>();
|
||||
private final Map<Long, String> customerNames = new HashMap<>();
|
||||
private Uri pendingAttachmentUri;
|
||||
|
||||
// APIs
|
||||
private ChatApi chatApi;
|
||||
private CustomerApi customerApi;
|
||||
private MessageApi messageApi;
|
||||
@Inject TokenManager tokenManager;
|
||||
@Inject @Named("baseUrl") String baseUrl;
|
||||
|
||||
// chat
|
||||
private Long currentUserId;
|
||||
private Long activeConversationId;
|
||||
private StompChatManager stompChatManager;
|
||||
private ActivityResultLauncher<Intent> attachmentLauncher;
|
||||
|
||||
/**
|
||||
* Initializes the attachment launcher to handle file selection from the gallery.
|
||||
*/
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
viewModel = new ViewModelProvider(this).get(ChatViewModel.class);
|
||||
attachmentLauncher = registerForActivityResult(
|
||||
new ActivityResultContracts.StartActivityForResult(),
|
||||
result -> {
|
||||
if (result.getResultCode() == Activity.RESULT_OK && result.getData() != null) {
|
||||
Uri uri = result.getData().getData();
|
||||
if (uri != null) {
|
||||
showAttachmentPreview(uri);
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Inflates the layout, initializes UI components, and sets up click listeners for messaging.
|
||||
*/
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater,
|
||||
ViewGroup container, Bundle savedInstanceState) {
|
||||
|
||||
View view = inflater.inflate(R.layout.fragment_chat, container, false);
|
||||
binding = FragmentChatBinding.inflate(inflater, container, false);
|
||||
|
||||
chatApi = RetrofitClient.getChatApi(requireContext());
|
||||
customerApi = RetrofitClient.getCustomerApi(requireContext());
|
||||
messageApi = RetrofitClient.getMessageApi(requireContext());
|
||||
binding.btnHamburger.setOnClickListener(v -> binding.chatDrawerLayout.openDrawer(GravityCompat.START));
|
||||
|
||||
drawerLayout = view.findViewById(R.id.chatDrawerLayout);
|
||||
rvChatList = view.findViewById(R.id.rvChatList);
|
||||
rvMessages = view.findViewById(R.id.rvMessages);
|
||||
etMessage = view.findViewById(R.id.etMessage);
|
||||
btnSend = view.findViewById(R.id.btnSend);
|
||||
tvChatTitle = view.findViewById(R.id.tvChatTitle);
|
||||
// Set editor action listener for message field to send when enter is pressed
|
||||
binding.etMessage.setOnEditorActionListener((v, actionId, event) -> {
|
||||
if (actionId == EditorInfo.IME_ACTION_SEND || actionId == EditorInfo.IME_NULL) {
|
||||
binding.btnSend.performClick();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
ImageButton hamburger = view.findViewById(R.id.btnHamburger);
|
||||
hamburger.setOnClickListener(v -> drawerLayout.openDrawer(GravityCompat.START));
|
||||
btnSend.setOnClickListener(v -> sendMessage());
|
||||
//When the send button is clicked check if there is an attachment and send using the correct helper function
|
||||
binding.btnSend.setOnClickListener(v -> {
|
||||
if (pendingAttachmentUri != null) {
|
||||
sendWithAttachment(pendingAttachmentUri);
|
||||
} else {
|
||||
sendMessage();
|
||||
}
|
||||
});
|
||||
|
||||
//When the attachment button is clicked open the file picker
|
||||
binding.btnAttach.setOnClickListener(v -> selectAttachment());
|
||||
binding.btnRemoveAttachment.setOnClickListener(v -> removeAttachment());
|
||||
|
||||
setupRecyclerViews();
|
||||
loadInitialData();
|
||||
|
||||
return view;
|
||||
return binding.getRoot();
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures the RecyclerViews for the conversation list and the message history.
|
||||
*/
|
||||
private void setupRecyclerViews() {
|
||||
// Set up Drawer menu to select conversation
|
||||
chatAdapter = new ChatAdapter(chatList, this);
|
||||
rvChatList.setLayoutManager(new LinearLayoutManager(getContext()));
|
||||
rvChatList.setAdapter(chatAdapter);
|
||||
binding.rvChatList.setLayoutManager(new LinearLayoutManager(getContext()));
|
||||
binding.rvChatList.setAdapter(chatAdapter);
|
||||
|
||||
// set up RecyclerView for selected chat to show messages
|
||||
messageAdapter = new MessageAdapter(messageList, null);
|
||||
LinearLayoutManager lm = new LinearLayoutManager(getContext());
|
||||
lm.setStackFromEnd(true);
|
||||
rvMessages.setLayoutManager(lm);
|
||||
rvMessages.setAdapter(messageAdapter);
|
||||
binding.rvMessages.setLayoutManager(lm);
|
||||
binding.rvMessages.setAdapter(messageAdapter);
|
||||
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() {
|
||||
TokenManager tm = TokenManager.getInstance(requireContext());
|
||||
String token = tm.getToken();
|
||||
currentUserId = tm.getUserId();
|
||||
String role = tm.getRole();
|
||||
String token = tokenManager.getToken();
|
||||
currentUserId = tokenManager.getUserId();
|
||||
String role = tokenManager.getRole();
|
||||
|
||||
messageAdapter.setCurrentUserId(currentUserId);
|
||||
messageAdapter.setToken(token);
|
||||
|
||||
// if token exist then connect to websocket
|
||||
if (token != null) {
|
||||
stompChatManager = new StompChatManager(token, role);
|
||||
stompChatManager = new StompChatManager(token, role, baseUrl);
|
||||
stompChatManager.setMessageListener(this);
|
||||
stompChatManager.setConversationListener(this);
|
||||
stompChatManager.setConnectionListener(this);
|
||||
@@ -126,89 +169,74 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis
|
||||
|
||||
if (getArguments() != null && getArguments().containsKey("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();
|
||||
}
|
||||
|
||||
//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() {
|
||||
customerApi.getAllCustomers(0, 100).enqueue(new Callback<PageResponse<CustomerDTO>>() {
|
||||
@Override
|
||||
public void onResponse(@NonNull Call<PageResponse<CustomerDTO>> call,
|
||||
@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) {
|
||||
viewModel.getAllCustomers(0, 100).observe(getViewLifecycleOwner(), resource -> {
|
||||
if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) {
|
||||
resource.data.getContent().forEach(c -> customerNames.put(c.getCustomerId(), c.getFullName()));
|
||||
loadConversations();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
//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() {
|
||||
chatApi.getAllConversations().enqueue(new Callback<List<ConversationDTO>>() {
|
||||
@Override
|
||||
public void onResponse(@NonNull Call<List<ConversationDTO>> call,
|
||||
@NonNull Response<List<ConversationDTO>> response) {
|
||||
if (response.isSuccessful() && response.body() != null) {
|
||||
chatList.clear();
|
||||
List<Chat> loaded = response.body().stream()
|
||||
.map(dto -> {
|
||||
String name = customerNames.getOrDefault(
|
||||
dto.getCustomerId(), "Customer #" + dto.getCustomerId());
|
||||
return new Chat(String.valueOf(dto.getId()),
|
||||
name, dto.getLastMessage(),
|
||||
dto.getCustomerId(), dto.getStaffId());
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
chatList.addAll(loaded);
|
||||
chatAdapter.notifyDataSetChanged();
|
||||
|
||||
if (activeConversationId != null) {
|
||||
setConversationActive(true);
|
||||
// Update title to customer name of active conversation
|
||||
for (Chat chat : chatList) {
|
||||
if (chat.getChatId().equals(String.valueOf(activeConversationId))) {
|
||||
tvChatTitle.setText(chat.getCustomerName());
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (stompChatManager != null) {
|
||||
stompChatManager.subscribeToConversation(activeConversationId);
|
||||
}
|
||||
loadMessageHistory(activeConversationId);
|
||||
} else {
|
||||
messageList.clear();
|
||||
messageAdapter.notifyDataSetChanged();
|
||||
setConversationActive(false);
|
||||
}
|
||||
viewModel.getAllConversations().observe(getViewLifecycleOwner(), resource -> {
|
||||
if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) {
|
||||
chatList.clear();
|
||||
for (ConversationDTO dto : resource.data) {
|
||||
String name = customerNames.getOrDefault(
|
||||
dto.getCustomerId(), "Customer #" + dto.getCustomerId());
|
||||
chatList.add(new Chat(String.valueOf(dto.getId()),
|
||||
name, dto.getLastMessage(),
|
||||
dto.getCustomerId(), dto.getStaffId()));
|
||||
}
|
||||
chatAdapter.notifyDataSetChanged();
|
||||
|
||||
if (activeConversationId != null) {
|
||||
setConversationActive(true);
|
||||
// Update title to customer name of active conversation
|
||||
for (Chat chat : chatList) {
|
||||
if (chat.getChatId().equals(String.valueOf(activeConversationId))) {
|
||||
binding.tvChatTitle.setText(chat.getCustomerName());
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (stompChatManager != null) {
|
||||
stompChatManager.subscribeToConversation(activeConversationId);
|
||||
}
|
||||
loadMessageHistory(activeConversationId);
|
||||
} else {
|
||||
messageList.clear();
|
||||
messageAdapter.notifyDataSetChanged();
|
||||
setConversationActive(false);
|
||||
}
|
||||
}
|
||||
@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
|
||||
public void onChatClick(Chat chat) {
|
||||
activeConversationId = Long.parseLong(chat.getChatId());
|
||||
setConversationActive(true);
|
||||
tvChatTitle.setText(chat.getCustomerName());
|
||||
drawerLayout.closeDrawer(GravityCompat.START);
|
||||
binding.tvChatTitle.setText(chat.getCustomerName());
|
||||
binding.chatDrawerLayout.closeDrawer(GravityCompat.START);
|
||||
|
||||
if (stompChatManager != null) {
|
||||
stompChatManager.subscribeToConversation(activeConversationId);
|
||||
@@ -217,63 +245,127 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis
|
||||
loadMessageHistory(activeConversationId);
|
||||
}
|
||||
|
||||
//helper function to load messages for selected chat
|
||||
/**
|
||||
* Fetches the full message history for a specific conversation from the ViewModel.
|
||||
*/
|
||||
private void loadMessageHistory(Long conversationId) {
|
||||
messageApi.getMessages(conversationId).enqueue(new Callback<List<MessageDTO>>() {
|
||||
@Override
|
||||
public void onResponse(@NonNull Call<List<MessageDTO>> call,
|
||||
@NonNull Response<List<MessageDTO>> response) {
|
||||
if (response.isSuccessful() && response.body() != null) {
|
||||
messageList.clear();
|
||||
for (MessageDTO dto : response.body()) {
|
||||
messageList.add(dtoToModel(dto));
|
||||
}
|
||||
messageAdapter.notifyDataSetChanged();
|
||||
scrollToBottom();
|
||||
viewModel.getMessages(conversationId).observe(getViewLifecycleOwner(), resource -> {
|
||||
if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) {
|
||||
messageList.clear();
|
||||
for (MessageDTO dto : resource.data) {
|
||||
messageList.add(dtoToModel(dto));
|
||||
}
|
||||
}
|
||||
@Override
|
||||
public void onFailure(@NonNull Call<List<MessageDTO>> call,
|
||||
@NonNull Throwable t) {
|
||||
Log.e(TAG, "Error loading messages", t);
|
||||
messageAdapter.notifyDataSetChanged();
|
||||
scrollToBottom();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
//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() {
|
||||
//check if a chat is selected
|
||||
if (activeConversationId == null) return;
|
||||
|
||||
//get the message from text field
|
||||
String text = etMessage.getText().toString().trim();
|
||||
String text = binding.etMessage.getText().toString().trim();
|
||||
if (text.isEmpty()) return;
|
||||
|
||||
//clear text field after sending
|
||||
etMessage.setText("");
|
||||
binding.etMessage.setText("");
|
||||
|
||||
//calls api to send the message
|
||||
messageApi.sendMessage(activeConversationId, new SendMessageRequest(text))
|
||||
.enqueue(new Callback<MessageDTO>() {
|
||||
@Override
|
||||
public void onResponse(@NonNull Call<MessageDTO> call,
|
||||
@NonNull Response<MessageDTO> response) {
|
||||
if (response.isSuccessful() && response.body() != null) {
|
||||
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);
|
||||
}
|
||||
});
|
||||
//calls viewmodel to send the message
|
||||
viewModel.sendMessage(activeConversationId, new SendMessageRequest(text)).observe(getViewLifecycleOwner(), resource -> {
|
||||
if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) {
|
||||
messageList.add(dtoToModel(resource.data));
|
||||
messageAdapter.notifyItemInserted(messageList.size() - 1);
|
||||
scrollToBottom();
|
||||
loadConversations();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// When a message is received updates the chat preview
|
||||
/**
|
||||
* Launches a file picker intent to select an attachment for the message.
|
||||
*/
|
||||
private void selectAttachment() {
|
||||
Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
|
||||
intent.setType("*/*");
|
||||
attachmentLauncher.launch(intent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays a preview of the selected attachment in the UI.
|
||||
*/
|
||||
private void showAttachmentPreview(Uri uri) {
|
||||
pendingAttachmentUri = uri;
|
||||
binding.layoutAttachmentPreview.setVisibility(View.VISIBLE);
|
||||
|
||||
String mimeType = requireContext().getContentResolver().getType(uri);
|
||||
String fileName = getFileName(uri);
|
||||
binding.tvPreviewName.setText(fileName);
|
||||
|
||||
// If the file is an image, display a thumbnail of the image as well
|
||||
if (mimeType != null && mimeType.startsWith("image/")) {
|
||||
binding.ivPreview.setVisibility(View.VISIBLE);
|
||||
Glide.with(this).load(uri).into(binding.ivPreview);
|
||||
} else {
|
||||
binding.ivPreview.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the current attachment selection and hides the preview UI.
|
||||
*/
|
||||
private void removeAttachment() {
|
||||
pendingAttachmentUri = null;
|
||||
binding.layoutAttachmentPreview.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the display name of the file from its Uri.
|
||||
*/
|
||||
private String getFileName(Uri uri) {
|
||||
String result = null;
|
||||
if (uri.getScheme().equals("content")) {
|
||||
try (Cursor cursor = requireContext().getContentResolver().query(uri, null, null, null, null)) {
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
int index = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME);
|
||||
if (index != -1) {
|
||||
result = cursor.getString(index);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (result == null) {
|
||||
result = uri.getPath();
|
||||
int cut = result.lastIndexOf('/');
|
||||
if (cut != -1) {
|
||||
result = result.substring(cut + 1);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles sending a message that includes a file attachment via the ViewModel.
|
||||
*/
|
||||
private void sendWithAttachment(Uri uri) {
|
||||
if (activeConversationId == null) return;
|
||||
String text = binding.etMessage.getText().toString().trim();
|
||||
binding.etMessage.setText("");
|
||||
removeAttachment();
|
||||
|
||||
if (!text.isEmpty()) {
|
||||
binding.etMessage.setText(text);
|
||||
}
|
||||
Toast.makeText(requireContext(), "File attachments are not supported", Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback triggered when a new message is received via the WebSocket.
|
||||
*/
|
||||
@Override
|
||||
public void onMessageReceived(MessageDTO dto) {
|
||||
//if there is no active selected conversation or the message received is for another chat, then just update the preview of last message
|
||||
@@ -287,81 +379,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
|
||||
messageList.add(dtoToModel(dto));
|
||||
messageAdapter.notifyItemInserted(messageList.size() - 1);
|
||||
scrollToBottom();
|
||||
requireActivity().runOnUiThread(() -> {
|
||||
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
|
||||
public void onConversationUpdated(ConversationDTO dto) {
|
||||
boolean updated = false;
|
||||
String name = customerNames.getOrDefault(
|
||||
dto.getCustomerId(), "Customer #" + dto.getCustomerId());
|
||||
requireActivity().runOnUiThread(() -> {
|
||||
boolean updated = false;
|
||||
String name = customerNames.getOrDefault(
|
||||
dto.getCustomerId(), "Customer #" + dto.getCustomerId());
|
||||
|
||||
for (int i = 0; i < chatList.size(); i++) {
|
||||
Chat existing = chatList.get(i);
|
||||
if (existing.getChatId().equals(String.valueOf(dto.getId()))) {
|
||||
chatList.set(i, new Chat(
|
||||
for (int i = 0; i < chatList.size(); i++) {
|
||||
Chat existing = chatList.get(i);
|
||||
if (existing.getChatId().equals(String.valueOf(dto.getId()))) {
|
||||
chatList.set(i, new Chat(
|
||||
String.valueOf(dto.getId()),
|
||||
name,
|
||||
dto.getLastMessage(),
|
||||
dto.getCustomerId(),
|
||||
dto.getStaffId()
|
||||
));
|
||||
chatAdapter.notifyItemChanged(i);
|
||||
updated = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!updated) {
|
||||
chatList.add(0, new Chat(
|
||||
String.valueOf(dto.getId()),
|
||||
name,
|
||||
dto.getLastMessage(),
|
||||
dto.getCustomerId(),
|
||||
dto.getStaffId()
|
||||
));
|
||||
chatAdapter.notifyItemChanged(i);
|
||||
updated = true;
|
||||
break;
|
||||
chatAdapter.notifyItemInserted(0);
|
||||
}
|
||||
}
|
||||
|
||||
if (!updated) {
|
||||
chatList.add(0, new Chat(
|
||||
String.valueOf(dto.getId()),
|
||||
name,
|
||||
dto.getLastMessage(),
|
||||
dto.getCustomerId(),
|
||||
dto.getStaffId()
|
||||
));
|
||||
chatAdapter.notifyItemInserted(0);
|
||||
}
|
||||
|
||||
if (activeConversationId != null && activeConversationId.equals(dto.getId())) {
|
||||
setConversationActive(true);
|
||||
tvChatTitle.setText(name);
|
||||
}
|
||||
if (activeConversationId != null && activeConversationId.equals(dto.getId())) {
|
||||
setConversationActive(true);
|
||||
binding.tvChatTitle.setText(name);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback triggered when the WebSocket connection is successfully opened.
|
||||
*/
|
||||
@Override
|
||||
public void onSocketOpened() {
|
||||
if (!isAdded()) {
|
||||
return;
|
||||
}
|
||||
loadConversations();
|
||||
if (activeConversationId != null) {
|
||||
loadMessageHistory(activeConversationId);
|
||||
}
|
||||
requireActivity().runOnUiThread(() -> {
|
||||
loadConversations();
|
||||
if (activeConversationId != null) {
|
||||
loadMessageHistory(activeConversationId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback triggered when the WebSocket connection is closed.
|
||||
*/
|
||||
@Override
|
||||
public void onSocketClosed() {
|
||||
if (!isAdded()) {
|
||||
return;
|
||||
}
|
||||
loadConversations();
|
||||
requireActivity().runOnUiThread(this::loadConversations);
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback triggered when a WebSocket connection error occurs.
|
||||
*/
|
||||
@Override
|
||||
public void onSocketError() {
|
||||
if (!isAdded()) {
|
||||
return;
|
||||
}
|
||||
loadConversations();
|
||||
if (activeConversationId != null) {
|
||||
loadMessageHistory(activeConversationId);
|
||||
}
|
||||
requireActivity().runOnUiThread(() -> {
|
||||
loadConversations();
|
||||
if (activeConversationId != null) {
|
||||
loadMessageHistory(activeConversationId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Helper function to convert DTO to message
|
||||
/**
|
||||
* Converts a MessageDTO into a Message object.
|
||||
*/
|
||||
private Message dtoToModel(MessageDTO dto) {
|
||||
Message m = new Message();
|
||||
m.setId(dto.getId());
|
||||
@@ -370,64 +483,80 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis
|
||||
m.setContent(dto.getContent());
|
||||
m.setTimestamp(dto.getTimestamp());
|
||||
m.setIsRead(dto.getIsRead());
|
||||
m.setAttachmentUrl(dto.getAttachmentUrl());
|
||||
m.setAttachmentName(dto.getAttachmentName());
|
||||
m.setAttachmentType(dto.getAttachmentType());
|
||||
return m;
|
||||
}
|
||||
|
||||
//Helper function to scroll to bottom of the chat
|
||||
/**
|
||||
* Scrolls the message history RecyclerView to the most recent message.
|
||||
*/
|
||||
private void scrollToBottom() {
|
||||
if (!messageList.isEmpty()) {
|
||||
rvMessages.post(() ->
|
||||
rvMessages.smoothScrollToPosition(messageList.size() - 1));
|
||||
binding.rvMessages.post(() ->
|
||||
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) {
|
||||
if (conversationId == null) {
|
||||
return;
|
||||
}
|
||||
for (int i = 0; i < chatList.size(); i++) {
|
||||
Chat existing = chatList.get(i);
|
||||
if (existing.getChatId().equals(String.valueOf(conversationId))) {
|
||||
Chat updated = new Chat(
|
||||
existing.getChatId(),
|
||||
existing.getCustomerName(),
|
||||
lastMessage,
|
||||
existing.getCustomerId(),
|
||||
existing.getStaffId()
|
||||
);
|
||||
chatList.set(i, updated);
|
||||
chatAdapter.notifyItemChanged(i);
|
||||
return;
|
||||
requireActivity().runOnUiThread(() -> {
|
||||
for (int i = 0; i < chatList.size(); i++) {
|
||||
Chat existing = chatList.get(i);
|
||||
if (existing.getChatId().equals(String.valueOf(conversationId))) {
|
||||
Chat updated = new Chat(
|
||||
existing.getChatId(),
|
||||
existing.getCustomerName(),
|
||||
lastMessage,
|
||||
existing.getCustomerId(),
|
||||
existing.getStaffId()
|
||||
);
|
||||
chatList.set(i, updated);
|
||||
chatAdapter.notifyItemChanged(i);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
//Helper function to enable or disable the send button when there is no active chat
|
||||
/**
|
||||
* Toggles the UI state based on whether a conversation is currently selected.
|
||||
*/
|
||||
private void setConversationActive(boolean active) {
|
||||
btnSend.setEnabled(active);
|
||||
etMessage.setEnabled(active);
|
||||
binding.btnSend.setEnabled(active);
|
||||
binding.etMessage.setEnabled(active);
|
||||
binding.btnAttach.setEnabled(active);
|
||||
if (!active) {
|
||||
activeConversationId = null;
|
||||
ChatNotificationService.activeConversationIdInUi = null;
|
||||
if (tvChatTitle != null) tvChatTitle.setText("Customer Chat");
|
||||
removeAttachment();
|
||||
if (binding != null && binding.tvChatTitle != null) binding.tvChatTitle.setText("Customer Chat");
|
||||
if (stompChatManager != null) {
|
||||
stompChatManager.clearConversationSubscription();
|
||||
}
|
||||
messageList.clear();
|
||||
messageAdapter.notifyDataSetChanged();
|
||||
etMessage.setText("");
|
||||
etMessage.setHint("Select a chat to start messaging");
|
||||
binding.etMessage.setText("");
|
||||
binding.etMessage.setHint("Select a chat to start messaging");
|
||||
} else {
|
||||
etMessage.setHint("Type a message...");
|
||||
binding.etMessage.setHint("Type a message...");
|
||||
ChatNotificationService.activeConversationIdInUi = activeConversationId;
|
||||
}
|
||||
}
|
||||
|
||||
// When fragment is destroyed, disconnect from websocket
|
||||
/**
|
||||
* Disconnects the WebSocket manager when the fragment view is destroyed.
|
||||
*/
|
||||
@Override
|
||||
public void onDestroyView() {
|
||||
super.onDestroyView();
|
||||
binding = null;
|
||||
ChatNotificationService.activeConversationIdInUi = null;
|
||||
if (stompChatManager != null) stompChatManager.disconnect();
|
||||
}
|
||||
|
||||
@@ -2,110 +2,74 @@ package com.example.petstoremobile.fragments;
|
||||
|
||||
import android.os.Bundle;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.view.GravityCompat;
|
||||
import androidx.drawerlayout.widget.DrawerLayout;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.navigation.NavController;
|
||||
import androidx.navigation.fragment.NavHostFragment;
|
||||
|
||||
import android.view.LayoutInflater;
|
||||
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.LinearLayout;
|
||||
|
||||
import com.example.petstoremobile.R;
|
||||
|
||||
import com.example.petstoremobile.api.auth.TokenManager;
|
||||
import com.example.petstoremobile.fragments.listfragments.PetFragment;
|
||||
import com.example.petstoremobile.fragments.listfragments.ServiceFragment;
|
||||
import com.example.petstoremobile.fragments.listfragments.StaffFragment;
|
||||
import com.example.petstoremobile.fragments.listfragments.SupplierFragment;
|
||||
import com.example.petstoremobile.fragments.listfragments.AdoptionFragment;
|
||||
import com.example.petstoremobile.fragments.listfragments.AppointmentFragment;
|
||||
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;
|
||||
import com.example.petstoremobile.fragments.listfragments.AnalyticsFragment;
|
||||
import com.example.petstoremobile.databinding.FragmentListBinding;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import dagger.hilt.android.AndroidEntryPoint;
|
||||
|
||||
//The Fragment for the displaying the list of entities to be viewed
|
||||
@AndroidEntryPoint
|
||||
public class ListFragment extends Fragment {
|
||||
|
||||
private DrawerLayout drawerLayout;
|
||||
private LinearLayout drawerPets, drawerServices, drawerSuppliers;
|
||||
private View touchBlocker;
|
||||
|
||||
// Adoptions, Appointments, Inventory, Products
|
||||
|
||||
private LinearLayout drawerAdoptions, drawerAppointments, drawerInventory, drawerProducts, drawerProductSupplier, drawerPurchaseOrderView, drawerSale;
|
||||
|
||||
private LinearLayout drawerAnalytics;
|
||||
|
||||
//Staff
|
||||
|
||||
private LinearLayout drawerStaff;
|
||||
private FragmentListBinding binding;
|
||||
private NavController innerNavController;
|
||||
|
||||
@Inject TokenManager tokenManager;
|
||||
|
||||
/**
|
||||
* Inflates the fragment layout, initializes navigation drawers, and applies role-based access control.
|
||||
*/
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container,
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
|
||||
Bundle savedInstanceState) {
|
||||
View view = inflater.inflate(R.layout.fragment_list, 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);
|
||||
drawerAnalytics = view.findViewById(R.id.drawerAnalytics);
|
||||
drawerPurchaseOrderView=view.findViewById(R.id.drawerPurchaseOrderView);
|
||||
drawerStaff = view.findViewById(R.id.drawerStaff);
|
||||
|
||||
binding = FragmentListBinding.inflate(inflater, container, false);
|
||||
|
||||
// Check user role and restrict access for STAFF
|
||||
String role = TokenManager.getInstance(requireContext()).getRole();
|
||||
String role = tokenManager.getRole();
|
||||
if ("STAFF".equalsIgnoreCase(role)) {
|
||||
drawerSuppliers.setVisibility(View.GONE);
|
||||
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());
|
||||
binding.drawerSuppliers.setVisibility(View.GONE);
|
||||
binding.drawerInventory.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
// Only show for ADMIN
|
||||
if ("ADMIN".equalsIgnoreCase(role)) {
|
||||
drawerStaff.setVisibility(View.VISIBLE);
|
||||
binding.drawerStaff.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
drawerStaff.setVisibility(View.GONE);
|
||||
binding.drawerStaff.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
//add Listeners to the drawer so user won't be able to interact with the innerContainer (the list fragments)
|
||||
//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
|
||||
@Override
|
||||
public void onDrawerOpened(View drawerView) {
|
||||
touchBlocker.setVisibility(View.VISIBLE);
|
||||
touchBlocker.setClickable(true);
|
||||
binding.touchBlocker.setVisibility(View.VISIBLE);
|
||||
binding.touchBlocker.setClickable(true);
|
||||
}
|
||||
|
||||
//When the drawer is closed, enable touches again
|
||||
@Override
|
||||
public void onDrawerClosed(View drawerView) {
|
||||
touchBlocker.setVisibility(View.GONE);
|
||||
touchBlocker.setClickable(false);
|
||||
binding.touchBlocker.setVisibility(View.GONE);
|
||||
binding.touchBlocker.setClickable(false);
|
||||
}
|
||||
|
||||
//unused methods
|
||||
@@ -116,99 +80,55 @@ public class ListFragment extends Fragment {
|
||||
});
|
||||
|
||||
// Click listeners for each drawer
|
||||
//Pets
|
||||
drawerPets.setOnClickListener(v -> {
|
||||
loadFragment(new PetFragment());
|
||||
drawerLayout.closeDrawers();
|
||||
});
|
||||
binding.drawerPets.setOnClickListener(v -> navigateTo(R.id.nav_pet));
|
||||
binding.drawerServices.setOnClickListener(v -> navigateTo(R.id.nav_service));
|
||||
binding.drawerSuppliers.setOnClickListener(v -> navigateTo(R.id.nav_supplier));
|
||||
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));
|
||||
binding.drawerStaff.setOnClickListener(v -> navigateTo(R.id.nav_staff));
|
||||
binding.drawerAnalytics.setOnClickListener(v -> navigateTo(R.id.nav_analytics));
|
||||
|
||||
//Services
|
||||
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();
|
||||
});
|
||||
|
||||
//Analytics
|
||||
|
||||
drawerAnalytics.setOnClickListener(v -> {
|
||||
loadFragment(new AnalyticsFragment());
|
||||
drawerLayout.closeDrawers();
|
||||
});
|
||||
|
||||
// Click listener
|
||||
drawerStaff.setOnClickListener(v -> {
|
||||
loadFragment(new StaffFragment());
|
||||
drawerLayout.closeDrawers();
|
||||
});
|
||||
|
||||
|
||||
|
||||
return view;
|
||||
return binding.getRoot();
|
||||
}
|
||||
|
||||
//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() {
|
||||
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();
|
||||
binding.drawerLayout.openDrawer(GravityCompat.START);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,186 +1,109 @@
|
||||
package com.example.petstoremobile.fragments;
|
||||
|
||||
import android.Manifest;
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
|
||||
import androidx.activity.result.ActivityResultLauncher;
|
||||
import androidx.activity.result.contract.ActivityResultContracts;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.core.content.FileProvider;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
|
||||
import android.provider.MediaStore;
|
||||
import android.text.InputType;
|
||||
import android.util.Log;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.Button;
|
||||
import android.widget.EditText;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.bumptech.glide.Glide;
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy;
|
||||
import com.bumptech.glide.load.model.GlideUrl;
|
||||
import com.bumptech.glide.load.model.LazyHeaders;
|
||||
import com.example.petstoremobile.R;
|
||||
import com.example.petstoremobile.activities.MainActivity;
|
||||
import com.example.petstoremobile.api.RetrofitClient;
|
||||
import com.example.petstoremobile.api.auth.AuthApi;
|
||||
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.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.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.FileOutputStream;
|
||||
import java.io.InputStream;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Named;
|
||||
|
||||
import dagger.hilt.android.AndroidEntryPoint;
|
||||
import okhttp3.MediaType;
|
||||
import okhttp3.MultipartBody;
|
||||
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 {
|
||||
|
||||
//initialize the view/controls
|
||||
private ImageView imgProfile;
|
||||
private TextView tvProfileName, tvProfileEmail, tvProfilePhone, tvProfileRole;
|
||||
private Uri photoUri;
|
||||
private FragmentProfileBinding binding;
|
||||
private UserDTO currentUser;
|
||||
private AuthViewModel viewModel;
|
||||
private boolean hasImage = false;
|
||||
|
||||
//Initialize the launchers for camera and gallery
|
||||
private ActivityResultLauncher<Intent> galleryLauncher;
|
||||
private ActivityResultLauncher<Uri> cameraLauncher;
|
||||
private ActivityResultLauncher<String> permissionLauncher;
|
||||
@Inject TokenManager tokenManager;
|
||||
@Inject @Named("baseUrl") String baseUrl;
|
||||
|
||||
//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
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
viewModel = new ViewModelProvider(this).get(AuthViewModel.class);
|
||||
|
||||
// Launcher to open gallery to select profile image
|
||||
galleryLauncher = registerForActivityResult(
|
||||
//open gallery
|
||||
new ActivityResultContracts.StartActivityForResult(),
|
||||
result -> {
|
||||
//if the user selects an image and its not null
|
||||
if (result.getResultCode() == Activity.RESULT_OK
|
||||
&& result.getData() != null) {
|
||||
//get the selected image and set the image to the profile
|
||||
Uri selectedImage = result.getData().getData();
|
||||
uploadAvatar(selectedImage);
|
||||
}
|
||||
}
|
||||
);
|
||||
imagePickerHelper = new ImagePickerHelper(this, "profile_photo.jpg", new ImagePickerHelper.ImagePickerListener() {
|
||||
@Override
|
||||
public void onImagePicked(Uri uri) {
|
||||
uploadAvatar(uri);
|
||||
}
|
||||
|
||||
// Launcher for camera to open and capture profile image
|
||||
cameraLauncher = registerForActivityResult(
|
||||
//open camera
|
||||
new ActivityResultContracts.TakePicture(),
|
||||
success -> {
|
||||
//if a photo is taken set the image profile to it otherwise do nothing
|
||||
if (success) {
|
||||
uploadAvatar(photoUri);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// Launcher to request camera permission
|
||||
permissionLauncher = registerForActivityResult(
|
||||
//ask user for camera permission
|
||||
new ActivityResultContracts.RequestPermission(),
|
||||
granted -> {
|
||||
//if the permission is granted launch the camera
|
||||
if (granted) {
|
||||
launchCamera();
|
||||
}
|
||||
else {
|
||||
//if the permission is denied then tell the user to grant it
|
||||
new AlertDialog.Builder(requireContext())
|
||||
.setTitle("Permission Permission Required")
|
||||
.setMessage("Please grant camera permission to use this feature")
|
||||
.setPositiveButton("Open Settings", (dialog, which) ->{
|
||||
//open the settings page to grant the permission when they click open settings
|
||||
Intent intent = new Intent(android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
|
||||
intent.setData(Uri.fromParts("package", requireContext().getPackageName(), null));
|
||||
startActivity(intent);
|
||||
})
|
||||
//close the dialog when the user clicks cancel
|
||||
.setNegativeButton("Cancel", null)
|
||||
.show();
|
||||
}
|
||||
}
|
||||
);
|
||||
@Override
|
||||
public void onImageRemoved() {
|
||||
deleteAvatar();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Inflates the fragment layout and sets up listeners for profile.
|
||||
*/
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container,
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
|
||||
Bundle savedInstanceState) {
|
||||
View view = inflater.inflate(R.layout.fragment_profile, 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);
|
||||
binding = FragmentProfileBinding.inflate(inflater, container, false);
|
||||
|
||||
//Load Profile Data from backend
|
||||
loadProfileData();
|
||||
|
||||
//Set up listeners for the buttons
|
||||
//Change photo button
|
||||
btnChangePhoto.setOnClickListener(v -> {
|
||||
//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(new String[]{"Take Photo", "Choose from Gallery"}, (dialog, which) -> {
|
||||
if (which == 0) {
|
||||
// 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 {
|
||||
// Choose Gallery
|
||||
Intent intent = new Intent(Intent.ACTION_PICK,
|
||||
MediaStore.Images.Media.EXTERNAL_CONTENT_URI);
|
||||
galleryLauncher.launch(intent);
|
||||
}
|
||||
})
|
||||
.show();
|
||||
binding.btnChangePhoto.setOnClickListener(v -> {
|
||||
imagePickerHelper.showImagePickerDialog("Change Profile Photo", hasImage);
|
||||
});
|
||||
|
||||
//Edit email button
|
||||
//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
|
||||
EditText input = new EditText(requireContext());
|
||||
input.setPadding(30,30,30,30);
|
||||
input.setText(tvProfileEmail.getText().toString());
|
||||
input.setText(binding.tvProfileEmail.getText().toString());
|
||||
|
||||
//set input type to email
|
||||
input.setInputType(android.text.InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS);
|
||||
@@ -203,18 +126,17 @@ public class ProfileFragment extends Fragment {
|
||||
|
||||
//Edit phone button
|
||||
//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
|
||||
EditText input = new EditText(requireContext());
|
||||
input.setPadding(30,30,30,30);
|
||||
input.setText(tvProfilePhone.getText().toString());
|
||||
input.setText(binding.tvProfilePhone.getText().toString());
|
||||
|
||||
//set input type to phone number
|
||||
input.setInputType(InputType.TYPE_CLASS_PHONE);
|
||||
input.setInputType(android.view.inputmethod.EditorInfo.TYPE_CLASS_PHONE);
|
||||
|
||||
//add canada phone number formatting to input (XXX) XXX-XXXX
|
||||
input.addTextChangedListener(new android.telephony.PhoneNumberFormattingTextWatcher("CA"));
|
||||
input.setFilters(new android.text.InputFilter[]{new android.text.InputFilter.LengthFilter(14)});
|
||||
//add canada phone number formatting to input
|
||||
UIUtils.formatPhoneInput(input);
|
||||
|
||||
|
||||
//Show alert dialog to user to enter new phone
|
||||
@@ -233,93 +155,71 @@ public class ProfileFragment extends Fragment {
|
||||
});
|
||||
|
||||
//Logout button
|
||||
btnLogout.setOnClickListener(v -> {
|
||||
binding.btnLogout.setOnClickListener(v -> {
|
||||
// Stop notification service before logging out so notifications stop
|
||||
Intent serviceIntent = new Intent(requireContext(), ChatNotificationService.class);
|
||||
android.content.Intent serviceIntent = new android.content.Intent(requireContext(), ChatNotificationService.class);
|
||||
requireContext().stopService(serviceIntent);
|
||||
|
||||
TokenManager.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
|
||||
Intent intent = new Intent(getActivity(), MainActivity.class);
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
android.content.Intent intent = new android.content.Intent(getActivity(), MainActivity.class);
|
||||
intent.addFlags(android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP | android.content.Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
//start the activity to go to login page and finish the current activity
|
||||
startActivity(intent);
|
||||
requireActivity().finish();
|
||||
});
|
||||
|
||||
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
|
||||
private void launchCamera() {
|
||||
//create a file in the cache directory to store the photo in
|
||||
File photoFile = new File(requireContext().getCacheDir(), "profile_photo.jpg");
|
||||
//get the uri for the file made
|
||||
photoUri = FileProvider.getUriForFile(requireContext(), requireContext().getPackageName() + ".fileprovider", photoFile);
|
||||
//launch the camera to capture the photo and save the photo to photoUri
|
||||
cameraLauncher.launch(photoUri);
|
||||
@Override
|
||||
public void onDestroyView() {
|
||||
super.onDestroyView();
|
||||
binding = null;
|
||||
}
|
||||
|
||||
//Helper function to call the backend to get profile data and load it to the view
|
||||
/**
|
||||
* Fetches current user profile data from the API and then updates the UI.
|
||||
*/
|
||||
private void loadProfileData() {
|
||||
AuthApi 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>() {
|
||||
@Override
|
||||
public void onResponse(Call<UserDTO> call, Response<UserDTO> response) {
|
||||
//if the response is successful and the body is not null then set the user to the view
|
||||
if (response.isSuccessful() && response.body() != null) {
|
||||
currentUser = response.body();
|
||||
//set the user data to the view
|
||||
binding.tvProfileName.setText(currentUser.getFullName());
|
||||
binding.tvProfileEmail.setText(currentUser.getEmail());
|
||||
binding.tvProfilePhone.setText(currentUser.getPhone());
|
||||
binding.tvProfileRole.setText(currentUser.getRole());
|
||||
|
||||
//set the user data to the view
|
||||
tvProfileName.setText(currentUser.getFullName());
|
||||
tvProfileEmail.setText(currentUser.getEmail());
|
||||
tvProfilePhone.setText(currentUser.getPhone());
|
||||
tvProfileRole.setText(currentUser.getRole());
|
||||
// get the avatar endpoint to load profile image and the token for authorization
|
||||
String avatarUrl = baseUrl + AuthApi.AVATAR_FILE_PATH;
|
||||
String token = tokenManager.getToken();
|
||||
|
||||
// get the avatar endpoint to load profile image and the token for authorization
|
||||
String avatarUrl = RetrofitClient.BASE_URL + AuthApi.AVATAR_FILE_PATH;
|
||||
String token = TokenManager.getInstance(requireContext()).getToken();
|
||||
|
||||
if (token != null) {
|
||||
// Create GlideUrl with token to fetch the image
|
||||
GlideUrl glideUrl = new GlideUrl(avatarUrl, new LazyHeaders.Builder()
|
||||
.addHeader("Authorization", "Bearer " + token)
|
||||
.build());
|
||||
|
||||
// Load image using Glide
|
||||
Glide.with(ProfileFragment.this)
|
||||
.load(glideUrl)
|
||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||
.skipMemoryCache(true)
|
||||
.placeholder(R.drawable.placeholder)
|
||||
.error(R.drawable.placeholder)
|
||||
.into(imgProfile);
|
||||
} else {
|
||||
// load placeholder image if token is null
|
||||
Glide.with(ProfileFragment.this)
|
||||
.load(R.drawable.placeholder)
|
||||
.into(imgProfile);
|
||||
GlideUtils.loadImageWithToken(requireContext(), binding.imgProfile, avatarUrl, token, R.drawable.placeholder, new GlideUtils.ImageLoadListener() {
|
||||
@Override
|
||||
public void onResourceReady() {
|
||||
hasImage = true;
|
||||
}
|
||||
}
|
||||
else {
|
||||
Log.e("onResponse: ", response.message());
|
||||
Toast.makeText(getContext(), "Failed to load profile: ", Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(Call<UserDTO> call, Throwable t) {
|
||||
Log.e("PROFILE", "onFailure: " + t.getMessage());
|
||||
Toast.makeText(getContext(), "Network error: could not load profile", Toast.LENGTH_SHORT).show();
|
||||
@Override
|
||||
public void onLoadFailed() {
|
||||
hasImage = false;
|
||||
}
|
||||
});
|
||||
} else if (resource.status == Resource.Status.ERROR) {
|
||||
Toast.makeText(getContext(), "Failed to load profile: " + resource.message, Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
//Helper function to call the backend to upload a profile image
|
||||
/**
|
||||
* Uploads the selected or captured image as the user's new avatar.
|
||||
*/
|
||||
private void uploadAvatar(Uri uri) {
|
||||
try {
|
||||
File file = getFileFromUri(uri);
|
||||
File file = FileUtils.getFileFromUri(requireContext(), uri);
|
||||
if (file == null) return;
|
||||
|
||||
// Create RequestBody for file upload
|
||||
@@ -327,24 +227,13 @@ public class ProfileFragment extends Fragment {
|
||||
MultipartBody.Part body = MultipartBody.Part.createFormData("avatar", file.getName(), requestFile);
|
||||
|
||||
//Call the backend to upload the avatar
|
||||
AuthApi authApi = RetrofitClient.getAuthApi(requireContext());
|
||||
authApi.uploadAvatar(body).enqueue(new Callback<UserDTO>() {
|
||||
@Override
|
||||
public void onResponse(Call<UserDTO> call, Response<UserDTO> response) {
|
||||
if (response.isSuccessful() && response.body() != null) {
|
||||
currentUser = response.body();
|
||||
Toast.makeText(requireContext(), "Avatar updated successfully", Toast.LENGTH_SHORT).show();
|
||||
// Reload image after successful upload
|
||||
loadProfileData();
|
||||
} 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();
|
||||
viewModel.uploadAvatar(body).observe(getViewLifecycleOwner(), resource -> {
|
||||
if (resource == null) return;
|
||||
if (resource.status == Resource.Status.SUCCESS) {
|
||||
Toast.makeText(getContext(), "Avatar updated successfully", Toast.LENGTH_SHORT).show();
|
||||
loadProfileData();
|
||||
} else if (resource.status == Resource.Status.ERROR) {
|
||||
Toast.makeText(getContext(), "Upload failed: " + resource.message, Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
});
|
||||
} catch (Exception e) {
|
||||
@@ -352,58 +241,39 @@ public class ProfileFragment extends Fragment {
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to create a temporary File object from a Uri for uploading the avatar
|
||||
private File getFileFromUri(Uri uri) {
|
||||
try {
|
||||
InputStream inputStream = requireContext().getContentResolver().openInputStream(uri);
|
||||
File tempFile = new File(requireContext().getCacheDir(), "upload_avatar.jpg");
|
||||
FileOutputStream outputStream = new FileOutputStream(tempFile);
|
||||
byte[] buffer = new byte[1024];
|
||||
int length;
|
||||
while ((length = inputStream.read(buffer)) > 0) {
|
||||
outputStream.write(buffer, 0, length);
|
||||
/**
|
||||
* Sends a request to the API to delete the current user's avatar image.
|
||||
*/
|
||||
private void deleteAvatar() {
|
||||
viewModel.deleteAvatar().observe(getViewLifecycleOwner(), resource -> {
|
||||
if (resource == null) return;
|
||||
if (resource.status == Resource.Status.SUCCESS) {
|
||||
hasImage = false;
|
||||
binding.imgProfile.setImageResource(R.drawable.placeholder);
|
||||
Toast.makeText(getContext(), "Avatar removed successfully", Toast.LENGTH_SHORT).show();
|
||||
} else if (resource.status == Resource.Status.ERROR) {
|
||||
Toast.makeText(getContext(), "Removal failed: " + resource.message, Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
outputStream.close();
|
||||
inputStream.close();
|
||||
return tempFile;
|
||||
} catch (Exception e) {
|
||||
Log.e("FILE_UTILS", "Error creating temp file", e);
|
||||
return null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
//Helper function to update a profile field in the backend
|
||||
/**
|
||||
* Updates a specific profile field (like email or phone) by sending a request to the API.
|
||||
*/
|
||||
private void updateProfileField(String fieldName, String value) {
|
||||
AuthApi authApi = RetrofitClient.getAuthApi(requireContext());
|
||||
Map<String, String> updates = new HashMap<>();
|
||||
updates.put(fieldName, value);
|
||||
|
||||
authApi.updateMe(updates).enqueue(new Callback<UserDTO>() {
|
||||
@Override
|
||||
public void onResponse(Call<UserDTO> call, Response<UserDTO> response) {
|
||||
if (response.isSuccessful() && response.body() != null) {
|
||||
currentUser = response.body();
|
||||
// Update the view with the new data from backend
|
||||
tvProfileEmail.setText(currentUser.getEmail());
|
||||
tvProfilePhone.setText(currentUser.getPhone());
|
||||
Toast.makeText(requireContext(), "Profile updated successfully", Toast.LENGTH_SHORT).show();
|
||||
} else {
|
||||
try {
|
||||
String errorJson = response.errorBody().string();
|
||||
ErrorResponse errorResponse = new Gson().fromJson(errorJson, ErrorResponse.class);
|
||||
String errorMessage = errorResponse.getMessage();
|
||||
Toast.makeText(requireContext(), errorMessage, Toast.LENGTH_LONG).show();
|
||||
} catch (Exception e) {
|
||||
Log.e("UPDATE_PROFILE", "Error parsing error body", e);
|
||||
Toast.makeText(requireContext(), "Failed to update profile", Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@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();
|
||||
viewModel.updateMe(updates).observe(getViewLifecycleOwner(), resource -> {
|
||||
if (resource == null) return;
|
||||
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
|
||||
currentUser = resource.data;
|
||||
Toast.makeText(getContext(), "Profile updated successfully", Toast.LENGTH_SHORT).show();
|
||||
// Update the view with the new data from backend
|
||||
binding.tvProfileEmail.setText(currentUser.getEmail());
|
||||
binding.tvProfilePhone.setText(currentUser.getPhone());
|
||||
} else if (resource.status == Resource.Status.ERROR) {
|
||||
Toast.makeText(getContext(), "Update failed: " + resource.message, Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,259 +1,374 @@
|
||||
package com.example.petstoremobile.fragments.listfragments;
|
||||
|
||||
import android.graphics.Color;
|
||||
import android.os.Bundle;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
import androidx.navigation.fragment.NavHostFragment;
|
||||
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.EditText;
|
||||
import android.widget.ImageButton;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.example.petstoremobile.R;
|
||||
import com.example.petstoremobile.adapters.AppointmentAdapter;
|
||||
import com.example.petstoremobile.api.AppointmentApi;
|
||||
import com.example.petstoremobile.api.PetApi;
|
||||
import com.example.petstoremobile.api.ServiceApi;
|
||||
import com.example.petstoremobile.api.RetrofitClient;
|
||||
import com.example.petstoremobile.databinding.FragmentAppointmentBinding;
|
||||
import com.example.petstoremobile.dtos.AppointmentDTO;
|
||||
import com.example.petstoremobile.dtos.ServiceDTO;
|
||||
import com.example.petstoremobile.dtos.PageResponse;
|
||||
import com.example.petstoremobile.dtos.PetDTO;
|
||||
import com.example.petstoremobile.dtos.StoreDTO;
|
||||
import com.example.petstoremobile.fragments.ListFragment;
|
||||
import com.example.petstoremobile.fragments.listfragments.detailfragments.AppointmentDetailFragment;
|
||||
import com.google.android.material.floatingactionbutton.FloatingActionButton;
|
||||
import com.example.petstoremobile.utils.BulkDeleteHandler;
|
||||
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.viewmodels.AuthViewModel;
|
||||
import com.example.petstoremobile.viewmodels.StoreViewModel;
|
||||
import com.prolificinteractive.materialcalendarview.CalendarDay;
|
||||
import com.prolificinteractive.materialcalendarview.CalendarMode;
|
||||
|
||||
import java.text.ParseException;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Calendar;
|
||||
import java.util.Date;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
import retrofit2.Call;
|
||||
import retrofit2.Callback;
|
||||
import retrofit2.Response;
|
||||
import dagger.hilt.android.AndroidEntryPoint;
|
||||
|
||||
@AndroidEntryPoint
|
||||
public class AppointmentFragment extends Fragment implements AppointmentAdapter.OnAppointmentClickListener {
|
||||
|
||||
private FragmentAppointmentBinding binding;
|
||||
private List<AppointmentDTO> appointmentList = new ArrayList<>();
|
||||
private List<AppointmentDTO> filteredList = new ArrayList<>();
|
||||
private List<PetDTO> petList = new ArrayList<>();
|
||||
private List<ServiceDTO> serviceList = new ArrayList<>();
|
||||
private List<StoreDTO> storeList = new ArrayList<>();
|
||||
|
||||
private AppointmentAdapter adapter;
|
||||
private AppointmentApi api;
|
||||
private SwipeRefreshLayout swipeRefreshLayout;
|
||||
private EditText etSearch;
|
||||
private ImageButton hamburger;
|
||||
private AppointmentViewModel appointmentViewModel;
|
||||
private StoreViewModel storeViewModel;
|
||||
private AuthViewModel authViewModel;
|
||||
private BulkDeleteHandler bulkDeleteHandler;
|
||||
|
||||
private CalendarDay selectedCalendarDay;
|
||||
private boolean isMonthMode = false;
|
||||
private Long currentUserId = null;
|
||||
private final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd", Locale.getDefault());
|
||||
|
||||
/**
|
||||
* Initializes the fragment and its associated ViewModels.
|
||||
*/
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container,
|
||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
appointmentViewModel = new ViewModelProvider(this).get(AppointmentViewModel.class);
|
||||
storeViewModel = new ViewModelProvider(this).get(StoreViewModel.class);
|
||||
authViewModel = new ViewModelProvider(this).get(AuthViewModel.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up the fragment's UI, including RecyclerView, search, swipe-to-refresh, and calendar.
|
||||
*/
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
|
||||
Bundle savedInstanceState) {
|
||||
View view = inflater.inflate(R.layout.fragment_appointment, container, false);
|
||||
binding = FragmentAppointmentBinding.inflate(inflater, container, false);
|
||||
|
||||
api = RetrofitClient.getAppointmentApi(requireContext());
|
||||
hamburger = view.findViewById(R.id.btnHamburger);
|
||||
setupRecyclerView();
|
||||
setupSearch();
|
||||
setupStatusFilter();
|
||||
setupStoreFilter();
|
||||
setupSwipeRefresh();
|
||||
setupCalendar();
|
||||
setupFilterToggle();
|
||||
setupMyAppointmentFilter();
|
||||
setupBulkDelete();
|
||||
|
||||
setupRecyclerView(view);
|
||||
setupSearch(view);
|
||||
setupSwipeRefresh(view);
|
||||
loadAppointmentData();
|
||||
loadPets();
|
||||
loadServices();
|
||||
binding.fabAddAppointment.setOnClickListener(v -> openAppointmentDetails(-1));
|
||||
|
||||
|
||||
FloatingActionButton fabAdd = view.findViewById(R.id.fabAddAppointment);
|
||||
fabAdd.setOnClickListener(v -> openAppointmentDetails(-1));
|
||||
|
||||
hamburger.setOnClickListener(v -> {
|
||||
ListFragment listFragment = (ListFragment) getParentFragment();
|
||||
if (listFragment != null)
|
||||
listFragment.openDrawer();
|
||||
});
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
private void setupSearch(View view) {
|
||||
etSearch = view.findViewById(R.id.etSearchAppointment);
|
||||
etSearch.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) {
|
||||
filterAppointments(s.toString());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterTextChanged(Editable s) {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void filterAppointments(String query) {
|
||||
filteredList.clear();
|
||||
if (query.isEmpty()) {
|
||||
filteredList.addAll(appointmentList);
|
||||
} else {
|
||||
String lower = query.toLowerCase();
|
||||
for (AppointmentDTO a : appointmentList) {
|
||||
if ((a.getCustomerName() != null && a.getCustomerName().toLowerCase().contains(lower))
|
||||
|| (a.getServiceType() != null && a.getServiceType().toLowerCase().contains(lower))
|
||||
|| (a.getPetName() != null && a.getPetName().toLowerCase().contains(lower))) {
|
||||
filteredList.add(a);
|
||||
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();
|
||||
}
|
||||
|
||||
private void setupBulkDelete() {
|
||||
bulkDeleteHandler = new BulkDeleteHandler(
|
||||
this,
|
||||
binding.layoutBulkDelete,
|
||||
binding.tvSelectionCount,
|
||||
binding.btnBulkDelete,
|
||||
adapter,
|
||||
"appointment",
|
||||
appointmentViewModel::bulkDeleteAppointments,
|
||||
this::loadAppointmentData
|
||||
);
|
||||
}
|
||||
|
||||
@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() {
|
||||
isMonthMode = !isMonthMode;
|
||||
binding.calendarView.state().edit()
|
||||
.setCalendarDisplayMode(isMonthMode ? CalendarMode.MONTHS : CalendarMode.WEEKS)
|
||||
.commit();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up the "My Appointments" filter button.
|
||||
*/
|
||||
private void setupMyAppointmentFilter() {
|
||||
binding.btnMyAppointments.setOnClickListener(v -> {
|
||||
loadAppointmentData();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches current user info to get the employeeId.
|
||||
*/
|
||||
private void loadCurrentUserInfo() {
|
||||
authViewModel.getMe().observe(getViewLifecycleOwner(), resource -> {
|
||||
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
|
||||
currentUserId = resource.data.getId();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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() {
|
||||
HashSet<CalendarDay> datesWithAppointments = new HashSet<>();
|
||||
for (AppointmentDTO appointment : appointmentList) {
|
||||
try {
|
||||
//Get the appointment date
|
||||
Date date = dateFormat.parse(appointment.getAppointmentDate());
|
||||
//if the date is not null, add it to the hashset
|
||||
if (date != null) {
|
||||
Calendar cal = Calendar.getInstance();
|
||||
cal.setTime(date);
|
||||
datesWithAppointments.add(CalendarDay.from(cal.get(Calendar.YEAR), cal.get(Calendar.MONTH) + 1, cal.get(Calendar.DAY_OF_MONTH)));
|
||||
}
|
||||
} catch (ParseException e) {
|
||||
Log.e("AppointmentFragment", "Error parsing date: " + appointment.getAppointmentDate());
|
||||
}
|
||||
}
|
||||
adapter.notifyDataSetChanged();
|
||||
//update the indicators to the calendar
|
||||
binding.calendarView.removeDecorators();
|
||||
binding.calendarView.addDecorator(new EventDecorator(Color.RED, datesWithAppointments));
|
||||
}
|
||||
|
||||
private void setupSwipeRefresh(View view) {
|
||||
swipeRefreshLayout = view.findViewById(R.id.swipeRefreshAppointment);
|
||||
swipeRefreshLayout.setOnRefreshListener(this::loadAppointmentData);
|
||||
/**
|
||||
* Configures the search bar for filtering.
|
||||
*/
|
||||
private void setupSearch() {
|
||||
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"};
|
||||
SpinnerUtils.setupStringFilterSpinner(requireContext(), binding.spinnerStatus, statuses, this::loadAppointmentData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures the store filter spinner.
|
||||
*/
|
||||
private void setupStoreFilter() {
|
||||
SpinnerUtils.setupFilterSpinner(binding.spinnerStore, this::loadAppointmentData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches store data to populate the store filter.
|
||||
*/
|
||||
private void loadStoreData() {
|
||||
storeViewModel.getAllStores(0, 100).observe(getViewLifecycleOwner(), resource -> {
|
||||
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
|
||||
storeList = resource.data.getContent();
|
||||
SpinnerUtils.populateWhiteSpinner(requireContext(), binding.spinnerStore, storeList,
|
||||
StoreDTO::getStoreName, "All Stores", -1L, StoreDTO::getStoreId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the SwipeRefreshLayout to allow manual data refreshing.
|
||||
*/
|
||||
private void setupSwipeRefresh() {
|
||||
binding.swipeRefreshAppointment.setOnRefreshListener(this::loadAppointmentData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigates to the appointment detail screen for editing or creating an appointment.
|
||||
*/
|
||||
private void openAppointmentDetails(int position) {
|
||||
AppointmentDetailFragment detailFragment = new AppointmentDetailFragment();
|
||||
Bundle args = new Bundle();
|
||||
|
||||
if (position != -1) {
|
||||
AppointmentDTO a = filteredList.get(position);
|
||||
AppointmentDTO a = appointmentList.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);
|
||||
ListFragment lf = (ListFragment) getParentFragment();
|
||||
if (lf != null) lf.loadFragment(detailFragment);
|
||||
}
|
||||
public void onAppointmentSaved(int position, AppointmentDTO appointment) {
|
||||
if (position == -1) {
|
||||
appointmentList.add(appointment);
|
||||
} else {
|
||||
appointmentList.set(position, appointment);
|
||||
}
|
||||
filterAppointments(etSearch.getText().toString());
|
||||
}
|
||||
|
||||
public void onAppointmentDeleted(int position) {
|
||||
appointmentList.remove(position);
|
||||
filterAppointments(etSearch.getText().toString());
|
||||
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);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSelectionChanged(int count) {
|
||||
if (bulkDeleteHandler != null) {
|
||||
bulkDeleteHandler.onSelectionChanged(count);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches appointment data from the server with all active filters.
|
||||
*/
|
||||
private void loadAppointmentData() {
|
||||
if (swipeRefreshLayout != null)
|
||||
swipeRefreshLayout.setRefreshing(true);
|
||||
api.getAllAppointments(0, 100).enqueue(new Callback<PageResponse<AppointmentDTO>>() {
|
||||
@Override
|
||||
public void onResponse(Call<PageResponse<AppointmentDTO>> call,
|
||||
Response<PageResponse<AppointmentDTO>> response) {
|
||||
if (swipeRefreshLayout != null)
|
||||
swipeRefreshLayout.setRefreshing(false);
|
||||
if (response.isSuccessful() && response.body() != null) {
|
||||
appointmentList.clear();
|
||||
appointmentList.addAll(response.body().getContent());
|
||||
filterAppointments(etSearch != null ? etSearch.getText().toString() : "");
|
||||
} else {
|
||||
Log.e("AppointmentFragment", "Error: " + response.message());
|
||||
Toast.makeText(getContext(), "Failed to load appointments", Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}
|
||||
|
||||
@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());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Load Pets
|
||||
private void loadPets() {
|
||||
PetApi petApi = RetrofitClient.getPetApi(requireContext());
|
||||
petApi.getAllPets(0,100).enqueue(new Callback<PageResponse<PetDTO>>() {
|
||||
|
||||
@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();
|
||||
|
||||
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();
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
private String getServiceName(Long id) {
|
||||
for (ServiceDTO s : serviceList) {
|
||||
if (s.getServiceId().equals(id))return s.getServiceName();
|
||||
String selectedDateString = null;
|
||||
if (selectedCalendarDay != null) {
|
||||
selectedDateString = String.format(Locale.getDefault(), "%04d-%02d-%02d",
|
||||
selectedCalendarDay.getYear(), selectedCalendarDay.getMonth(), selectedCalendarDay.getDay());
|
||||
}
|
||||
return "";
|
||||
|
||||
Long employeeId = null;
|
||||
if (binding.btnMyAppointments.isChecked()) {
|
||||
employeeId = currentUserId;
|
||||
}
|
||||
|
||||
if (status.equals("All Statuses")) status = null;
|
||||
else status = status.toUpperCase();
|
||||
|
||||
appointmentViewModel.getAllAppointments(0, 500, query, status, storeId, selectedDateString, employeeId).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.swipeRefreshAppointment.setRefreshing(true);
|
||||
break;
|
||||
case SUCCESS:
|
||||
// Hide loading indicator and display data
|
||||
binding.swipeRefreshAppointment.setRefreshing(false);
|
||||
if (resource.data != null) {
|
||||
appointmentList.clear();
|
||||
appointmentList.addAll(resource.data.getContent());
|
||||
updateCalendarDecorators();
|
||||
adapter.notifyDataSetChanged();
|
||||
}
|
||||
break;
|
||||
case ERROR:
|
||||
// Hide loading indicator and toast error message
|
||||
binding.swipeRefreshAppointment.setRefreshing(false);
|
||||
Toast.makeText(getContext(), "Failed to load appointments: " + resource.message, Toast.LENGTH_SHORT).show();
|
||||
Log.e("AppointmentFragment", "Error loading appointments: " + resource.message);
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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);
|
||||
/**
|
||||
* Initializes the RecyclerView for displaying appointments.
|
||||
*/
|
||||
private void setupRecyclerView() {
|
||||
adapter = new AppointmentAdapter(appointmentList, this);
|
||||
binding.recyclerViewAppointments.setLayoutManager(new LinearLayoutManager(getContext()));
|
||||
binding.recyclerViewAppointments.setAdapter(adapter);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,213 +1,182 @@
|
||||
package com.example.petstoremobile.fragments.listfragments;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
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.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 androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
import androidx.navigation.fragment.NavHostFragment;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
|
||||
|
||||
import com.example.petstoremobile.R;
|
||||
import com.example.petstoremobile.adapters.InventoryAdapter;
|
||||
import com.example.petstoremobile.api.CategoryApi;
|
||||
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.databinding.FragmentInventoryBinding;
|
||||
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.listfragments.detailfragments.InventoryDetailFragment;
|
||||
import com.google.android.material.floatingactionbutton.FloatingActionButton;
|
||||
import com.example.petstoremobile.utils.BulkDeleteHandler;
|
||||
import com.example.petstoremobile.viewmodels.InventoryViewModel;
|
||||
import com.example.petstoremobile.utils.Resource;
|
||||
import com.example.petstoremobile.utils.SpinnerUtils;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import retrofit2.Call;
|
||||
import retrofit2.Callback;
|
||||
import retrofit2.Response;
|
||||
import dagger.hilt.android.AndroidEntryPoint;
|
||||
|
||||
@AndroidEntryPoint
|
||||
public class InventoryFragment extends Fragment implements InventoryAdapter.OnInventoryClickListener {
|
||||
|
||||
private static final String TAG = "InventoryFragment";
|
||||
private static final int PAGE_SIZE = 20;
|
||||
|
||||
private FragmentInventoryBinding binding;
|
||||
private final List<InventoryDTO> inventoryList = new ArrayList<>();
|
||||
private final List<CategoryDTO> categoryList = new ArrayList<>();
|
||||
private List<StoreDTO> storeList = new ArrayList<>();
|
||||
private InventoryAdapter adapter;
|
||||
private InventoryApi inventoryApi;
|
||||
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;
|
||||
private InventoryViewModel viewModel;
|
||||
private BulkDeleteHandler bulkDeleteHandler;
|
||||
|
||||
// Pagination
|
||||
private int currentPage = 0;
|
||||
private boolean isLastPage = 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();
|
||||
setupBulkDelete();
|
||||
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();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return binding.getRoot();
|
||||
}
|
||||
|
||||
private void setupBulkDelete() {
|
||||
bulkDeleteHandler = new BulkDeleteHandler(
|
||||
this,
|
||||
binding.layoutBulkDelete,
|
||||
binding.tvSelectionCount,
|
||||
binding.btnBulkDelete,
|
||||
adapter,
|
||||
"inventory item",
|
||||
viewModel::bulkDeleteInventory,
|
||||
() -> loadInventory(true)
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container,
|
||||
Bundle savedInstanceState) {
|
||||
View view = inflater.inflate(R.layout.fragment_inventory, container, false);
|
||||
|
||||
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;
|
||||
public void onDestroyView() {
|
||||
super.onDestroyView();
|
||||
binding = null;
|
||||
}
|
||||
|
||||
// Categories
|
||||
private void loadCategories() {
|
||||
categoryApi.getAllCategories(0, 100).enqueue(new Callback<PageResponse<CategoryDTO>>() {
|
||||
@Override
|
||||
public void onResponse(Call<PageResponse<CategoryDTO>> call,
|
||||
Response<PageResponse<CategoryDTO>> response) {
|
||||
if (response.isSuccessful() && response.body() != null) {
|
||||
categoryList.clear();
|
||||
categoryList.addAll(response.body().getContent());
|
||||
setupCategorySpinner();
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 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);
|
||||
|
||||
@Override
|
||||
public void onFailure(Call<PageResponse<CategoryDTO>> call, Throwable t) {
|
||||
Log.e(TAG, "Failed to load categories", t);
|
||||
// Still setup spinner with just "All"
|
||||
setupCategorySpinner();
|
||||
// Reset filters when closing
|
||||
binding.etSearchInventory.setText("");
|
||||
binding.spinnerStore.setSelection(0);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void setupCategorySpinner() {
|
||||
// First item is always "All Categories"
|
||||
List<String> categoryNames = new ArrayList<>();
|
||||
categoryNames.add("All Categories");
|
||||
for (CategoryDTO c : categoryList) {
|
||||
categoryNames.add(c.getCategoryName());
|
||||
}
|
||||
|
||||
ArrayAdapter<String> spinnerAdapter = new ArrayAdapter<>(
|
||||
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();
|
||||
}
|
||||
/**
|
||||
* Sets up the search bar for filtering.
|
||||
*/
|
||||
private void setupSearch() {
|
||||
binding.etSearchInventory.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) {
|
||||
loadInventory(true);
|
||||
}
|
||||
@Override public void afterTextChanged(Editable s) {}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNothingSelected(AdapterView<?> parent) {
|
||||
/**
|
||||
* Configures the store filter spinner.
|
||||
*/
|
||||
private void setupStoreFilter() {
|
||||
SpinnerUtils.setupFilterSpinner(binding.spinnerStore, () -> loadInventory(true));
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches store data to populate the store filter.
|
||||
*/
|
||||
private void loadStoreData() {
|
||||
viewModel.getAllStores(0, 100).observe(getViewLifecycleOwner(), resource -> {
|
||||
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
|
||||
storeList = resource.data.getContent();
|
||||
SpinnerUtils.populateWhiteSpinner(requireContext(), binding.spinnerStore, storeList,
|
||||
StoreDTO::getStoreName, "All Stores", -1L, StoreDTO::getStoreId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Search
|
||||
|
||||
private void setupSearch(View view) {
|
||||
etSearch = view.findViewById(R.id.etSearchInventory);
|
||||
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);
|
||||
/**
|
||||
* Initializes the RecyclerView with a layout manager, and adapter.
|
||||
*/
|
||||
private void setupRecyclerView() {
|
||||
adapter = new InventoryAdapter(inventoryList, this);
|
||||
rv.setLayoutManager(new LinearLayoutManager(getContext()));
|
||||
rv.setAdapter(adapter);
|
||||
binding.recyclerViewInventory.setLayoutManager(new LinearLayoutManager(getContext()));
|
||||
binding.recyclerViewInventory.setAdapter(adapter);
|
||||
|
||||
rv.addOnScrollListener(new RecyclerView.OnScrollListener() {
|
||||
binding.recyclerViewInventory.addOnScrollListener(new RecyclerView.OnScrollListener() {
|
||||
@Override
|
||||
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
|
||||
public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
|
||||
if (dy <= 0)
|
||||
return;
|
||||
LinearLayoutManager lm = (LinearLayoutManager) rv.getLayoutManager();
|
||||
LinearLayoutManager lm = (LinearLayoutManager) binding.recyclerViewInventory.getLayoutManager();
|
||||
if (lm == null)
|
||||
return;
|
||||
int visible = lm.getChildCount();
|
||||
@@ -220,144 +189,90 @@ public class InventoryFragment extends Fragment implements InventoryAdapter.OnIn
|
||||
});
|
||||
}
|
||||
|
||||
private void setupSwipeRefresh(View view) {
|
||||
swipeRefreshLayout = view.findViewById(R.id.swipeRefreshInventory);
|
||||
swipeRefreshLayout.setOnRefreshListener(() -> loadInventory(true));
|
||||
/**
|
||||
* Sets up the SwipeRefreshLayout to reload the first page of inventory items.
|
||||
*/
|
||||
private void setupSwipeRefresh() {
|
||||
binding.swipeRefreshInventory.setOnRefreshListener(() -> loadInventory(true));
|
||||
}
|
||||
|
||||
// Load inventory
|
||||
/**
|
||||
* Fetches a page of inventory items from the API.
|
||||
*/
|
||||
private void loadInventory(boolean reset) {
|
||||
if (isLoading)
|
||||
return;
|
||||
isLoading = true;
|
||||
if (isLoading) return;
|
||||
|
||||
if (reset) {
|
||||
currentPage = 0;
|
||||
isLastPage = false;
|
||||
}
|
||||
|
||||
// Build query: combine search text + selected category
|
||||
String q = buildQuery();
|
||||
// Search text from input
|
||||
String query = binding.etSearchInventory != null ? binding.etSearchInventory.getText().toString().trim() : "";
|
||||
if (query.isEmpty()) query = null;
|
||||
|
||||
inventoryApi.getAllInventory(q, currentPage, PAGE_SIZE, "inventoryId,asc")
|
||||
.enqueue(new Callback<PageResponse<InventoryDTO>>() {
|
||||
@Override
|
||||
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;
|
||||
Long storeId = null;
|
||||
if (binding.spinnerStore.getSelectedItemPosition() > 0 && !storeList.isEmpty()) {
|
||||
storeId = storeList.get(binding.spinnerStore.getSelectedItemPosition() - 1).getStoreId();
|
||||
}
|
||||
return q;
|
||||
}
|
||||
|
||||
// Bulk delete
|
||||
private void confirmBulkDelete() {
|
||||
List<Long> ids = adapter.getSelectedIds();
|
||||
if (ids.isEmpty())
|
||||
return;
|
||||
//Load all inventory items from the backend using viewModel
|
||||
viewModel.getAllInventory(query, storeId, currentPage, PAGE_SIZE, "product.prodName").observe(getViewLifecycleOwner(), resource -> {
|
||||
if (resource == null) return;
|
||||
|
||||
new androidx.appcompat.app.AlertDialog.Builder(requireContext())
|
||||
.setTitle("Delete " + ids.size() + " item(s)?")
|
||||
.setMessage("This cannot be undone.")
|
||||
.setPositiveButton("Delete", (d, w) -> bulkDelete(ids))
|
||||
.setNegativeButton("Cancel", null)
|
||||
.show();
|
||||
}
|
||||
|
||||
private void bulkDelete(List<Long> ids) {
|
||||
inventoryApi.bulkDeleteInventory(new BulkDeleteRequest(ids))
|
||||
.enqueue(new Callback<Void>() {
|
||||
@Override
|
||||
public void onResponse(Call<Void> call, Response<Void> response) {
|
||||
if (response.isSuccessful()) {
|
||||
adapter.clearSelection();
|
||||
hideBulkDeleteBar();
|
||||
loadInventory(true);
|
||||
Toast.makeText(getContext(), ids.size() + " item(s) deleted", Toast.LENGTH_SHORT).show();
|
||||
} else {
|
||||
Toast.makeText(getContext(), "Delete failed", Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
// 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++;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(Call<Void> call, Throwable t) {
|
||||
Toast.makeText(getContext(), "Network error", Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
});
|
||||
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;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void hideBulkDeleteBar() {
|
||||
if (btnBulkDelete != null)
|
||||
btnBulkDelete.setVisibility(View.GONE);
|
||||
if (tvSelectionCount != null)
|
||||
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) {
|
||||
InventoryDetailFragment detail = new InventoryDetailFragment();
|
||||
Bundle args = new Bundle();
|
||||
|
||||
if (inv != null) {
|
||||
args.putLong("inventoryId", inv.getInventoryId());
|
||||
args.putLong("prodId", inv.getProdId() != null ? inv.getProdId() : -1);
|
||||
args.putString("productName", inv.getProductName());
|
||||
args.putString("categoryName", inv.getCategoryName());
|
||||
args.putInt("quantity", inv.getQuantity() != null ? inv.getQuantity() : 0);
|
||||
}
|
||||
|
||||
detail.setArguments(args);
|
||||
detail.setInventoryFragment(this);
|
||||
|
||||
ListFragment lf = (ListFragment) getParentFragment();
|
||||
if (lf != null)
|
||||
lf.loadFragment(detail);
|
||||
NavHostFragment.findNavController(this).navigate(R.id.nav_inventory_detail, args);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reloads inventory data when changes occur.
|
||||
*/
|
||||
public void onInventoryChanged() {
|
||||
loadInventory(true);
|
||||
}
|
||||
|
||||
// Adapter callbacks
|
||||
|
||||
/**
|
||||
* Handles item click in the inventory list.
|
||||
*/
|
||||
@Override
|
||||
public void onInventoryClick(int position) {
|
||||
if (position >= 0 && position < inventoryList.size()) {
|
||||
@@ -365,14 +280,13 @@ public class InventoryFragment extends Fragment implements InventoryAdapter.OnIn
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the bulk deletion UI visibility and count when items are selected or deselected.
|
||||
*/
|
||||
@Override
|
||||
public void onSelectionChanged(int selectedCount) {
|
||||
if (selectedCount > 0) {
|
||||
btnBulkDelete.setVisibility(View.VISIBLE);
|
||||
tvSelectionCount.setVisibility(View.VISIBLE);
|
||||
tvSelectionCount.setText(selectedCount + " selected");
|
||||
} else {
|
||||
hideBulkDeleteBar();
|
||||
if (bulkDeleteHandler != null) {
|
||||
bulkDeleteHandler.onSelectionChanged(selectedCount);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,10 +2,12 @@ package com.example.petstoremobile.fragments.listfragments;
|
||||
|
||||
import android.os.Bundle;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
import androidx.navigation.fragment.NavHostFragment;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
|
||||
|
||||
import android.text.Editable;
|
||||
import android.text.TextWatcher;
|
||||
@@ -13,219 +15,264 @@ import android.util.Log;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
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 com.example.petstoremobile.R;
|
||||
import com.example.petstoremobile.adapters.PetAdapter;
|
||||
import com.example.petstoremobile.api.PetApi;
|
||||
import com.example.petstoremobile.api.RetrofitClient;
|
||||
import com.example.petstoremobile.dtos.PageResponse;
|
||||
import com.example.petstoremobile.databinding.FragmentPetBinding;
|
||||
import com.example.petstoremobile.dtos.PetDTO;
|
||||
import com.example.petstoremobile.dtos.StoreDTO;
|
||||
import com.example.petstoremobile.fragments.ListFragment;
|
||||
import com.example.petstoremobile.fragments.listfragments.detailfragments.PetDetailFragment;
|
||||
import com.example.petstoremobile.fragments.listfragments.listprofilefragments.PetProfileFragment;
|
||||
import com.google.android.material.floatingactionbutton.FloatingActionButton;
|
||||
import com.example.petstoremobile.utils.BulkDeleteHandler;
|
||||
import com.example.petstoremobile.utils.Resource;
|
||||
import com.example.petstoremobile.utils.SpinnerUtils;
|
||||
import com.example.petstoremobile.viewmodels.PetViewModel;
|
||||
import com.example.petstoremobile.viewmodels.StoreViewModel;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import retrofit2.Call;
|
||||
import retrofit2.Callback;
|
||||
import retrofit2.Response;
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Named;
|
||||
|
||||
import dagger.hilt.android.AndroidEntryPoint;
|
||||
|
||||
@AndroidEntryPoint
|
||||
public class PetFragment extends Fragment implements PetAdapter.OnPetClickListener {
|
||||
private FragmentPetBinding binding;
|
||||
private List<PetDTO> petList = new ArrayList<>();
|
||||
private List<PetDTO> filteredList = new ArrayList<>();
|
||||
private ImageButton hamburger;
|
||||
private List<StoreDTO> storeList = new ArrayList<>();
|
||||
private PetAdapter adapter;
|
||||
private PetApi api;
|
||||
private SwipeRefreshLayout swipeRefreshLayout;
|
||||
private EditText etSearch;
|
||||
private Spinner spinnerStatus;
|
||||
private PetViewModel viewModel;
|
||||
private StoreViewModel storeViewModel;
|
||||
private BulkDeleteHandler bulkDeleteHandler;
|
||||
|
||||
//load pet view
|
||||
@Inject @Named("baseUrl") String baseUrl;
|
||||
|
||||
/**
|
||||
* Initializes the fragment and its associated ViewModels.
|
||||
*/
|
||||
@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) {
|
||||
View view = inflater.inflate(R.layout.fragment_pet, container, false);
|
||||
binding = FragmentPetBinding.inflate(inflater, container, false);
|
||||
|
||||
//get retrofit
|
||||
api = RetrofitClient.getPetApi(requireContext());
|
||||
setupRecyclerView();
|
||||
setupSearch();
|
||||
setupStatusFilter();
|
||||
setupSpeciesFilter();
|
||||
setupStoreFilter();
|
||||
setupSwipeRefresh();
|
||||
setupFilterToggle();
|
||||
setupBulkDelete();
|
||||
|
||||
hamburger = view.findViewById(R.id.btnHamburger);
|
||||
binding.fabAddPet.setOnClickListener(v -> openPetDetails());
|
||||
|
||||
setupRecyclerView(view);
|
||||
setupSearch(view);
|
||||
setupStatusFilter(view);
|
||||
setupSwipeRefresh(view);
|
||||
loadPetData();
|
||||
|
||||
|
||||
//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();
|
||||
binding.btnHamburger.setOnClickListener(v -> {
|
||||
Fragment parent = getParentFragment();
|
||||
if (parent != null) {
|
||||
Fragment grandParent = parent.getParentFragment();
|
||||
if (grandParent instanceof ListFragment) {
|
||||
((ListFragment) grandParent).openDrawer();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return view;
|
||||
return binding.getRoot();
|
||||
}
|
||||
|
||||
private void setupSearch(View view) {
|
||||
etSearch = view.findViewById(R.id.etSearchPet);
|
||||
etSearch.addTextChangedListener(new TextWatcher() {
|
||||
private void setupBulkDelete() {
|
||||
bulkDeleteHandler = new BulkDeleteHandler(
|
||||
this,
|
||||
binding.layoutBulkDelete,
|
||||
binding.tvSelectionCount,
|
||||
binding.btnBulkDelete,
|
||||
adapter,
|
||||
"pet",
|
||||
viewModel::bulkDeletePets,
|
||||
this::loadPetData
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyView() {
|
||||
super.onDestroyView();
|
||||
binding = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reloads data every time the fragment becomes visible.
|
||||
*/
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
loadPetData();
|
||||
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.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 onTextChanged(CharSequence s, int start, int before, int count) {
|
||||
filterPets();
|
||||
loadPetData();
|
||||
}
|
||||
@Override public void afterTextChanged(Editable s) {}
|
||||
});
|
||||
}
|
||||
|
||||
//Setup the status filter spinner
|
||||
private void setupStatusFilter(View view) {
|
||||
spinnerStatus = view.findViewById(R.id.spinnerStatus);
|
||||
String[] statuses = {"All Statuses", "Available", "Adopted"};
|
||||
ArrayAdapter<String> adapter = new ArrayAdapter<>(requireContext(), android.R.layout.simple_spinner_item, statuses);
|
||||
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
|
||||
spinnerStatus.setAdapter(adapter);
|
||||
/**
|
||||
* Configures the status filter spinner.
|
||||
*/
|
||||
private void setupStatusFilter() {
|
||||
String[] statuses = {"All Statuses", "Available", "Adopted", "Owned"};
|
||||
SpinnerUtils.setupStringFilterSpinner(requireContext(), binding.spinnerStatus, statuses, this::loadPetData);
|
||||
}
|
||||
|
||||
spinnerStatus.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
|
||||
@Override
|
||||
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
|
||||
filterPets();
|
||||
/**
|
||||
* Configures the species filter spinner with species.
|
||||
*/
|
||||
private void setupSpeciesFilter() {
|
||||
String[] species = {"All Species", "Dog", "Cat", "Bird", "Rabbit", "Fish", "Hamster"};
|
||||
SpinnerUtils.setupStringFilterSpinner(requireContext(), binding.spinnerSpecies, species, this::loadPetData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures the store filter spinner.
|
||||
*/
|
||||
private void setupStoreFilter() {
|
||||
SpinnerUtils.setupFilterSpinner(binding.spinnerStore, this::loadPetData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches store data to populate the store filter.
|
||||
*/
|
||||
private void loadStoreData() {
|
||||
storeViewModel.getAllStores(0, 100).observe(getViewLifecycleOwner(), resource -> {
|
||||
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
|
||||
storeList = resource.data.getContent();
|
||||
SpinnerUtils.populateWhiteSpinner(requireContext(), binding.spinnerStore, storeList,
|
||||
StoreDTO::getStoreName, "All Stores", -1L, StoreDTO::getStoreId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNothingSelected(AdapterView<?> parent) {}
|
||||
});
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
adapter.notifyDataSetChanged();
|
||||
/**
|
||||
* Sets up the SwipeRefreshLayout.
|
||||
*/
|
||||
private void setupSwipeRefresh() {
|
||||
binding.swipeRefreshPet.setOnRefreshListener(this::loadPetData);
|
||||
}
|
||||
|
||||
private void setupSwipeRefresh(View view) {
|
||||
swipeRefreshLayout = view.findViewById(R.id.swipeRefreshPet);
|
||||
swipeRefreshLayout.setOnRefreshListener(() -> {
|
||||
loadPetData();
|
||||
});
|
||||
}
|
||||
|
||||
//Open pet profile
|
||||
/**
|
||||
* Navigates to the pet profile screen.
|
||||
*/
|
||||
private void openPetProfile(int position) {
|
||||
PetProfileFragment profileFragment = new PetProfileFragment();
|
||||
|
||||
//Make a bundle to pass data to the profile fragment
|
||||
Bundle args = new Bundle();
|
||||
PetDTO pet = filteredList.get(position);
|
||||
args.putInt("petId", pet.getPetId().intValue());
|
||||
args.putString("petName", pet.getPetName());
|
||||
args.putString("petSpecies", pet.getPetSpecies());
|
||||
args.putString("petBreed", pet.getPetBreed());
|
||||
args.putInt("petAge", pet.getPetAge());
|
||||
args.putString("petStatus", pet.getPetStatus());
|
||||
|
||||
try {
|
||||
args.putDouble("petPrice", Double.parseDouble(pet.getPetPrice()));
|
||||
} catch (Exception e) {
|
||||
args.putDouble("petPrice", 0.0);
|
||||
}
|
||||
|
||||
//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);
|
||||
}
|
||||
PetDTO pet = petList.get(position);
|
||||
args.putLong("petId", pet.getPetId());
|
||||
NavHostFragment.findNavController(this).navigate(R.id.nav_pet_profile, args);
|
||||
}
|
||||
|
||||
//Open the pet detail view for adding
|
||||
private void openPetDetails(int position) {
|
||||
PetDetailFragment detailFragment = new PetDetailFragment();
|
||||
|
||||
//get ListFragment to load the detail view
|
||||
ListFragment listFragment = (ListFragment) getParentFragment();
|
||||
if (listFragment != null) {
|
||||
listFragment.loadFragment(detailFragment);
|
||||
}
|
||||
/**
|
||||
* Navigates to the pet detail screen.
|
||||
*/
|
||||
private void openPetDetails() {
|
||||
NavHostFragment.findNavController(this).navigate(R.id.nav_pet_detail);
|
||||
}
|
||||
|
||||
// Called by PetAdapter when a row is clicked to open the details view
|
||||
@Override
|
||||
public void onPetClick(int position) {
|
||||
openPetProfile(position);
|
||||
}
|
||||
|
||||
// Helper function to get a list of all pets from the backend
|
||||
private void loadPetData() {
|
||||
if (swipeRefreshLayout != null) {
|
||||
swipeRefreshLayout.setRefreshing(true);
|
||||
@Override
|
||||
public void onSelectionChanged(int selectedCount) {
|
||||
if (bulkDeleteHandler != null) {
|
||||
bulkDeleteHandler.onSelectionChanged(selectedCount);
|
||||
}
|
||||
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 {
|
||||
Log.e("onResponse: ", response.message());
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Fetches pet data from the server with all active filters.
|
||||
*/
|
||||
private void loadPetData() {
|
||||
String query = binding.etSearchPet.getText().toString().trim();
|
||||
String status = binding.spinnerStatus.getSelectedItem() != null ? binding.spinnerStatus.getSelectedItem().toString() : "All Statuses";
|
||||
String species = binding.spinnerSpecies.getSelectedItem() != null ? binding.spinnerSpecies.getSelectedItem().toString() : "All Species";
|
||||
|
||||
Long storeId = null;
|
||||
if (binding.spinnerStore.getSelectedItemPosition() > 0 && !storeList.isEmpty()) {
|
||||
storeId = storeList.get(binding.spinnerStore.getSelectedItemPosition() - 1).getStoreId();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(Call<PageResponse<PetDTO>> call, Throwable t) {
|
||||
if (swipeRefreshLayout != null) {
|
||||
swipeRefreshLayout.setRefreshing(false);
|
||||
}
|
||||
Toast.makeText(getContext(),
|
||||
"Failed to load pets", Toast.LENGTH_SHORT).show();
|
||||
Log.e("onFailure: ", t.getMessage());
|
||||
if (status.equals("All Statuses")) status = null;
|
||||
if (species.equals("All Species")) species = null;
|
||||
|
||||
viewModel.getAllPets(0, 100, query, status, species, storeId, "petName").observe(getViewLifecycleOwner(), resource -> {
|
||||
if (resource == null) return;
|
||||
|
||||
switch (resource.status) {
|
||||
case LOADING:
|
||||
binding.swipeRefreshPet.setRefreshing(true);
|
||||
break;
|
||||
case SUCCESS:
|
||||
binding.swipeRefreshPet.setRefreshing(false);
|
||||
if (resource.data != null) {
|
||||
petList.clear();
|
||||
petList.addAll(resource.data.getContent());
|
||||
adapter.notifyDataSetChanged();
|
||||
}
|
||||
break;
|
||||
case ERROR:
|
||||
binding.swipeRefreshPet.setRefreshing(false);
|
||||
Toast.makeText(getContext(), "Failed to load pets: " + resource.message, Toast.LENGTH_SHORT).show();
|
||||
Log.e("PetFragment", "Error loading pets: " + resource.message);
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
//set up the recyclerview and adapter
|
||||
private void setupRecyclerView(View view) {
|
||||
RecyclerView recyclerView = view.findViewById(R.id.recyclerViewPets);
|
||||
adapter = new PetAdapter(filteredList, this);
|
||||
recyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
|
||||
recyclerView.setAdapter(adapter);
|
||||
/**
|
||||
* Initializes the RecyclerView.
|
||||
*/
|
||||
private void setupRecyclerView() {
|
||||
adapter = new PetAdapter(petList, this);
|
||||
adapter.setBaseUrl(baseUrl);
|
||||
binding.recyclerViewPets.setLayoutManager(new LinearLayoutManager(getContext()));
|
||||
binding.recyclerViewPets.setAdapter(adapter);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,133 +1,226 @@
|
||||
package com.example.petstoremobile.fragments.listfragments;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.text.*;
|
||||
import android.util.Log;
|
||||
import android.view.*;
|
||||
import android.widget.*;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
import androidx.navigation.fragment.NavHostFragment;
|
||||
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.Toast;
|
||||
|
||||
import com.example.petstoremobile.R;
|
||||
import com.example.petstoremobile.adapters.ProductAdapter;
|
||||
import com.example.petstoremobile.api.RetrofitClient;
|
||||
import com.example.petstoremobile.dtos.PageResponse;
|
||||
import com.example.petstoremobile.databinding.FragmentProductBinding;
|
||||
import com.example.petstoremobile.dtos.CategoryDTO;
|
||||
import com.example.petstoremobile.dtos.ProductDTO;
|
||||
import com.example.petstoremobile.fragments.ListFragment;
|
||||
import com.example.petstoremobile.fragments.listfragments.detailfragments.ProductDetailFragment;
|
||||
import com.google.android.material.floatingactionbutton.FloatingActionButton;
|
||||
import java.util.*;
|
||||
import retrofit2.*;
|
||||
import com.example.petstoremobile.utils.Resource;
|
||||
import com.example.petstoremobile.utils.SpinnerUtils;
|
||||
import com.example.petstoremobile.viewmodels.ProductViewModel;
|
||||
|
||||
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 {
|
||||
|
||||
private FragmentProductBinding binding;
|
||||
private List<ProductDTO> productList = new ArrayList<>();
|
||||
private List<ProductDTO> filteredList = new ArrayList<>();
|
||||
private List<CategoryDTO> categoryList = new ArrayList<>();
|
||||
private ProductAdapter adapter;
|
||||
private SwipeRefreshLayout swipeRefresh;
|
||||
private EditText etSearch;
|
||||
private ProductViewModel viewModel;
|
||||
|
||||
@Inject @Named("baseUrl") String baseUrl;
|
||||
|
||||
/**
|
||||
* Initializes the fragment and its associated ProductViewModel.
|
||||
*/
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container,
|
||||
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) {
|
||||
View view = inflater.inflate(R.layout.fragment_product, container, false);
|
||||
binding = FragmentProductBinding.inflate(inflater, container, false);
|
||||
|
||||
setupRecyclerView(view);
|
||||
setupSearch(view);
|
||||
setupSwipeRefresh(view);
|
||||
loadProducts();
|
||||
setupRecyclerView();
|
||||
setupSearch();
|
||||
setupCategoryFilter();
|
||||
setupSwipeRefresh();
|
||||
setupFilterToggle();
|
||||
|
||||
FloatingActionButton fab = view.findViewById(R.id.fabAddProduct);
|
||||
fab.setOnClickListener(v -> openDetail(-1));
|
||||
binding.fabAddProduct.setOnClickListener(v -> openProductDetails(-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);
|
||||
adapter = new ProductAdapter(filteredList, this);
|
||||
rv.setLayoutManager(new LinearLayoutManager(getContext()));
|
||||
rv.setAdapter(adapter);
|
||||
}
|
||||
|
||||
private void setupSearch(View view) {
|
||||
etSearch = view.findViewById(R.id.etSearchProduct);
|
||||
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.swipeRefreshProduct);
|
||||
swipeRefresh.setOnRefreshListener(this::loadProducts);
|
||||
}
|
||||
|
||||
private void filter(String query) {
|
||||
filteredList.clear();
|
||||
if (query.isEmpty()) {
|
||||
filteredList.addAll(productList);
|
||||
} else {
|
||||
String lower = query.toLowerCase();
|
||||
for (ProductDTO p : productList) {
|
||||
if ((p.getProdName() != null && p.getProdName().toLowerCase().contains(lower))
|
||||
|| (p.getCategoryName() != null && p.getCategoryName().toLowerCase().contains(lower))) {
|
||||
filteredList.add(p);
|
||||
binding.btnHamburgerProduct.setOnClickListener(v -> {
|
||||
Fragment parent = getParentFragment();
|
||||
if (parent != null) {
|
||||
Fragment grandParent = parent.getParentFragment();
|
||||
if (grandParent instanceof ListFragment) {
|
||||
((ListFragment) grandParent).openDrawer();
|
||||
}
|
||||
}
|
||||
}
|
||||
adapter.notifyDataSetChanged();
|
||||
}
|
||||
});
|
||||
|
||||
private void loadProducts() {
|
||||
if (swipeRefresh != null) swipeRefresh.setRefreshing(true);
|
||||
RetrofitClient.getProductApi(requireContext()).getAllProducts(null, 0, 100)
|
||||
.enqueue(new Callback<PageResponse<ProductDTO>>() {
|
||||
public void onResponse(Call<PageResponse<ProductDTO>> c,
|
||||
Response<PageResponse<ProductDTO>> r) {
|
||||
if (swipeRefresh != null) swipeRefresh.setRefreshing(false);
|
||||
if (r.isSuccessful() && r.body() != null) {
|
||||
productList.clear();
|
||||
productList.addAll(r.body().getContent());
|
||||
filter(etSearch != null ? etSearch.getText().toString() : "");
|
||||
} 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();
|
||||
Bundle args = new Bundle();
|
||||
if (position != -1) {
|
||||
ProductDTO p = filteredList.get(position);
|
||||
args.putLong("prodId", p.getProdId());
|
||||
args.putString("prodName", p.getProdName());
|
||||
args.putString("prodDesc", p.getProdDesc() != null ? p.getProdDesc() : "");
|
||||
args.putString("prodPrice", p.getProdPrice() != null ? p.getProdPrice().toString() : "");
|
||||
args.putLong("categoryId", p.getCategoryId() != null ? p.getCategoryId() : -1);
|
||||
}
|
||||
detail.setArguments(args);
|
||||
ListFragment lf = (ListFragment) getParentFragment();
|
||||
if (lf != null) lf.loadFragment(detail);
|
||||
return binding.getRoot();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onProductClick(int position) { openDetail(position); }
|
||||
}
|
||||
public void onDestroyView() {
|
||||
super.onDestroyView();
|
||||
binding = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reloads data every time the fragment becomes visible.
|
||||
*/
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
loadProductData();
|
||||
loadCategoryData();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.etSearchProduct.setText("");
|
||||
binding.spinnerCategory.setSelection(0);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures the search bar for triggering data load from backend.
|
||||
*/
|
||||
private void setupSearch() {
|
||||
binding.etSearchProduct.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) {
|
||||
loadProductData();
|
||||
}
|
||||
@Override public void afterTextChanged(Editable s) {}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures the category filter spinner.
|
||||
*/
|
||||
private void setupCategoryFilter() {
|
||||
SpinnerUtils.setupFilterSpinner(binding.spinnerCategory, this::loadProductData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches category data to populate the category filter.
|
||||
*/
|
||||
private void loadCategoryData() {
|
||||
viewModel.getAllCategories(0, 100).observe(getViewLifecycleOwner(), resource -> {
|
||||
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
|
||||
categoryList = resource.data.getContent();
|
||||
SpinnerUtils.populateWhiteSpinner(requireContext(), binding.spinnerCategory, categoryList,
|
||||
CategoryDTO::getCategoryName, "All Categories", -1L, CategoryDTO::getCategoryId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up the SwipeRefreshLayout.
|
||||
*/
|
||||
private void setupSwipeRefresh() {
|
||||
binding.swipeRefreshProduct.setOnRefreshListener(this::loadProductData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigates to the product detail screen.
|
||||
*/
|
||||
private void openProductDetails(int position) {
|
||||
Bundle args = new Bundle();
|
||||
if (position != -1) {
|
||||
ProductDTO product = productList.get(position);
|
||||
args.putLong("prodId", product.getProdId());
|
||||
}
|
||||
NavHostFragment.findNavController(this).navigate(R.id.nav_product_detail, args);
|
||||
}
|
||||
|
||||
@Override
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,134 +1,278 @@
|
||||
package com.example.petstoremobile.fragments.listfragments;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.text.*;
|
||||
import android.text.Editable;
|
||||
import android.text.TextWatcher;
|
||||
import android.util.Log;
|
||||
import android.view.*;
|
||||
import android.widget.*;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
import androidx.navigation.fragment.NavHostFragment;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
|
||||
|
||||
import com.example.petstoremobile.R;
|
||||
import com.example.petstoremobile.adapters.ProductSupplierAdapter;
|
||||
import com.example.petstoremobile.api.RetrofitClient;
|
||||
import com.example.petstoremobile.dtos.PageResponse;
|
||||
import com.example.petstoremobile.databinding.FragmentProductSupplierBinding;
|
||||
import com.example.petstoremobile.dtos.ProductDTO;
|
||||
import com.example.petstoremobile.dtos.ProductSupplierDTO;
|
||||
import com.example.petstoremobile.dtos.SupplierDTO;
|
||||
import com.example.petstoremobile.fragments.ListFragment;
|
||||
import com.example.petstoremobile.fragments.listfragments.detailfragments.ProductSupplierDetailFragment;
|
||||
import com.google.android.material.floatingactionbutton.FloatingActionButton;
|
||||
import java.util.*;
|
||||
import retrofit2.*;
|
||||
import com.example.petstoremobile.utils.BulkDeleteHandler;
|
||||
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.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import dagger.hilt.android.AndroidEntryPoint;
|
||||
|
||||
@AndroidEntryPoint
|
||||
public class ProductSupplierFragment extends Fragment
|
||||
implements ProductSupplierAdapter.OnProductSupplierClickListener {
|
||||
|
||||
private FragmentProductSupplierBinding binding;
|
||||
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 SwipeRefreshLayout swipeRefresh;
|
||||
private EditText etSearch;
|
||||
private ProductSupplierViewModel viewModel;
|
||||
private ProductViewModel productViewModel;
|
||||
private SupplierViewModel supplierViewModel;
|
||||
private BulkDeleteHandler bulkDeleteHandler;
|
||||
|
||||
/**
|
||||
* Initializes the fragment and its associated ViewModels.
|
||||
*/
|
||||
@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) {
|
||||
View view = inflater.inflate(R.layout.fragment_product_supplier, container, false);
|
||||
binding = FragmentProductSupplierBinding.inflate(inflater, container, false);
|
||||
|
||||
setupRecyclerView(view);
|
||||
setupSearch(view);
|
||||
setupSwipeRefresh(view);
|
||||
loadData();
|
||||
setupRecyclerView();
|
||||
setupSearch();
|
||||
setupProductFilter();
|
||||
setupSupplierFilter();
|
||||
setupSwipeRefresh();
|
||||
setupFilterToggle();
|
||||
setupBulkDelete();
|
||||
|
||||
FloatingActionButton fab = view.findViewById(R.id.fabAddPS);
|
||||
fab.setOnClickListener(v -> openDetail(-1));
|
||||
binding.fabAddPS.setOnClickListener(v -> openDetail(-1));
|
||||
|
||||
ImageButton hamburger = view.findViewById(R.id.btnHamburgerPS);
|
||||
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.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);
|
||||
binding.btnHamburgerPS.setOnClickListener(v -> {
|
||||
Fragment parent = getParentFragment();
|
||||
if (parent != null) {
|
||||
Fragment grandParent = parent.getParentFragment();
|
||||
if (grandParent instanceof ListFragment) {
|
||||
((ListFragment) grandParent).openDrawer();
|
||||
}
|
||||
}
|
||||
}
|
||||
adapter.notifyDataSetChanged();
|
||||
});
|
||||
|
||||
return binding.getRoot();
|
||||
}
|
||||
|
||||
private void loadData() {
|
||||
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);
|
||||
private void setupBulkDelete() {
|
||||
bulkDeleteHandler = new BulkDeleteHandler(
|
||||
this,
|
||||
binding.layoutBulkDelete,
|
||||
binding.tvSelectionCount,
|
||||
binding.btnBulkDelete,
|
||||
adapter,
|
||||
"relationship",
|
||||
viewModel::bulkDeleteProductSuppliers,
|
||||
this::loadData
|
||||
);
|
||||
}
|
||||
|
||||
@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() {
|
||||
SpinnerUtils.setupFilterSpinner(binding.spinnerProduct, this::loadData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures the supplier filter spinner.
|
||||
*/
|
||||
private void setupSupplierFilter() {
|
||||
SpinnerUtils.setupFilterSpinner(binding.spinnerSupplier, this::loadData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches products and suppliers to populate the filters.
|
||||
*/
|
||||
private void loadFilterData() {
|
||||
productViewModel.getAllProducts(null, null, 0, 100, "prodName").observe(getViewLifecycleOwner(), resource -> {
|
||||
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
|
||||
productList = resource.data.getContent();
|
||||
SpinnerUtils.populateWhiteSpinner(requireContext(), binding.spinnerProduct, productList,
|
||||
ProductDTO::getProdName, "All Products", -1L, ProductDTO::getProdId);
|
||||
}
|
||||
});
|
||||
|
||||
supplierViewModel.getAllSuppliers(0, 100, null, "supCompany").observe(getViewLifecycleOwner(), resource -> {
|
||||
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
|
||||
supplierList = resource.data.getContent();
|
||||
SpinnerUtils.populateWhiteSpinner(requireContext(), binding.spinnerSupplier, supplierList,
|
||||
SupplierDTO::getSupCompany, "All Suppliers", -1L, SupplierDTO::getSupId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up the SwipeRefreshLayout to allow manual reloading of product-supplier data.
|
||||
*/
|
||||
private void setupSwipeRefresh() {
|
||||
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
|
||||
public void onProductSupplierClick(int position) { openDetail(position); }
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSelectionChanged(int count) {
|
||||
if (bulkDeleteHandler != null) {
|
||||
bulkDeleteHandler.onSelectionChanged(count);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,139 +1,224 @@
|
||||
package com.example.petstoremobile.fragments.listfragments;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.text.*;
|
||||
import android.text.Editable;
|
||||
import android.text.TextWatcher;
|
||||
import android.util.Log;
|
||||
import android.view.*;
|
||||
import android.widget.*;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
import androidx.navigation.fragment.NavHostFragment;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
|
||||
|
||||
import com.example.petstoremobile.R;
|
||||
import com.example.petstoremobile.adapters.PurchaseOrderAdapter;
|
||||
import com.example.petstoremobile.api.RetrofitClient;
|
||||
import com.example.petstoremobile.dtos.PageResponse;
|
||||
import com.example.petstoremobile.databinding.FragmentPurchaseOrderBinding;
|
||||
import com.example.petstoremobile.dtos.PurchaseOrderDTO;
|
||||
import com.example.petstoremobile.dtos.StoreDTO;
|
||||
import com.example.petstoremobile.fragments.ListFragment;
|
||||
import com.example.petstoremobile.fragments.listfragments.detailfragments.PurchaseOrderDetailFragment;
|
||||
import java.util.*;
|
||||
import retrofit2.*;
|
||||
import com.example.petstoremobile.utils.Resource;
|
||||
import com.example.petstoremobile.utils.SpinnerUtils;
|
||||
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
|
||||
implements PurchaseOrderAdapter.OnPurchaseOrderClickListener {
|
||||
|
||||
private FragmentPurchaseOrderBinding binding;
|
||||
private List<PurchaseOrderDTO> poList = new ArrayList<>();
|
||||
private List<PurchaseOrderDTO> filteredList = new ArrayList<>();
|
||||
private List<StoreDTO> storeList = new ArrayList<>();
|
||||
private PurchaseOrderAdapter adapter;
|
||||
private SwipeRefreshLayout swipeRefresh;
|
||||
private EditText etSearch;
|
||||
private PurchaseOrderViewModel viewModel;
|
||||
private StoreViewModel storeViewModel;
|
||||
|
||||
/**
|
||||
* Initializes the fragment and its associated ViewModels.
|
||||
*/
|
||||
@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) {
|
||||
View view = inflater.inflate(R.layout.fragment_purchase_order, container, false);
|
||||
binding = FragmentPurchaseOrderBinding.inflate(inflater, container, false);
|
||||
|
||||
setupRecyclerView(view);
|
||||
setupSearch(view);
|
||||
setupSwipeRefresh(view);
|
||||
loadData();
|
||||
setupRecyclerView();
|
||||
setupSearch();
|
||||
setupStoreFilter();
|
||||
setupSwipeRefresh();
|
||||
setupFilterToggle();
|
||||
|
||||
ImageButton hamburger = view.findViewById(R.id.btnHamburgerPO);
|
||||
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.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);
|
||||
binding.btnHamburgerPO.setOnClickListener(v -> {
|
||||
Fragment parent = getParentFragment();
|
||||
if (parent != null) {
|
||||
Fragment grandParent = parent.getParentFragment();
|
||||
if (grandParent instanceof ListFragment) {
|
||||
((ListFragment) grandParent).openDrawer();
|
||||
}
|
||||
}
|
||||
}
|
||||
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() {
|
||||
SpinnerUtils.setupFilterSpinner(binding.spinnerStore, this::loadData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches store data to populate the store filter.
|
||||
*/
|
||||
private void loadStoreData() {
|
||||
storeViewModel.getAllStores(0, 100).observe(getViewLifecycleOwner(), resource -> {
|
||||
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
|
||||
storeList = resource.data.getContent();
|
||||
SpinnerUtils.populateWhiteSpinner(requireContext(), binding.spinnerStore, storeList,
|
||||
StoreDTO::getStoreName, "All Stores", -1L, StoreDTO::getStoreId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the RecyclerView with a layout manager and adapter.
|
||||
*/
|
||||
private void setupRecyclerView() {
|
||||
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() {
|
||||
if (swipeRefresh != null)
|
||||
swipeRefresh.setRefreshing(true);
|
||||
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();
|
||||
}
|
||||
}
|
||||
String query = binding.etSearchPO != null ? binding.etSearchPO.getText().toString().trim() : "";
|
||||
if (query.isEmpty()) query = null;
|
||||
|
||||
public void onFailure(Call<PageResponse<PurchaseOrderDTO>> c, Throwable t) {
|
||||
if (swipeRefresh != null)
|
||||
swipeRefresh.setRefreshing(false);
|
||||
Log.e("POFragment", t.getMessage());
|
||||
Long storeId = null;
|
||||
if (binding.spinnerStore.getSelectedItemPosition() > 0 && !storeList.isEmpty()) {
|
||||
storeId = storeList.get(binding.spinnerStore.getSelectedItemPosition() - 1).getStoreId();
|
||||
}
|
||||
|
||||
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) {
|
||||
PurchaseOrderDetailFragment detail = new PurchaseOrderDetailFragment();
|
||||
Bundle args = new Bundle();
|
||||
PurchaseOrderDTO po = filteredList.get(position);
|
||||
PurchaseOrderDTO po = poList.get(position);
|
||||
args.putLong("purchaseOrderId", po.getPurchaseOrderId());
|
||||
args.putString("supplierName", po.getSupplierName());
|
||||
args.putString("orderDate", po.getOrderDate());
|
||||
args.putString("status", po.getStatus());
|
||||
detail.setArguments(args);
|
||||
ListFragment lf = (ListFragment) getParentFragment();
|
||||
if (lf != null)
|
||||
lf.loadFragment(detail);
|
||||
NavHostFragment.findNavController(this).navigate(R.id.nav_purchase_order_detail, args);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles item click in the purchase order list.
|
||||
*/
|
||||
@Override
|
||||
public void onPurchaseOrderClick(int position) {
|
||||
openDetail(position);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,97 +1,97 @@
|
||||
package com.example.petstoremobile.fragments.listfragments;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.text.*;
|
||||
import android.util.Log;
|
||||
import android.view.*;
|
||||
import android.widget.*;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.navigation.fragment.NavHostFragment;
|
||||
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.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import com.example.petstoremobile.R;
|
||||
import com.example.petstoremobile.adapters.SaleAdapter;
|
||||
import com.example.petstoremobile.api.RetrofitClient;
|
||||
import com.example.petstoremobile.dtos.PageResponse;
|
||||
import com.example.petstoremobile.dtos.SaleDTO;
|
||||
import com.example.petstoremobile.api.SaleApi;
|
||||
import com.example.petstoremobile.databinding.FragmentSaleBinding;
|
||||
import com.example.petstoremobile.fragments.ListFragment;
|
||||
import com.example.petstoremobile.fragments.listfragments.detailfragments.SaleDetailFragment;
|
||||
import com.google.android.material.floatingactionbutton.FloatingActionButton;
|
||||
import com.example.petstoremobile.fragments.listfragments.detailfragments.RefundFragment;
|
||||
import java.util.*;
|
||||
import retrofit2.*;
|
||||
import com.example.petstoremobile.models.Sale;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import dagger.hilt.android.AndroidEntryPoint;
|
||||
|
||||
@AndroidEntryPoint
|
||||
public class SaleFragment extends Fragment implements SaleAdapter.OnSaleClickListener {
|
||||
|
||||
private List<SaleDTO> saleList = new ArrayList<>();
|
||||
private List<SaleDTO> filteredList = new ArrayList<>();
|
||||
private FragmentSaleBinding binding;
|
||||
private List<Sale> saleList = new ArrayList<>();
|
||||
private List<Sale> filteredList = new ArrayList<>();
|
||||
private SaleAdapter adapter;
|
||||
private SwipeRefreshLayout swipeRefresh;
|
||||
private EditText etSearch;
|
||||
|
||||
@Inject SaleApi api;
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container,
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
|
||||
Bundle savedInstanceState) {
|
||||
View view = inflater.inflate(R.layout.fragment_sale, container, false);
|
||||
binding = FragmentSaleBinding.inflate(inflater, container, false);
|
||||
|
||||
setupRecyclerView(view);
|
||||
setupSearch(view);
|
||||
setupSwipeRefresh(view);
|
||||
loadSales();
|
||||
setupRecyclerView();
|
||||
loadSaleData();
|
||||
setupSearch();
|
||||
setupSwipeRefresh();
|
||||
|
||||
FloatingActionButton fab = view.findViewById(R.id.fabAddSale);
|
||||
fab.setOnClickListener(v -> openDetail(-1, null));
|
||||
|
||||
ImageButton hamburger = view.findViewById(R.id.btnHamburgerSale);
|
||||
hamburger.setOnClickListener(v -> {
|
||||
ListFragment lf = (ListFragment) getParentFragment();
|
||||
if (lf != null) lf.openDrawer();
|
||||
// Make the hamburger button open the drawer from listFragment
|
||||
binding.btnHamburger.setOnClickListener(v -> {
|
||||
Fragment parent = getParentFragment();
|
||||
if (parent != null) {
|
||||
Fragment grandParent = parent.getParentFragment();
|
||||
if (grandParent instanceof ListFragment) {
|
||||
((ListFragment) grandParent).openDrawer();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// ← moved inside onCreateView
|
||||
Button btnRefund = view.findViewById(R.id.btnOpenRefund);
|
||||
btnRefund.setOnClickListener(v -> {
|
||||
RefundFragment refundFragment = new RefundFragment();
|
||||
ListFragment lf = (ListFragment) getParentFragment();
|
||||
if (lf != null) lf.loadFragment(refundFragment);
|
||||
});
|
||||
|
||||
return view;
|
||||
return binding.getRoot();
|
||||
}
|
||||
|
||||
private void setupRecyclerView(View view) {
|
||||
RecyclerView rv = view.findViewById(R.id.recyclerViewSales);
|
||||
adapter = new SaleAdapter(filteredList, this);
|
||||
rv.setLayoutManager(new LinearLayoutManager(getContext()));
|
||||
rv.setAdapter(adapter);
|
||||
@Override
|
||||
public void onDestroyView() {
|
||||
super.onDestroyView();
|
||||
binding = null;
|
||||
}
|
||||
|
||||
private void setupSearch(View view) {
|
||||
etSearch = view.findViewById(R.id.etSearchSale);
|
||||
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 setupSearch() {
|
||||
binding.etSearchSale.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) {
|
||||
filterSales(s.toString());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterTextChanged(Editable s) {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void setupSwipeRefresh(View view) {
|
||||
swipeRefresh = view.findViewById(R.id.swipeRefreshSale);
|
||||
swipeRefresh.setOnRefreshListener(this::loadSales);
|
||||
}
|
||||
|
||||
private void filter(String query) {
|
||||
private void filterSales(String query) {
|
||||
filteredList.clear();
|
||||
if (query.isEmpty()) {
|
||||
filteredList.addAll(saleList);
|
||||
} else {
|
||||
String lower = query.toLowerCase();
|
||||
for (SaleDTO s : saleList) {
|
||||
if ((s.getEmployeeName() != null && s.getEmployeeName().toLowerCase().contains(lower))
|
||||
|| (s.getStoreName() != null && s.getStoreName().toLowerCase().contains(lower))
|
||||
|| (s.getPaymentMethod() != null && s.getPaymentMethod().toLowerCase().contains(lower))) {
|
||||
for (Sale s : saleList) {
|
||||
if (s.getItemName().toLowerCase().contains(lower)
|
||||
|| s.getEmployeeName().toLowerCase().contains(lower)
|
||||
|| s.getSaleDate().toLowerCase().contains(lower)
|
||||
|| s.getPaymentMethod().toLowerCase().contains(lower)
|
||||
|| String.valueOf(s.getSaleId()).contains(lower)) {
|
||||
filteredList.add(s);
|
||||
}
|
||||
}
|
||||
@@ -99,44 +99,47 @@ public class SaleFragment extends Fragment implements SaleAdapter.OnSaleClickLis
|
||||
adapter.notifyDataSetChanged();
|
||||
}
|
||||
|
||||
public void loadSales() {
|
||||
if (swipeRefresh != null) swipeRefresh.setRefreshing(true);
|
||||
RetrofitClient.getSaleApi(requireContext()).getAllSales(0, 100)
|
||||
.enqueue(new Callback<PageResponse<SaleDTO>>() {
|
||||
public void onResponse(Call<PageResponse<SaleDTO>> c,
|
||||
Response<PageResponse<SaleDTO>> r) {
|
||||
if (swipeRefresh != null) swipeRefresh.setRefreshing(false);
|
||||
if (r.isSuccessful() && r.body() != null) {
|
||||
saleList.clear();
|
||||
saleList.addAll(r.body().getContent());
|
||||
filter(etSearch != null ? etSearch.getText().toString() : "");
|
||||
} else {
|
||||
Toast.makeText(getContext(), "Failed to load sales",
|
||||
Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}
|
||||
public void onFailure(Call<PageResponse<SaleDTO>> c, Throwable t) {
|
||||
if (swipeRefresh != null) swipeRefresh.setRefreshing(false);
|
||||
Log.e("SaleFragment", t.getMessage());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void openDetail(int position, SaleDTO sale) {
|
||||
SaleDetailFragment detail = new SaleDetailFragment();
|
||||
Bundle args = new Bundle();
|
||||
if (position != -1 && sale != null) {
|
||||
args.putLong("saleId", sale.getSaleId());
|
||||
args.putBoolean("isRefund", Boolean.TRUE.equals(sale.getIsRefund()));
|
||||
args.putBoolean("viewOnly", true);
|
||||
}
|
||||
detail.setArguments(args);
|
||||
ListFragment lf = (ListFragment) getParentFragment();
|
||||
if (lf != null) lf.loadFragment(detail);
|
||||
private void setupSwipeRefresh() {
|
||||
binding.swipeRefreshSale.setOnRefreshListener(() -> {
|
||||
loadSaleData();
|
||||
binding.swipeRefreshSale.setRefreshing(false);
|
||||
});
|
||||
}
|
||||
|
||||
// When a sale row is clicked, open the refund screen for that sale
|
||||
@Override
|
||||
public void onSaleClick(int position) {
|
||||
openDetail(position, filteredList.get(position));
|
||||
Sale sale = filteredList.get(position);
|
||||
Bundle args = new Bundle();
|
||||
args.putInt("saleId", sale.getSaleId());
|
||||
args.putString("saleDate", sale.getSaleDate());
|
||||
args.putString("employeeName", sale.getEmployeeName());
|
||||
args.putDouble("total", sale.getTotal());
|
||||
args.putString("paymentMethod", sale.getPaymentMethod());
|
||||
|
||||
NavHostFragment.findNavController(this).navigate(R.id.nav_refund_detail, args);
|
||||
}
|
||||
}
|
||||
|
||||
public void reloadSales() {
|
||||
loadSaleData();
|
||||
}
|
||||
|
||||
// TODO: Replace with actual API call - GET v1/sales
|
||||
private void loadSaleData() {
|
||||
saleList.clear();
|
||||
saleList.add(new Sale(1, "2026-03-01", "John Smith", "Premium Dog Food", 2, 45.99, 91.98, "Card", false));
|
||||
saleList.add(new Sale(2, "2026-03-02", "Jane Doe", "Cat Toy Bundle", 1, 19.99, 19.99, "Cash", false));
|
||||
saleList.add(new Sale(3, "2026-03-03", "John Smith", "Pet Shampoo", 3, 12.99, 38.97, "Card", false));
|
||||
saleList.add(new Sale(4, "2026-03-04", "Jane Doe", "Dog Bed - Large", 1, 89.99, 89.99, "Cash", true));
|
||||
filteredList.clear();
|
||||
filteredList.addAll(saleList);
|
||||
if (adapter != null)
|
||||
adapter.notifyDataSetChanged();
|
||||
}
|
||||
|
||||
private void setupRecyclerView() {
|
||||
adapter = new SaleAdapter(filteredList, this);
|
||||
binding.recyclerViewSales.setLayoutManager(new LinearLayoutManager(getContext()));
|
||||
binding.recyclerViewSales.setAdapter(adapter);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,189 +1,241 @@
|
||||
package com.example.petstoremobile.fragments.listfragments;
|
||||
|
||||
import android.os.Bundle;
|
||||
|
||||
import androidx.fragment.app.Fragment;
|
||||
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.EditText;
|
||||
import android.widget.ImageButton;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
import androidx.navigation.fragment.NavHostFragment;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.example.petstoremobile.R;
|
||||
import com.example.petstoremobile.adapters.ServiceAdapter;
|
||||
import com.example.petstoremobile.api.RetrofitClient;
|
||||
import com.example.petstoremobile.api.ServiceApi;
|
||||
import com.example.petstoremobile.dtos.PageResponse;
|
||||
import com.example.petstoremobile.databinding.FragmentServiceBinding;
|
||||
import com.example.petstoremobile.dtos.ServiceDTO;
|
||||
import com.example.petstoremobile.fragments.ListFragment;
|
||||
import com.example.petstoremobile.fragments.listfragments.detailfragments.ServiceDetailFragment;
|
||||
import com.google.android.material.floatingactionbutton.FloatingActionButton;
|
||||
import com.example.petstoremobile.utils.BulkDeleteHandler;
|
||||
import com.example.petstoremobile.utils.Resource;
|
||||
import com.example.petstoremobile.viewmodels.ServiceViewModel;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import retrofit2.Call;
|
||||
import retrofit2.Callback;
|
||||
import retrofit2.Response;
|
||||
import dagger.hilt.android.AndroidEntryPoint;
|
||||
|
||||
/**
|
||||
* Fragment class for displaying a list of services in a RecyclerView.
|
||||
*/
|
||||
@AndroidEntryPoint
|
||||
public class ServiceFragment extends Fragment implements ServiceAdapter.OnServiceClickListener {
|
||||
|
||||
private List<ServiceDTO> serviceList = new ArrayList<>();
|
||||
private List<ServiceDTO> filteredList = new ArrayList<>();
|
||||
private static final String TAG = "ServiceFragment";
|
||||
private static final int PAGE_SIZE = 20;
|
||||
|
||||
private FragmentServiceBinding binding;
|
||||
private final List<ServiceDTO> serviceList = new ArrayList<>();
|
||||
private ServiceAdapter adapter;
|
||||
private ImageButton hamburger;
|
||||
private ServiceApi api;
|
||||
private SwipeRefreshLayout swipeRefreshLayout;
|
||||
private EditText etSearch;
|
||||
private ServiceViewModel viewModel;
|
||||
private BulkDeleteHandler bulkDeleteHandler;
|
||||
|
||||
//load service view
|
||||
// Pagination
|
||||
private int currentPage = 0;
|
||||
private boolean isLastPage = false;
|
||||
private boolean isLoading = false;
|
||||
|
||||
/**
|
||||
* Initializes the fragment and its associated ViewModel.
|
||||
*/
|
||||
@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) {
|
||||
View view = inflater.inflate(R.layout.fragment_service, container, false);
|
||||
binding = FragmentServiceBinding.inflate(inflater, container, false);
|
||||
|
||||
api = RetrofitClient.getServiceApi(requireContext());
|
||||
hamburger = view.findViewById(R.id.btnHamburger);
|
||||
setupRecyclerView();
|
||||
setupSearch();
|
||||
setupSwipeRefresh();
|
||||
setupFilterToggle();
|
||||
setupBulkDelete();
|
||||
loadServices(true);
|
||||
|
||||
setupRecyclerView(view);
|
||||
setupSearch(view);
|
||||
setupSwipeRefresh(view);
|
||||
loadServiceData();
|
||||
binding.fabAddService.setOnClickListener(v -> openDetail(null));
|
||||
|
||||
//Add button to opens the add dialog
|
||||
FloatingActionButton fabAddService = view.findViewById(R.id.fabAddService);
|
||||
fabAddService.setOnClickListener(v -> openServiceDetails(-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();
|
||||
binding.btnHamburger.setOnClickListener(v -> {
|
||||
Fragment parent = getParentFragment();
|
||||
if (parent != null) {
|
||||
Fragment grandParent = parent.getParentFragment();
|
||||
if (grandParent instanceof ListFragment) {
|
||||
((ListFragment) grandParent).openDrawer();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return view;
|
||||
return binding.getRoot();
|
||||
}
|
||||
|
||||
private void setupSearch(View view) {
|
||||
etSearch = view.findViewById(R.id.etSearchService);
|
||||
etSearch.addTextChangedListener(new TextWatcher() {
|
||||
private void setupBulkDelete() {
|
||||
bulkDeleteHandler = new BulkDeleteHandler(
|
||||
this,
|
||||
binding.layoutBulkDelete,
|
||||
binding.tvSelectionCount,
|
||||
binding.btnBulkDelete,
|
||||
adapter,
|
||||
"service",
|
||||
viewModel::bulkDeleteServices,
|
||||
() -> loadServices(true)
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyView() {
|
||||
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 filters when closing
|
||||
binding.etSearchService.setText("");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up 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 onTextChanged(CharSequence s, int start, int before, int count) {
|
||||
filterServices(s.toString());
|
||||
loadServices(true);
|
||||
}
|
||||
@Override public void afterTextChanged(Editable s) {}
|
||||
});
|
||||
}
|
||||
|
||||
private void filterServices(String query) {
|
||||
filteredList.clear();
|
||||
if (query.isEmpty()) {
|
||||
filteredList.addAll(serviceList);
|
||||
} else {
|
||||
String lower = query.toLowerCase();
|
||||
for (ServiceDTO s : serviceList) {
|
||||
if (s.getServiceName().toLowerCase().contains(lower)
|
||||
|| s.getServiceDesc().toLowerCase().contains(lower)) {
|
||||
filteredList.add(s);
|
||||
/**
|
||||
* Initializes the RecyclerView with a layout manager and adapter.
|
||||
*/
|
||||
private void setupRecyclerView() {
|
||||
adapter = new ServiceAdapter(serviceList, this);
|
||||
binding.recyclerViewServices.setLayoutManager(new LinearLayoutManager(getContext()));
|
||||
binding.recyclerViewServices.setAdapter(adapter);
|
||||
|
||||
binding.recyclerViewServices.addOnScrollListener(new RecyclerView.OnScrollListener() {
|
||||
@Override
|
||||
public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
|
||||
if (dy <= 0) return;
|
||||
LinearLayoutManager lm = (LinearLayoutManager) binding.recyclerViewServices.getLayoutManager();
|
||||
if (lm == null) return;
|
||||
int visible = lm.getChildCount();
|
||||
int total = lm.getItemCount();
|
||||
int firstVis = lm.findFirstVisibleItemPosition();
|
||||
if (!isLoading && !isLastPage && (visible + firstVis) >= total - 3) {
|
||||
loadServices(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
adapter.notifyDataSetChanged();
|
||||
}
|
||||
|
||||
private void setupSwipeRefresh(View view) {
|
||||
swipeRefreshLayout = view.findViewById(R.id.swipeRefreshService);
|
||||
swipeRefreshLayout.setOnRefreshListener(() -> {
|
||||
loadServiceData();
|
||||
});
|
||||
}
|
||||
|
||||
//Open the service detail view depending on the mode
|
||||
private void openServiceDetails(int position) {
|
||||
ServiceDetailFragment detailFragment = new ServiceDetailFragment();
|
||||
|
||||
//Make a bundle to pass data to the detail fragment
|
||||
Bundle args = new Bundle();
|
||||
args.putInt("position", position);
|
||||
|
||||
//if editing a service, add the service data to the bundle
|
||||
if (position != -1) {
|
||||
ServiceDTO service = filteredList.get(position);
|
||||
args.putInt("serviceId", service.getServiceId().intValue());
|
||||
args.putString("serviceName", service.getServiceName());
|
||||
args.putString("serviceDesc", service.getServiceDesc());
|
||||
args.putInt("serviceDuration", service.getServiceDuration());
|
||||
args.putDouble("servicePrice", service.getServicePrice());
|
||||
}
|
||||
|
||||
//send the bundle to the detail fragment to display
|
||||
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);
|
||||
}
|
||||
/**
|
||||
* Sets up the SwipeRefreshLayout.
|
||||
*/
|
||||
private void setupSwipeRefresh() {
|
||||
binding.swipeRefreshService.setOnRefreshListener(() -> loadServices(true));
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches a page of services from the API.
|
||||
*/
|
||||
private void loadServices(boolean reset) {
|
||||
if (isLoading) return;
|
||||
|
||||
if (reset) {
|
||||
currentPage = 0;
|
||||
isLastPage = false;
|
||||
}
|
||||
|
||||
String query = binding.etSearchService.getText().toString().trim();
|
||||
if (query.isEmpty()) query = null;
|
||||
|
||||
viewModel.getAllServices(currentPage, PAGE_SIZE, query, "serviceName").observe(getViewLifecycleOwner(), resource -> {
|
||||
if (resource == null) return;
|
||||
|
||||
switch (resource.status) {
|
||||
case LOADING:
|
||||
isLoading = true;
|
||||
binding.swipeRefreshService.setRefreshing(true);
|
||||
break;
|
||||
case SUCCESS:
|
||||
isLoading = false;
|
||||
binding.swipeRefreshService.setRefreshing(false);
|
||||
if (resource.data != null) {
|
||||
if (reset) serviceList.clear();
|
||||
serviceList.addAll(resource.data.getContent());
|
||||
adapter.notifyDataSetChanged();
|
||||
isLastPage = resource.data.isLast();
|
||||
if (!isLastPage) currentPage++;
|
||||
}
|
||||
break;
|
||||
case ERROR:
|
||||
isLoading = false;
|
||||
binding.swipeRefreshService.setRefreshing(false);
|
||||
Log.e(TAG, "Error: " + resource.message);
|
||||
Toast.makeText(getContext(), "Failed to load services: " + resource.message, Toast.LENGTH_SHORT).show();
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigates to the service detail screen.
|
||||
*/
|
||||
private void openDetail(ServiceDTO service) {
|
||||
Bundle args = new Bundle();
|
||||
if (service != null) {
|
||||
args.putLong("serviceId", service.getServiceId());
|
||||
}
|
||||
NavHostFragment.findNavController(this).navigate(R.id.nav_service_detail, args);
|
||||
}
|
||||
|
||||
// Called by ServiceAdapter when a row is clicked to open the details view
|
||||
@Override
|
||||
public void onServiceClick(int position) {
|
||||
openServiceDetails(position);
|
||||
}
|
||||
|
||||
// Helper function to get a list of all services from the backend
|
||||
private void loadServiceData() {
|
||||
if (swipeRefreshLayout != null) {
|
||||
swipeRefreshLayout.setRefreshing(true);
|
||||
if (position >= 0 && position < serviceList.size()) {
|
||||
openDetail(serviceList.get(position));
|
||||
}
|
||||
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 {
|
||||
Log.e("onResponse: ", response.message());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(Call<PageResponse<ServiceDTO>> call, Throwable t) {
|
||||
if (swipeRefreshLayout != null) {
|
||||
swipeRefreshLayout.setRefreshing(false);
|
||||
}
|
||||
if (getContext() != null) {
|
||||
Toast.makeText(getContext(),
|
||||
"Failed to load services", Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
Log.e("onFailure: ", t.getMessage());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
//set up the recyclerview and adapter
|
||||
private void setupRecyclerView(View view) {
|
||||
RecyclerView recyclerView = view.findViewById(R.id.recyclerViewServices);
|
||||
adapter = new ServiceAdapter(filteredList, this);
|
||||
recyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
|
||||
recyclerView.setAdapter(adapter);
|
||||
@Override
|
||||
public void onSelectionChanged(int count) {
|
||||
if (bulkDeleteHandler != null) {
|
||||
bulkDeleteHandler.onSelectionChanged(count);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,10 +2,12 @@ package com.example.petstoremobile.fragments.listfragments;
|
||||
|
||||
import android.os.Bundle;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
import androidx.navigation.fragment.NavHostFragment;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
|
||||
|
||||
import android.text.Editable;
|
||||
import android.text.TextWatcher;
|
||||
@@ -13,180 +15,205 @@ import android.util.Log;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.EditText;
|
||||
import android.widget.ImageButton;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.example.petstoremobile.R;
|
||||
import com.example.petstoremobile.adapters.SupplierAdapter;
|
||||
import com.example.petstoremobile.api.RetrofitClient;
|
||||
import com.example.petstoremobile.api.SupplierApi;
|
||||
import com.example.petstoremobile.dtos.PageResponse;
|
||||
import com.example.petstoremobile.databinding.FragmentSupplierBinding;
|
||||
import com.example.petstoremobile.dtos.SupplierDTO;
|
||||
import com.example.petstoremobile.fragments.ListFragment;
|
||||
import com.example.petstoremobile.fragments.listfragments.detailfragments.SupplierDetailFragment;
|
||||
import com.google.android.material.floatingactionbutton.FloatingActionButton;
|
||||
import com.example.petstoremobile.utils.BulkDeleteHandler;
|
||||
import com.example.petstoremobile.utils.Resource;
|
||||
import com.example.petstoremobile.viewmodels.SupplierViewModel;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import retrofit2.Call;
|
||||
import retrofit2.Callback;
|
||||
import retrofit2.Response;
|
||||
import dagger.hilt.android.AndroidEntryPoint;
|
||||
|
||||
@AndroidEntryPoint
|
||||
public class SupplierFragment extends Fragment implements SupplierAdapter.OnSupplierClickListener {
|
||||
|
||||
private FragmentSupplierBinding binding;
|
||||
private List<SupplierDTO> supplierList = new ArrayList<>();
|
||||
private List<SupplierDTO> filteredList = new ArrayList<>();
|
||||
private SupplierAdapter adapter;
|
||||
private ImageButton hamburger;
|
||||
private SupplierApi api;
|
||||
private SwipeRefreshLayout swipeRefreshLayout;
|
||||
private EditText etSearch;
|
||||
private SupplierViewModel viewModel;
|
||||
private BulkDeleteHandler bulkDeleteHandler;
|
||||
|
||||
//load supplier view
|
||||
/**
|
||||
* Initializes the fragment and its associated SupplierViewModel.
|
||||
*/
|
||||
@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) {
|
||||
View view = inflater.inflate(R.layout.fragment_supplier, container, false);
|
||||
binding = FragmentSupplierBinding.inflate(inflater, container, false);
|
||||
|
||||
api = RetrofitClient.getSupplierApi(requireContext());
|
||||
hamburger = view.findViewById(R.id.btnHamburger);
|
||||
|
||||
setupRecyclerView(view);
|
||||
setupSearch(view);
|
||||
setupSwipeRefresh(view);
|
||||
setupRecyclerView();
|
||||
setupSearch();
|
||||
setupSwipeRefresh();
|
||||
setupFilterToggle();
|
||||
setupBulkDelete();
|
||||
loadSupplierData();
|
||||
|
||||
//Add button to opens the add dialog
|
||||
FloatingActionButton fabAddSupplier = view.findViewById(R.id.fabAddSupplier);
|
||||
fabAddSupplier.setOnClickListener(v -> openSupplierDetails(-1));
|
||||
binding.fabAddSupplier.setOnClickListener(v -> openSupplierDetails(-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();
|
||||
binding.btnHamburger.setOnClickListener(v -> {
|
||||
Fragment parent = getParentFragment();
|
||||
if (parent != null) {
|
||||
Fragment grandParent = parent.getParentFragment();
|
||||
if (grandParent instanceof ListFragment) {
|
||||
((ListFragment) grandParent).openDrawer();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return view;
|
||||
return binding.getRoot();
|
||||
}
|
||||
|
||||
private void setupSearch(View view) {
|
||||
etSearch = view.findViewById(R.id.etSearchSupplier);
|
||||
etSearch.addTextChangedListener(new TextWatcher() {
|
||||
private void setupBulkDelete() {
|
||||
bulkDeleteHandler = new BulkDeleteHandler(
|
||||
this,
|
||||
binding.layoutBulkDelete,
|
||||
binding.tvSelectionCount,
|
||||
binding.btnBulkDelete,
|
||||
adapter,
|
||||
"supplier",
|
||||
viewModel::bulkDeleteSuppliers,
|
||||
this::loadSupplierData
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyView() {
|
||||
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 onTextChanged(CharSequence s, int start, int before, int count) {
|
||||
filterSuppliers(s.toString());
|
||||
loadSupplierData();
|
||||
}
|
||||
@Override public void afterTextChanged(Editable s) {}
|
||||
});
|
||||
}
|
||||
|
||||
private void filterSuppliers(String query) {
|
||||
filteredList.clear();
|
||||
if (query.isEmpty()) {
|
||||
filteredList.addAll(supplierList);
|
||||
} else {
|
||||
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();
|
||||
/**
|
||||
* Sets up the SwipeRefreshLayout to allow manual reloading of supplier data.
|
||||
*/
|
||||
private void setupSwipeRefresh() {
|
||||
binding.swipeRefreshSupplier.setOnRefreshListener(this::loadSupplierData);
|
||||
}
|
||||
|
||||
private void setupSwipeRefresh(View view) {
|
||||
swipeRefreshLayout = view.findViewById(R.id.swipeRefreshSupplier);
|
||||
swipeRefreshLayout.setOnRefreshListener(() -> {
|
||||
loadSupplierData();
|
||||
});
|
||||
}
|
||||
|
||||
//Open the supplier detail view depending on the mode
|
||||
/**
|
||||
* Navigates to the supplier detail screen for editing an existing record or adding a new one.
|
||||
*/
|
||||
private void openSupplierDetails(int position) {
|
||||
SupplierDetailFragment detailFragment = new SupplierDetailFragment();
|
||||
|
||||
//Make a bundle to pass data to the detail fragment
|
||||
Bundle args = new Bundle();
|
||||
args.putInt("position", position);
|
||||
|
||||
//if editing a supplier, add the supplier data to the bundle
|
||||
//if editing a supplier, add the supplier id to the bundle
|
||||
if (position != -1) {
|
||||
SupplierDTO supplier = filteredList.get(position);
|
||||
args.putInt("supId", supplier.getSupId().intValue());
|
||||
args.putString("supCompany", supplier.getSupCompany());
|
||||
args.putString("supContactFirstName", supplier.getSupContactFirstName());
|
||||
args.putString("supContactLastName", supplier.getSupContactLastName());
|
||||
args.putString("supEmail", supplier.getSupEmail());
|
||||
args.putString("supPhone", supplier.getSupPhone());
|
||||
SupplierDTO supplier = supplierList.get(position);
|
||||
args.putLong("supId", supplier.getSupId());
|
||||
}
|
||||
|
||||
//send the bundle to the detail fragment to display
|
||||
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);
|
||||
}
|
||||
NavHostFragment.findNavController(this).navigate(R.id.nav_supplier_detail, args);
|
||||
}
|
||||
|
||||
|
||||
// Called by SupplierAdapter when a row is clicked to open the details view
|
||||
/**
|
||||
* Handles item click in the supplier list.
|
||||
*/
|
||||
@Override
|
||||
public void onSupplierClick(int position) {
|
||||
openSupplierDetails(position);
|
||||
}
|
||||
|
||||
// Helper function to get a list of all suppliers from the backend
|
||||
private void loadSupplierData() {
|
||||
if (swipeRefreshLayout != null) {
|
||||
swipeRefreshLayout.setRefreshing(true);
|
||||
@Override
|
||||
public void onSelectionChanged(int count) {
|
||||
if (bulkDeleteHandler != null) {
|
||||
bulkDeleteHandler.onSelectionChanged(count);
|
||||
}
|
||||
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 {
|
||||
Log.e("onResponse: ", response.message());
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Fetches all supplier data from the server through the ViewModel and updates the UI.
|
||||
*/
|
||||
private void loadSupplierData() {
|
||||
String query = binding.etSearchSupplier != null ? binding.etSearchSupplier.getText().toString().trim() : "";
|
||||
if (query.isEmpty()) query = null;
|
||||
|
||||
@Override
|
||||
public void onFailure(Call<PageResponse<SupplierDTO>> call, Throwable t) {
|
||||
if (swipeRefreshLayout != null) {
|
||||
swipeRefreshLayout.setRefreshing(false);
|
||||
}
|
||||
if (getContext() != null) {
|
||||
Toast.makeText(getContext(),
|
||||
"Failed to load suppliers", Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
Log.e("onFailure: ", t.getMessage());
|
||||
//Load suppliers from the backend with query and default sort
|
||||
viewModel.getAllSuppliers(0, 100, query, "supCompany").observe(getViewLifecycleOwner(), resource -> {
|
||||
if (resource == null) return;
|
||||
|
||||
// Check the status to see if the resource is loaded and display the data
|
||||
switch (resource.status) {
|
||||
case LOADING:
|
||||
// Show loading indicator
|
||||
binding.swipeRefreshSupplier.setRefreshing(true);
|
||||
break;
|
||||
case SUCCESS:
|
||||
// Hide loading indicator and display data
|
||||
binding.swipeRefreshSupplier.setRefreshing(false);
|
||||
if (resource.data != null) {
|
||||
supplierList.clear();
|
||||
supplierList.addAll(resource.data.getContent());
|
||||
adapter.notifyDataSetChanged();
|
||||
}
|
||||
break;
|
||||
case ERROR:
|
||||
// Hide loading indicator and toast error message
|
||||
binding.swipeRefreshSupplier.setRefreshing(false);
|
||||
if (getContext() != null) {
|
||||
Toast.makeText(getContext(), "Failed to load suppliers: " + resource.message, Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
Log.e("SupplierFragment", "Error loading suppliers: " + resource.message);
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
//set up the recyclerview and adapter
|
||||
private void setupRecyclerView(View view) {
|
||||
RecyclerView recyclerView = view.findViewById(R.id.recyclerViewSuppliers);
|
||||
adapter = new SupplierAdapter(filteredList, this);
|
||||
recyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
|
||||
recyclerView.setAdapter(adapter);
|
||||
/**
|
||||
* Initializes the RecyclerView with a layout manager and adapter for displaying suppliers.
|
||||
*/
|
||||
private void setupRecyclerView() {
|
||||
adapter = new SupplierAdapter(supplierList, this);
|
||||
binding.recyclerViewSuppliers.setLayoutManager(new LinearLayoutManager(getContext()));
|
||||
binding.recyclerViewSuppliers.setAdapter(adapter);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,78 +2,109 @@ package com.example.petstoremobile.fragments.listfragments.detailfragments;
|
||||
|
||||
import android.app.DatePickerDialog;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
import android.view.*;
|
||||
import android.widget.*;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import com.example.petstoremobile.R;
|
||||
import com.example.petstoremobile.api.*;
|
||||
import com.example.petstoremobile.dtos.*;
|
||||
import com.example.petstoremobile.fragments.ListFragment;
|
||||
import java.util.*;
|
||||
import retrofit2.*;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
import androidx.navigation.fragment.NavHostFragment;
|
||||
|
||||
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 com.example.petstoremobile.viewmodels.StoreViewModel;
|
||||
import com.example.petstoremobile.viewmodels.UserViewModel;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.*;
|
||||
|
||||
import dagger.hilt.android.AndroidEntryPoint;
|
||||
|
||||
/**
|
||||
* Fragment for displaying and editing adoption request details.
|
||||
*/
|
||||
@AndroidEntryPoint
|
||||
public class AdoptionDetailFragment extends Fragment {
|
||||
|
||||
private TextView tvMode, tvAdoptionId;
|
||||
private EditText etAdoptionDate;
|
||||
private Spinner spinnerPet, spinnerCustomer, spinnerEmployee, spinnerStatus;
|
||||
private Button btnSave, btnDelete, btnBack;
|
||||
private FragmentAdoptionDetailBinding binding;
|
||||
|
||||
private long adoptionId = -1;
|
||||
private boolean isEditing = false;
|
||||
private long preselectedPetId = -1;
|
||||
private long preselectedCustomerId = -1;
|
||||
|
||||
private List<EmployeeDTO> employeeList = new ArrayList<>();
|
||||
private long preselectedStoreId = -1;
|
||||
private long preselectedEmployeeId = -1;
|
||||
|
||||
private List<PetDTO> petList = new ArrayList<>();
|
||||
private List<CustomerDTO> customerList = new ArrayList<>();
|
||||
private List<StoreDTO> storeList = new ArrayList<>();
|
||||
private List<UserDTO> employeeList = new ArrayList<>();
|
||||
|
||||
private final String[] STATUSES = {"Pending", "Approved", "Rejected"};
|
||||
private final String[] STATUSES = {"Pending", "Completed", "Cancelled"};
|
||||
|
||||
private AdoptionViewModel adoptionViewModel;
|
||||
private PetViewModel petViewModel;
|
||||
private CustomerViewModel customerViewModel;
|
||||
private StoreViewModel storeViewModel;
|
||||
private UserViewModel userViewModel;
|
||||
|
||||
@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);
|
||||
storeViewModel = new ViewModelProvider(this).get(StoreViewModel.class);
|
||||
userViewModel = new ViewModelProvider(this).get(UserViewModel.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
|
||||
Bundle savedInstanceState) {
|
||||
View view = inflater.inflate(R.layout.fragment_adoption_detail, container, false);
|
||||
initViews(view);
|
||||
binding = FragmentAdoptionDetailBinding.inflate(inflater, container, false);
|
||||
return binding.getRoot();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
setupSpinners();
|
||||
setupDatePicker();
|
||||
loadData();
|
||||
loadSpinnersData();
|
||||
handleArguments();
|
||||
|
||||
btnBack.setOnClickListener(v -> navigateBack());
|
||||
btnSave.setOnClickListener(v -> saveAdoption());
|
||||
btnDelete.setOnClickListener(v -> confirmDelete());
|
||||
return view;
|
||||
binding.btnAdoptionBack.setOnClickListener(v -> navigateBack());
|
||||
binding.btnSaveAdoption.setOnClickListener(v -> saveAdoption());
|
||||
binding.btnDeleteAdoption.setOnClickListener(v -> confirmDelete());
|
||||
}
|
||||
|
||||
private void initViews(View v) {
|
||||
tvMode = v.findViewById(R.id.tvAdoptionMode);
|
||||
tvAdoptionId = v.findViewById(R.id.tvAdoptionId);
|
||||
etAdoptionDate = v.findViewById(R.id.etAdoptionDate);
|
||||
spinnerPet = v.findViewById(R.id.spinnerAdoptionPet);
|
||||
spinnerCustomer= v.findViewById(R.id.spinnerAdoptionCustomer);
|
||||
spinnerEmployee = v.findViewById(R.id.spinnerAdoptionEmployee);
|
||||
spinnerStatus = v.findViewById(R.id.spinnerAdoptionStatus);
|
||||
btnSave = v.findViewById(R.id.btnSaveAdoption);
|
||||
btnDelete = v.findViewById(R.id.btnDeleteAdoption);
|
||||
btnBack = v.findViewById(R.id.btnAdoptionBack);
|
||||
@Override
|
||||
public void onDestroyView() {
|
||||
super.onDestroyView();
|
||||
binding = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures the spinner for adoption status.
|
||||
*/
|
||||
private void setupSpinners() {
|
||||
spinnerStatus.setAdapter(new ArrayAdapter<>(requireContext(),
|
||||
android.R.layout.simple_spinner_item, STATUSES));
|
||||
SpinnerUtils.setupStringSpinner(requireContext(), binding.spinnerAdoptionStatus, STATUSES);
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures the date picker dialog for the adoption date field.
|
||||
*/
|
||||
private void setupDatePicker() {
|
||||
etAdoptionDate.setOnClickListener(v -> {
|
||||
binding.etAdoptionDate.setOnClickListener(v -> {
|
||||
Calendar c = Calendar.getInstance();
|
||||
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)),
|
||||
c.get(Calendar.YEAR),
|
||||
c.get(Calendar.MONTH),
|
||||
@@ -81,217 +112,238 @@ public class AdoptionDetailFragment extends Fragment {
|
||||
});
|
||||
}
|
||||
|
||||
private void loadData() {
|
||||
/**
|
||||
* Fetches required data for spinners from the backend.
|
||||
*/
|
||||
private void loadSpinnersData() {
|
||||
loadPets();
|
||||
loadCustomers();
|
||||
loadStores();
|
||||
loadEmployees();
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the list of pets from the API.
|
||||
*/
|
||||
private void loadPets() {
|
||||
RetrofitClient.getPetApi(requireContext()).getAllPets(0, 200)
|
||||
.enqueue(new Callback<PageResponse<PetDTO>>() {
|
||||
public void onResponse(Call<PageResponse<PetDTO>> c,
|
||||
Response<PageResponse<PetDTO>> r) {
|
||||
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 ArrayAdapter<>(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;
|
||||
}
|
||||
petViewModel.getAllPets(0, 200, null, null, null, null, "petName").observe(getViewLifecycleOwner(), resource -> {
|
||||
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
|
||||
petList = resource.data.getContent();
|
||||
refreshPetSpinner();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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() {
|
||||
RetrofitClient.getCustomerApi(requireContext()).getAllCustomers(0, 200)
|
||||
.enqueue(new Callback<PageResponse<CustomerDTO>>() {
|
||||
public void onResponse(Call<PageResponse<CustomerDTO>> c,
|
||||
Response<PageResponse<CustomerDTO>> r) {
|
||||
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 ArrayAdapter<>(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;
|
||||
}
|
||||
customerViewModel.getAllCustomers(0, 200).observe(getViewLifecycleOwner(), resource -> {
|
||||
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
|
||||
customerList = resource.data.getContent();
|
||||
refreshCustomerSpinner();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Populates the customer selection spinner with data.
|
||||
*/
|
||||
private void refreshCustomerSpinner() {
|
||||
SpinnerUtils.populateSpinner(requireContext(), binding.spinnerAdoptionCustomer, customerList,
|
||||
item -> item.getFirstName() + " " + item.getLastName(),
|
||||
"-- Select Customer --",
|
||||
preselectedCustomerId, CustomerDTO::getCustomerId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the list of stores from the API.
|
||||
*/
|
||||
private void loadStores() {
|
||||
storeViewModel.getAllStores(0, 200).observe(getViewLifecycleOwner(), resource -> {
|
||||
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
|
||||
storeList = resource.data.getContent();
|
||||
refreshStoreSpinner();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Populates the store selection spinner with data.
|
||||
*/
|
||||
private void refreshStoreSpinner() {
|
||||
SpinnerUtils.populateSpinner(requireContext(), binding.spinnerAdoptionStore, storeList,
|
||||
StoreDTO::getStoreName, "-- Select Store --",
|
||||
preselectedStoreId, StoreDTO::getStoreId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the list of employees from the API.
|
||||
*/
|
||||
private void loadEmployees() {
|
||||
RetrofitClient.getEmployeeApi(requireContext()).getAllEmployees(0, 100)
|
||||
.enqueue(new Callback<PageResponse<EmployeeDTO>>() {
|
||||
public void onResponse(Call<PageResponse<EmployeeDTO>> c,
|
||||
Response<PageResponse<EmployeeDTO>> r) {
|
||||
if (r.isSuccessful() && r.body() != null) {
|
||||
employeeList = r.body().getContent();
|
||||
List<String> names = new ArrayList<>();
|
||||
names.add("-- Select Employee --");
|
||||
for (EmployeeDTO e : employeeList)
|
||||
names.add(e.getFullName() + " (" + e.getRole() + ")");
|
||||
spinnerEmployee.setAdapter(new ArrayAdapter<>(requireContext(),
|
||||
android.R.layout.simple_spinner_item, names));
|
||||
if (preselectedEmployeeId != -1) {
|
||||
for (int i = 0; i < employeeList.size(); i++) {
|
||||
if (employeeList.get(i).getEmployeeId() == preselectedEmployeeId) {
|
||||
spinnerEmployee.setSelection(i + 1); break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
public void onFailure(Call<PageResponse<EmployeeDTO>> c, Throwable t) {
|
||||
Log.e("ADOPTION", "Employee load failed: " + t.getMessage());
|
||||
}
|
||||
});
|
||||
userViewModel.getUsers("STAFF", 0, 100).observe(getViewLifecycleOwner(), resource -> {
|
||||
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
|
||||
employeeList = resource.data.getContent();
|
||||
refreshEmployeeSpinner();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Populates the employee selection spinner with data.
|
||||
*/
|
||||
private void refreshEmployeeSpinner() {
|
||||
SpinnerUtils.populateSpinner(requireContext(), binding.spinnerAdoptionEmployee, employeeList,
|
||||
UserDTO::getFullName, "-- Select Staff --",
|
||||
preselectedEmployeeId, UserDTO::getId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles arguments to determine if the fragment is in edit or add mode.
|
||||
*/
|
||||
private void handleArguments() {
|
||||
Bundle a = getArguments();
|
||||
if (a != null && a.containsKey("adoptionId")) {
|
||||
isEditing = true;
|
||||
adoptionId = a.getLong("adoptionId");
|
||||
preselectedPetId = a.getLong("petId", -1);
|
||||
preselectedCustomerId = a.getLong("customerId", -1);
|
||||
preselectedEmployeeId = a.getLong("employeeId", -1);
|
||||
tvMode.setText("Edit Adoption");
|
||||
tvAdoptionId.setText("ID: " + adoptionId);
|
||||
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;
|
||||
}
|
||||
}
|
||||
adoptionId = a.getLong("adoptionId");
|
||||
binding.tvAdoptionMode.setText("Edit Adoption");
|
||||
binding.tvAdoptionId.setText("ID: " + adoptionId);
|
||||
binding.tvAdoptionId.setVisibility(View.VISIBLE);
|
||||
binding.btnDeleteAdoption.setVisibility(View.VISIBLE);
|
||||
loadAdoptionData();
|
||||
} else {
|
||||
tvMode.setText("Add Adoption");
|
||||
btnDelete.setVisibility(View.GONE);
|
||||
tvAdoptionId.setVisibility(View.GONE);
|
||||
binding.tvAdoptionMode.setText("Add Adoption");
|
||||
binding.btnDeleteAdoption.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;
|
||||
preselectedStoreId = a.getSourceStoreId() != null ? a.getSourceStoreId() : -1;
|
||||
preselectedEmployeeId = a.getEmployeeId() != null ? a.getEmployeeId() : -1;
|
||||
|
||||
binding.etAdoptionDate.setText(a.getAdoptionDate());
|
||||
binding.etAdoptionFee.setText(a.getAdoptionFee() != null ? a.getAdoptionFee().toString() : "");
|
||||
SpinnerUtils.setSelectionByValue(binding.spinnerAdoptionStatus, a.getAdoptionStatus());
|
||||
|
||||
refreshPetSpinner();
|
||||
refreshCustomerSpinner();
|
||||
refreshStoreSpinner();
|
||||
refreshEmployeeSpinner();
|
||||
} 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() {
|
||||
if (spinnerCustomer.getSelectedItemPosition() == 0) {
|
||||
if (binding.spinnerAdoptionCustomer.getSelectedItemPosition() == 0) {
|
||||
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;
|
||||
}
|
||||
Long employeeId = null;
|
||||
if (spinnerEmployee.getSelectedItemPosition() > 0) {
|
||||
employeeId = employeeList
|
||||
.get(spinnerEmployee.getSelectedItemPosition() - 1)
|
||||
.getEmployeeId();
|
||||
if (binding.spinnerAdoptionStore.getSelectedItemPosition() == 0) {
|
||||
Toast.makeText(getContext(), "Select a store", Toast.LENGTH_SHORT).show(); return;
|
||||
}
|
||||
String date = etAdoptionDate.getText().toString().trim();
|
||||
String date = binding.etAdoptionDate.getText().toString().trim();
|
||||
if (date.isEmpty()) {
|
||||
Toast.makeText(getContext(), "Select a date", Toast.LENGTH_SHORT).show(); return;
|
||||
}
|
||||
|
||||
CustomerDTO customer = customerList.get(spinnerCustomer.getSelectedItemPosition() - 1);
|
||||
PetDTO pet = petList.get(spinnerPet.getSelectedItemPosition() - 1);
|
||||
String status = STATUSES[spinnerStatus.getSelectedItemPosition()];
|
||||
BigDecimal fee = BigDecimal.ZERO;
|
||||
String feeStr = binding.etAdoptionFee.getText().toString().trim();
|
||||
if (!feeStr.isEmpty()) {
|
||||
try {
|
||||
fee = new BigDecimal(feeStr);
|
||||
} catch (NumberFormatException e) {
|
||||
Toast.makeText(getContext(), "Invalid fee format", Toast.LENGTH_SHORT).show();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
CustomerDTO customer = customerList.get(binding.spinnerAdoptionCustomer.getSelectedItemPosition() - 1);
|
||||
PetDTO pet = petList.get(binding.spinnerAdoptionPet.getSelectedItemPosition() - 1);
|
||||
StoreDTO store = storeList.get(binding.spinnerAdoptionStore.getSelectedItemPosition() - 1);
|
||||
|
||||
Long employeeId = null;
|
||||
if (binding.spinnerAdoptionEmployee.getSelectedItemPosition() > 0) {
|
||||
employeeId = employeeList.get(binding.spinnerAdoptionEmployee.getSelectedItemPosition() - 1).getId();
|
||||
}
|
||||
|
||||
String status = STATUSES[binding.spinnerAdoptionStatus.getSelectedItemPosition()];
|
||||
|
||||
AdoptionDTO dto = new AdoptionDTO(
|
||||
pet.getPetId(),
|
||||
customer.getCustomerId(),
|
||||
employeeId,
|
||||
store.getStoreId(),
|
||||
date,
|
||||
status,
|
||||
employeeId
|
||||
fee
|
||||
);
|
||||
|
||||
Log.d("ADOPTION_SAVE", "petId=" + pet.getPetId()
|
||||
+ " customerId=" + customer.getCustomerId()
|
||||
+ " date=" + date + " status=" + status);
|
||||
|
||||
AdoptionApi api = RetrofitClient.getAdoptionApi(requireContext());
|
||||
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 {
|
||||
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<>() {
|
||||
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();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows a confirmation dialog before deleting an adoption request.
|
||||
*/
|
||||
private void confirmDelete() {
|
||||
new AlertDialog.Builder(requireContext())
|
||||
.setTitle("Delete Adoption?")
|
||||
.setPositiveButton("Yes", (d, w) ->
|
||||
RetrofitClient.getAdoptionApi(requireContext())
|
||||
.deleteAdoption(adoptionId)
|
||||
.enqueue(new Callback<Void>() {
|
||||
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();
|
||||
DialogUtils.showDeleteConfirmDialog(requireContext(), "Adoption", () ->
|
||||
adoptionViewModel.deleteAdoption(adoptionId).observe(getViewLifecycleOwner(), resource -> {
|
||||
if (resource.status == Resource.Status.SUCCESS) {
|
||||
Toast.makeText(getContext(), "Deleted", Toast.LENGTH_SHORT).show();
|
||||
navigateBack();
|
||||
} else if (resource.status == Resource.Status.ERROR) {
|
||||
Toast.makeText(getContext(), "Delete failed: " + resource.message, Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigates back to the previous fragment.
|
||||
*/
|
||||
private void navigateBack() {
|
||||
ListFragment lf = (ListFragment) getParentFragment();
|
||||
if (lf != null) lf.getChildFragmentManager().popBackStack();
|
||||
NavHostFragment.findNavController(this).popBackStack();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,22 +6,34 @@ import android.util.Log;
|
||||
import android.view.*;
|
||||
import android.widget.*;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import com.example.petstoremobile.R;
|
||||
import com.example.petstoremobile.api.*;
|
||||
import com.example.petstoremobile.dtos.*;
|
||||
import com.example.petstoremobile.fragments.ListFragment;
|
||||
import java.util.*;
|
||||
import retrofit2.*;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
import androidx.navigation.fragment.NavHostFragment;
|
||||
|
||||
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 com.example.petstoremobile.viewmodels.UserViewModel;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
import dagger.hilt.android.AndroidEntryPoint;
|
||||
|
||||
/**
|
||||
* Fragment for displaying and editing appointment details.
|
||||
*/
|
||||
@AndroidEntryPoint
|
||||
public class AppointmentDetailFragment extends Fragment {
|
||||
|
||||
private TextView tvMode, tvAppointmentId;
|
||||
private EditText etAppointmentDate;
|
||||
private Spinner spinnerPet, spinnerService, spinnerStatus, spinnerHour, spinnerMinute;
|
||||
private Spinner spinnerCustomer, spinnerStore;
|
||||
private Button btnSave, btnDelete, btnBack;
|
||||
private FragmentAppointmentDetailBinding binding;
|
||||
|
||||
private long appointmentId = -1;
|
||||
private boolean isEditing = false;
|
||||
@@ -29,66 +41,82 @@ public class AppointmentDetailFragment extends Fragment {
|
||||
private long preselectedServiceId = -1;
|
||||
private long preselectedCustomerId = -1;
|
||||
private long preselectedStoreId = -1;
|
||||
private long preselectedStaffId = -1;
|
||||
|
||||
private List<PetDTO> petList = new ArrayList<>();
|
||||
private List<ServiceDTO> serviceList = new ArrayList<>();
|
||||
private List<CustomerDTO> customerList = new ArrayList<>();
|
||||
private List<StoreDTO> storeList = new ArrayList<>();
|
||||
private List<AppointmentDTO> allAppointments = new ArrayList<>();
|
||||
private List<UserDTO> staffList = new ArrayList<>();
|
||||
|
||||
private final Integer[] HOURS = {9,10,11,12,13,14,15,16,17};
|
||||
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;
|
||||
private UserViewModel userViewModel;
|
||||
|
||||
@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);
|
||||
userViewModel = new ViewModelProvider(this).get(UserViewModel.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||
View view = inflater.inflate(R.layout.fragment_appointment_detail, container, false);
|
||||
initViews(view);
|
||||
binding = FragmentAppointmentDetailBinding.inflate(inflater, container, false);
|
||||
return binding.getRoot();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
setupSpinners();
|
||||
setupDatePicker();
|
||||
loadData();
|
||||
loadSpinnersData();
|
||||
handleArguments();
|
||||
|
||||
btnBack.setOnClickListener(v -> navigateBack());
|
||||
btnSave.setOnClickListener(v -> saveAppointment());
|
||||
btnDelete.setOnClickListener(v -> confirmDelete());
|
||||
return view;
|
||||
binding.btnApptBack.setOnClickListener(v -> navigateBack());
|
||||
binding.btnSaveAppointment.setOnClickListener(v -> saveAppointment());
|
||||
binding.btnDeleteAppointment.setOnClickListener(v -> confirmDelete());
|
||||
}
|
||||
|
||||
private void initViews(View v) {
|
||||
tvMode = v.findViewById(R.id.tvApptMode);
|
||||
tvAppointmentId = v.findViewById(R.id.tvAppointmentId);
|
||||
etAppointmentDate= v.findViewById(R.id.etAppointmentDate);
|
||||
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);
|
||||
@Override
|
||||
public void onDestroyView() {
|
||||
super.onDestroyView();
|
||||
binding = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures the adapters for spinners.
|
||||
*/
|
||||
private void setupSpinners() {
|
||||
spinnerStatus.setAdapter(new ArrayAdapter<>(requireContext(),
|
||||
android.R.layout.simple_spinner_item, STATUSES));
|
||||
SpinnerUtils.setupStringSpinner(requireContext(), binding.spinnerAppointmentStatus,
|
||||
new String[]{"Booked", "Completed", "Cancelled", "Missed"});
|
||||
|
||||
String[] hours = new String[HOURS.length];
|
||||
for (int i = 0; i < HOURS.length; i++)
|
||||
hours[i] = String.format("%02d:00", HOURS[i]);
|
||||
spinnerHour.setAdapter(new ArrayAdapter<>(requireContext(),
|
||||
android.R.layout.simple_spinner_item, hours));
|
||||
spinnerMinute.setAdapter(new ArrayAdapter<>(requireContext(),
|
||||
android.R.layout.simple_spinner_item, new String[]{"00","15","30","45"}));
|
||||
SpinnerUtils.setupStringSpinner(requireContext(), binding.spinnerHour, hours);
|
||||
SpinnerUtils.setupStringSpinner(requireContext(), binding.spinnerMinute, new String[]{"00","15","30","45"});
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures the date picker dialog for the appointment date field.
|
||||
*/
|
||||
private void setupDatePicker() {
|
||||
etAppointmentDate.setOnClickListener(v -> {
|
||||
binding.etAppointmentDate.setOnClickListener(v -> {
|
||||
Calendar c = Calendar.getInstance();
|
||||
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)),
|
||||
c.get(Calendar.YEAR), c.get(Calendar.MONTH),
|
||||
c.get(Calendar.DAY_OF_MONTH));
|
||||
@@ -97,218 +125,233 @@ public class AppointmentDetailFragment extends Fragment {
|
||||
});
|
||||
}
|
||||
|
||||
private void loadData() {
|
||||
/**
|
||||
* Fetches all required data for spinners from the backend.
|
||||
*/
|
||||
private void loadSpinnersData() {
|
||||
loadPets();
|
||||
loadServices();
|
||||
loadCustomers();
|
||||
loadStores();
|
||||
loadAllAppointments();
|
||||
loadStaff();
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the list of pets from the ViewModel.
|
||||
*/
|
||||
private void loadPets() {
|
||||
RetrofitClient.getPetApi(requireContext()).getAllPets(0, 200)
|
||||
.enqueue(new Callback<PageResponse<PetDTO>>() {
|
||||
public void onResponse(Call<PageResponse<PetDTO>> c, Response<PageResponse<PetDTO>> r) {
|
||||
if (r.isSuccessful() && r.body() != null) {
|
||||
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 ArrayAdapter<>(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;
|
||||
}
|
||||
petViewModel.getAllPets(0, 200, null, null, null, null, "petName").observe(getViewLifecycleOwner(), resource -> {
|
||||
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
|
||||
petList = resource.data.getContent();
|
||||
refreshPetSpinner();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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() {
|
||||
RetrofitClient.getServiceApi(requireContext()).getAllServices(0, 200)
|
||||
.enqueue(new Callback<PageResponse<ServiceDTO>>() {
|
||||
public void onResponse(Call<PageResponse<ServiceDTO>> c, Response<PageResponse<ServiceDTO>> r) {
|
||||
if (r.isSuccessful() && r.body() != null) {
|
||||
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 ArrayAdapter<>(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;
|
||||
}
|
||||
serviceViewModel.getAllServices(0, 200, null, "serviceName").observe(getViewLifecycleOwner(), resource -> {
|
||||
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
|
||||
serviceList = resource.data.getContent();
|
||||
refreshServiceSpinner();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Populates the service selection spinner.
|
||||
*/
|
||||
private void refreshServiceSpinner() {
|
||||
SpinnerUtils.populateSpinner(requireContext(), binding.spinnerService, serviceList,
|
||||
ServiceDTO::getServiceName, "-- Select Service --",
|
||||
preselectedServiceId, ServiceDTO::getServiceId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the list of customers from the API.
|
||||
*/
|
||||
private void loadCustomers() {
|
||||
RetrofitClient.getCustomerApi(requireContext()).getAllCustomers(0, 200)
|
||||
.enqueue(new Callback<PageResponse<CustomerDTO>>() {
|
||||
public void onResponse(Call<PageResponse<CustomerDTO>> c, Response<PageResponse<CustomerDTO>> r) {
|
||||
if (r.isSuccessful() && r.body() != null) {
|
||||
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 ArrayAdapter<>(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;
|
||||
}
|
||||
customerViewModel.getAllCustomers(0, 200).observe(getViewLifecycleOwner(), resource -> {
|
||||
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
|
||||
customerList = resource.data.getContent();
|
||||
refreshCustomerSpinner();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Populates the customer selection spinner.
|
||||
*/
|
||||
private void refreshCustomerSpinner() {
|
||||
SpinnerUtils.populateSpinner(requireContext(), binding.spinnerCustomer, customerList,
|
||||
item -> item.getFirstName() + " " + item.getLastName(),
|
||||
"-- Select Customer --",
|
||||
preselectedCustomerId, CustomerDTO::getCustomerId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the list of stores from the API.
|
||||
*/
|
||||
private void loadStores() {
|
||||
RetrofitClient.getStoreApi(requireContext()).getAllStores(0, 50)
|
||||
.enqueue(new Callback<PageResponse<StoreDTO>>() {
|
||||
public void onResponse(Call<PageResponse<StoreDTO>> c, Response<PageResponse<StoreDTO>> r) {
|
||||
if (r.isSuccessful() && r.body() != null) {
|
||||
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 ArrayAdapter<>(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;
|
||||
}
|
||||
storeViewModel.getAllStores(0, 50).observe(getViewLifecycleOwner(), resource -> {
|
||||
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
|
||||
storeList = resource.data.getContent();
|
||||
refreshStoreSpinner();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void loadAllAppointments() {
|
||||
RetrofitClient.getAppointmentApi(requireContext()).getAllAppointments(0, 500)
|
||||
.enqueue(new Callback<PageResponse<AppointmentDTO>>() {
|
||||
public void onResponse(Call<PageResponse<AppointmentDTO>> c, Response<PageResponse<AppointmentDTO>> r) {
|
||||
if (r.isSuccessful() && r.body() != null)
|
||||
allAppointments = r.body().getContent();
|
||||
}
|
||||
public void onFailure(Call<PageResponse<AppointmentDTO>> c, Throwable t) {}
|
||||
});
|
||||
/**
|
||||
* Populates the store selection spinner.
|
||||
*/
|
||||
private void refreshStoreSpinner() {
|
||||
SpinnerUtils.populateSpinner(requireContext(), binding.spinnerStore, storeList,
|
||||
StoreDTO::getStoreName, "-- Select Store --",
|
||||
preselectedStoreId, StoreDTO::getStoreId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the list of staff from the API.
|
||||
*/
|
||||
private void loadStaff() {
|
||||
userViewModel.getUsers("STAFF", 0, 100).observe(getViewLifecycleOwner(), resource -> {
|
||||
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
|
||||
staffList = resource.data.getContent();
|
||||
refreshStaffSpinner();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Populates the staff selection spinner.
|
||||
*/
|
||||
private void refreshStaffSpinner() {
|
||||
SpinnerUtils.populateSpinner(requireContext(), binding.spinnerStaff, staffList,
|
||||
UserDTO::getFullName, "-- Select Staff --",
|
||||
preselectedStaffId, UserDTO::getId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles arguments to determine if the fragment is in edit or add mode.
|
||||
*/
|
||||
private void handleArguments() {
|
||||
Bundle a = getArguments();
|
||||
if (a != null && a.containsKey("appointmentId")) {
|
||||
isEditing = true;
|
||||
appointmentId = a.getLong("appointmentId");
|
||||
preselectedPetId = a.getLong("petId", -1);
|
||||
preselectedServiceId= a.getLong("serviceId", -1);
|
||||
preselectedCustomerId = a.getLong("customerId", -1);
|
||||
preselectedStoreId = a.getLong("storeId", -1);
|
||||
|
||||
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; }
|
||||
|
||||
appointmentId = a.getLong("appointmentId");
|
||||
binding.tvApptMode.setText("Edit Appointment");
|
||||
binding.tvAppointmentId.setText("ID: " + appointmentId);
|
||||
binding.tvAppointmentId.setVisibility(View.VISIBLE);
|
||||
binding.btnDeleteAppointment.setVisibility(View.VISIBLE);
|
||||
loadAppointmentData();
|
||||
} else {
|
||||
tvMode.setText("Add Appointment");
|
||||
btnDelete.setVisibility(View.GONE);
|
||||
tvAppointmentId.setVisibility(View.GONE);
|
||||
binding.tvApptMode.setText("Add Appointment");
|
||||
binding.btnDeleteAppointment.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;
|
||||
preselectedStaffId = (a.getEmployeeId() != null) ? a.getEmployeeId() : -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();
|
||||
refreshStaffSpinner();
|
||||
} else if (resource.status == Resource.Status.ERROR) {
|
||||
Toast.makeText(getContext(), "Failed to load appointment: " + resource.message, Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates input and saves the appointment to the backend.
|
||||
*/
|
||||
private void saveAppointment() {
|
||||
if (spinnerCustomer.getSelectedItemPosition() == 0) {
|
||||
if (binding.spinnerCustomer.getSelectedItemPosition() == 0) {
|
||||
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;
|
||||
}
|
||||
if (spinnerPet.getSelectedItemPosition() == 0) {
|
||||
if (binding.spinnerPet.getSelectedItemPosition() == 0) {
|
||||
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;
|
||||
}
|
||||
String date = etAppointmentDate.getText().toString().trim();
|
||||
String date = binding.etAppointmentDate.getText().toString().trim();
|
||||
if (date.isEmpty()) {
|
||||
Toast.makeText(getContext(), "Select a date", Toast.LENGTH_SHORT).show(); return;
|
||||
}
|
||||
|
||||
CustomerDTO customer = customerList.get(spinnerCustomer.getSelectedItemPosition() - 1);
|
||||
StoreDTO store = storeList.get(spinnerStore.getSelectedItemPosition() - 1);
|
||||
PetDTO pet = petList.get(spinnerPet.getSelectedItemPosition() - 1);
|
||||
ServiceDTO service = serviceList.get(spinnerService.getSelectedItemPosition() - 1);
|
||||
CustomerDTO customer = customerList.get(binding.spinnerCustomer.getSelectedItemPosition() - 1);
|
||||
StoreDTO store = storeList.get(binding.spinnerStore.getSelectedItemPosition() - 1);
|
||||
PetDTO pet = petList.get(binding.spinnerPet.getSelectedItemPosition() - 1);
|
||||
ServiceDTO service = serviceList.get(binding.spinnerService.getSelectedItemPosition() - 1);
|
||||
|
||||
Long employeeId = null;
|
||||
if (binding.spinnerStaff.getSelectedItemPosition() > 0) {
|
||||
employeeId = staffList.get(binding.spinnerStaff.getSelectedItemPosition() - 1).getId();
|
||||
}
|
||||
|
||||
String time = String.format("%02d:%02d",
|
||||
HOURS[spinnerHour.getSelectedItemPosition()],
|
||||
MINUTES[spinnerMinute.getSelectedItemPosition()]);
|
||||
String status = STATUSES[spinnerStatus.getSelectedItemPosition()];
|
||||
HOURS[binding.spinnerHour.getSelectedItemPosition()],
|
||||
MINUTES[binding.spinnerMinute.getSelectedItemPosition()]);
|
||||
|
||||
// Get status and convert to uppercase for backend
|
||||
String status = binding.spinnerAppointmentStatus.getSelectedItem().toString().toUpperCase();
|
||||
|
||||
|
||||
// Validate future date+time if status is Booked
|
||||
if ("Booked".equalsIgnoreCase(status)) {
|
||||
// Validate future date+time if status is BOOKED
|
||||
if ("BOOKED".equalsIgnoreCase(status)) {
|
||||
try {
|
||||
String[] dateParts = date.split("-");
|
||||
String[] timeParts = time.split(":");
|
||||
@@ -322,7 +365,7 @@ public class AppointmentDetailFragment extends Fragment {
|
||||
0
|
||||
);
|
||||
if (selected.before(Calendar.getInstance())) {
|
||||
showErrorDialog("Invalid Time",
|
||||
DialogUtils.showInfoDialog(requireContext(), "Invalid Time",
|
||||
"Booked appointments must be in the future. " +
|
||||
"Please select a future date and time.");
|
||||
return;
|
||||
@@ -337,107 +380,80 @@ public class AppointmentDetailFragment extends Fragment {
|
||||
customer.getCustomerId(),
|
||||
store.getStoreId(),
|
||||
service.getServiceId(),
|
||||
employeeId,
|
||||
date,
|
||||
time,
|
||||
status,
|
||||
Collections.singletonList(pet.getPetId())
|
||||
pet.getPetId()
|
||||
);
|
||||
|
||||
Log.d("APPT_SAVE", "customerId=" + customer.getCustomerId()
|
||||
+ " storeId=" + store.getStoreId()
|
||||
+ " serviceId=" + service.getServiceId()
|
||||
+ " petId=" + pet.getPetId()
|
||||
+ " date=" + date + " time=" + time);
|
||||
androidx.lifecycle.Observer<Resource<AppointmentDTO>> observer = resource -> {
|
||||
if (resource.status == Resource.Status.SUCCESS) {
|
||||
Toast.makeText(getContext(), isEditing ? "Updated" : "Saved", Toast.LENGTH_SHORT).show();
|
||||
navigateBack();
|
||||
} else if (resource.status == Resource.Status.ERROR) {
|
||||
handleSaveError(resource.message);
|
||||
}
|
||||
};
|
||||
|
||||
AppointmentApi api = RetrofitClient.getAppointmentApi(requireContext());
|
||||
if (isEditing) {
|
||||
api.updateAppointment(appointmentId, dto).enqueue(simpleCallback("Updated"));
|
||||
appointmentViewModel.updateAppointment(appointmentId, dto).observe(getViewLifecycleOwner(), observer);
|
||||
} else {
|
||||
api.createAppointment(dto).enqueue(simpleCallback("Saved"));
|
||||
appointmentViewModel.createAppointment(dto).observe(getViewLifecycleOwner(), observer);
|
||||
}
|
||||
}
|
||||
|
||||
private Callback<AppointmentDTO> simpleCallback(String msg) {
|
||||
return new Callback<>() {
|
||||
public void onResponse(Call<AppointmentDTO> c, Response<AppointmentDTO> r) {
|
||||
Log.d("APPT_SAVE", "Response: " + r.code());
|
||||
if (r.isSuccessful()) {
|
||||
Toast.makeText(getContext(), msg, Toast.LENGTH_SHORT).show();
|
||||
navigateBack();
|
||||
} else {
|
||||
try {
|
||||
String errorBody = r.errorBody().string();
|
||||
Log.e("APPT_SAVE", "Error: " + errorBody);
|
||||
|
||||
// Show proper dialog based on error type
|
||||
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.");
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Handles errors that occur during the saving process.
|
||||
*/
|
||||
private void handleSaveError(String errorMessage) {
|
||||
if (errorMessage != null) {
|
||||
Log.e("APPT_SAVE", "Error: " + errorMessage);
|
||||
if (errorMessage.toLowerCase().contains("future")) {
|
||||
DialogUtils.showInfoDialog(requireContext(), "Invalid Date/Time",
|
||||
"Booked appointments must be scheduled in the future.");
|
||||
} else if (errorMessage.toLowerCase().contains("not available")) {
|
||||
showNoAvailabilityDialog();
|
||||
} else {
|
||||
Toast.makeText(getContext(), "Operation failed", Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
|
||||
public void onFailure(Call<AppointmentDTO> c, Throwable t) {
|
||||
Log.e("APPT_SAVE", "Failure: " + t.getMessage());
|
||||
Toast.makeText(getContext(), "Network error", Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
};
|
||||
} else {
|
||||
Toast.makeText(getContext(), "Something went wrong", Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows a specialized dialog when a time slot is not available.
|
||||
*/
|
||||
private void showNoAvailabilityDialog() {
|
||||
new AlertDialog.Builder(requireContext())
|
||||
new androidx.appcompat.app.AlertDialog.Builder(requireContext())
|
||||
.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())
|
||||
.setNegativeButton("Cancel Booking", (d, w) -> navigateBack())
|
||||
.setCancelable(false)
|
||||
.show();
|
||||
}
|
||||
|
||||
private void showErrorDialog(String title, String message) {
|
||||
new AlertDialog.Builder(requireContext())
|
||||
.setTitle(title)
|
||||
.setMessage(message)
|
||||
.setPositiveButton("OK", null)
|
||||
.show();
|
||||
}
|
||||
/**
|
||||
* Shows a confirmation dialog and handles the deletion of an appointment.
|
||||
*/
|
||||
private void confirmDelete() {
|
||||
new AlertDialog.Builder(requireContext())
|
||||
.setTitle("Delete Appointment?")
|
||||
.setPositiveButton("Yes", (d, w) ->
|
||||
RetrofitClient.getAppointmentApi(requireContext())
|
||||
.deleteAppointment(appointmentId)
|
||||
.enqueue(new Callback<Void>() {
|
||||
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();
|
||||
DialogUtils.showDeleteConfirmDialog(requireContext(), "Appointment", () ->
|
||||
appointmentViewModel.deleteAppointment(appointmentId).observe(getViewLifecycleOwner(), resource -> {
|
||||
if (resource.status == Resource.Status.SUCCESS) {
|
||||
Toast.makeText(getContext(), "Deleted", Toast.LENGTH_SHORT).show();
|
||||
navigateBack();
|
||||
} else if (resource.status == Resource.Status.ERROR) {
|
||||
Toast.makeText(getContext(), "Delete failed", Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigates back to the previous screen.
|
||||
*/
|
||||
private void navigateBack() {
|
||||
ListFragment lf = (ListFragment) getParentFragment();
|
||||
if (lf != null) lf.getChildFragmentManager().popBackStack();
|
||||
NavHostFragment.findNavController(this).popBackStack();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,291 +1,236 @@
|
||||
package com.example.petstoremobile.fragments.listfragments.detailfragments;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.text.Editable;
|
||||
import android.text.TextWatcher;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ArrayAdapter;
|
||||
import android.widget.AutoCompleteTextView;
|
||||
import android.widget.Button;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
import androidx.navigation.fragment.NavHostFragment;
|
||||
|
||||
import com.example.petstoremobile.R;
|
||||
import com.example.petstoremobile.api.InventoryApi;
|
||||
import com.example.petstoremobile.api.ProductApi;
|
||||
import com.example.petstoremobile.api.RetrofitClient;
|
||||
import com.example.petstoremobile.databinding.FragmentInventoryDetailBinding;
|
||||
import com.example.petstoremobile.dtos.InventoryDTO;
|
||||
import com.example.petstoremobile.dtos.InventoryRequest;
|
||||
import com.example.petstoremobile.dtos.PageResponse;
|
||||
import com.example.petstoremobile.dtos.ProductDTO;
|
||||
import com.example.petstoremobile.fragments.ListFragment;
|
||||
import com.example.petstoremobile.fragments.listfragments.InventoryFragment;
|
||||
import com.example.petstoremobile.dtos.StoreDTO;
|
||||
import com.example.petstoremobile.utils.InputValidator;
|
||||
import com.example.petstoremobile.utils.Resource;
|
||||
import com.example.petstoremobile.utils.SpinnerUtils;
|
||||
import com.example.petstoremobile.viewmodels.InventoryViewModel;
|
||||
import com.example.petstoremobile.viewmodels.ProductViewModel;
|
||||
import com.example.petstoremobile.viewmodels.StoreViewModel;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import retrofit2.Call;
|
||||
import retrofit2.Callback;
|
||||
import retrofit2.Response;
|
||||
import dagger.hilt.android.AndroidEntryPoint;
|
||||
|
||||
/**
|
||||
* Fragment for displaying and editing inventory item details.
|
||||
*/
|
||||
@AndroidEntryPoint
|
||||
public class InventoryDetailFragment extends Fragment {
|
||||
|
||||
private TextView tvMode, tvInventoryId, tvProductInfo;
|
||||
private AutoCompleteTextView etProductSearch;
|
||||
private android.widget.EditText etQuantity;
|
||||
private Button btnSave, btnDelete, btnBack;
|
||||
private FragmentInventoryDetailBinding binding;
|
||||
|
||||
private InventoryApi inventoryApi;
|
||||
private ProductApi productApi;
|
||||
private InventoryFragment inventoryFragment;
|
||||
private InventoryViewModel inventoryViewModel;
|
||||
private ProductViewModel productViewModel;
|
||||
private StoreViewModel storeViewModel;
|
||||
|
||||
private boolean isEditing = false;
|
||||
private long inventoryId = -1;
|
||||
private long preselectedStoreId = -1;
|
||||
private long preselectedProductId = -1;
|
||||
|
||||
// The product selected from the dropdown
|
||||
private ProductDTO selectedProduct = null;
|
||||
private List<StoreDTO> storeList = new ArrayList<>();
|
||||
private List<ProductDTO> productList = new ArrayList<>();
|
||||
|
||||
// For debouncing product search
|
||||
private final Handler searchHandler = new Handler(Looper.getMainLooper());
|
||||
private Runnable searchRunnable;
|
||||
/**
|
||||
* 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);
|
||||
storeViewModel = new ViewModelProvider(this).get(StoreViewModel.class);
|
||||
}
|
||||
|
||||
// Dropdown list
|
||||
private final List<ProductDTO> productSuggestions = new ArrayList<>();
|
||||
private ArrayAdapter<String> dropdownAdapter;
|
||||
/**
|
||||
* Inflates the layout.
|
||||
*/
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
|
||||
Bundle savedInstanceState) {
|
||||
binding = FragmentInventoryDetailBinding.inflate(inflater, container, false);
|
||||
return binding.getRoot();
|
||||
}
|
||||
|
||||
public void setInventoryFragment(InventoryFragment fragment) {
|
||||
this.inventoryFragment = fragment;
|
||||
/**
|
||||
* Sets up UI components after the view is created.
|
||||
*/
|
||||
@Override
|
||||
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
|
||||
loadSpinnersData();
|
||||
handleArguments();
|
||||
|
||||
binding.btnInventoryBack.setOnClickListener(v -> navigateBack());
|
||||
binding.btnSaveInventory.setOnClickListener(v -> saveInventory());
|
||||
binding.btnDeleteInventory.setOnClickListener(v -> confirmDelete());
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container,
|
||||
Bundle savedInstanceState) {
|
||||
View view = inflater.inflate(R.layout.fragment_inventory_detail, container, false);
|
||||
|
||||
inventoryApi = RetrofitClient.getInventoryApi(requireContext());
|
||||
productApi = RetrofitClient.getProductApi(requireContext());
|
||||
|
||||
initViews(view);
|
||||
setupProductSearch();
|
||||
handleArguments();
|
||||
|
||||
btnBack.setOnClickListener(v -> navigateBack());
|
||||
btnSave.setOnClickListener(v -> saveInventory());
|
||||
btnDelete.setOnClickListener(v -> confirmDelete());
|
||||
|
||||
return view;
|
||||
public void onDestroyView() {
|
||||
super.onDestroyView();
|
||||
binding = null;
|
||||
}
|
||||
|
||||
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
|
||||
dropdownAdapter = new ArrayAdapter<>(requireContext(),
|
||||
android.R.layout.simple_dropdown_item_1line, new ArrayList<>());
|
||||
etProductSearch.setAdapter(dropdownAdapter);
|
||||
etProductSearch.setThreshold(1); // start showing after 1 character
|
||||
/**
|
||||
* Fetches required data for spinners from the backend.
|
||||
*/
|
||||
private void loadSpinnersData() {
|
||||
loadStores();
|
||||
loadProducts();
|
||||
}
|
||||
|
||||
// Product search dropdown
|
||||
private void setupProductSearch() {
|
||||
etProductSearch.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) {
|
||||
// Clear selected product when user is typing again
|
||||
selectedProduct = null;
|
||||
tvProductInfo.setVisibility(View.GONE);
|
||||
|
||||
if (searchRunnable != null)
|
||||
searchHandler.removeCallbacks(searchRunnable);
|
||||
String query = s.toString().trim();
|
||||
if (query.isEmpty())
|
||||
return;
|
||||
|
||||
searchRunnable = () -> searchProducts(query);
|
||||
searchHandler.postDelayed(searchRunnable, 400);
|
||||
}
|
||||
});
|
||||
|
||||
// When user picks an item from the dropdown
|
||||
etProductSearch.setOnItemClickListener((parent, view, position, id) -> {
|
||||
if (position < productSuggestions.size()) {
|
||||
selectedProduct = productSuggestions.get(position);
|
||||
// Show product details below the search box
|
||||
tvProductInfo.setText(
|
||||
"ID: " + selectedProduct.getProdId()
|
||||
+ " • " + selectedProduct.getCategoryName());
|
||||
tvProductInfo.setVisibility(View.VISIBLE);
|
||||
/**
|
||||
* Loads the list of stores for the spinner.
|
||||
*/
|
||||
private void loadStores() {
|
||||
storeViewModel.getAllStores(0, 100).observe(getViewLifecycleOwner(), resource -> {
|
||||
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
|
||||
storeList = resource.data.getContent();
|
||||
refreshStoreSpinner();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void searchProducts(String query) {
|
||||
productApi.getAllProducts(query, 0, 20).enqueue(new Callback<PageResponse<ProductDTO>>() {
|
||||
@Override
|
||||
public void onResponse(Call<PageResponse<ProductDTO>> call,
|
||||
Response<PageResponse<ProductDTO>> response) {
|
||||
if (response.isSuccessful() && response.body() != null) {
|
||||
productSuggestions.clear();
|
||||
productSuggestions.addAll(response.body().getContent());
|
||||
private void refreshStoreSpinner() {
|
||||
SpinnerUtils.populateSpinner(requireContext(), binding.spinnerInventoryStore, storeList,
|
||||
StoreDTO::getStoreName, "-- Select Store --",
|
||||
preselectedStoreId, StoreDTO::getStoreId);
|
||||
}
|
||||
|
||||
// Build display strings: "Product Name (ID: X)"
|
||||
List<String> names = new ArrayList<>();
|
||||
for (ProductDTO p : productSuggestions) {
|
||||
names.add(p.getProdName() + " (ID: " + p.getProdId() + ")");
|
||||
}
|
||||
|
||||
dropdownAdapter.clear();
|
||||
dropdownAdapter.addAll(names);
|
||||
dropdownAdapter.notifyDataSetChanged();
|
||||
etProductSearch.showDropDown();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(Call<PageResponse<ProductDTO>> call, Throwable t) {
|
||||
Toast.makeText(getContext(), "Failed to load products", Toast.LENGTH_SHORT).show();
|
||||
/**
|
||||
* Loads the list of products for the spinner.
|
||||
*/
|
||||
private void loadProducts() {
|
||||
productViewModel.getAllProducts(null, null, 0, 500, "prodName").observe(getViewLifecycleOwner(), resource -> {
|
||||
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
|
||||
productList = resource.data.getContent();
|
||||
refreshProductSpinner();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Arguments (edit mode)
|
||||
private void refreshProductSpinner() {
|
||||
SpinnerUtils.populateSpinner(requireContext(), binding.spinnerInventoryProduct, productList,
|
||||
ProductDTO::getProdName, "-- Select Product --",
|
||||
preselectedProductId, ProductDTO::getProdId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles fragment arguments to determine if we are in edit or add mode.
|
||||
*/
|
||||
private void handleArguments() {
|
||||
Bundle args = getArguments();
|
||||
if (args != null && args.containsKey("inventoryId")) {
|
||||
isEditing = true;
|
||||
inventoryId = args.getLong("inventoryId");
|
||||
|
||||
tvMode.setText("Edit Inventory");
|
||||
tvInventoryId.setText("Inventory ID: " + inventoryId);
|
||||
tvInventoryId.setVisibility(View.VISIBLE);
|
||||
binding.tvInventoryMode.setText("Edit Inventory");
|
||||
binding.tvInventoryId.setText("Inventory ID: " + inventoryId);
|
||||
binding.tvInventoryId.setVisibility(View.VISIBLE);
|
||||
binding.btnDeleteInventory.setVisibility(View.VISIBLE);
|
||||
binding.btnSaveInventory.setText("Save");
|
||||
|
||||
// Pre-fill search box with existing product name
|
||||
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");
|
||||
loadInventoryData();
|
||||
} else {
|
||||
isEditing = false;
|
||||
tvMode.setText("Add Inventory");
|
||||
tvInventoryId.setVisibility(View.GONE);
|
||||
tvProductInfo.setVisibility(View.GONE);
|
||||
btnDelete.setVisibility(View.GONE);
|
||||
btnSave.setText("Add");
|
||||
binding.tvInventoryMode.setText("Add Inventory");
|
||||
binding.tvInventoryId.setVisibility(View.GONE);
|
||||
binding.btnDeleteInventory.setVisibility(View.GONE);
|
||||
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.etQuantity.setText(String.valueOf(inv.getQuantity()));
|
||||
preselectedStoreId = inv.getStoreId() != null ? inv.getStoreId() : -1;
|
||||
preselectedProductId = inv.getProdId() != null ? inv.getProdId() : -1;
|
||||
|
||||
refreshStoreSpinner();
|
||||
refreshProductSpinner();
|
||||
} else if (resource.status == Resource.Status.ERROR) {
|
||||
Toast.makeText(getContext(), "Failed to load inventory: " + resource.message, Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates input and saves the current inventory item details to the backend.
|
||||
*/
|
||||
private void saveInventory() {
|
||||
if (selectedProduct == null) {
|
||||
etProductSearch.setError("Please select a product from the list");
|
||||
etProductSearch.requestFocus();
|
||||
if (binding.spinnerInventoryStore.getSelectedItemPosition() == 0) {
|
||||
Toast.makeText(getContext(), "Please select a store", Toast.LENGTH_SHORT).show();
|
||||
return;
|
||||
}
|
||||
if (binding.spinnerInventoryProduct.getSelectedItemPosition() == 0) {
|
||||
Toast.makeText(getContext(), "Please select a product", Toast.LENGTH_SHORT).show();
|
||||
return;
|
||||
}
|
||||
|
||||
String quantityStr = etQuantity.getText().toString().trim();
|
||||
if (quantityStr.isEmpty()) {
|
||||
etQuantity.setError("Quantity is required");
|
||||
etQuantity.requestFocus();
|
||||
if (!InputValidator.isNotEmpty(binding.etQuantity, "Quantity") ||
|
||||
!InputValidator.isPositiveInteger(binding.etQuantity, "Quantity")) {
|
||||
return;
|
||||
}
|
||||
|
||||
int quantity;
|
||||
try {
|
||||
quantity = Integer.parseInt(quantityStr);
|
||||
} catch (NumberFormatException e) {
|
||||
etQuantity.setError("Invalid quantity");
|
||||
return;
|
||||
}
|
||||
int quantity = Integer.parseInt(binding.etQuantity.getText().toString().trim());
|
||||
StoreDTO store = storeList.get(binding.spinnerInventoryStore.getSelectedItemPosition() - 1);
|
||||
ProductDTO product = productList.get(binding.spinnerInventoryProduct.getSelectedItemPosition() - 1);
|
||||
|
||||
if (quantity < 0) {
|
||||
etQuantity.setError("Quantity must be 0 or more");
|
||||
etQuantity.requestFocus();
|
||||
return;
|
||||
}
|
||||
|
||||
InventoryRequest request = new InventoryRequest(selectedProduct.getProdId(), quantity);
|
||||
InventoryDTO request = new InventoryDTO(product.getProdId(), store.getStoreId(), quantity);
|
||||
setButtonsEnabled(false);
|
||||
|
||||
if (isEditing) {
|
||||
inventoryApi.updateInventory(inventoryId, request).enqueue(new Callback<InventoryDTO>() {
|
||||
@Override
|
||||
public void onResponse(Call<InventoryDTO> call, Response<InventoryDTO> response) {
|
||||
setButtonsEnabled(true);
|
||||
if (response.isSuccessful()) {
|
||||
Toast.makeText(getContext(), "Inventory updated", Toast.LENGTH_SHORT).show();
|
||||
notifyParentAndGoBack();
|
||||
} 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();
|
||||
inventoryViewModel.updateInventory(inventoryId, request).observe(getViewLifecycleOwner(), resource -> {
|
||||
setButtonsEnabled(true);
|
||||
if (resource.status == Resource.Status.SUCCESS) {
|
||||
Toast.makeText(getContext(), "Inventory updated", Toast.LENGTH_SHORT).show();
|
||||
navigateBack();
|
||||
} else if (resource.status == Resource.Status.ERROR) {
|
||||
Toast.makeText(getContext(), "Error: " + resource.message, Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
inventoryApi.createInventory(request).enqueue(new Callback<InventoryDTO>() {
|
||||
@Override
|
||||
public void onResponse(Call<InventoryDTO> call, Response<InventoryDTO> response) {
|
||||
setButtonsEnabled(true);
|
||||
if (response.isSuccessful()) {
|
||||
Toast.makeText(getContext(), "Inventory created", Toast.LENGTH_SHORT).show();
|
||||
notifyParentAndGoBack();
|
||||
} 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();
|
||||
inventoryViewModel.createInventory(request).observe(getViewLifecycleOwner(), resource -> {
|
||||
setButtonsEnabled(true);
|
||||
if (resource.status == Resource.Status.SUCCESS) {
|
||||
Toast.makeText(getContext(), "Inventory created", Toast.LENGTH_SHORT).show();
|
||||
navigateBack();
|
||||
} else if (resource.status == Resource.Status.ERROR) {
|
||||
Toast.makeText(getContext(), "Error: " + resource.message, Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Delete
|
||||
/**
|
||||
* Shows a confirmation dialog before deleting an inventory item.
|
||||
*/
|
||||
private void confirmDelete() {
|
||||
new AlertDialog.Builder(requireContext())
|
||||
.setTitle("Delete inventory item?")
|
||||
@@ -295,45 +240,35 @@ public class InventoryDetailFragment extends Fragment {
|
||||
.show();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a request to the API to delete the inventory item.
|
||||
*/
|
||||
private void deleteInventory() {
|
||||
setButtonsEnabled(false);
|
||||
inventoryApi.deleteInventory(inventoryId).enqueue(new Callback<Void>() {
|
||||
@Override
|
||||
public void onResponse(Call<Void> call, Response<Void> response) {
|
||||
setButtonsEnabled(true);
|
||||
if (response.isSuccessful()) {
|
||||
Toast.makeText(getContext(), "Inventory deleted", Toast.LENGTH_SHORT).show();
|
||||
notifyParentAndGoBack();
|
||||
} 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();
|
||||
inventoryViewModel.deleteInventory(inventoryId).observe(getViewLifecycleOwner(), resource -> {
|
||||
setButtonsEnabled(true);
|
||||
if (resource.status == Resource.Status.SUCCESS) {
|
||||
Toast.makeText(getContext(), "Inventory deleted", Toast.LENGTH_SHORT).show();
|
||||
navigateBack();
|
||||
} else if (resource.status == Resource.Status.ERROR) {
|
||||
Toast.makeText(getContext(), "Delete failed: " + resource.message, Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Helpers
|
||||
|
||||
private void notifyParentAndGoBack() {
|
||||
if (inventoryFragment != null)
|
||||
inventoryFragment.onInventoryChanged();
|
||||
navigateBack();
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigates back to the previous fragment.
|
||||
*/
|
||||
private void navigateBack() {
|
||||
ListFragment lf = (ListFragment) getParentFragment();
|
||||
if (lf != null)
|
||||
lf.getChildFragmentManager().popBackStack();
|
||||
NavHostFragment.findNavController(this).popBackStack();
|
||||
}
|
||||
|
||||
/**
|
||||
* Enables or disables action buttons.
|
||||
*/
|
||||
private void setButtonsEnabled(boolean enabled) {
|
||||
btnSave.setEnabled(enabled);
|
||||
btnDelete.setEnabled(enabled);
|
||||
btnBack.setEnabled(enabled);
|
||||
binding.btnSaveInventory.setEnabled(enabled);
|
||||
binding.btnDeleteInventory.setEnabled(enabled);
|
||||
binding.btnInventoryBack.setEnabled(enabled);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,83 +4,140 @@ import android.os.Bundle;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
import androidx.navigation.fragment.NavHostFragment;
|
||||
|
||||
import android.util.Log;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ArrayAdapter;
|
||||
import android.widget.Button;
|
||||
import android.widget.EditText;
|
||||
import android.widget.AdapterView;
|
||||
import android.widget.Spinner;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.example.petstoremobile.R;
|
||||
import com.example.petstoremobile.api.PetApi;
|
||||
import com.example.petstoremobile.api.RetrofitClient;
|
||||
import com.example.petstoremobile.databinding.FragmentPetDetailBinding;
|
||||
import com.example.petstoremobile.dtos.CustomerDTO;
|
||||
import com.example.petstoremobile.dtos.PetDTO;
|
||||
import com.example.petstoremobile.fragments.ListFragment;
|
||||
import com.example.petstoremobile.fragments.listfragments.PetFragment;
|
||||
import com.example.petstoremobile.dtos.StoreDTO;
|
||||
import com.example.petstoremobile.utils.ActivityLogger;
|
||||
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.CustomerViewModel;
|
||||
import com.example.petstoremobile.viewmodels.PetViewModel;
|
||||
import com.example.petstoremobile.viewmodels.StoreViewModel;
|
||||
|
||||
import retrofit2.Call;
|
||||
import retrofit2.Callback;
|
||||
import retrofit2.Response;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
import dagger.hilt.android.AndroidEntryPoint;
|
||||
|
||||
/**
|
||||
* Fragment for displaying and editing pet details.
|
||||
*/
|
||||
@AndroidEntryPoint
|
||||
public class PetDetailFragment extends Fragment {
|
||||
|
||||
private TextView tvMode, tvPetId;
|
||||
private EditText etPetName, etPetSpecies, etPetBreed, etPetAge, etPetPrice;
|
||||
private Spinner spinnerPetStatus;
|
||||
private Button btnSavePet, btnDeletePet, btnBack;
|
||||
private int petId;
|
||||
private FragmentPetDetailBinding binding;
|
||||
private long petId;
|
||||
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
|
||||
public void setPetFragment(PetFragment fragment) {
|
||||
this.petFragment = fragment;
|
||||
private PetViewModel viewModel;
|
||||
private CustomerViewModel customerViewModel;
|
||||
private StoreViewModel storeViewModel;
|
||||
private List<CustomerDTO> customerList = new ArrayList<>();
|
||||
private List<StoreDTO> storeList = new ArrayList<>();
|
||||
private Long selectedCustomerId = null;
|
||||
private Long 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
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container,
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
|
||||
Bundle savedInstanceState) {
|
||||
View view = inflater.inflate(R.layout.fragment_pet_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();
|
||||
loadCustomers();
|
||||
loadStores();
|
||||
handleArguments();
|
||||
|
||||
//set button click listeners
|
||||
btnBack.setOnClickListener(v -> navigateBack());
|
||||
btnSavePet.setOnClickListener(v -> savePet());
|
||||
btnDeletePet.setOnClickListener(v -> deletePet());
|
||||
|
||||
return view;
|
||||
binding.btnBack.setOnClickListener(v -> navigateBack());
|
||||
binding.btnSavePet.setOnClickListener(v -> savePet());
|
||||
binding.btnDeletePet.setOnClickListener(v -> deletePet());
|
||||
}
|
||||
|
||||
//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() {
|
||||
// Validates all fields using InputValidator
|
||||
if (!InputValidator.isNotEmpty(etPetName, "Pet Name")) return;
|
||||
if (!InputValidator.isNotEmpty(etPetSpecies, "Species")) return;
|
||||
if (!InputValidator.isNotEmpty(etPetBreed, "Breed")) return;
|
||||
if (!InputValidator.isPositiveInteger(etPetAge, "Age")) return;
|
||||
if (!InputValidator.isPositiveDecimal(etPetPrice, "Price")) return;
|
||||
if (!InputValidator.isNotEmpty(binding.etPetName, "Pet Name")) return;
|
||||
if (!InputValidator.isNotEmpty(binding.etPetSpecies, "Species")) return;
|
||||
if (!InputValidator.isNotEmpty(binding.etPetBreed, "Breed")) return;
|
||||
if (!InputValidator.isPositiveInteger(binding.etPetAge, "Age")) return;
|
||||
if (!InputValidator.isPositiveDecimal(binding.etPetPrice, "Price")) return;
|
||||
|
||||
//get all the values from the fields
|
||||
String name = etPetName.getText().toString().trim();
|
||||
String species = etPetSpecies.getText().toString().trim();
|
||||
String breed = etPetBreed.getText().toString().trim();
|
||||
int age = Integer.parseInt(etPetAge.getText().toString().trim());
|
||||
String priceStr = etPetPrice.getText().toString().trim();
|
||||
String status = spinnerPetStatus.getSelectedItem().toString();
|
||||
String name = binding.etPetName.getText().toString().trim();
|
||||
String species = binding.etPetSpecies.getText().toString().trim();
|
||||
String breed = binding.etPetBreed.getText().toString().trim();
|
||||
int age = Integer.parseInt(binding.etPetAge.getText().toString().trim());
|
||||
double price = Double.parseDouble(binding.etPetPrice.getText().toString().trim());
|
||||
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
|
||||
PetDTO petDTO = new PetDTO();
|
||||
@@ -88,167 +145,221 @@ public class PetDetailFragment extends Fragment {
|
||||
petDTO.setPetSpecies(species);
|
||||
petDTO.setPetBreed(breed);
|
||||
petDTO.setPetAge(age);
|
||||
petDTO.setPetPrice(priceStr);
|
||||
petDTO.setPetPrice(price);
|
||||
petDTO.setPetStatus(status);
|
||||
|
||||
PetApi petApi = RetrofitClient.getPetApi(requireContext());
|
||||
petDTO.setCustomerId(customerId);
|
||||
petDTO.setStoreId(storeId);
|
||||
|
||||
//check if the pet is being edited or added
|
||||
if (isEditing) {
|
||||
// Update existing pet
|
||||
petDTO.setPetId((long) petId);
|
||||
petApi.updatePet((long) petId, petDTO).enqueue(new Callback<PetDTO>() {
|
||||
@Override
|
||||
public void onResponse(Call<PetDTO> call, Response<PetDTO> response) {
|
||||
if (response.isSuccessful()) {
|
||||
ActivityLogger.logChange(requireContext(), "Pet", "UPDATED", petId);
|
||||
Toast.makeText(getContext(), "Pet updated successfully!", Toast.LENGTH_SHORT).show();
|
||||
navigateBack();
|
||||
} else {
|
||||
Toast.makeText(getContext(), "Failed to update pet: " + response.code(), Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}
|
||||
|
||||
@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();
|
||||
petDTO.setPetId(petId);
|
||||
viewModel.updatePet(petId, petDTO).observe(getViewLifecycleOwner(), resource -> {
|
||||
if (resource.status == Resource.Status.SUCCESS) {
|
||||
ActivityLogger.logChange(requireContext(), "Pet", "UPDATED", (int) petId);
|
||||
Toast.makeText(getContext(), "Pet updated successfully!", Toast.LENGTH_SHORT).show();
|
||||
navigateToPetList();
|
||||
} else if (resource.status == Resource.Status.ERROR) {
|
||||
Toast.makeText(getContext(), "Error: " + resource.message, Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// Add new pet
|
||||
petApi.createPet(petDTO).enqueue(new Callback<PetDTO>() {
|
||||
@Override
|
||||
public void onResponse(Call<PetDTO> call, Response<PetDTO> response) {
|
||||
if (response.isSuccessful()) {
|
||||
ActivityLogger.log(requireContext(), "Added new Pet: " + name);
|
||||
Toast.makeText(getContext(), "Pet added successfully!", Toast.LENGTH_SHORT).show();
|
||||
navigateBack();
|
||||
} else {
|
||||
Toast.makeText(getContext(), "Failed to add pet: " + response.code(), Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}
|
||||
|
||||
@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();
|
||||
viewModel.createPet(petDTO).observe(getViewLifecycleOwner(), resource -> {
|
||||
if (resource.status == Resource.Status.SUCCESS) {
|
||||
ActivityLogger.log(requireContext(), "Added new Pet: " + name);
|
||||
Toast.makeText(getContext(), "Pet added successfully!", Toast.LENGTH_SHORT).show();
|
||||
navigateToPetList();
|
||||
} else if (resource.status == Resource.Status.ERROR) {
|
||||
Toast.makeText(getContext(), "Error: " + resource.message, Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
//Method to Delete a pet
|
||||
/**
|
||||
* Displays a confirmation dialog and handles the deletion of a pet.
|
||||
*/
|
||||
private void deletePet() {
|
||||
//Alert the user to confirm the delete
|
||||
new AlertDialog.Builder(requireContext())
|
||||
.setTitle("Delete Pet")
|
||||
.setMessage("Are you sure you want to delete " + etPetName.getText().toString() + "?")
|
||||
.setPositiveButton("Delete", (dialog, which) -> {
|
||||
PetApi petApi = RetrofitClient.getPetApi(requireContext());
|
||||
//if they say yes then delete the pet
|
||||
petApi.deletePet((long) petId).enqueue(new Callback<Void>() {
|
||||
@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();
|
||||
DialogUtils.showDeleteConfirmDialog(requireContext(), "Pet", () ->
|
||||
viewModel.deletePet(petId).observe(getViewLifecycleOwner(), resource -> {
|
||||
if (resource.status == Resource.Status.SUCCESS) {
|
||||
ActivityLogger.logChange(requireContext(), "Pet", "DELETED", (int) petId);
|
||||
Toast.makeText(getContext(), "Pet deleted successfully!", Toast.LENGTH_SHORT).show();
|
||||
navigateToPetList();
|
||||
} else if (resource.status == Resource.Status.ERROR) {
|
||||
Toast.makeText(getContext(), "Delete failed: " + resource.message, Toast.LENGTH_SHORT).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() {
|
||||
ListFragment listFragment = (ListFragment) getParentFragment();
|
||||
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();
|
||||
}
|
||||
NavHostFragment.findNavController(this).popBackStack();
|
||||
}
|
||||
|
||||
//helper function to check if pet is being edited or added and show the view accordingly
|
||||
/**
|
||||
* Handles arguments passed to the fragment to determine if it's in edit or add mode.
|
||||
*/
|
||||
private void handleArguments() {
|
||||
// Pet is being edited if the bundle contains a petId
|
||||
if (getArguments() != null && getArguments().containsKey("petId")) {
|
||||
// Get pet data from arguments and populate fields
|
||||
isEditing = true;
|
||||
petId = getArguments().getInt("petId");
|
||||
tvMode.setText("Edit Pet");
|
||||
tvPetId.setText("ID: " + petId);
|
||||
etPetName.setText(getArguments().getString("petName"));
|
||||
etPetSpecies.setText(getArguments().getString("petSpecies"));
|
||||
etPetBreed.setText(getArguments().getString("petBreed"));
|
||||
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);
|
||||
petId = getArguments().getLong("petId");
|
||||
binding.tvMode.setText("Edit Pet");
|
||||
binding.tvPetId.setText("ID: " + petId);
|
||||
binding.tvPetId.setVisibility(View.VISIBLE);
|
||||
binding.btnDeletePet.setVisibility(View.VISIBLE);
|
||||
loadPetData();
|
||||
} else {
|
||||
// Pet is being added
|
||||
// Set default values for add a new pet
|
||||
isEditing = false;
|
||||
tvMode.setText("Add Pet");
|
||||
tvPetId.setVisibility(View.GONE);
|
||||
btnDeletePet.setVisibility(View.GONE);
|
||||
btnSavePet.setText("Add");
|
||||
binding.tvMode.setText("Add Pet");
|
||||
binding.tvPetId.setVisibility(View.GONE);
|
||||
binding.btnDeletePet.setVisibility(View.GONE);
|
||||
binding.btnSavePet.setText("Add");
|
||||
}
|
||||
}
|
||||
|
||||
//helper function to get controls from layout
|
||||
private void initViews(View view) {
|
||||
tvMode = view.findViewById(R.id.tvMode);
|
||||
tvPetId = view.findViewById(R.id.tvPetId);
|
||||
etPetName = view.findViewById(R.id.etPetName);
|
||||
etPetSpecies = view.findViewById(R.id.etPetSpecies);
|
||||
etPetBreed = view.findViewById(R.id.etPetBreed);
|
||||
etPetAge = view.findViewById(R.id.etPetAge);
|
||||
etPetPrice = view.findViewById(R.id.etPetPrice);
|
||||
spinnerPetStatus = view.findViewById(R.id.spinnerPetStatus);
|
||||
btnSavePet = view.findViewById(R.id.btnSavePet);
|
||||
btnDeletePet = view.findViewById(R.id.btnDeletePet);
|
||||
btnBack = view.findViewById(R.id.btnBack);
|
||||
}
|
||||
/**
|
||||
* Fetches specific pet details from the backend using the ID.
|
||||
*/
|
||||
private void loadPetData() {
|
||||
viewModel.getPetById(petId).observe(getViewLifecycleOwner(), resource -> {
|
||||
if (resource == null) return;
|
||||
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
|
||||
PetDTO p = resource.data;
|
||||
binding.etPetName.setText(p.getPetName());
|
||||
binding.etPetSpecies.setText(p.getPetSpecies());
|
||||
binding.etPetBreed.setText(p.getPetBreed());
|
||||
binding.etPetAge.setText(String.valueOf(p.getPetAge()));
|
||||
if (p.getPetPrice() != null) {
|
||||
binding.etPetPrice.setText(String.format(Locale.getDefault(), "%.2f", p.getPetPrice()));
|
||||
}
|
||||
SpinnerUtils.setSelectionByValue(binding.spinnerPetStatus, p.getPetStatus());
|
||||
|
||||
selectedCustomerId = p.getCustomerId();
|
||||
updateCustomerSpinnerSelection();
|
||||
|
||||
//helper function to set up the spinner menu for pet status
|
||||
private void setupSpinner() {
|
||||
ArrayAdapter<String> adapter = new ArrayAdapter<String>(requireContext(),
|
||||
android.R.layout.simple_spinner_item,
|
||||
new String[]{"Available", "Adopted"}) {
|
||||
|
||||
//Override the getView method for the spinner to make the text color darker for more readability
|
||||
@NonNull
|
||||
@Override
|
||||
public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) {
|
||||
View view = super.getView(position, convertView, parent);
|
||||
((TextView) view).setTextColor(ContextCompat.getColor(requireContext(), R.color.text_dark));
|
||||
return view;
|
||||
selectedStoreId = p.getStoreId();
|
||||
updateStoreSpinnerSelection();
|
||||
} else if (resource.status == Resource.Status.ERROR) {
|
||||
Toast.makeText(getContext(), "Failed to load pet: " + resource.message, Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
};
|
||||
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
|
||||
spinnerPetStatus.setAdapter(adapter);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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() {
|
||||
SpinnerUtils.setupStringSpinner(requireContext(), binding.spinnerPetStatus,
|
||||
new String[]{"Available", "Adopted", "Owned"});
|
||||
|
||||
binding.spinnerPetStatus.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
|
||||
@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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,190 +1,321 @@
|
||||
package com.example.petstoremobile.fragments.listfragments.detailfragments;
|
||||
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
import android.view.*;
|
||||
import android.widget.*;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
import androidx.navigation.fragment.NavHostFragment;
|
||||
|
||||
import com.bumptech.glide.Glide;
|
||||
import com.example.petstoremobile.R;
|
||||
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.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.math.BigDecimal;
|
||||
import java.util.*;
|
||||
import retrofit2.*;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import javax.inject.Named;
|
||||
|
||||
import dagger.hilt.android.AndroidEntryPoint;
|
||||
import okhttp3.MediaType;
|
||||
import okhttp3.MultipartBody;
|
||||
import okhttp3.RequestBody;
|
||||
|
||||
/**
|
||||
* Fragment for displaying and editing product details, including image selection.
|
||||
*/
|
||||
@AndroidEntryPoint
|
||||
public class ProductDetailFragment extends Fragment {
|
||||
|
||||
private TextView tvMode, tvProductId;
|
||||
private EditText etProductName, etProductDesc, etProductPrice;
|
||||
private Spinner spinnerCategory;
|
||||
private Button btnSave, btnDelete, btnBack;
|
||||
private FragmentProductDetailBinding binding;
|
||||
|
||||
private long prodId = -1;
|
||||
private boolean isEditing = false;
|
||||
private long preselectedCategoryId = -1;
|
||||
private boolean hasImage = false;
|
||||
private boolean isImageChanged = false;
|
||||
private boolean isImageRemoved = false;
|
||||
|
||||
private List<CategoryDTO> categoryList = new ArrayList<>();
|
||||
private Uri photoUri;
|
||||
private ProductViewModel viewModel;
|
||||
private ImagePickerHelper imagePickerHelper;
|
||||
|
||||
@Inject @Named("baseUrl") String baseUrl;
|
||||
@Inject TokenManager tokenManager;
|
||||
|
||||
/**
|
||||
* Initializes activity launchers and the ImagePickerHelper.
|
||||
*/
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
viewModel = new ViewModelProvider(this).get(ProductViewModel.class);
|
||||
|
||||
imagePickerHelper = new ImagePickerHelper(this, "product_photo.jpg", new ImagePickerHelper.ImagePickerListener() {
|
||||
@Override
|
||||
public void onImagePicked(Uri uri) {
|
||||
photoUri = uri;
|
||||
Glide.with(ProductDetailFragment.this).load(uri).into(binding.ivProductImage);
|
||||
hasImage = true;
|
||||
isImageChanged = true;
|
||||
isImageRemoved = false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onImageRemoved() {
|
||||
photoUri = null;
|
||||
hasImage = false;
|
||||
isImageChanged = false;
|
||||
isImageRemoved = true;
|
||||
binding.ivProductImage.setImageResource(R.drawable.placeholder2);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Inflates the layout.
|
||||
*/
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
|
||||
Bundle savedInstanceState) {
|
||||
View view = inflater.inflate(R.layout.fragment_product_detail, container, false);
|
||||
initViews(view);
|
||||
binding = FragmentProductDetailBinding.inflate(inflater, container, false);
|
||||
return binding.getRoot();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up UI components and listeners after the view is created.
|
||||
*/
|
||||
@Override
|
||||
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
|
||||
loadCategories();
|
||||
handleArguments();
|
||||
|
||||
btnBack.setOnClickListener(v -> navigateBack());
|
||||
btnSave.setOnClickListener(v -> saveProduct());
|
||||
btnDelete.setOnClickListener(v -> confirmDelete());
|
||||
return view;
|
||||
binding.btnProductBack.setOnClickListener(v -> navigateBack());
|
||||
binding.btnSaveProduct.setOnClickListener(v -> saveProduct());
|
||||
binding.btnDeleteProduct.setOnClickListener(v -> confirmDelete());
|
||||
binding.ivProductImage.setOnClickListener(v -> imagePickerHelper.showImagePickerDialog("Select Product Image", hasImage));
|
||||
}
|
||||
|
||||
private void initViews(View v) {
|
||||
tvMode = v.findViewById(R.id.tvProductMode);
|
||||
tvProductId = v.findViewById(R.id.tvProductId);
|
||||
etProductName = v.findViewById(R.id.etProductName);
|
||||
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);
|
||||
@Override
|
||||
public void onDestroyView() {
|
||||
super.onDestroyView();
|
||||
binding = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches all product categories for the selection spinner.
|
||||
*/
|
||||
private void loadCategories() {
|
||||
RetrofitClient.getCategoryApi(requireContext()).getAllCategories(0, 100)
|
||||
.enqueue(new Callback<PageResponse<CategoryDTO>>() {
|
||||
public void onResponse(Call<PageResponse<CategoryDTO>> c,
|
||||
Response<PageResponse<CategoryDTO>> r) {
|
||||
if (r.isSuccessful() && r.body() != null) {
|
||||
categoryList = r.body().getContent();
|
||||
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 ArrayAdapter<>(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;
|
||||
}
|
||||
viewModel.getAllCategories(0, 100).observe(getViewLifecycleOwner(), resource -> {
|
||||
if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) {
|
||||
categoryList = resource.data.getContent();
|
||||
SpinnerUtils.populateSpinner(requireContext(), binding.spinnerProductCategory, categoryList,
|
||||
CategoryDTO::getCategoryName, "-- Select Category --",
|
||||
preselectedCategoryId, CategoryDTO::getCategoryId);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the fragment was opened with existing product data for editing.
|
||||
*/
|
||||
private void handleArguments() {
|
||||
Bundle a = getArguments();
|
||||
if (a != null && a.containsKey("prodId")) {
|
||||
isEditing = true;
|
||||
prodId = a.getLong("prodId");
|
||||
preselectedCategoryId = a.getLong("categoryId", -1);
|
||||
|
||||
tvMode.setText("Edit Product");
|
||||
tvProductId.setText("ID: " + prodId);
|
||||
tvProductId.setVisibility(View.VISIBLE);
|
||||
etProductName.setText(a.getString("prodName"));
|
||||
etProductDesc.setText(a.getString("prodDesc"));
|
||||
etProductPrice.setText(a.getString("prodPrice"));
|
||||
btnDelete.setVisibility(View.VISIBLE);
|
||||
binding.tvProductMode.setText("Edit Product");
|
||||
binding.tvProductId.setText("ID: " + prodId);
|
||||
binding.tvProductId.setVisibility(View.VISIBLE);
|
||||
binding.btnDeleteProduct.setVisibility(View.VISIBLE);
|
||||
loadProductData();
|
||||
loadProductImage();
|
||||
} else {
|
||||
tvMode.setText("Add Product");
|
||||
btnDelete.setVisibility(View.GONE);
|
||||
tvProductId.setVisibility(View.GONE);
|
||||
binding.tvProductMode.setText("Add Product");
|
||||
binding.btnDeleteProduct.setVisibility(View.GONE);
|
||||
binding.tvProductId.setVisibility(View.GONE);
|
||||
hasImage = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void saveProduct() {
|
||||
String name = etProductName.getText().toString().trim();
|
||||
String desc = etProductDesc.getText().toString().trim();
|
||||
String priceStr = etProductPrice.getText().toString().trim();
|
||||
/**
|
||||
* Loads the product data from the backend.
|
||||
*/
|
||||
private void loadProductData() {
|
||||
viewModel.getProductById(prodId).observe(getViewLifecycleOwner(), resource -> {
|
||||
if (resource == null) return;
|
||||
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
|
||||
ProductDTO p = resource.data;
|
||||
binding.etProductName.setText(p.getProdName());
|
||||
binding.etProductDesc.setText(p.getProdDesc());
|
||||
binding.etProductPrice.setText(p.getProdPrice() != null ? p.getProdPrice().toString() : "");
|
||||
preselectedCategoryId = p.getCategoryId() != null ? p.getCategoryId() : -1;
|
||||
|
||||
// Refresh spinner selection once data is loaded
|
||||
if (!categoryList.isEmpty()) {
|
||||
SpinnerUtils.populateSpinner(requireContext(), binding.spinnerProductCategory, categoryList,
|
||||
CategoryDTO::getCategoryName, "-- Select Category --",
|
||||
preselectedCategoryId, CategoryDTO::getCategoryId);
|
||||
}
|
||||
} else if (resource.status == Resource.Status.ERROR) {
|
||||
Toast.makeText(getContext(), "Failed to load product: " + resource.message, Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (name.isEmpty()) {
|
||||
etProductName.setError("Enter product name"); return;
|
||||
/**
|
||||
* 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();
|
||||
}
|
||||
if (spinnerCategory.getSelectedItemPosition() == 0) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Uploads the selected image file to the server.
|
||||
*/
|
||||
private void uploadProductImageAndNavigate(Uri uri, String successMsg) {
|
||||
File file = FileUtils.getFileFromUri(requireContext(), uri);
|
||||
if (file == null) {
|
||||
Toast.makeText(getContext(), successMsg, Toast.LENGTH_SHORT).show();
|
||||
navigateBack();
|
||||
return;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
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);
|
||||
BigDecimal price;
|
||||
try {
|
||||
price = new BigDecimal(priceStr);
|
||||
} catch (Exception e) {
|
||||
etProductPrice.setError("Invalid price"); return;
|
||||
}
|
||||
String name = binding.etProductName.getText().toString().trim();
|
||||
String desc = binding.etProductDesc.getText().toString().trim();
|
||||
BigDecimal price = new BigDecimal(binding.etProductPrice.getText().toString().trim());
|
||||
|
||||
CategoryDTO category = categoryList.get(binding.spinnerProductCategory.getSelectedItemPosition() - 1);
|
||||
ProductDTO dto = new ProductDTO(name, category.getCategoryId(), desc, price);
|
||||
|
||||
Log.d("PRODUCT_SAVE", "name=" + name + " categoryId=" + category.getCategoryId()
|
||||
+ " price=" + price);
|
||||
|
||||
ProductApi api = RetrofitClient.getProductApi(requireContext());
|
||||
if (isEditing) {
|
||||
api.updateProduct(prodId, dto).enqueue(simpleCallback("Updated"));
|
||||
viewModel.updateProduct(prodId, dto).observe(getViewLifecycleOwner(), resource -> {
|
||||
if (resource != null && resource.status != Resource.Status.LOADING) {
|
||||
if (resource.status == Resource.Status.SUCCESS) {
|
||||
performPendingImageActions("Updated");
|
||||
} else {
|
||||
Toast.makeText(getContext(), "Error: " + resource.message, Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
api.createProduct(dto).enqueue(simpleCallback("Saved"));
|
||||
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<>() {
|
||||
public void onResponse(Call<ProductDTO> c, Response<ProductDTO> r) {
|
||||
if (r.isSuccessful()) {
|
||||
Toast.makeText(getContext(), msg, Toast.LENGTH_SHORT).show();
|
||||
navigateBack();
|
||||
} else {
|
||||
try {
|
||||
String err = r.errorBody().string();
|
||||
Log.e("PRODUCT_SAVE", "Error: " + err);
|
||||
Toast.makeText(getContext(), "Error " + r.code(), Toast.LENGTH_SHORT).show();
|
||||
} catch (Exception e) {
|
||||
Log.e("PRODUCT_SAVE", "Failed to read error");
|
||||
}
|
||||
}
|
||||
}
|
||||
public void onFailure(Call<ProductDTO> c, Throwable t) {
|
||||
Log.e("PRODUCT_SAVE", "Failure: " + t.getMessage());
|
||||
Toast.makeText(getContext(), "Network error", Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays a confirmation dialog before deleting the product.
|
||||
*/
|
||||
private void confirmDelete() {
|
||||
new AlertDialog.Builder(requireContext())
|
||||
.setTitle("Delete Product?")
|
||||
.setPositiveButton("Yes", (d, w) ->
|
||||
RetrofitClient.getProductApi(requireContext())
|
||||
.deleteProduct(prodId)
|
||||
.enqueue(new Callback<Void>() {
|
||||
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();
|
||||
DialogUtils.showDeleteConfirmDialog(requireContext(), "Product", () ->
|
||||
viewModel.deleteProduct(prodId).observe(getViewLifecycleOwner(), resource -> {
|
||||
if (resource != null && resource.status == Resource.Status.SUCCESS) {
|
||||
navigateBack();
|
||||
} else if (resource != null && resource.status == Resource.Status.ERROR) {
|
||||
Toast.makeText(getContext(), "Delete failed: " + resource.message, Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigates back to the previous fragment.
|
||||
*/
|
||||
private void navigateBack() {
|
||||
ListFragment lf = (ListFragment) getParentFragment();
|
||||
if (lf != null) lf.getChildFragmentManager().popBackStack();
|
||||
NavHostFragment.findNavController(this).popBackStack();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,26 +1,36 @@
|
||||
package com.example.petstoremobile.fragments.listfragments.detailfragments;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
import android.view.*;
|
||||
import android.widget.*;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import com.example.petstoremobile.R;
|
||||
import com.example.petstoremobile.api.*;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
import androidx.navigation.fragment.NavHostFragment;
|
||||
|
||||
import com.example.petstoremobile.databinding.FragmentProductSupplierDetailBinding;
|
||||
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.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 {
|
||||
|
||||
private TextView tvMode;
|
||||
private Spinner spinnerProduct, spinnerSupplier;
|
||||
private EditText etCost;
|
||||
private Button btnSave, btnDelete, btnBack;
|
||||
private FragmentProductSupplierDetailBinding binding;
|
||||
|
||||
private boolean isEditing = false;
|
||||
private long editProductId = -1;
|
||||
@@ -31,190 +41,171 @@ public class ProductSupplierDetailFragment extends Fragment {
|
||||
private List<ProductDTO> productList = 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
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
|
||||
Bundle savedInstanceState) {
|
||||
View view = inflater.inflate(R.layout.fragment_product_supplier_detail, container, false);
|
||||
initViews(view);
|
||||
loadData();
|
||||
binding = FragmentProductSupplierDetailBinding.inflate(inflater, container, false);
|
||||
return binding.getRoot();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
loadSpinnersData();
|
||||
handleArguments();
|
||||
|
||||
btnBack.setOnClickListener(v -> navigateBack());
|
||||
btnSave.setOnClickListener(v -> save());
|
||||
btnDelete.setOnClickListener(v -> confirmDelete());
|
||||
return view;
|
||||
binding.btnPSBack.setOnClickListener(v -> navigateBack());
|
||||
binding.btnSavePS.setOnClickListener(v -> save());
|
||||
binding.btnDeletePS.setOnClickListener(v -> confirmDelete());
|
||||
}
|
||||
|
||||
private void initViews(View v) {
|
||||
tvMode = v.findViewById(R.id.tvPSMode);
|
||||
spinnerProduct = v.findViewById(R.id.spinnerPSProduct);
|
||||
spinnerSupplier = v.findViewById(R.id.spinnerPSSupplier);
|
||||
etCost = v.findViewById(R.id.etPSCost);
|
||||
btnSave = v.findViewById(R.id.btnSavePS);
|
||||
btnDelete = v.findViewById(R.id.btnDeletePS);
|
||||
btnBack = v.findViewById(R.id.btnPSBack);
|
||||
@Override
|
||||
public void onDestroyView() {
|
||||
super.onDestroyView();
|
||||
binding = null;
|
||||
}
|
||||
|
||||
private void loadData() {
|
||||
/**
|
||||
* Fetches products and suppliers to populate the spinners.
|
||||
*/
|
||||
private void loadSpinnersData() {
|
||||
loadProducts();
|
||||
loadSuppliers();
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the list of products from the API.
|
||||
*/
|
||||
private void loadProducts() {
|
||||
RetrofitClient.getProductApi(requireContext()).getAllProducts(null, 0, 200)
|
||||
.enqueue(new Callback<PageResponse<ProductDTO>>() {
|
||||
public void onResponse(Call<PageResponse<ProductDTO>> c,
|
||||
Response<PageResponse<ProductDTO>> r) {
|
||||
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 ArrayAdapter<>(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;
|
||||
}
|
||||
productViewModel.getAllProducts(null, null, 0, 200, "prodName").observe(getViewLifecycleOwner(), resource -> {
|
||||
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
|
||||
productList = resource.data.getContent();
|
||||
refreshProductSpinner();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void refreshProductSpinner() {
|
||||
SpinnerUtils.populateSpinner(requireContext(), binding.spinnerPSProduct, productList,
|
||||
ProductDTO::getProdName, "-- Select Product --",
|
||||
preselectedProductId, ProductDTO::getProdId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the list of suppliers from the API.
|
||||
*/
|
||||
private void loadSuppliers() {
|
||||
RetrofitClient.getSupplierApi(requireContext()).getAllSuppliers(0, 200)
|
||||
.enqueue(new Callback<PageResponse<SupplierDTO>>() {
|
||||
public void onResponse(Call<PageResponse<SupplierDTO>> c,
|
||||
Response<PageResponse<SupplierDTO>> r) {
|
||||
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 ArrayAdapter<>(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;
|
||||
}
|
||||
supplierViewModel.getAllSuppliers(0, 200, null, "supCompany").observe(getViewLifecycleOwner(), resource -> {
|
||||
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
|
||||
supplierList = resource.data.getContent();
|
||||
refreshSupplierSpinner();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void refreshSupplierSpinner() {
|
||||
SpinnerUtils.populateSpinner(requireContext(), binding.spinnerPSSupplier, supplierList,
|
||||
SupplierDTO::getSupCompany, "-- Select Supplier --",
|
||||
preselectedSupplierId, SupplierDTO::getSupId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles arguments to determine if the fragment is in edit or add mode.
|
||||
*/
|
||||
private void handleArguments() {
|
||||
Bundle a = getArguments();
|
||||
if (a != null && a.containsKey("productId")) {
|
||||
if (a != null && a.containsKey("productId") && a.containsKey("supplierId")) {
|
||||
isEditing = true;
|
||||
editProductId = a.getLong("productId");
|
||||
editSupplierId = a.getLong("supplierId");
|
||||
preselectedProductId = editProductId;
|
||||
editProductId = a.getLong("productId");
|
||||
editSupplierId = a.getLong("supplierId");
|
||||
preselectedProductId = editProductId;
|
||||
preselectedSupplierId = editSupplierId;
|
||||
etCost.setText(a.getString("cost"));
|
||||
tvMode.setText("Edit Product Supplier");
|
||||
btnDelete.setVisibility(View.VISIBLE);
|
||||
|
||||
binding.tvPSMode.setText("Edit Product Supplier");
|
||||
binding.btnDeletePS.setVisibility(View.VISIBLE);
|
||||
|
||||
} else {
|
||||
tvMode.setText("Add Product Supplier");
|
||||
btnDelete.setVisibility(View.GONE);
|
||||
binding.tvPSMode.setText("Add Product Supplier");
|
||||
binding.btnDeletePS.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Validates input and saves the product-supplier to the backend.
|
||||
*/
|
||||
private void save() {
|
||||
if (spinnerProduct.getSelectedItemPosition() == 0) {
|
||||
if (binding.spinnerPSProduct.getSelectedItemPosition() == 0) {
|
||||
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;
|
||||
}
|
||||
String costStr = etCost.getText().toString().trim();
|
||||
if (costStr.isEmpty()) {
|
||||
etCost.setError("Enter cost"); return;
|
||||
|
||||
if (!InputValidator.isNotEmpty(binding.etPSCost, "Cost") ||
|
||||
!InputValidator.isPositiveDecimal(binding.etPSCost, "Cost")) {
|
||||
return;
|
||||
}
|
||||
|
||||
ProductDTO product = productList.get(spinnerProduct.getSelectedItemPosition() - 1);
|
||||
SupplierDTO supplier = supplierList.get(spinnerSupplier.getSelectedItemPosition() - 1);
|
||||
BigDecimal cost;
|
||||
try {
|
||||
cost = new BigDecimal(costStr);
|
||||
} catch (Exception e) {
|
||||
etCost.setError("Invalid cost"); return;
|
||||
}
|
||||
ProductDTO product = productList.get(binding.spinnerPSProduct.getSelectedItemPosition() - 1);
|
||||
SupplierDTO supplier = supplierList.get(binding.spinnerPSSupplier.getSelectedItemPosition() - 1);
|
||||
BigDecimal cost = new BigDecimal(binding.etPSCost.getText().toString().trim());
|
||||
|
||||
ProductSupplierDTO dto = new ProductSupplierDTO(
|
||||
product.getProdId(), supplier.getSupId(), cost);
|
||||
|
||||
ProductSupplierApi api = RetrofitClient.getProductSupplierApi(requireContext());
|
||||
if (isEditing) {
|
||||
api.updateProductSupplier(editProductId, editSupplierId, dto)
|
||||
.enqueue(simpleCallback("Updated"));
|
||||
psViewModel.updateProductSupplier(editProductId, editSupplierId, 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 {
|
||||
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<>() {
|
||||
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();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows a confirmation dialog before deleting a product-supplier relationship.
|
||||
*/
|
||||
private void confirmDelete() {
|
||||
new AlertDialog.Builder(requireContext())
|
||||
.setTitle("Delete?")
|
||||
.setPositiveButton("Yes", (d, w) ->
|
||||
RetrofitClient.getProductSupplierApi(requireContext())
|
||||
.deleteProductSupplier(editProductId, editSupplierId)
|
||||
.enqueue(new Callback<Void>() {
|
||||
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();
|
||||
DialogUtils.showDeleteConfirmDialog(requireContext(), "Product Supplier", () ->
|
||||
psViewModel.deleteProductSupplier(editProductId, editSupplierId).observe(getViewLifecycleOwner(), resource -> {
|
||||
if (resource.status == Resource.Status.SUCCESS) {
|
||||
Toast.makeText(getContext(), "Deleted", Toast.LENGTH_SHORT).show();
|
||||
navigateBack();
|
||||
} else if (resource.status == Resource.Status.ERROR) {
|
||||
Toast.makeText(getContext(), "Delete failed: " + resource.message, Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigates back to the previous screen.
|
||||
*/
|
||||
private void navigateBack() {
|
||||
ListFragment lf = (ListFragment) getParentFragment();
|
||||
if (lf != null) lf.getChildFragmentManager().popBackStack();
|
||||
NavHostFragment.findNavController(this).popBackStack();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,53 +3,107 @@ package com.example.petstoremobile.fragments.listfragments.detailfragments;
|
||||
import android.graphics.Color;
|
||||
import android.os.Bundle;
|
||||
import android.view.*;
|
||||
import android.widget.*;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import com.example.petstoremobile.R;
|
||||
import com.example.petstoremobile.fragments.ListFragment;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
import androidx.navigation.fragment.NavHostFragment;
|
||||
|
||||
import com.example.petstoremobile.databinding.FragmentPurchaseOrderDetailBinding;
|
||||
import com.example.petstoremobile.dtos.PurchaseOrderDTO;
|
||||
import com.example.petstoremobile.utils.Resource;
|
||||
import com.example.petstoremobile.viewmodels.PurchaseOrderViewModel;
|
||||
|
||||
import dagger.hilt.android.AndroidEntryPoint;
|
||||
|
||||
/**
|
||||
* Fragment for displaying the information of a purchase order.
|
||||
*/
|
||||
@AndroidEntryPoint
|
||||
public class PurchaseOrderDetailFragment extends Fragment {
|
||||
|
||||
private TextView tvId, tvSupplier, tvDate, tvStatus;
|
||||
private Button btnBack;
|
||||
private FragmentPurchaseOrderDetailBinding binding;
|
||||
private PurchaseOrderViewModel viewModel;
|
||||
private long purchaseOrderId;
|
||||
|
||||
@Override
|
||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
viewModel = new ViewModelProvider(this).get(PurchaseOrderViewModel.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Inflates the layout.
|
||||
*/
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
|
||||
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);
|
||||
tvDate = view.findViewById(R.id.tvPODetailDate);
|
||||
tvStatus = view.findViewById(R.id.tvPODetailStatus);
|
||||
btnBack = view.findViewById(R.id.btnPOBack);
|
||||
/**
|
||||
* Initializes views and populates order data from backend after the view is created.
|
||||
*/
|
||||
@Override
|
||||
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
|
||||
Bundle a = getArguments();
|
||||
if (a != null) {
|
||||
tvId.setText("PO #" + a.getLong("purchaseOrderId"));
|
||||
tvSupplier.setText(a.getString("supplierName"));
|
||||
tvDate.setText(a.getString("orderDate"));
|
||||
handleArguments();
|
||||
|
||||
String status = a.getString("status", "");
|
||||
tvStatus.setText(status);
|
||||
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();
|
||||
binding.btnPOBack.setOnClickListener(v -> {
|
||||
NavHostFragment.findNavController(this).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.tvPODetailStore.setText(po.getStoreName() != null ? po.getStoreName() : "N/A");
|
||||
binding.tvPODetailDate.setText(po.getOrderDate());
|
||||
|
||||
String status = po.getStatus() != null ? po.getStatus() : "";
|
||||
binding.tvPODetailStatus.setText(status);
|
||||
switch (status.toUpperCase()) {
|
||||
case "RECEIVED":
|
||||
binding.tvPODetailStatus.setTextColor(Color.parseColor("#4CAF50"));
|
||||
break;
|
||||
case "PLACED":
|
||||
binding.tvPODetailStatus.setTextColor(Color.parseColor("#2196F3"));
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,75 +2,87 @@ package com.example.petstoremobile.fragments.listfragments.detailfragments;
|
||||
|
||||
import android.os.Bundle;
|
||||
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
import androidx.navigation.fragment.NavHostFragment;
|
||||
|
||||
import android.util.Log;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.Button;
|
||||
import android.widget.EditText;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.example.petstoremobile.R;
|
||||
import com.example.petstoremobile.api.RetrofitClient;
|
||||
import com.example.petstoremobile.api.ServiceApi;
|
||||
import com.example.petstoremobile.databinding.FragmentServiceDetailBinding;
|
||||
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.DialogUtils;
|
||||
import com.example.petstoremobile.utils.InputValidator;
|
||||
import com.example.petstoremobile.utils.Resource;
|
||||
import com.example.petstoremobile.viewmodels.ServiceViewModel;
|
||||
|
||||
import retrofit2.Call;
|
||||
import retrofit2.Callback;
|
||||
import retrofit2.Response;
|
||||
import dagger.hilt.android.AndroidEntryPoint;
|
||||
|
||||
/**
|
||||
* Fragment for displaying and editing service details.
|
||||
*/
|
||||
@AndroidEntryPoint
|
||||
public class ServiceDetailFragment extends Fragment {
|
||||
|
||||
private TextView tvMode, tvServiceId;
|
||||
private EditText etServiceName, etServiceDesc, etServiceDuration, etServicePrice;
|
||||
private Button btnSaveService, btnDeleteService, btnBack;
|
||||
private int serviceId;
|
||||
private FragmentServiceDetailBinding binding;
|
||||
private long serviceId;
|
||||
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
|
||||
public void setServiceFragment(ServiceFragment fragment) {
|
||||
this.serviceFragment = fragment;
|
||||
private ServiceViewModel viewModel;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
viewModel = new ViewModelProvider(this).get(ServiceViewModel.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container,
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
|
||||
Bundle savedInstanceState) {
|
||||
View view = inflater.inflate(R.layout.fragment_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
|
||||
initViews(view);
|
||||
handleArguments();
|
||||
|
||||
//set button click listeners
|
||||
btnBack.setOnClickListener(v -> navigateBack());
|
||||
btnSaveService.setOnClickListener(v -> saveService());
|
||||
btnDeleteService.setOnClickListener(v -> deleteService());
|
||||
|
||||
return view;
|
||||
binding.btnBack.setOnClickListener(v -> navigateBack());
|
||||
binding.btnSaveService.setOnClickListener(v -> saveService());
|
||||
binding.btnDeleteService.setOnClickListener(v -> deleteService());
|
||||
}
|
||||
|
||||
//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() {
|
||||
// Validates all fields using InputValidator
|
||||
if (!InputValidator.isNotEmpty(etServiceName, "Service Name")) return;
|
||||
if (!InputValidator.isNotEmpty(etServiceDesc, "Description")) return;
|
||||
if (!InputValidator.isPositiveInteger(etServiceDuration, "Duration")) return;
|
||||
if (!InputValidator.isPositiveDecimal(etServicePrice, "Price")) return;
|
||||
if (!InputValidator.isNotEmpty(binding.etServiceName, "Service Name")) return;
|
||||
if (!InputValidator.isNotEmpty(binding.etServiceDesc, "Description")) return;
|
||||
if (!InputValidator.isPositiveInteger(binding.etServiceDuration, "Duration")) return;
|
||||
if (!InputValidator.isPositiveDecimal(binding.etServicePrice, "Price")) return;
|
||||
|
||||
//get all the values from the fields
|
||||
String name = etServiceName.getText().toString().trim();
|
||||
String desc = etServiceDesc.getText().toString().trim();
|
||||
int duration = Integer.parseInt(etServiceDuration.getText().toString().trim());
|
||||
double price = Double.parseDouble(etServicePrice.getText().toString().trim());
|
||||
String name = binding.etServiceName.getText().toString().trim();
|
||||
String desc = binding.etServiceDesc.getText().toString().trim();
|
||||
int duration = Integer.parseInt(binding.etServiceDuration.getText().toString().trim());
|
||||
double price = Double.parseDouble(binding.etServicePrice.getText().toString().trim());
|
||||
|
||||
//create a service object to send to the API
|
||||
ServiceDTO serviceDTO = new ServiceDTO();
|
||||
@@ -79,130 +91,94 @@ public class ServiceDetailFragment extends Fragment {
|
||||
serviceDTO.setServiceDuration(duration);
|
||||
serviceDTO.setServicePrice(price);
|
||||
|
||||
ServiceApi serviceApi = RetrofitClient.getServiceApi(requireContext());
|
||||
|
||||
//check if the service is being edited or added
|
||||
if (isEditing) {
|
||||
// Update existing service
|
||||
serviceDTO.setServiceId((long) serviceId);
|
||||
serviceApi.updateService((long) serviceId, serviceDTO).enqueue(new Callback<ServiceDTO>() {
|
||||
@Override
|
||||
public void onResponse(Call<ServiceDTO> call, Response<ServiceDTO> response) {
|
||||
if (response.isSuccessful()) {
|
||||
ActivityLogger.logChange(requireContext(), "Service", "UPDATED", serviceId);
|
||||
Toast.makeText(getContext(), "Service updated successfully!", Toast.LENGTH_SHORT).show();
|
||||
navigateBack();
|
||||
} else {
|
||||
Toast.makeText(getContext(), "Failed to update service: " + response.code(), Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}
|
||||
|
||||
@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();
|
||||
serviceDTO.setServiceId(serviceId);
|
||||
viewModel.updateService(serviceId, serviceDTO).observe(getViewLifecycleOwner(), resource -> {
|
||||
if (resource.status == Resource.Status.SUCCESS) {
|
||||
ActivityLogger.logChange(requireContext(), "Service", "UPDATED", (int) serviceId);
|
||||
Toast.makeText(getContext(), "Service updated successfully!", Toast.LENGTH_SHORT).show();
|
||||
navigateBack();
|
||||
} else if (resource.status == Resource.Status.ERROR) {
|
||||
Toast.makeText(getContext(), "Error: " + resource.message, Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// Add new service
|
||||
serviceApi.createService(serviceDTO).enqueue(new Callback<ServiceDTO>() {
|
||||
@Override
|
||||
public void onResponse(Call<ServiceDTO> call, Response<ServiceDTO> response) {
|
||||
if (response.isSuccessful()) {
|
||||
ActivityLogger.log(requireContext(), "Added new Service: " + name);
|
||||
Toast.makeText(getContext(), "Service added successfully!", Toast.LENGTH_SHORT).show();
|
||||
navigateBack();
|
||||
} else {
|
||||
Toast.makeText(getContext(), "Failed to add service: " + response.code(), Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}
|
||||
|
||||
@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();
|
||||
viewModel.createService(serviceDTO).observe(getViewLifecycleOwner(), resource -> {
|
||||
if (resource.status == Resource.Status.SUCCESS) {
|
||||
ActivityLogger.log(requireContext(), "Added new Service: " + name);
|
||||
Toast.makeText(getContext(), "Service added successfully!", Toast.LENGTH_SHORT).show();
|
||||
navigateBack();
|
||||
} else if (resource.status == Resource.Status.ERROR) {
|
||||
Toast.makeText(getContext(), "Error: " + resource.message, Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
//Method to Delete a service
|
||||
/**
|
||||
* Displays a confirmation dialog and handles the deletion of a service.
|
||||
*/
|
||||
private void deleteService() {
|
||||
//Alert the user to confirm the delete
|
||||
new AlertDialog.Builder(requireContext())
|
||||
.setTitle("Delete Service")
|
||||
.setMessage("Are you sure you want to delete " + etServiceName.getText().toString() + "?")
|
||||
.setPositiveButton("Delete", (dialog, which) -> {
|
||||
ServiceApi serviceApi = RetrofitClient.getServiceApi(requireContext());
|
||||
serviceApi.deleteService((long) serviceId).enqueue(new Callback<Void>() {
|
||||
@Override
|
||||
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();
|
||||
DialogUtils.showDeleteConfirmDialog(requireContext(), "Service", () ->
|
||||
viewModel.deleteService(serviceId).observe(getViewLifecycleOwner(), resource -> {
|
||||
if (resource.status == Resource.Status.SUCCESS) {
|
||||
ActivityLogger.logChange(requireContext(), "Service", "DELETED", (int) serviceId);
|
||||
Toast.makeText(getContext(), "Service deleted successfully!", Toast.LENGTH_SHORT).show();
|
||||
navigateBack();
|
||||
} else if (resource.status == Resource.Status.ERROR) {
|
||||
Toast.makeText(getContext(), "Delete failed: " + resource.message, Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
//Helper method to navigate back to the list
|
||||
/**
|
||||
* Navigates back to the previous screen.
|
||||
*/
|
||||
private void navigateBack() {
|
||||
ListFragment listFragment = (ListFragment) getParentFragment();
|
||||
if (listFragment != null) {
|
||||
listFragment.getChildFragmentManager().popBackStack();
|
||||
}
|
||||
NavHostFragment.findNavController(this).popBackStack();
|
||||
}
|
||||
|
||||
//helper function to check if service is being edited or added and show the view accordingly
|
||||
/**
|
||||
* Handles arguments passed to the fragment to determine if it's in edit or add mode.
|
||||
*/
|
||||
private void handleArguments() {
|
||||
// Service is being edited if the bundle contains a serviceId
|
||||
if (getArguments() != null && getArguments().containsKey("serviceId")) {
|
||||
// Get service data from arguments and populate fields
|
||||
isEditing = true;
|
||||
serviceId = getArguments().getInt("serviceId");
|
||||
tvMode.setText("Edit Service");
|
||||
tvServiceId.setText("ID: " + serviceId);
|
||||
etServiceName.setText(getArguments().getString("serviceName"));
|
||||
etServiceDesc.setText(getArguments().getString("serviceDesc"));
|
||||
etServiceDuration.setText(String.valueOf(getArguments().getInt("serviceDuration")));
|
||||
etServicePrice.setText(String.valueOf(getArguments().getDouble("servicePrice")));
|
||||
btnDeleteService.setVisibility(View.VISIBLE);
|
||||
serviceId = getArguments().getLong("serviceId");
|
||||
binding.tvMode.setText("Edit Service");
|
||||
binding.tvServiceId.setText("ID: " + serviceId);
|
||||
binding.btnDeleteService.setVisibility(View.VISIBLE);
|
||||
loadServiceData();
|
||||
} else {
|
||||
// Service is being added
|
||||
// Set default values for add a new service
|
||||
isEditing = false;
|
||||
tvMode.setText("Add Service");
|
||||
tvServiceId.setVisibility(View.GONE);
|
||||
btnDeleteService.setVisibility(View.GONE);
|
||||
btnSaveService.setText("Add");
|
||||
binding.tvMode.setText("Add Service");
|
||||
binding.tvServiceId.setVisibility(View.GONE);
|
||||
binding.btnDeleteService.setVisibility(View.GONE);
|
||||
binding.btnSaveService.setText("Add");
|
||||
}
|
||||
}
|
||||
|
||||
//helper function to get controls from layout
|
||||
private void initViews(View view) {
|
||||
tvMode = view.findViewById(R.id.tvMode);
|
||||
tvServiceId = view.findViewById(R.id.tvServiceId);
|
||||
etServiceName = view.findViewById(R.id.etServiceName);
|
||||
etServiceDesc = view.findViewById(R.id.etServiceDesc);
|
||||
etServiceDuration = view.findViewById(R.id.etServiceDuration);
|
||||
etServicePrice = view.findViewById(R.id.etServicePrice);
|
||||
btnSaveService = view.findViewById(R.id.btnSaveService);
|
||||
btnDeleteService = view.findViewById(R.id.btnDeleteService);
|
||||
btnBack = view.findViewById(R.id.btnBack);
|
||||
/**
|
||||
* Fetches specific service details from the backend using the ID.
|
||||
*/
|
||||
private void loadServiceData() {
|
||||
viewModel.getServiceById(serviceId).observe(getViewLifecycleOwner(), resource -> {
|
||||
if (resource == null) return;
|
||||
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
|
||||
ServiceDTO s = resource.data;
|
||||
binding.etServiceName.setText(s.getServiceName());
|
||||
binding.etServiceDesc.setText(s.getServiceDesc());
|
||||
binding.etServiceDuration.setText(String.valueOf(s.getServiceDuration()));
|
||||
binding.etServicePrice.setText(String.valueOf(s.getServicePrice()));
|
||||
} else if (resource.status == Resource.Status.ERROR) {
|
||||
Toast.makeText(getContext(), "Failed to load service: " + resource.message, Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,77 +2,91 @@ package com.example.petstoremobile.fragments.listfragments.detailfragments;
|
||||
|
||||
import android.os.Bundle;
|
||||
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
import androidx.navigation.fragment.NavHostFragment;
|
||||
|
||||
import android.util.Log;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.Button;
|
||||
import android.widget.EditText;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.example.petstoremobile.R;
|
||||
import com.example.petstoremobile.api.RetrofitClient;
|
||||
import com.example.petstoremobile.api.SupplierApi;
|
||||
import com.example.petstoremobile.databinding.FragmentSupplierDetailBinding;
|
||||
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.DialogUtils;
|
||||
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 retrofit2.Callback;
|
||||
import retrofit2.Response;
|
||||
import dagger.hilt.android.AndroidEntryPoint;
|
||||
|
||||
/**
|
||||
* Fragment for displaying and editing supplier details.
|
||||
*/
|
||||
@AndroidEntryPoint
|
||||
public class SupplierDetailFragment extends Fragment {
|
||||
|
||||
private TextView tvMode, tvSupId;
|
||||
private EditText etSupCompany, etSupContactFirstName, etSupContactLastName, etSupEmail, etSupPhone;
|
||||
private Button btnSaveSupplier, btnDeleteSupplier, btnBack;
|
||||
private int supId;
|
||||
private FragmentSupplierDetailBinding binding;
|
||||
private long supId;
|
||||
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
|
||||
public void setSupplierFragment(SupplierFragment fragment) {
|
||||
this.supplierFragment = fragment;
|
||||
private SupplierViewModel viewModel;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
viewModel = new ViewModelProvider(this).get(SupplierViewModel.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container,
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
|
||||
Bundle savedInstanceState) {
|
||||
View view = inflater.inflate(R.layout.fragment_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();
|
||||
|
||||
//set button click listeners
|
||||
btnBack.setOnClickListener(v -> navigateBack());
|
||||
btnSaveSupplier.setOnClickListener(v -> saveSupplier());
|
||||
btnDeleteSupplier.setOnClickListener(v -> deleteSupplier());
|
||||
|
||||
return view;
|
||||
binding.btnBack.setOnClickListener(v -> navigateBack());
|
||||
binding.btnSaveSupplier.setOnClickListener(v -> saveSupplier());
|
||||
binding.btnDeleteSupplier.setOnClickListener(v -> deleteSupplier());
|
||||
}
|
||||
|
||||
//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() {
|
||||
// Validates all fields using InputValidator
|
||||
if (!InputValidator.isNotEmpty(etSupCompany, "Company Name")) return;
|
||||
if (!InputValidator.isNotEmpty(etSupContactFirstName, "First Name")) return;
|
||||
if (!InputValidator.isNotEmpty(etSupContactLastName, "Last Name")) return;
|
||||
if (!InputValidator.isValidEmail(etSupEmail)) return;
|
||||
if (!InputValidator.isValidPhone(etSupPhone)) return;
|
||||
if (!InputValidator.isNotEmpty(binding.etSupCompany, "Company Name")) return;
|
||||
if (!InputValidator.isNotEmpty(binding.etSupContactFirstName, "First Name")) return;
|
||||
if (!InputValidator.isNotEmpty(binding.etSupContactLastName, "Last Name")) return;
|
||||
if (!InputValidator.isValidEmail(binding.etSupEmail)) return;
|
||||
if (!InputValidator.isValidPhone(binding.etSupPhone)) return;
|
||||
|
||||
//get all the values from the fields
|
||||
String company = etSupCompany.getText().toString().trim();
|
||||
String firstName = etSupContactFirstName.getText().toString().trim();
|
||||
String lastName = etSupContactLastName.getText().toString().trim();
|
||||
String email = etSupEmail.getText().toString().trim();
|
||||
String phone = etSupPhone.getText().toString().trim();
|
||||
String company = binding.etSupCompany.getText().toString().trim();
|
||||
String firstName = binding.etSupContactFirstName.getText().toString().trim();
|
||||
String lastName = binding.etSupContactLastName.getText().toString().trim();
|
||||
String email = binding.etSupEmail.getText().toString().trim();
|
||||
String phone = binding.etSupPhone.getText().toString().trim();
|
||||
|
||||
//create a supplier object to send to the API
|
||||
SupplierDTO supplierDTO = new SupplierDTO();
|
||||
@@ -82,137 +96,97 @@ public class SupplierDetailFragment extends Fragment {
|
||||
supplierDTO.setSupEmail(email);
|
||||
supplierDTO.setSupPhone(phone);
|
||||
|
||||
SupplierApi supplierApi = RetrofitClient.getSupplierApi(requireContext());
|
||||
|
||||
//check if the supplier is being edited or added
|
||||
if (isEditing) {
|
||||
// Update existing supplier
|
||||
supplierDTO.setSupId((long) supId);
|
||||
supplierApi.updateSupplier((long) supId, supplierDTO).enqueue(new Callback<SupplierDTO>() {
|
||||
@Override
|
||||
public void onResponse(Call<SupplierDTO> call, Response<SupplierDTO> response) {
|
||||
if (response.isSuccessful()) {
|
||||
ActivityLogger.logChange(requireContext(), "Supplier", "UPDATED", supId);
|
||||
Toast.makeText(getContext(), "Supplier updated successfully!", Toast.LENGTH_SHORT).show();
|
||||
navigateBack();
|
||||
} else {
|
||||
Toast.makeText(getContext(), "Failed to update supplier: " + response.code(), Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}
|
||||
|
||||
@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();
|
||||
supplierDTO.setSupId(supId);
|
||||
viewModel.updateSupplier(supId, supplierDTO).observe(getViewLifecycleOwner(), resource -> {
|
||||
if (resource.status == Resource.Status.SUCCESS) {
|
||||
ActivityLogger.logChange(requireContext(), "Supplier", "UPDATED", (int) supId);
|
||||
Toast.makeText(getContext(), "Supplier updated successfully!", Toast.LENGTH_SHORT).show();
|
||||
navigateBack();
|
||||
} else if (resource.status == Resource.Status.ERROR) {
|
||||
Toast.makeText(getContext(), "Error: " + resource.message, Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// Add new supplier
|
||||
supplierApi.createSupplier(supplierDTO).enqueue(new Callback<SupplierDTO>() {
|
||||
@Override
|
||||
public void onResponse(Call<SupplierDTO> call, Response<SupplierDTO> response) {
|
||||
if (response.isSuccessful()) {
|
||||
ActivityLogger.log(requireContext(), "Added new Supplier: " + company);
|
||||
Toast.makeText(getContext(), "Supplier added successfully!", Toast.LENGTH_SHORT).show();
|
||||
navigateBack();
|
||||
} else {
|
||||
Toast.makeText(getContext(), "Failed to add supplier: " + response.code(), Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}
|
||||
|
||||
@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();
|
||||
viewModel.createSupplier(supplierDTO).observe(getViewLifecycleOwner(), resource -> {
|
||||
if (resource.status == Resource.Status.SUCCESS) {
|
||||
ActivityLogger.log(requireContext(), "Added new Supplier: " + company);
|
||||
Toast.makeText(getContext(), "Supplier added successfully!", Toast.LENGTH_SHORT).show();
|
||||
navigateBack();
|
||||
} else if (resource.status == Resource.Status.ERROR) {
|
||||
Toast.makeText(getContext(), "Error: " + resource.message, Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
//Method to Delete a supplier
|
||||
/**
|
||||
* Displays a confirmation dialog and handles the deletion of a supplier.
|
||||
*/
|
||||
private void deleteSupplier() {
|
||||
//Alert the user to confirm the delete
|
||||
new AlertDialog.Builder(requireContext())
|
||||
.setTitle("Delete Supplier")
|
||||
.setMessage("Are you sure you want to delete " + etSupCompany.getText().toString() + "?")
|
||||
.setPositiveButton("Delete", (dialog, which) -> {
|
||||
SupplierApi supplierApi = RetrofitClient.getSupplierApi(requireContext());
|
||||
supplierApi.deleteSupplier((long) supId).enqueue(new Callback<Void>() {
|
||||
@Override
|
||||
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();
|
||||
DialogUtils.showDeleteConfirmDialog(requireContext(), "Supplier", () ->
|
||||
viewModel.deleteSupplier(supId).observe(getViewLifecycleOwner(), resource -> {
|
||||
if (resource.status == Resource.Status.SUCCESS) {
|
||||
ActivityLogger.logChange(requireContext(), "Supplier", "DELETED", (int) supId);
|
||||
Toast.makeText(getContext(), "Supplier deleted successfully!", Toast.LENGTH_SHORT).show();
|
||||
navigateBack();
|
||||
} else if (resource.status == Resource.Status.ERROR) {
|
||||
Toast.makeText(getContext(), "Delete failed: " + resource.message, Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
//Helper method to navigate back to the list
|
||||
/**
|
||||
* Navigates back to the previous screen.
|
||||
*/
|
||||
private void navigateBack() {
|
||||
ListFragment listFragment = (ListFragment) getParentFragment();
|
||||
if (listFragment != null) {
|
||||
listFragment.getChildFragmentManager().popBackStack();
|
||||
}
|
||||
NavHostFragment.findNavController(this).popBackStack();
|
||||
}
|
||||
|
||||
//helper function to check if supplier is being edited or added and show the view accordingly
|
||||
/**
|
||||
* Handles arguments passed to the fragment to determine if it's in edit or add mode.
|
||||
*/
|
||||
private void handleArguments() {
|
||||
// Supplier is being edited if the bundle contains a supId
|
||||
if (getArguments() != null && getArguments().containsKey("supId")) {
|
||||
// Get supplier data from arguments and populate fields
|
||||
isEditing = true;
|
||||
supId = getArguments().getInt("supId");
|
||||
tvMode.setText("Edit Supplier");
|
||||
tvSupId.setText("ID: " + supId);
|
||||
etSupCompany.setText(getArguments().getString("supCompany"));
|
||||
etSupContactFirstName.setText(getArguments().getString("supContactFirstName"));
|
||||
etSupContactLastName.setText(getArguments().getString("supContactLastName"));
|
||||
etSupEmail.setText(getArguments().getString("supEmail"));
|
||||
etSupPhone.setText(getArguments().getString("supPhone"));
|
||||
btnDeleteSupplier.setVisibility(View.VISIBLE);
|
||||
supId = getArguments().getLong("supId");
|
||||
binding.tvMode.setText("Edit Supplier");
|
||||
binding.tvSupId.setText("ID: " + supId);
|
||||
binding.tvSupId.setVisibility(View.VISIBLE);
|
||||
binding.btnDeleteSupplier.setVisibility(View.VISIBLE);
|
||||
loadSupplierData();
|
||||
} else {
|
||||
// Supplier is being added
|
||||
// Set default values for add a new supplier
|
||||
isEditing = false;
|
||||
tvMode.setText("Add Supplier");
|
||||
tvSupId.setVisibility(View.GONE);
|
||||
btnDeleteSupplier.setVisibility(View.GONE);
|
||||
btnSaveSupplier.setText("Add");
|
||||
binding.tvMode.setText("Add Supplier");
|
||||
binding.tvSupId.setVisibility(View.GONE);
|
||||
binding.btnDeleteSupplier.setVisibility(View.GONE);
|
||||
binding.btnSaveSupplier.setText("Add");
|
||||
}
|
||||
}
|
||||
|
||||
//helper function to get controls from layout
|
||||
private void initViews(View view) {
|
||||
tvMode = view.findViewById(R.id.tvMode);
|
||||
tvSupId = view.findViewById(R.id.tvSupId);
|
||||
etSupCompany = view.findViewById(R.id.etSupCompany);
|
||||
etSupContactFirstName = view.findViewById(R.id.etSupContactFirstName);
|
||||
etSupContactLastName = view.findViewById(R.id.etSupContactLastName);
|
||||
etSupEmail = view.findViewById(R.id.etSupEmail);
|
||||
etSupPhone = view.findViewById(R.id.etSupPhone);
|
||||
|
||||
// Add phone number formatting (CA) and limit length to 14 characters
|
||||
etSupPhone.addTextChangedListener(new android.telephony.PhoneNumberFormattingTextWatcher("CA"));
|
||||
etSupPhone.setFilters(new android.text.InputFilter[]{new android.text.InputFilter.LengthFilter(14)});
|
||||
|
||||
btnSaveSupplier = view.findViewById(R.id.btnSaveSupplier);
|
||||
btnDeleteSupplier = view.findViewById(R.id.btnDeleteSupplier);
|
||||
btnBack = view.findViewById(R.id.btnBack);
|
||||
/**
|
||||
* Fetches specific supplier details from the backend using the ID.
|
||||
*/
|
||||
private void loadSupplierData() {
|
||||
viewModel.getSupplierById(supId).observe(getViewLifecycleOwner(), resource -> {
|
||||
if (resource == null) return;
|
||||
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
|
||||
SupplierDTO s = resource.data;
|
||||
binding.etSupCompany.setText(s.getSupCompany());
|
||||
binding.etSupContactFirstName.setText(s.getSupContactFirstName());
|
||||
binding.etSupContactLastName.setText(s.getSupContactLastName());
|
||||
binding.etSupEmail.setText(s.getSupEmail());
|
||||
binding.etSupPhone.setText(s.getSupPhone());
|
||||
} else if (resource.status == Resource.Status.ERROR) {
|
||||
Toast.makeText(getContext(), "Failed to load supplier: " + resource.message, Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,262 +1,210 @@
|
||||
package com.example.petstoremobile.fragments.listfragments.listprofilefragments;
|
||||
|
||||
import android.Manifest;
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.graphics.Color;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
|
||||
import androidx.activity.result.ActivityResultLauncher;
|
||||
import androidx.activity.result.contract.ActivityResultContracts;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.core.content.FileProvider;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
import androidx.navigation.fragment.NavHostFragment;
|
||||
|
||||
import android.provider.MediaStore;
|
||||
import android.util.Log;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.Button;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.bumptech.glide.Glide;
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy;
|
||||
import com.example.petstoremobile.R;
|
||||
import com.example.petstoremobile.api.PetApi;
|
||||
import com.example.petstoremobile.api.RetrofitClient;
|
||||
import com.example.petstoremobile.fragments.ListFragment;
|
||||
import com.example.petstoremobile.fragments.listfragments.detailfragments.PetDetailFragment;
|
||||
import com.example.petstoremobile.api.auth.TokenManager;
|
||||
import com.example.petstoremobile.databinding.FragmentPetProfileBinding;
|
||||
import com.example.petstoremobile.dtos.PetDTO;
|
||||
import com.example.petstoremobile.utils.FileUtils;
|
||||
import com.example.petstoremobile.utils.GlideUtils;
|
||||
import com.example.petstoremobile.utils.ImagePickerHelper;
|
||||
import com.example.petstoremobile.utils.Resource;
|
||||
import com.example.petstoremobile.viewmodels.PetViewModel;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.InputStream;
|
||||
import java.util.Locale;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Named;
|
||||
|
||||
import dagger.hilt.android.AndroidEntryPoint;
|
||||
import okhttp3.MediaType;
|
||||
import okhttp3.MultipartBody;
|
||||
import okhttp3.RequestBody;
|
||||
import retrofit2.Call;
|
||||
import retrofit2.Callback;
|
||||
import retrofit2.Response;
|
||||
|
||||
@AndroidEntryPoint
|
||||
public class PetProfileFragment extends Fragment {
|
||||
|
||||
private TextView tvPetName, tvPetSpecies, tvPetBreed, tvPetAge, tvPetPrice;
|
||||
private Button btnBack, btnEditPet, btnChangePhoto;
|
||||
private ImageView imgPet;
|
||||
private Uri photoUri;
|
||||
private int petId;
|
||||
private FragmentPetProfileBinding binding;
|
||||
private long petId;
|
||||
private boolean hasImage = false;
|
||||
|
||||
// launchers for camera and gallery
|
||||
private ActivityResultLauncher<Intent> galleryLauncher;
|
||||
private ActivityResultLauncher<Uri> cameraLauncher;
|
||||
private ActivityResultLauncher<String> permissionLauncher;
|
||||
@Inject @Named("baseUrl") String baseUrl;
|
||||
@Inject TokenManager tokenManager;
|
||||
|
||||
private PetViewModel viewModel;
|
||||
private ImagePickerHelper imagePickerHelper;
|
||||
|
||||
|
||||
/**
|
||||
* Initializes activity launchers for gallery, camera, and permissions.
|
||||
*/
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
viewModel = new ViewModelProvider(this).get(PetViewModel.class);
|
||||
|
||||
// Launcher to open gallery to select image
|
||||
galleryLauncher = registerForActivityResult(
|
||||
new ActivityResultContracts.StartActivityForResult(),
|
||||
result -> {
|
||||
if (result.getResultCode() == Activity.RESULT_OK && result.getData() != null) {
|
||||
Uri selectedImage = result.getData().getData();
|
||||
uploadPetImage(selectedImage);
|
||||
}
|
||||
}
|
||||
);
|
||||
imagePickerHelper = new ImagePickerHelper(this, "pet_photo.jpg", new ImagePickerHelper.ImagePickerListener() {
|
||||
@Override
|
||||
public void onImagePicked(Uri uri) {
|
||||
uploadPetImage(uri);
|
||||
}
|
||||
|
||||
// Launcher for camera to open and capture image
|
||||
cameraLauncher = registerForActivityResult(
|
||||
new ActivityResultContracts.TakePicture(),
|
||||
success -> {
|
||||
if (success) {
|
||||
uploadPetImage(photoUri);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// Launcher to request camera permission
|
||||
permissionLauncher = registerForActivityResult(
|
||||
new ActivityResultContracts.RequestPermission(),
|
||||
granted -> {
|
||||
if (granted) {
|
||||
launchCamera();
|
||||
} else {
|
||||
new AlertDialog.Builder(requireContext())
|
||||
.setTitle("Permission Required")
|
||||
.setMessage("Please grant camera permission to use this feature")
|
||||
.setPositiveButton("Open Settings", (dialog, which) -> {
|
||||
Intent intent = new Intent(android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
|
||||
intent.setData(Uri.fromParts("package", requireContext().getPackageName(), null));
|
||||
startActivity(intent);
|
||||
})
|
||||
.setNegativeButton("Cancel", null)
|
||||
.show();
|
||||
}
|
||||
}
|
||||
);
|
||||
@Override
|
||||
public void onImageRemoved() {
|
||||
deletePetImage();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Inflates the layout using view binding, initializes views, and sets up click listeners.
|
||||
*/
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container,
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
|
||||
Bundle savedInstanceState) {
|
||||
View view = inflater.inflate(R.layout.fragment_pet_profile, container, false);
|
||||
|
||||
// Initialize views
|
||||
tvPetName = view.findViewById(R.id.tvPetName);
|
||||
tvPetSpecies = view.findViewById(R.id.tvPetSpecies);
|
||||
tvPetBreed = view.findViewById(R.id.tvPetBreed);
|
||||
tvPetAge = view.findViewById(R.id.tvPetAge);
|
||||
tvPetPrice = view.findViewById(R.id.tvPetPrice);
|
||||
btnBack = view.findViewById(R.id.btnBack);
|
||||
btnEditPet = view.findViewById(R.id.btnEditPet);
|
||||
btnChangePhoto = view.findViewById(R.id.btnChangePhoto);
|
||||
imgPet = view.findViewById(R.id.imgPet);
|
||||
|
||||
binding = FragmentPetProfileBinding.inflate(inflater, container, false);
|
||||
|
||||
// Set pet details to display
|
||||
if (getArguments() != null) {
|
||||
petId = getArguments().getInt("petId");
|
||||
tvPetName.setText(getArguments().getString("petName"));
|
||||
tvPetSpecies.setText(getArguments().getString("petSpecies"));
|
||||
tvPetBreed.setText(getArguments().getString("petBreed"));
|
||||
tvPetAge.setText(String.format(Locale.getDefault(), "%d yr(s)", getArguments().getInt("petAge")));
|
||||
tvPetPrice.setText(String.format(Locale.getDefault(), "$%.2f", getArguments().getDouble("petPrice")));
|
||||
|
||||
// Load pet image from backend
|
||||
loadPetImage(petId);
|
||||
petId = getArguments().getLong("petId");
|
||||
loadPetData();
|
||||
loadPetImage((int) petId);
|
||||
}
|
||||
|
||||
//set button click listeners
|
||||
btnBack.setOnClickListener(v -> {
|
||||
//get the list fragment and pop the back stack to return to the previous view (PetFragment)
|
||||
ListFragment listFragment = (ListFragment) getParentFragment();
|
||||
if (listFragment != null) {
|
||||
listFragment.getChildFragmentManager().popBackStack();
|
||||
}
|
||||
binding.btnBack.setOnClickListener(v -> {
|
||||
NavHostFragment.findNavController(this).popBackStack();
|
||||
});
|
||||
|
||||
//Make the edit button go to the pet detail view
|
||||
btnEditPet.setOnClickListener(v -> {
|
||||
if (getArguments() == null) return;
|
||||
|
||||
PetDetailFragment detailFragment = new PetDetailFragment();
|
||||
//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);
|
||||
}
|
||||
binding.btnEditPet.setOnClickListener(v -> {
|
||||
Bundle args = new Bundle();
|
||||
args.putLong("petId", petId);
|
||||
NavHostFragment.findNavController(this).navigate(R.id.nav_pet_detail, args);
|
||||
});
|
||||
|
||||
//Make change photo button ask user to select a new photo
|
||||
btnChangePhoto.setOnClickListener(v -> {
|
||||
new AlertDialog.Builder(requireContext())
|
||||
.setTitle("Change Pet Photo")
|
||||
.setItems(new String[]{"Take Photo", "Choose from Gallery"}, (dialog, which) -> {
|
||||
if (which == 0) {
|
||||
// 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 {
|
||||
Intent intent = new Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI);
|
||||
galleryLauncher.launch(intent);
|
||||
}
|
||||
})
|
||||
.show();
|
||||
binding.btnChangePhoto.setOnClickListener(v -> {
|
||||
imagePickerHelper.showImagePickerDialog("Change Pet Photo", hasImage);
|
||||
});
|
||||
|
||||
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) {
|
||||
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)
|
||||
.load(imageUrl)
|
||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||
.skipMemoryCache(true)
|
||||
.placeholder(R.drawable.placeholder)
|
||||
.error(R.drawable.placeholder)
|
||||
.into(imgPet);
|
||||
GlideUtils.loadImageWithToken(requireContext(), binding.imgPet, imageUrl, token, R.drawable.placeholder, new GlideUtils.ImageLoadListener() {
|
||||
@Override
|
||||
public void onResourceReady() {
|
||||
hasImage = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadFailed() {
|
||||
hasImage = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Helper function to upload pet image to backend
|
||||
/**
|
||||
* Uploads a selected or captured image a pet photo through the ViewModel.
|
||||
*/
|
||||
private void uploadPetImage(Uri uri) {
|
||||
try {
|
||||
File file = getFileFromUri(uri);
|
||||
File file = FileUtils.getFileFromUri(requireContext(), uri);
|
||||
if (file == null) return;
|
||||
|
||||
// Create RequestBody for file upload
|
||||
RequestBody requestFile = RequestBody.create(file, MediaType.parse(requireContext().getContentResolver().getType(uri)));
|
||||
MultipartBody.Part body = MultipartBody.Part.createFormData("image", file.getName(), requestFile);
|
||||
|
||||
// Call the backend to upload the image
|
||||
PetApi petApi = RetrofitClient.getPetApi(requireContext());
|
||||
petApi.uploadPetImage((long) petId, body).enqueue(new Callback<Void>() {
|
||||
@Override
|
||||
public void onResponse(Call<Void> call, Response<Void> response) {
|
||||
if (response.isSuccessful()) {
|
||||
Toast.makeText(requireContext(), "Pet photo updated successfully", Toast.LENGTH_SHORT).show();
|
||||
// Reload image after successful upload
|
||||
loadPetImage(petId);
|
||||
// Use ViewModel to upload image
|
||||
viewModel.uploadPetImage(petId, body).observe(getViewLifecycleOwner(), resource -> {
|
||||
if (resource != null && resource.status != Resource.Status.LOADING) {
|
||||
if (resource.status == Resource.Status.SUCCESS) {
|
||||
Toast.makeText(getContext(), "Pet photo updated successfully", Toast.LENGTH_SHORT).show();
|
||||
loadPetImage((int) petId);
|
||||
} else {
|
||||
Toast.makeText(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) {
|
||||
Log.e("UPLOAD_PET_IMAGE", "Error: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// 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);
|
||||
/**
|
||||
* Sends a request to the ViewModel to remove the current pet photo.
|
||||
*/
|
||||
private void deletePetImage() {
|
||||
viewModel.deletePetImage(petId).observe(getViewLifecycleOwner(), resource -> {
|
||||
if (resource != null && resource.status != Resource.Status.LOADING) {
|
||||
if (resource.status == Resource.Status.SUCCESS) {
|
||||
Toast.makeText(getContext(), "Pet photo removed", Toast.LENGTH_SHORT).show();
|
||||
hasImage = false;
|
||||
binding.imgPet.setImageResource(R.drawable.placeholder);
|
||||
} else {
|
||||
Toast.makeText(getContext(), "Delete failed: " + resource.message, Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,9 @@ public class Message {
|
||||
private String content;
|
||||
private String timestamp;
|
||||
private Boolean isRead;
|
||||
private String attachmentUrl;
|
||||
private String attachmentName;
|
||||
private String attachmentType;
|
||||
|
||||
public Message() {}
|
||||
|
||||
@@ -33,4 +36,13 @@ public class Message {
|
||||
|
||||
public Boolean getIsRead() { return isRead; }
|
||||
public void setIsRead(Boolean isRead) { this.isRead = isRead; }
|
||||
|
||||
public String getAttachmentUrl() { return attachmentUrl; }
|
||||
public void setAttachmentUrl(String attachmentUrl) { this.attachmentUrl = attachmentUrl; }
|
||||
|
||||
public String getAttachmentName() { return attachmentName; }
|
||||
public void setAttachmentName(String attachmentName) { this.attachmentName = attachmentName; }
|
||||
|
||||
public String getAttachmentType() { return attachmentType; }
|
||||
public void setAttachmentType(String attachmentType) { this.attachmentType = attachmentType; }
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
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.BulkDeleteRequest;
|
||||
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, String query, String status, Long storeId, String date, Long employeeId) {
|
||||
return executeCall(adoptionApi.getAllAdoptions(page, size, query, status, storeId, date, employeeId));
|
||||
}
|
||||
|
||||
/**
|
||||
* 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));
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a request to the API to delete multiple adoption records.
|
||||
*/
|
||||
public LiveData<Resource<Void>> bulkDeleteAdoptions(BulkDeleteRequest request) {
|
||||
return executeCall(adoptionApi.bulkDeleteAdoptions(request));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
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.BulkDeleteRequest;
|
||||
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));
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a request to the API to delete multiple appointment records.
|
||||
*/
|
||||
public LiveData<Resource<Void>> bulkDeleteAppointments(BulkDeleteRequest request) {
|
||||
return executeCall(appointmentApi.bulkDeleteAppointments(request));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
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.AvatarUploadResponse;
|
||||
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<AvatarUploadResponse>> 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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
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;
|
||||
|
||||
/**
|
||||
* 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));
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches a paginated list of customers.
|
||||
*/
|
||||
public LiveData<Resource<PageResponse<CustomerDTO>>> getAllCustomers(int page, int size) {
|
||||
return executeCall(customerApi.getAllCustomers(page, size));
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
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.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, Long storeId, int page, int size, String sort) {
|
||||
return executeCall(inventoryApi.getAllInventory(page, size, query, 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(InventoryDTO request) {
|
||||
return executeCall(inventoryApi.createInventory(request));
|
||||
}
|
||||
|
||||
public LiveData<Resource<InventoryDTO>> updateInventory(Long id, InventoryDTO 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));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
package com.example.petstoremobile.repositories;
|
||||
|
||||
import androidx.lifecycle.LiveData;
|
||||
|
||||
import com.example.petstoremobile.api.PetApi;
|
||||
import com.example.petstoremobile.dtos.BulkDeleteRequest;
|
||||
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));
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a request to the API to delete multiple pet records.
|
||||
*/
|
||||
public LiveData<Resource<Void>> bulkDeletePets(BulkDeleteRequest request) {
|
||||
return executeCall(petApi.bulkDeletePets(request));
|
||||
}
|
||||
|
||||
/**
|
||||
* 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));
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package com.example.petstoremobile.repositories;
|
||||
|
||||
import androidx.lifecycle.LiveData;
|
||||
|
||||
import com.example.petstoremobile.api.ProductSupplierApi;
|
||||
import com.example.petstoremobile.dtos.BulkDeleteRequest;
|
||||
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));
|
||||
}
|
||||
|
||||
public LiveData<Resource<Void>> bulkDeleteProductSuppliers(BulkDeleteRequest request) {
|
||||
return executeCall(api.bulkDeleteProductSuppliers(request));
|
||||
}
|
||||
}
|
||||
@@ -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 purchaseOrderApi;
|
||||
|
||||
@Inject
|
||||
public PurchaseOrderRepository(PurchaseOrderApi purchaseOrderApi) {
|
||||
super("PurchaseOrderRepository");
|
||||
this.purchaseOrderApi = purchaseOrderApi;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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(purchaseOrderApi.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(purchaseOrderApi.getPurchaseOrderById(id));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package com.example.petstoremobile.repositories;
|
||||
|
||||
import androidx.lifecycle.LiveData;
|
||||
|
||||
import com.example.petstoremobile.api.ServiceApi;
|
||||
import com.example.petstoremobile.dtos.BulkDeleteRequest;
|
||||
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));
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a request to the API to delete multiple services.
|
||||
*/
|
||||
public LiveData<Resource<Void>> bulkDeleteServices(BulkDeleteRequest request) {
|
||||
return executeCall(serviceApi.bulkDeleteServices(request));
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package com.example.petstoremobile.repositories;
|
||||
|
||||
import androidx.lifecycle.LiveData;
|
||||
|
||||
import com.example.petstoremobile.api.SupplierApi;
|
||||
import com.example.petstoremobile.dtos.BulkDeleteRequest;
|
||||
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));
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a request to the API to delete multiple supplier records.
|
||||
*/
|
||||
public LiveData<Resource<Void>> bulkDeleteSuppliers(BulkDeleteRequest request) {
|
||||
return executeCall(supplierApi.bulkDeleteSuppliers(request));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package com.example.petstoremobile.repositories;
|
||||
|
||||
import androidx.lifecycle.LiveData;
|
||||
|
||||
import com.example.petstoremobile.api.UserApi;
|
||||
import com.example.petstoremobile.dtos.PageResponse;
|
||||
import com.example.petstoremobile.dtos.UserDTO;
|
||||
import com.example.petstoremobile.utils.Resource;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Singleton;
|
||||
|
||||
@Singleton
|
||||
public class UserRepository extends BaseRepository {
|
||||
private final UserApi userApi;
|
||||
|
||||
@Inject
|
||||
public UserRepository(UserApi userApi) {
|
||||
super("UserRepository");
|
||||
this.userApi = userApi;
|
||||
}
|
||||
|
||||
public LiveData<Resource<PageResponse<UserDTO>>> getUsers(String role, int page, int size) {
|
||||
return executeCall(userApi.getUsers(role, page, size));
|
||||
}
|
||||
}
|
||||
@@ -8,24 +8,30 @@ import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import com.example.petstoremobile.api.ChatApi;
|
||||
import com.example.petstoremobile.api.CustomerApi;
|
||||
import com.example.petstoremobile.api.RetrofitClient;
|
||||
import com.example.petstoremobile.api.auth.TokenManager;
|
||||
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.utils.NotificationHelper;
|
||||
import com.example.petstoremobile.utils.RetrofitUtils;
|
||||
import com.example.petstoremobile.websocket.StompChatManager;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Named;
|
||||
|
||||
import dagger.hilt.android.AndroidEntryPoint;
|
||||
import retrofit2.Call;
|
||||
import retrofit2.Callback;
|
||||
import retrofit2.Response;
|
||||
|
||||
// Service to receive notifications when a new conversation is created
|
||||
@AndroidEntryPoint
|
||||
public class ChatNotificationService extends Service {
|
||||
private static final String TAG = "ChatNotificationService";
|
||||
|
||||
@@ -37,6 +43,11 @@ public class ChatNotificationService extends Service {
|
||||
private final Map<Long, String> customerIdToName = new HashMap<>();
|
||||
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
|
||||
@Override
|
||||
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
|
||||
private void connectWebSocket() {
|
||||
//get the token and role from the shared preferences
|
||||
TokenManager tm = TokenManager.getInstance(this);
|
||||
String token = tm.getToken();
|
||||
String role = tm.getRole();
|
||||
currentUserId = tm.getUserId();
|
||||
String token = tokenManager.getToken();
|
||||
String role = tokenManager.getRole();
|
||||
currentUserId = tokenManager.getUserId();
|
||||
|
||||
if (token != null && stompChatManager == null) {
|
||||
//load customers to have names associated with customer ids
|
||||
CustomerApi customerApi = RetrofitClient.getCustomerApi(this);
|
||||
customerApi.getAllCustomers(0, 1000).enqueue(new Callback<PageResponse<CustomerDTO>>() {
|
||||
@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);
|
||||
customerApi.getAllCustomers(0, 1000).enqueue(RetrofitUtils.createSilentCallback(TAG, result -> {
|
||||
for (CustomerDTO customer : result.getContent()) {
|
||||
customerIdToName.put(customer.getCustomerId(), customer.getFullName());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(@NonNull Call<PageResponse<CustomerDTO>> call, @NonNull Throwable t) {
|
||||
Log.e(TAG, "Failed to load customers", t);
|
||||
loadConversationsAndStartStomp(token, role);
|
||||
}
|
||||
});
|
||||
loadConversationsAndStartStomp(token, role);
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
private void loadConversationsAndStartStomp(String token, String role) {
|
||||
// Fetch existing conversations
|
||||
ChatApi chatApi = RetrofitClient.getChatApi(this);
|
||||
chatApi.getAllConversations().enqueue(new Callback<List<ConversationDTO>>() {
|
||||
@Override
|
||||
public void onResponse(@NonNull Call<List<ConversationDTO>> call, @NonNull Response<List<ConversationDTO>> response) {
|
||||
if (response.isSuccessful() && response.body() != null) {
|
||||
for (ConversationDTO conversation : response.body()) {
|
||||
if (conversation.getId() != null) {
|
||||
knownConversationIds.add(conversation.getId());
|
||||
conversationToCustomerId.put(conversation.getId(), conversation.getCustomerId());
|
||||
// subscribe to existing conversations to get message notifications
|
||||
if (stompChatManager != null) {
|
||||
stompChatManager.subscribeToConversation(conversation.getId());
|
||||
}
|
||||
}
|
||||
chatApi.getAllConversations().enqueue(RetrofitUtils.createSilentCallback(TAG, result -> {
|
||||
for (ConversationDTO conversation : result) {
|
||||
if (conversation.getId() != null) {
|
||||
knownConversationIds.add(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);
|
||||
}
|
||||
|
||||
@Override
|
||||
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);
|
||||
}
|
||||
});
|
||||
Log.d(TAG, "Loaded " + knownConversationIds.size() + " existing conversations");
|
||||
startStomp(token, role);
|
||||
}));
|
||||
}
|
||||
|
||||
private void startStomp(String token, String role) {
|
||||
if (stompChatManager != null) return;
|
||||
|
||||
stompChatManager = new StompChatManager(token, role);
|
||||
stompChatManager = new StompChatManager(token, role, baseUrl);
|
||||
|
||||
// Listen for messages in existing conversations
|
||||
stompChatManager.setMessageListener(message -> {
|
||||
@@ -193,20 +177,9 @@ public class ChatNotificationService extends Service {
|
||||
|
||||
// Helper function to fetch customer name for a conversation
|
||||
private void fetchCustomerName(Long customerId) {
|
||||
CustomerApi customerApi = RetrofitClient.getCustomerApi(this);
|
||||
customerApi.getCustomerById(customerId).enqueue(new Callback<CustomerDTO>() {
|
||||
@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);
|
||||
}
|
||||
});
|
||||
customerApi.getCustomerById(customerId).enqueue(RetrofitUtils.createSilentCallback(TAG, result -> {
|
||||
customerIdToName.put(customerId, result.getFullName());
|
||||
}));
|
||||
}
|
||||
|
||||
//When the service is destroyed, disconnect from the websocket
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
package com.example.petstoremobile.utils;
|
||||
|
||||
import android.view.View;
|
||||
import android.widget.Button;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.lifecycle.LiveData;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* A helper class to handle the UI and logic for bulk deletion across different fragments.
|
||||
* Now supports String keys to accommodate both simple and composite keys.
|
||||
*/
|
||||
public class BulkDeleteHandler {
|
||||
|
||||
/**
|
||||
* Interface that adapters must implement to support bulk selection.
|
||||
*/
|
||||
public interface SelectableAdapter {
|
||||
List<String> getSelectedKeys();
|
||||
void clearSelection();
|
||||
}
|
||||
|
||||
/**
|
||||
* Functional interface for the API call execution.
|
||||
*/
|
||||
public interface BulkDeleteOperation {
|
||||
LiveData<Resource<Void>> execute(List<String> keys);
|
||||
}
|
||||
|
||||
private final Fragment fragment;
|
||||
private final View layoutBar;
|
||||
private final TextView tvCount;
|
||||
private final SelectableAdapter adapter;
|
||||
private final BulkDeleteOperation operation;
|
||||
private final Runnable onSuccess;
|
||||
private final String itemName;
|
||||
|
||||
public BulkDeleteHandler(Fragment fragment,
|
||||
View layoutBar,
|
||||
TextView tvCount,
|
||||
Button btnDelete,
|
||||
SelectableAdapter adapter,
|
||||
String itemName,
|
||||
BulkDeleteOperation operation,
|
||||
Runnable onSuccess) {
|
||||
this.fragment = fragment;
|
||||
this.layoutBar = layoutBar;
|
||||
this.tvCount = tvCount;
|
||||
this.adapter = adapter;
|
||||
this.operation = operation;
|
||||
this.onSuccess = onSuccess;
|
||||
this.itemName = itemName;
|
||||
|
||||
btnDelete.setOnClickListener(v -> confirmDelete());
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the UI when the selection count changes.
|
||||
*/
|
||||
public void onSelectionChanged(int selectedCount) {
|
||||
if (selectedCount > 0) {
|
||||
layoutBar.setVisibility(View.VISIBLE);
|
||||
tvCount.setText(selectedCount + " selected");
|
||||
} else {
|
||||
hideBar();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Hides the bulk delete bar and resets state.
|
||||
*/
|
||||
public void hideBar() {
|
||||
if (layoutBar != null) {
|
||||
layoutBar.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows the confirmation dialog.
|
||||
*/
|
||||
private void confirmDelete() {
|
||||
List<String> keys = adapter.getSelectedKeys();
|
||||
if (keys.isEmpty()) return;
|
||||
|
||||
DialogUtils.showBulkDeleteConfirmDialog(fragment.requireContext(), keys.size(), () -> performDelete(keys));
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes the deletion via the provided operation.
|
||||
*/
|
||||
private void performDelete(List<String> keys) {
|
||||
operation.execute(keys).observe(fragment.getViewLifecycleOwner(), resource -> {
|
||||
if (resource != null && resource.status != Resource.Status.LOADING) {
|
||||
if (resource.status == Resource.Status.SUCCESS) {
|
||||
adapter.clearSelection();
|
||||
hideBar();
|
||||
onSuccess.run();
|
||||
Toast.makeText(fragment.getContext(), keys.size() + " " + itemName + "(s) deleted", Toast.LENGTH_SHORT).show();
|
||||
} else {
|
||||
Toast.makeText(fragment.getContext(), "Delete failed: " + resource.message, Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
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 confirmation dialog with specific "Delete" and "Cancel" buttons.
|
||||
*/
|
||||
public static void showBulkDeleteConfirmDialog(Context context, int count, DialogCallback callback) {
|
||||
new AlertDialog.Builder(context)
|
||||
.setTitle("Delete " + count + " item(s)?")
|
||||
.setMessage("This cannot be undone.")
|
||||
.setPositiveButton("Delete", (dialog, which) -> callback.onConfirm())
|
||||
.setNegativeButton("Cancel", null)
|
||||
.show();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package com.example.petstoremobile.utils;
|
||||
|
||||
import com.prolificinteractive.materialcalendarview.CalendarDay;
|
||||
import com.prolificinteractive.materialcalendarview.DayViewDecorator;
|
||||
import com.prolificinteractive.materialcalendarview.DayViewFacade;
|
||||
import com.prolificinteractive.materialcalendarview.spans.DotSpan;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.HashSet;
|
||||
|
||||
public class EventDecorator implements DayViewDecorator {
|
||||
|
||||
private final int color;
|
||||
private final HashSet<CalendarDay> dates;
|
||||
|
||||
public EventDecorator(int color, Collection<CalendarDay> dates) {
|
||||
this.color = color;
|
||||
this.dates = new HashSet<>(dates);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldDecorate(CalendarDay day) {
|
||||
return dates.contains(day);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void decorate(DayViewFacade view) {
|
||||
view.addSpan(new DotSpan(8, color));
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
package com.example.petstoremobile.utils;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.widget.ImageView;
|
||||
import androidx.annotation.Nullable;
|
||||
import com.bumptech.glide.Glide;
|
||||
import com.bumptech.glide.load.DataSource;
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy;
|
||||
import com.bumptech.glide.load.engine.GlideException;
|
||||
import com.bumptech.glide.load.model.GlideUrl;
|
||||
import com.bumptech.glide.load.model.LazyHeaders;
|
||||
import com.bumptech.glide.request.RequestListener;
|
||||
import com.bumptech.glide.request.target.Target;
|
||||
import com.example.petstoremobile.R;
|
||||
|
||||
/**
|
||||
* Utility class for loading images using Glide with authentication tokens.
|
||||
*/
|
||||
public class GlideUtils {
|
||||
|
||||
/**
|
||||
* interface to check the status of the image load.
|
||||
*/
|
||||
public interface ImageLoadListener {
|
||||
void onResourceReady();
|
||||
void onLoadFailed();
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads an image from a URL into an ImageView with token.
|
||||
*/
|
||||
public static void loadImageWithToken(Context context, ImageView imageView, String url, String token, int placeholder) {
|
||||
loadImageWithToken(context, imageView, url, token, placeholder, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads an image from a URL into an ImageView with token and listener.
|
||||
*/
|
||||
public static void loadImageWithToken(Context context, ImageView imageView, String url, String token, int placeholder, ImageLoadListener listener) {
|
||||
if (url == null) {
|
||||
imageView.setImageResource(placeholder);
|
||||
if (listener != null) listener.onLoadFailed();
|
||||
return;
|
||||
}
|
||||
|
||||
Object loadTarget = url;
|
||||
if (token != null && url.startsWith("http")) {
|
||||
loadTarget = new GlideUrl(url, new LazyHeaders.Builder()
|
||||
.addHeader("Authorization", "Bearer " + token)
|
||||
.build());
|
||||
}
|
||||
|
||||
Glide.with(context)
|
||||
.load(loadTarget)
|
||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||
.skipMemoryCache(true)
|
||||
.placeholder(placeholder)
|
||||
.error(placeholder)
|
||||
.listener(new RequestListener<Drawable>() {
|
||||
@Override
|
||||
public boolean onLoadFailed(@Nullable GlideException e, Object model, Target<Drawable> target, boolean isFirstResource) {
|
||||
if (listener != null) listener.onLoadFailed();
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onResourceReady(Drawable resource, Object model, Target<Drawable> target, DataSource dataSource, boolean isFirstResource) {
|
||||
if (listener != null) listener.onResourceReady();
|
||||
return false;
|
||||
}
|
||||
})
|
||||
.into(imageView);
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads an image from a URL into an ImageView with token and applies circle cropping for image.
|
||||
*/
|
||||
public static void loadImageWithTokenCircle(Context context, ImageView imageView, String url, String token, int placeholder) {
|
||||
loadImageWithTokenCircle(context, imageView, url, token, placeholder, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads an image from a URL into an ImageView with token, circle cropping, and listener.
|
||||
*/
|
||||
public static void loadImageWithTokenCircle(Context context, ImageView imageView, String url, String token, int placeholder, ImageLoadListener listener) {
|
||||
if (url == null) {
|
||||
imageView.setImageResource(placeholder);
|
||||
if (listener != null) listener.onLoadFailed();
|
||||
return;
|
||||
}
|
||||
|
||||
Object loadTarget = url;
|
||||
if (token != null && url.startsWith("http")) {
|
||||
loadTarget = new GlideUrl(url, new LazyHeaders.Builder()
|
||||
.addHeader("Authorization", "Bearer " + token)
|
||||
.build());
|
||||
}
|
||||
|
||||
Glide.with(context)
|
||||
.load(loadTarget)
|
||||
.circleCrop()
|
||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||
.skipMemoryCache(true)
|
||||
.placeholder(placeholder)
|
||||
.error(placeholder)
|
||||
.listener(new RequestListener<Drawable>() {
|
||||
@Override
|
||||
public boolean onLoadFailed(@Nullable GlideException e, Object model, Target<Drawable> target, boolean isFirstResource) {
|
||||
if (listener != null) listener.onLoadFailed();
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onResourceReady(Drawable resource, Object model, Target<Drawable> target, DataSource dataSource, boolean isFirstResource) {
|
||||
if (listener != null) listener.onResourceReady();
|
||||
return false;
|
||||
}
|
||||
})
|
||||
.into(imageView);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
package com.example.petstoremobile.utils;
|
||||
|
||||
import android.Manifest;
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.net.Uri;
|
||||
import android.provider.MediaStore;
|
||||
import androidx.activity.result.ActivityResultLauncher;
|
||||
import androidx.activity.result.contract.ActivityResultContracts;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.core.content.FileProvider;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Helper class to handle image picking from camera or gallery.
|
||||
*/
|
||||
public class ImagePickerHelper {
|
||||
|
||||
/**
|
||||
* Listener interface to handle the results of image picking.
|
||||
*/
|
||||
public interface ImagePickerListener {
|
||||
/**
|
||||
* Called when an image has been successfully selected or captured.
|
||||
*/
|
||||
void onImagePicked(Uri uri);
|
||||
/**
|
||||
* Called when the user chooses to remove the existing image.
|
||||
*/
|
||||
void onImageRemoved();
|
||||
}
|
||||
|
||||
private final Fragment fragment;
|
||||
private final ImagePickerListener listener;
|
||||
private final ActivityResultLauncher<Intent> galleryLauncher;
|
||||
private final ActivityResultLauncher<Uri> cameraLauncher;
|
||||
private final ActivityResultLauncher<String> permissionLauncher;
|
||||
private Uri photoUri;
|
||||
private final String tempFileName;
|
||||
|
||||
/**
|
||||
* Constructor for ImagePickerHelper.
|
||||
* Registers activity launchers for gallery, camera, and permissions.
|
||||
*/
|
||||
public ImagePickerHelper(Fragment fragment, String tempFileName, ImagePickerListener listener) {
|
||||
this.fragment = fragment;
|
||||
this.tempFileName = tempFileName;
|
||||
this.listener = listener;
|
||||
|
||||
// Launcher to open gallery to select image
|
||||
galleryLauncher = fragment.registerForActivityResult(
|
||||
new ActivityResultContracts.StartActivityForResult(),
|
||||
result -> {
|
||||
if (result.getResultCode() == Activity.RESULT_OK && result.getData() != null) {
|
||||
Uri selectedImage = result.getData().getData();
|
||||
if (selectedImage != null) {
|
||||
listener.onImagePicked(selectedImage);
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// Launcher for camera to open and capture image
|
||||
cameraLauncher = fragment.registerForActivityResult(
|
||||
new ActivityResultContracts.TakePicture(),
|
||||
success -> {
|
||||
if (success && photoUri != null) {
|
||||
listener.onImagePicked(photoUri);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// Launcher to request camera permission
|
||||
permissionLauncher = fragment.registerForActivityResult(
|
||||
new ActivityResultContracts.RequestPermission(),
|
||||
granted -> {
|
||||
if (granted) {
|
||||
launchCamera();
|
||||
} else {
|
||||
showPermissionDeniedDialog();
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows a dialog to choose between camera, gallery, and optionally remove photo.
|
||||
*/
|
||||
public void showImagePickerDialog(String title, boolean hasImage) {
|
||||
List<String> options = new ArrayList<>();
|
||||
options.add("Take Photo");
|
||||
options.add("Choose from Gallery");
|
||||
if (hasImage) {
|
||||
options.add("Remove Photo");
|
||||
}
|
||||
|
||||
new AlertDialog.Builder(fragment.requireContext())
|
||||
.setTitle(title)
|
||||
.setItems(options.toArray(new String[0]), (dialog, which) -> {
|
||||
String selected = options.get(which);
|
||||
if (selected.equals("Take Photo")) {
|
||||
checkCameraPermission();
|
||||
} else if (selected.equals("Choose from Gallery")) {
|
||||
launchGallery();
|
||||
} else if (selected.equals("Remove Photo")) {
|
||||
listener.onImageRemoved();
|
||||
}
|
||||
})
|
||||
.show();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if camera permission is granted and launches camera or requests permission.
|
||||
*/
|
||||
private void checkCameraPermission() {
|
||||
if (ContextCompat.checkSelfPermission(fragment.requireContext(), Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED) {
|
||||
launchCamera();
|
||||
} else {
|
||||
permissionLauncher.launch(Manifest.permission.CAMERA);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares a temporary file and launches the camera app.
|
||||
*/
|
||||
private void launchCamera() {
|
||||
File photoFile = new File(fragment.requireContext().getCacheDir(), tempFileName);
|
||||
photoUri = FileProvider.getUriForFile(fragment.requireContext(), fragment.requireContext().getPackageName() + ".fileprovider", photoFile);
|
||||
cameraLauncher.launch(photoUri);
|
||||
}
|
||||
|
||||
/**
|
||||
* Launches the gallery app to select an existing image.
|
||||
*/
|
||||
private void launchGallery() {
|
||||
Intent intent = new Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI);
|
||||
galleryLauncher.launch(intent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows a dialog explaining why camera permission is needed when denied.
|
||||
*/
|
||||
private void showPermissionDeniedDialog() {
|
||||
new AlertDialog.Builder(fragment.requireContext())
|
||||
.setTitle("Permission Required")
|
||||
.setMessage("Please grant camera permission to use this feature")
|
||||
.setPositiveButton("Open Settings", (dialog, which) -> {
|
||||
Intent intent = new Intent(android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
|
||||
intent.setData(Uri.fromParts("package", fragment.requireContext().getPackageName(), null));
|
||||
fragment.startActivity(intent);
|
||||
})
|
||||
.setNegativeButton("Cancel", null)
|
||||
.show();
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,9 @@
|
||||
package com.example.petstoremobile.utils;
|
||||
|
||||
import android.view.View;
|
||||
import android.widget.EditText;
|
||||
import android.widget.Spinner;
|
||||
import android.widget.TextView;
|
||||
|
||||
public class InputValidator {
|
||||
|
||||
@@ -94,4 +97,21 @@ public class InputValidator {
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user