Azure deployment setup #297

Closed
RecentRunner wants to merge 429 commits from azure-deploy into main
947 changed files with 66242 additions and 10232 deletions

73
.github/workflows/deploy.yml vendored Normal file
View File

@@ -0,0 +1,73 @@
name: Build and Deploy
on:
push:
branches: [main, azure-deploy]
env:
REGISTRY: ghcr.io
jobs:
build-and-deploy:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set image names (lowercase)
run: |
OWNER=$(echo "${{ github.repository_owner }}" | tr '[:upper:]' '[:lower:]')
echo "BACKEND_IMAGE=ghcr.io/${OWNER}/petshop-backend" >> $GITHUB_ENV
echo "FRONTEND_IMAGE=ghcr.io/${OWNER}/petshop-web" >> $GITHUB_ENV
- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push backend image
uses: docker/build-push-action@v5
with:
context: ./backend
push: true
tags: ${{ env.BACKEND_IMAGE }}:latest
- name: Build and push frontend image
uses: docker/build-push-action@v5
with:
context: ./web
push: true
tags: ${{ env.FRONTEND_IMAGE }}:latest
build-args: |
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=${{ secrets.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY }}
- name: Log in to Azure
uses: azure/login@v1
with:
creds: ${{ secrets.AZURE_CREDENTIALS }}
- name: Deploy backend
run: |
az containerapp update \
--name ${{ secrets.AZURE_BACKEND_APP_NAME }} \
--resource-group ${{ secrets.AZURE_RESOURCE_GROUP }} \
--image ${{ env.BACKEND_IMAGE }}:latest \
--registry-server ${{ env.REGISTRY }} \
--registry-username ${{ github.actor }} \
--registry-password ${{ secrets.GITHUB_TOKEN }}
- name: Deploy frontend
run: |
az containerapp update \
--name ${{ secrets.AZURE_FRONTEND_APP_NAME }} \
--resource-group ${{ secrets.AZURE_RESOURCE_GROUP }} \
--image ${{ env.FRONTEND_IMAGE }}:latest \
--registry-server ${{ env.REGISTRY }} \
--registry-username ${{ github.actor }} \
--registry-password ${{ secrets.GITHUB_TOKEN }}

5
.gitignore vendored Normal file
View File

@@ -0,0 +1,5 @@
*.zip
.local/
commit-patches/
temp_photos/
.env

10
.idea/.gitignore generated vendored Normal file
View File

@@ -0,0 +1,10 @@
# Default ignored files
/shelf/
/workspace.xml
# Ignored default folder with query files
/queries/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml
# Editor-based HTTP Client requests
/httpRequests/

1570
.idea/caches/deviceStreaming.xml generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="JAVA_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

8
.idea/markdown.xml generated Normal file
View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="MarkdownSettings">
<option name="previewPanelProviderInfo">
<ProviderInfo name="Compose (experimental)" className="com.intellij.markdown.compose.preview.ComposePanelProvider" />
</option>
</component>
</project>

5
.idea/misc.xml generated Normal file
View File

@@ -0,0 +1,5 @@
<project version="4">
<component name="ProjectRootManager" version="2">
<output url="file://$PROJECT_DIR$/out" />
</component>
</project>

8
.idea/modules.xml generated Normal file
View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/group-2-threaded-project-petshop.iml" filepath="$PROJECT_DIR$/.idea/group-2-threaded-project-petshop.iml" />
</modules>
</component>
</project>

6
.idea/vcs.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>

3
android/.gitignore vendored
View File

@@ -1,4 +1,5 @@
*.iml
nohup.out
.gradle
/local.properties
/.idea/*
@@ -16,6 +17,8 @@
/app/src/androidTest/
/app/src/test/
.DS_Store
/.project
/.settings/
/build
/captures
.externalNativeBuild

View File

@@ -1,5 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GradleMigrationSettings" migrationVersion="1" />
<component name="GradleSettings">
<option name="linkedExternalProjectsSettings">
<GradleProjectSettings>

View File

@@ -1,4 +1,3 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="jbr-21" project-jdk-type="JavaSDK">

6
android/.idea/vcs.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$/.." vcs="Git" />
</component>
</project>

View File

@@ -1,3 +1,7 @@
/build
/nohup.out
/.classpath
/.project
/.settings/
/src/test/
/src/androidTest/

View File

@@ -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,31 +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.9.1")
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")
// Testing
testImplementation(libs.junit)
androidTestImplementation(libs.ext.junit)
androidTestImplementation(libs.espresso.core)
}
}

View File

@@ -8,6 +8,7 @@
android:name="android.permission.READ_EXTERNAL_STORAGE"
android:maxSdkVersion="32" />
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-feature
android:name="android.hardware.camera"
@@ -24,10 +25,18 @@
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.PetStoreMobile">
<service
android:name=".services.ChatNotificationService"
android:exported="false" />
<activity
android:name=".activities.HomeActivity"
android:windowSoftInputMode="adjustResize"
android:exported="false" />
<activity
android:name=".activities.ForgotPasswordActivity"
android:exported="false" />
<activity
android:name=".activities.MainActivity"
android:exported="true">

Binary file not shown.

After

Width:  |  Height:  |  Size: 109 KiB

View File

@@ -1,13 +1,12 @@
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() {
super.onCreate();
// Clear login data on app so when the application closes, the user is logged out and have to re-login
TokenManager.getInstance(this).clearLoginData();
}
}

View File

@@ -0,0 +1,84 @@
package com.example.petstoremobile.activities;
import android.os.Bundle;
import android.view.View;
import android.widget.Toast;
import androidx.activity.EdgeToEdge;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.graphics.Insets;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowInsetsCompat;
import com.example.petstoremobile.api.auth.AuthApi;
import com.example.petstoremobile.databinding.ActivityForgotPasswordBinding;
import com.example.petstoremobile.utils.InputValidator;
import java.util.HashMap;
import java.util.Map;
import javax.inject.Inject;
import dagger.hilt.android.AndroidEntryPoint;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
@AndroidEntryPoint
public class ForgotPasswordActivity extends AppCompatActivity {
@Inject
AuthApi authApi;
private ActivityForgotPasswordBinding binding;
@Override
protected void onCreate(Bundle savedInstanceState) {
EdgeToEdge.enable(this);
super.onCreate(savedInstanceState);
binding = ActivityForgotPasswordBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
ViewCompat.setOnApplyWindowInsetsListener(binding.forgotPasswordRoot, (v, insets) -> {
Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom);
return insets;
});
binding.btnSubmit.setOnClickListener(v -> {
if (!InputValidator.isValidEmail(binding.etEmail)) return;
String email = binding.etEmail.getText().toString().trim();
sendResetLink(email);
});
binding.btnBackToLogin.setOnClickListener(v -> finish());
}
private void sendResetLink(String email) {
binding.btnSubmit.setEnabled(false);
Map<String, String> body = new HashMap<>();
body.put("usernameOrEmail", email);
authApi.forgotPassword(body).enqueue(new Callback<Void>() {
@Override
public void onResponse(Call<Void> call, Response<Void> response) {
if (binding == null) return;
binding.btnSubmit.setEnabled(true);
Toast.makeText(ForgotPasswordActivity.this,
"If this email is registered, a reset link will be sent.",
Toast.LENGTH_LONG).show();
finish();
}
@Override
public void onFailure(Call<Void> call, Throwable t) {
if (binding == null) return;
binding.btnSubmit.setEnabled(true);
Toast.makeText(ForgotPasswordActivity.this,
"Could not send reset link. Please try again.",
Toast.LENGTH_LONG).show();
}
});
}
}

View File

@@ -1,69 +1,115 @@
package com.example.petstoremobile.activities;
import android.Manifest;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.os.Build;
import android.os.Bundle;
import android.util.Log;
import androidx.activity.EdgeToEdge;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.content.ContextCompat;
import androidx.core.graphics.Insets;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowCompat;
import androidx.core.view.WindowInsetsCompat;
import androidx.core.view.WindowInsetsControllerCompat;
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.google.android.material.bottomnavigation.BottomNavigationView;
import com.example.petstoremobile.databinding.ActivityHomeBinding;
import com.example.petstoremobile.services.ChatNotificationService;
import dagger.hilt.android.AndroidEntryPoint;
@AndroidEntryPoint
public class HomeActivity extends AppCompatActivity {
private ActivityHomeBinding binding;
private NavController navController;
// Launcher to ask for notification permission
private final ActivityResultLauncher<String> requestPermissionLauncher =
registerForActivityResult(new ActivityResultContracts.RequestPermission(), isGranted -> {
if (!isGranted) {
Log.w("HomeActivity", "Notification permission denied");
}
});
/**
* Sets up the home screen, initializes bottom navigation, and handles incoming navigation intents.
*/
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
EdgeToEdge.enable(this);
setContentView(R.layout.activity_home);
super.onCreate(savedInstanceState);
binding = ActivityHomeBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> {
ViewCompat.setOnApplyWindowInsetsListener(binding.main, (v, insets) -> {
Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom);
return insets;
});
//get the bottom navbar from the layout
BottomNavigationView bottomNav = findViewById(R.id.bottom_navigation);
// Load ListFragment by default only if this is a fresh start
if (savedInstanceState == null) {
loadFragment(new ListFragment());
bottomNav.setSelectedItemId(R.id.nav_list);
// Initialize Navigation Component
NavHostFragment navHostFragment = (NavHostFragment) getSupportFragmentManager()
.findFragmentById(R.id.nav_host_fragment);
if (navHostFragment != null) {
navController = navHostFragment.getNavController();
NavigationUI.setupWithNavController(binding.bottomNavigation, navController);
}
//when an item in the bar is selected, load the corresponding fragment
bottomNav.setOnItemSelectedListener(item -> {
//load the list fragment by default if it's a fresh start
if (savedInstanceState == null) {
handleIntent(getIntent());
}
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;
// Start the notification service and request for notification permission
startNotificationService();
requestNotificationPermission();
}
/**
* 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);
}
/**
* 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"))) {
if (binding.bottomNavigation != null) {
// Navigate by selecting the bottom nav item.
binding.bottomNavigation.setSelectedItemId(R.id.nav_chat);
}
return false;
});
}
}
//helper function to load a fragment
private void loadFragment(Fragment fragment) {
getSupportFragmentManager()
.beginTransaction()
.replace(R.id.fragment_container, fragment)
.commit();
/**
* Starts the background service responsible for monitoring chat notifications.
*/
private void startNotificationService() {
Intent serviceIntent = new Intent(this, ChatNotificationService.class);
startService(serviceIntent);
}
}
/**
* 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) {
requestPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS);
}
}
}
}

View File

@@ -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,120 +10,141 @@ 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.databinding.ActivityMainBinding;
import com.example.petstoremobile.viewmodels.AuthViewModel;
import com.example.petstoremobile.utils.Resource;
import com.example.petstoremobile.utils.UIUtils;
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
if (TokenManager.getInstance(this).isLoggedIn()) {
Intent intent = new Intent(this, HomeActivity.class);
startActivity(intent);
finish();
return;
if (tokenManager.isLoggedIn()) {
if ("CUSTOMER".equalsIgnoreCase(tokenManager.getRole())) {
// If a customer somehow remained logged in, clear them out
tokenManager.clearLoginData();
} else {
startActivity(new Intent(this, HomeActivity.class));
finish();
return;
}
}
EdgeToEdge.enable(this);
setContentView(R.layout.activity_main);
binding = ActivityMainBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
viewModel = new ViewModelProvider(this).get(AuthViewModel.class);
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> {
ViewCompat.setOnApplyWindowInsetsListener(binding.main, (v, insets) -> {
Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
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) {
//save login data in shared preferences
TokenManager.getInstance(MainActivity.this).saveLoginData(
response.body().getToken(),
response.body().getUsername(),
response.body().getRole()
);
//fetch user id from api then login to home activity
RetrofitClient.getAuthApi(MainActivity.this).getCurrentUser()
.enqueue(new Callback<AuthDTO.UserResponse>() {
@Override
public void onResponse(Call<AuthDTO.UserResponse> call,
Response<AuthDTO.UserResponse> 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<AuthDTO.UserResponse> 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 {
Toast.makeText(MainActivity.this, "Login failed", Toast.LENGTH_SHORT).show();
tvLoginStatus.setText("Login failed");
}
}
@Override
public void onFailure(Call<AuthDTO.LoginResponse> call, Throwable t) {
Toast.makeText(MainActivity.this, "Login failed", Toast.LENGTH_SHORT).show();
tvLoginStatus.setText("Login failed");
}
});
// Set click listener for forgot password link
binding.tvForgotPassword.setOnClickListener(v -> {
startActivity(new Intent(this, ForgotPasswordActivity.class));
});
}
}
/**
* 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;
switch (resource.status) {
case LOADING:
UIUtils.setViewsEnabled(false, binding.btnLogin);
binding.tvLoginStatus.setText("Logging in...");
break;
case SUCCESS:
if (resource.data != null) {
String role = resource.data.getRole();
if ("CUSTOMER".equalsIgnoreCase(role)) {
UIUtils.setViewsEnabled(true, binding.btnLogin);
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();
}
}
break;
case ERROR:
UIUtils.setViewsEnabled(true, binding.btnLogin);
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());
tokenManager.savePrimaryStoreId(resource.data.getStoreId());
}
Toast.makeText(this, "Login successful", Toast.LENGTH_SHORT).show();
startActivity(new Intent(this, HomeActivity.class));
finish();
}
});
}
}

View File

@@ -0,0 +1,58 @@
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.dtos.ActivityLogDTO;
import com.example.petstoremobile.utils.DateTimeUtils;
import java.util.List;
public class ActivityLogAdapter extends RecyclerView.Adapter<ActivityLogAdapter.ViewHolder> {
private final List<ActivityLogDTO> items;
public ActivityLogAdapter(List<ActivityLogDTO> items) {
this.items = items;
}
@NonNull
@Override
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.item_activity_log, parent, false);
return new ViewHolder(view);
}
@Override
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
ActivityLogDTO log = items.get(position);
holder.tvActivity.setText(log.getActivity());
holder.tvUser.setText(log.getFullName() + " (" + log.getUsername() + ")");
holder.tvMeta.setText(log.getStoreName() + " · " + log.getRole());
String timestamp = log.getLogTimestamp();
String date = DateTimeUtils.extractDate(timestamp);
String time = (timestamp != null && timestamp.length() >= 16) ? timestamp.substring(11, 16) : null;
holder.tvTimestamp.setText(date != null && time != null ? date + " " + time : date);
}
@Override
public int getItemCount() { return items.size(); }
public static class ViewHolder extends RecyclerView.ViewHolder {
TextView tvActivity, tvUser, tvMeta, tvTimestamp;
public ViewHolder(@NonNull View itemView) {
super(itemView);
tvActivity = itemView.findViewById(R.id.tvLogActivity);
tvUser = itemView.findViewById(R.id.tvLogUser);
tvMeta = itemView.findViewById(R.id.tvLogMeta);
tvTimestamp = itemView.findViewById(R.id.tvLogTimestamp);
}
}
}

View File

@@ -1,79 +1,128 @@
package com.example.petstoremobile.adapters;
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.models.Adoption;
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<Adoption> adoptionList;
private OnAdoptionClickListener adoptionClickListener;
private List<AdoptionDTO> adoptionList;
private OnAdoptionClickListener listener;
private final SelectionHelper selectionHelper;
// Interface for adoption click on recycler view
public interface OnAdoptionClickListener {
void onAdoptionClick(int position);
void onSelectionChanged(int count);
}
// Constructor
public AdoptionAdapter(List<Adoption> adoptionList, OnAdoptionClickListener adoptionClickListener) {
public AdoptionAdapter(List<AdoptionDTO> adoptionList, OnAdoptionClickListener listener) {
this.adoptionList = adoptionList;
this.adoptionClickListener = adoptionClickListener;
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();
}
});
}
// Get the controls of each row in recycler view
public static class AdoptionViewHolder extends RecyclerView.ViewHolder {
TextView tvAdopterName, tvPetName, tvAdoptionDate, tvAdoptionStatus;
@Override
public List<String> getSelectedKeys() {
return selectionHelper.getSelectedKeys();
}
public AdoptionViewHolder(@NonNull View v) {
super(v);
tvAdopterName = v.findViewById(R.id.tvAdopterName);
tvPetName = v.findViewById(R.id.tvAdoptionPetName);
tvAdoptionDate = v.findViewById(R.id.tvAdoptionDate);
tvAdoptionStatus = v.findViewById(R.id.tvAdoptionStatus);
@Override
public void clearSelection() {
selectionHelper.clearSelection();
}
public static class AdoptionViewHolder extends RecyclerView.ViewHolder {
final ItemAdoptionBinding binding;
public AdoptionViewHolder(@NonNull ItemAdoptionBinding binding) {
super(binding.getRoot());
this.binding = binding;
}
}
// Create a new row view
@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);
}
// Populate the row with adoption data
@Override
public void onBindViewHolder(@NonNull AdoptionViewHolder holder, int position) {
Adoption adoption = adoptionList.get(position);
AdoptionDTO a = adoptionList.get(position);
ItemAdoptionBinding binding = holder.binding;
holder.tvAdopterName.setText(adoption.getAdopterName());
holder.tvPetName.setText("Pet: " + adoption.getPetName());
holder.tvAdoptionDate.setText("Date: " + adoption.getAdoptionDate());
holder.tvAdoptionStatus.setText(adoption.getStatus());
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() : "");
// Set the status color depending on adoption status
if (adoption.getStatus().equals("Approved")) {
holder.tvAdoptionStatus.setBackgroundColor(Color.parseColor("#4CAF50"));
} else if (adoption.getStatus().equals("Pending")) {
holder.tvAdoptionStatus.setBackgroundColor(Color.parseColor("#FF9800"));
} else {
holder.tvAdoptionStatus.setBackgroundColor(Color.parseColor("#F44336"));
String status = a.getAdoptionStatus() != null ? a.getAdoptionStatus() : "";
binding.tvAdoptionStatus.setText(status);
switch (status) {
case "Completed":
binding.tvAdoptionStatus.setBackgroundColor(Color.parseColor("#4CAF50"));
break;
case "Pending":
binding.tvAdoptionStatus.setBackgroundColor(Color.parseColor("#FF9800"));
break;
case "Cancelled":
binding.tvAdoptionStatus.setBackgroundColor(Color.parseColor("#F44336"));
break;
default:
binding.tvAdoptionStatus.setBackgroundColor(Color.parseColor("#9E9E9E"));
break;
}
// When a row is clicked, open the detail view
holder.itemView.setOnClickListener(v -> adoptionClickListener.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();
}
public int getItemCount() { return adoptionList.size(); }
}

View File

@@ -1,82 +1,127 @@
package com.example.petstoremobile.adapters;
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.models.Appointment;
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<Appointment> appointmentList;
private List<AppointmentDTO> appointmentList;
private OnAppointmentClickListener appointmentClickListener;
private final SelectionHelper selectionHelper;
// Interface for appointment click on recycler view
public interface OnAppointmentClickListener {
void onAppointmentClick(int position);
void onSelectionChanged(int count);
}
// Constructor
public AppointmentAdapter(List<Appointment> appointmentList, OnAppointmentClickListener appointmentClickListener) {
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();
}
});
}
// Get the controls of each row in recycler view
public static class AppointmentViewHolder extends RecyclerView.ViewHolder {
TextView tvCustomerName, tvPetName, tvServiceType, tvDateTime, tvAppointmentStatus;
@Override
public List<String> getSelectedKeys() {
return selectionHelper.getSelectedKeys();
}
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);
@Override
public void clearSelection() {
selectionHelper.clearSelection();
}
public static class AppointmentViewHolder extends RecyclerView.ViewHolder {
private final ItemAppointmentBinding binding;
public AppointmentViewHolder(@NonNull ItemAppointmentBinding binding) {
super(binding.getRoot());
this.binding = binding;
}
}
// Create a new row view
@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);
}
// Populate the row with appointment data
@Override
public void onBindViewHolder(@NonNull AppointmentViewHolder holder, int position) {
Appointment appointment = appointmentList.get(position);
AppointmentDTO a = appointmentList.get(position);
ItemAppointmentBinding binding = holder.binding;
holder.tvCustomerName.setText(appointment.getCustomerName());
holder.tvPetName.setText("Pet: " + appointment.getPetName());
holder.tvServiceType.setText(appointment.getServiceType());
holder.tvDateTime.setText(appointment.getAppointmentDate() + " at " + appointment.getAppointmentTime());
holder.tvAppointmentStatus.setText(appointment.getStatus());
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() : ""));
// Set the status color depending on appointment status
switch (appointment.getStatus()) {
case "Confirmed":
holder.tvAppointmentStatus.setBackgroundColor(Color.parseColor("#4CAF50"));
String status = a.getStatus() != null ? a.getStatus() : "";
binding.tvAppointmentStatus.setText(status);
switch (status.toUpperCase()) {
case "BOOKED":
binding.tvAppointmentStatus.setBackgroundColor(Color.parseColor("#2196F3")); // blue
break;
case "Pending":
holder.tvAppointmentStatus.setBackgroundColor(Color.parseColor("#FF9800"));
case "COMPLETED":
binding.tvAppointmentStatus.setBackgroundColor(Color.parseColor("#4CAF50")); // green
break;
case "CANCELLED":
binding.tvAppointmentStatus.setBackgroundColor(Color.parseColor("#F44336")); // red
break;
default:
holder.tvAppointmentStatus.setBackgroundColor(Color.parseColor("#F44336"));
binding.tvAppointmentStatus.setBackgroundColor(Color.parseColor("#9E9E9E")); // gray
break;
}
// When a row is clicked, open the detail view
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

View File

@@ -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;
}
}

View File

@@ -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;
}
}
}

View File

@@ -0,0 +1,138 @@
package com.example.petstoremobile.adapters;
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.core.content.ContextCompat;
import androidx.recyclerview.widget.RecyclerView;
import com.example.petstoremobile.R;
import com.example.petstoremobile.dtos.CouponDTO;
import com.example.petstoremobile.utils.DateTimeUtils;
import java.math.BigDecimal;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
public class CouponAdapter extends RecyclerView.Adapter<CouponAdapter.ViewHolder> {
private final List<CouponDTO> coupons;
private final OnCouponClickListener listener;
private boolean selectionMode = false;
private final Set<Long> selectedIds = new HashSet<>();
public interface OnCouponClickListener {
void onCouponClick(CouponDTO coupon);
void onSelectionChanged(int count);
}
public CouponAdapter(List<CouponDTO> coupons, OnCouponClickListener listener) {
this.coupons = coupons;
this.listener = listener;
}
@NonNull
@Override
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_coupon, parent, false);
return new ViewHolder(view);
}
@Override
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
CouponDTO coupon = coupons.get(position);
holder.tvCouponCode.setText(coupon.getCouponCode());
String discountText = "";
if ("PERCENT".equals(coupon.getDiscountType())) {
discountText = coupon.getDiscountValue().stripTrailingZeros().toPlainString() + "% OFF";
} else {
discountText = "$" + coupon.getDiscountValue().stripTrailingZeros().toPlainString() + " OFF";
}
holder.tvCouponDiscount.setText(discountText);
holder.tvCouponMinOrder.setText("Min order: $" + coupon.getMinOrderAmount().stripTrailingZeros().toPlainString());
if (coupon.getEndsAt() != null) {
holder.tvCouponExpiry.setText("Expires: " + DateTimeUtils.extractDate(coupon.getEndsAt()));
holder.tvCouponExpiry.setVisibility(View.VISIBLE);
} else {
holder.tvCouponExpiry.setVisibility(View.GONE);
}
if (Boolean.TRUE.equals(coupon.getActive())) {
holder.tvCouponStatus.setText("ACTIVE");
holder.tvCouponStatus.setBackgroundTintList(ContextCompat.getColorStateList(holder.itemView.getContext(), R.color.primary_dark));
} else {
holder.tvCouponStatus.setText("INACTIVE");
holder.tvCouponStatus.setBackgroundTintList(ContextCompat.getColorStateList(holder.itemView.getContext(), R.color.accent_coral));
}
holder.cbSelectCoupon.setVisibility(selectionMode ? View.VISIBLE : View.GONE);
holder.cbSelectCoupon.setChecked(selectedIds.contains(coupon.getCouponId()));
holder.itemView.setOnClickListener(v -> {
if (selectionMode) {
toggleSelection(coupon.getCouponId());
} else {
listener.onCouponClick(coupon);
}
});
holder.itemView.setOnLongClickListener(v -> {
if (!selectionMode) {
setSelectionMode(true);
toggleSelection(coupon.getCouponId());
return true;
}
return false;
});
holder.cbSelectCoupon.setOnClickListener(v -> toggleSelection(coupon.getCouponId()));
}
private void toggleSelection(Long id) {
if (selectedIds.contains(id)) {
selectedIds.remove(id);
} else {
selectedIds.add(id);
}
notifyDataSetChanged();
listener.onSelectionChanged(selectedIds.size());
}
public void setSelectionMode(boolean selectionMode) {
this.selectionMode = selectionMode;
if (!selectionMode) selectedIds.clear();
notifyDataSetChanged();
listener.onSelectionChanged(selectedIds.size());
}
public Set<Long> getSelectedIds() {
return selectedIds;
}
@Override
public int getItemCount() {
return coupons.size();
}
public static class ViewHolder extends RecyclerView.ViewHolder {
TextView tvCouponCode, tvCouponDiscount, tvCouponMinOrder, tvCouponExpiry, tvCouponStatus;
CheckBox cbSelectCoupon;
public ViewHolder(@NonNull View itemView) {
super(itemView);
tvCouponCode = itemView.findViewById(R.id.tvCouponCode);
tvCouponDiscount = itemView.findViewById(R.id.tvCouponDiscount);
tvCouponMinOrder = itemView.findViewById(R.id.tvCouponMinOrder);
tvCouponExpiry = itemView.findViewById(R.id.tvCouponExpiry);
tvCouponStatus = itemView.findViewById(R.id.tvCouponStatus);
cbSelectCoupon = itemView.findViewById(R.id.cbSelectCoupon);
}
}
}

View File

@@ -0,0 +1,80 @@
package com.example.petstoremobile.adapters;
import android.graphics.Color;
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.api.UserApi;
import com.example.petstoremobile.databinding.ItemCustomerBinding;
import com.example.petstoremobile.dtos.CustomerDTO;
import com.example.petstoremobile.utils.GlideUtils;
import java.util.List;
public class CustomerAdapter extends RecyclerView.Adapter<CustomerAdapter.CustomerViewHolder> {
private List<CustomerDTO> list;
private OnCustomerClickListener listener;
private String baseUrl;
private String token;
public interface OnCustomerClickListener {
void onCustomerClick(int position);
}
public CustomerAdapter(List<CustomerDTO> list, OnCustomerClickListener listener) {
this.list = list;
this.listener = listener;
}
public void setBaseUrl(String baseUrl) { this.baseUrl = baseUrl; }
public void setToken(String token) { this.token = token; }
public static class CustomerViewHolder extends RecyclerView.ViewHolder {
final ItemCustomerBinding binding;
public CustomerViewHolder(@NonNull ItemCustomerBinding binding) {
super(binding.getRoot());
this.binding = binding;
}
}
@NonNull
@Override
public CustomerViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
ItemCustomerBinding binding = ItemCustomerBinding.inflate(
LayoutInflater.from(parent.getContext()), parent, false);
return new CustomerViewHolder(binding);
}
@Override
public void onBindViewHolder(@NonNull CustomerViewHolder holder, int position) {
CustomerDTO c = list.get(position);
ItemCustomerBinding b = holder.binding;
b.tvCustomerFullName.setText(c.getFullName() != null ? c.getFullName() : "");
b.tvCustomerUsername.setText("@" + (c.getUsername() != null ? c.getUsername() : ""));
b.tvCustomerEmail.setText(c.getEmail() != null ? c.getEmail() : "");
int points = c.getLoyaltyPoints() != null ? c.getLoyaltyPoints() : 0;
b.tvCustomerLoyalty.setText(points + " pts");
boolean active = Boolean.TRUE.equals(c.getActive());
b.tvCustomerStatus.setText(active ? "Active" : "Inactive");
b.tvCustomerStatus.setTextColor(active ? Color.parseColor("#4CAF50") : Color.parseColor("#F44336"));
if (baseUrl != null && c.getCustomerId() != null) {
String imageUrl = baseUrl + String.format(UserApi.AVATAR_PATH, c.getCustomerId());
GlideUtils.loadImageWithTokenCircle(holder.itemView.getContext(), b.ivCustomerProfile, imageUrl, token, R.drawable.placeholder);
} else {
b.ivCustomerProfile.setImageResource(R.drawable.placeholder);
}
holder.itemView.setOnClickListener(v -> listener.onCustomerClick(position));
}
@Override
public int getItemCount() { return list.size(); }
}

View File

@@ -0,0 +1,95 @@
package com.example.petstoremobile.adapters;
import android.graphics.Color;
import android.view.*;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import com.example.petstoremobile.R;
import com.example.petstoremobile.api.UserApi;
import com.example.petstoremobile.databinding.ItemEmployeeBinding;
import com.example.petstoremobile.dtos.EmployeeDTO;
import com.example.petstoremobile.utils.GlideUtils;
import java.util.List;
public class EmployeeAdapter extends RecyclerView.Adapter<EmployeeAdapter.EmployeeViewHolder> {
private List<EmployeeDTO> list;
private OnEmployeeClickListener listener;
private String baseUrl;
private String token;
public interface OnEmployeeClickListener {
void onEmployeeClick(int position);
}
public EmployeeAdapter(List<EmployeeDTO> list, OnEmployeeClickListener listener) {
this.list = list;
this.listener = listener;
}
public void setBaseUrl(String baseUrl) {
this.baseUrl = baseUrl;
}
public void setToken(String token) {
this.token = token;
}
public static class EmployeeViewHolder extends RecyclerView.ViewHolder {
private final ItemEmployeeBinding binding;
public EmployeeViewHolder(@NonNull ItemEmployeeBinding binding) {
super(binding.getRoot());
this.binding = binding;
}
}
@NonNull
@Override
public EmployeeViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
ItemEmployeeBinding binding = ItemEmployeeBinding.inflate(
LayoutInflater.from(parent.getContext()), parent, false);
return new EmployeeViewHolder(binding);
}
@Override
public void onBindViewHolder(@NonNull EmployeeViewHolder holder, int position) {
EmployeeDTO e = list.get(position);
ItemEmployeeBinding binding = holder.binding;
binding.tvEmployeeFullName.setText(e.getFullName() != null ? e.getFullName() : "");
binding.tvEmployeeUsername.setText("@" + (e.getUsername() != null ? e.getUsername() : ""));
binding.tvEmployeeEmail.setText(e.getEmail() != null ? e.getEmail() : "");
// Role badge
String role = e.getRole() != null ? e.getRole() : "STAFF";
binding.tvEmployeeRole.setText(role);
if ("ADMIN".equalsIgnoreCase(role)) {
binding.tvEmployeeRole.setBackgroundColor(Color.parseColor("#1a759f"));
} else {
binding.tvEmployeeRole.setBackgroundColor(Color.parseColor("#577590"));
}
// Status text and color
boolean active = Boolean.TRUE.equals(e.getActive());
binding.tvEmployeeStatus.setText(active ? "Active" : "Inactive");
binding.tvEmployeeStatus.setTextColor(active ? Color.parseColor("#4CAF50") : Color.parseColor("#F44336"));
// Profile image
if (baseUrl != null && e.getId() != null) {
String imageUrl = baseUrl + String.format(UserApi.AVATAR_PATH, e.getId());
GlideUtils.loadImageWithTokenCircle(holder.itemView.getContext(), binding.ivEmployeeProfile, imageUrl, token, R.drawable.placeholder);
} else {
binding.ivEmployeeProfile.setImageResource(R.drawable.placeholder);
}
holder.itemView.setOnClickListener(v -> listener.onEmployeeClick(position));
}
@Override
public int getItemCount() { return list.size(); }
}

View File

@@ -1,75 +1,122 @@
package com.example.petstoremobile.adapters;
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.models.Inventory;
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.List;
public class InventoryAdapter extends RecyclerView.Adapter<InventoryAdapter.InventoryViewHolder> {
public class InventoryAdapter extends RecyclerView.Adapter<InventoryAdapter.InventoryViewHolder> implements BulkDeleteHandler.SelectableAdapter {
private List<Inventory> inventoryList;
private OnInventoryClickListener inventoryClickListener;
private final List<InventoryDTO> inventoryList;
private final OnInventoryClickListener clickListener;
private final SelectionHelper selectionHelper;
// Interface for inventory click on recycler view
public interface OnInventoryClickListener {
void onInventoryClick(int position);
void onSelectionChanged(int selectedCount);
}
// Constructor
public InventoryAdapter(List<Inventory> inventoryList, OnInventoryClickListener inventoryClickListener) {
public InventoryAdapter(List<InventoryDTO> inventoryList, OnInventoryClickListener clickListener) {
this.inventoryList = inventoryList;
this.inventoryClickListener = inventoryClickListener;
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
public static class InventoryViewHolder extends RecyclerView.ViewHolder {
TextView tvItemName, tvCategory, tvQuantity, tvUnitPrice, tvSupplier;
@Override
public List<String> getSelectedKeys() {
return selectionHelper.getSelectedKeys();
}
public InventoryViewHolder(@NonNull View v) {
super(v);
tvItemName = v.findViewById(R.id.tvItemName);
tvCategory = v.findViewById(R.id.tvCategory);
tvQuantity = v.findViewById(R.id.tvQuantity);
tvUnitPrice = v.findViewById(R.id.tvUnitPrice);
tvSupplier = v.findViewById(R.id.tvInvSupplier);
@Override
public void clearSelection() {
selectionHelper.clearSelection();
}
public static class InventoryViewHolder extends RecyclerView.ViewHolder {
final ItemInventoryBinding binding;
public InventoryViewHolder(@NonNull ItemInventoryBinding binding) {
super(binding.getRoot());
this.binding = binding;
}
}
// Create a new row view
@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);
}
// Populate the row with inventory data
@Override
public void onBindViewHolder(@NonNull InventoryViewHolder holder, int position) {
Inventory inventory = inventoryList.get(position);
InventoryDTO inv = inventoryList.get(position);
ItemInventoryBinding binding = holder.binding;
holder.tvItemName.setText(inventory.getItemName());
holder.tvCategory.setText(inventory.getCategory());
holder.tvQuantity.setText("Qty: " + inventory.getQuantity());
holder.tvUnitPrice.setText("$" + String.format("%.2f", inventory.getUnitPrice()));
holder.tvSupplier.setText("Supplier: " + inventory.getSupplier());
// Column: Product Name
binding.tvProductName.setText(inv.getProductName() != null ? inv.getProductName() : "");
// Highlight low stock items in red
if (inventory.getQuantity() <= 5) {
holder.tvQuantity.setTextColor(Color.parseColor("#F44336"));
// Column: Store Name
binding.tvInventoryStore.setText("Store: " + (inv.getStoreName() != null ? inv.getStoreName() : ""));
// Column: Quantity
int qty = inv.getQuantity() != null ? inv.getQuantity() : 0;
binding.tvQuantity.setText("Stock: " + qty);
// Low stock = red, normal = green (like desktop reorder concept)
if (qty <= 5) {
binding.tvQuantity.setTextColor(Color.parseColor("#F44336"));
} else {
holder.tvQuantity.setTextColor(Color.parseColor("#4CAF50"));
binding.tvQuantity.setTextColor(Color.parseColor("#4CAF50"));
}
// When a row is clicked, open the detail view
holder.itemView.setOnClickListener(v -> inventoryClickListener.onInventoryClick(position));
String key = String.valueOf(inv.getInventoryId());
// Bulk delete selection mode
if (selectionHelper.isInSelectionMode()) {
binding.cbSelectInventory.setVisibility(View.VISIBLE);
binding.cbSelectInventory.setChecked(selectionHelper.isSelected(key));
} else {
binding.cbSelectInventory.setVisibility(View.GONE);
binding.cbSelectInventory.setChecked(false);
}
holder.itemView.setOnClickListener(v -> {
if (selectionHelper.isInSelectionMode()) {
selectionHelper.toggleSelection(key);
notifyItemChanged(position);
} else {
clickListener.onInventoryClick(holder.getAdapterPosition());
}
});
holder.itemView.setOnLongClickListener(v -> {
if (!selectionHelper.isInSelectionMode()) {
selectionHelper.startSelection(key);
}
return true;
});
}
@Override

View File

@@ -3,10 +3,18 @@ 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.bumptech.glide.signature.ObjectKey;
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;
@@ -15,12 +23,26 @@ public class MessageAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder
private static final int TYPE_SENT = 1;
private static final int TYPE_RECEIVED = 2;
public interface OnAttachmentClickListener {
void onAttachmentClick(Message message);
}
private final List<Message> messages;
private Long currentUserId;
private String token;
private String baseUrl;
private OnAttachmentClickListener attachmentClickListener;
public MessageAdapter(List<Message> messages, Long currentUserId) {
this.messages = messages;
this.currentUserId = currentUserId;
setHasStableIds(true);
}
@Override
public long getItemId(int position) {
Message m = messages.get(position);
return m.getId() != null ? m.getId() : position;
}
public void setCurrentUserId(Long id) {
@@ -28,6 +50,18 @@ public class MessageAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder
notifyDataSetChanged();
}
public void setToken(String token) {
this.token = token;
}
public void setBaseUrl(String baseUrl) {
this.baseUrl = baseUrl;
}
public void setOnAttachmentClickListener(OnAttachmentClickListener listener) {
this.attachmentClickListener = listener;
}
@Override
public int getItemViewType(int position) {
Message m = messages.get(position);
@@ -41,38 +75,127 @@ 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, baseUrl, attachmentClickListener);
if (holder instanceof ReceivedHolder) ((ReceivedHolder) holder).bind(m, token, baseUrl, attachmentClickListener);
}
@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, String baseUrl, OnAttachmentClickListener listener) {
// Check for Text
if (m.getContent() != null && !m.getContent().isEmpty()) {
binding.tvMessageContent.setVisibility(View.VISIBLE);
binding.tvMessageContent.setText(m.getContent());
} else {
binding.tvMessageContent.setVisibility(View.GONE);
}
// Check for Attachment
displayAttachment(m, binding.ivAttachment, binding.tvAttachmentName, token, baseUrl);
View.OnClickListener click = v -> {
if (listener != null) listener.onAttachmentClick(m);
};
binding.ivAttachment.setOnClickListener(click);
binding.tvAttachmentName.setOnClickListener(click);
}
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, String baseUrl, OnAttachmentClickListener listener) {
// Check for Text
if (m.getContent() != null && !m.getContent().isEmpty()) {
binding.tvMessageContent.setVisibility(View.VISIBLE);
binding.tvMessageContent.setText(m.getContent());
} else {
binding.tvMessageContent.setVisibility(View.GONE);
}
// Check for Attachment
displayAttachment(m, binding.ivAttachment, binding.tvAttachmentName, token, baseUrl);
View.OnClickListener click = v -> {
if (listener != null) listener.onAttachmentClick(m);
};
binding.ivAttachment.setOnClickListener(click);
binding.tvAttachmentName.setOnClickListener(click);
}
}
// helper function to display the attachment to the chat bubble
private static void displayAttachment(Message m, ImageView iv, TextView tvName, String token, String baseUrl) {
// Check if there's an attachment by looking at name or mime type
if (m.getAttachmentName() != null || m.getAttachmentMimeType() != null) {
// Construct the download URL using the message ID
String url;
if (baseUrl != null) {
String cleanBase = baseUrl.endsWith("/") ? baseUrl.substring(0, baseUrl.length() - 1) : baseUrl;
url = cleanBase + "/api/v1/chat/messages/" + m.getId() + "/attachment";
} else {
url = m.getAttachmentUrl(); // Fallback
}
if (url == null) {
Glide.with(iv.getContext()).clear(iv);
iv.setVisibility(View.GONE);
tvName.setVisibility(View.GONE);
return;
}
if (m.getAttachmentMimeType() != null && m.getAttachmentMimeType().startsWith("image/")) {
iv.setVisibility(View.VISIBLE);
tvName.setVisibility(View.GONE);
Object loadTarget = url;
if (token != null) {
loadTarget = new GlideUrl(url, new LazyHeaders.Builder()
.addHeader("Authorization", "Bearer " + token)
.build());
}
// Use a signature to prevent Glide from showing stale cached images for the same URL/ID
String signatureKey = (m.getTimestamp() != null ? m.getTimestamp() : "") + m.getId();
Glide.with(iv.getContext()).clear(iv);
Glide.with(iv.getContext())
.load(loadTarget)
.signature(new ObjectKey(signatureKey))
.diskCacheStrategy(DiskCacheStrategy.ALL)
.placeholder(R.drawable.placeholder)
.error(R.drawable.placeholder)
.into(iv);
} else {
Glide.with(iv.getContext()).clear(iv);
iv.setVisibility(View.GONE);
tvName.setVisibility(View.VISIBLE);
tvName.setText(m.getAttachmentName() != null ? m.getAttachmentName() : "Attachment");
}
} else {
Glide.with(iv.getContext()).clear(iv);
iv.setVisibility(View.GONE);
tvName.setVisibility(View.GONE);
}
void bind(Message m) { tvMessage.setText(m.getContent()); }
}
}

View File

@@ -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,85 @@ 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"));
//Set the status color depending on availability. If available, green, If Pending, yellow, otherwise red
if (pet.getPetStatus() != null) {
switch (pet.getPetStatus()) {
case "Available":
binding.tvPetStatus.setBackgroundColor(Color.parseColor("#4CAF50"));
break;
case "Pending":
binding.tvPetStatus.setBackgroundColor(Color.parseColor("#FF9800"));
break;
default:
binding.tvPetStatus.setBackgroundColor(Color.parseColor("#F44336"));
break;
}
} else {
holder.tvPetStatus.setBackgroundColor(Color.parseColor("#F44336"));
binding.tvPetStatus.setBackgroundColor(Color.parseColor("#9E9E9E"));
}
// 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();
}
}
}

View File

@@ -1,72 +1,77 @@
package com.example.petstoremobile.adapters;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import android.view.*;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import com.example.petstoremobile.R;
import com.example.petstoremobile.models.Product;
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<Product> productList;
private OnProductClickListener productClickListener;
private List<ProductDTO> productList;
private OnProductClickListener listener;
private String baseUrl;
private String token;
// Interface for product click on recycler view
public interface OnProductClickListener {
void onProductClick(int position);
}
// Constructor
public ProductAdapter(List<Product> productList, OnProductClickListener productClickListener) {
public ProductAdapter(List<ProductDTO> productList, OnProductClickListener listener) {
this.productList = productList;
this.productClickListener = productClickListener;
this.listener = listener;
}
// Get the controls of each row in recycler view
public static class ProductViewHolder extends RecyclerView.ViewHolder {
TextView tvProductName, tvProductDesc, tvCategory, tvProductPrice, tvStockQuantity;
public void setBaseUrl(String baseUrl) {
this.baseUrl = baseUrl;
}
public ProductViewHolder(@NonNull View v) {
super(v);
tvProductName = v.findViewById(R.id.tvProductName);
tvProductDesc = v.findViewById(R.id.tvProductDesc);
tvCategory = v.findViewById(R.id.tvProductCategory);
tvProductPrice = v.findViewById(R.id.tvProductPrice);
tvStockQuantity = v.findViewById(R.id.tvStockQuantity);
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;
}
}
// Create a new row view
@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);
}
// Populate the row with product data
@Override
public void onBindViewHolder(@NonNull ProductViewHolder holder, int position) {
Product product = productList.get(position);
ProductDTO p = productList.get(position);
ItemProductBinding binding = holder.binding;
holder.tvProductName.setText(product.getProductName());
holder.tvProductDesc.setText(product.getProductDesc());
holder.tvCategory.setText(product.getCategory());
holder.tvProductPrice.setText("$" + String.format("%.2f", product.getProductPrice()));
holder.tvStockQuantity.setText("Stock: " + product.getStockQuantity());
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() : "");
// When a row is clicked, open the detail view
holder.itemView.setOnClickListener(v -> productClickListener.onProductClick(position));
// 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));
}
@Override
public int getItemCount() {
return productList.size();
}
}
public int getItemCount() { return productList.size(); }
}

View File

@@ -0,0 +1,109 @@
package com.example.petstoremobile.adapters;
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.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> implements BulkDeleteHandler.SelectableAdapter {
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 {
final ItemProductSupplierBinding binding;
public PSViewHolder(@NonNull ItemProductSupplierBinding binding) {
super(binding.getRoot());
this.binding = binding;
}
}
@NonNull
@Override
public PSViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
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);
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(); }
}

View File

@@ -0,0 +1,80 @@
package com.example.petstoremobile.adapters;
import android.graphics.Color;
import android.view.LayoutInflater;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import com.example.petstoremobile.databinding.ItemPurchaseOrderBinding;
import com.example.petstoremobile.dtos.PurchaseOrderDTO;
import java.util.List;
public class PurchaseOrderAdapter extends RecyclerView.Adapter<PurchaseOrderAdapter.POViewHolder> {
private List<PurchaseOrderDTO> list;
private OnPurchaseOrderClickListener listener;
public interface OnPurchaseOrderClickListener {
void onPurchaseOrderClick(int position);
}
public PurchaseOrderAdapter(List<PurchaseOrderDTO> list, OnPurchaseOrderClickListener listener) {
this.list = list;
this.listener = listener;
}
public static class POViewHolder extends RecyclerView.ViewHolder {
final ItemPurchaseOrderBinding binding;
public POViewHolder(@NonNull ItemPurchaseOrderBinding binding) {
super(binding.getRoot());
this.binding = binding;
}
}
@NonNull
@Override
public POViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
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);
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() : "";
binding.tvPOStatus.setText(status);
switch (status.toUpperCase()) {
case "RECEIVED":
binding.tvPOStatus.setBackgroundColor(Color.parseColor("#4CAF50"));
break;
case "PLACED":
binding.tvPOStatus.setBackgroundColor(Color.parseColor("#2196F3"));
break;
case "PENDING":
binding.tvPOStatus.setBackgroundColor(Color.parseColor("#FF9800"));
break;
case "CANCELLED":
binding.tvPOStatus.setBackgroundColor(Color.parseColor("#F44336"));
break;
default:
binding.tvPOStatus.setBackgroundColor(Color.parseColor("#9E9E9E"));
break;
}
holder.itemView.setOnClickListener(v -> listener.onPurchaseOrderClick(position));
}
@Override
public int getItemCount() {
return list.size();
}
}

View File

@@ -0,0 +1,76 @@
package com.example.petstoremobile.adapters;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.core.content.ContextCompat;
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;
public class SaleAdapter extends RecyclerView.Adapter<SaleAdapter.SaleViewHolder> {
private List<SaleDTO> saleList;
private OnSaleClickListener listener;
public interface OnSaleClickListener {
void onSaleClick(int position);
}
public SaleAdapter(List<SaleDTO> saleList, OnSaleClickListener listener) {
this.saleList = saleList;
this.listener = listener;
}
public static class SaleViewHolder extends RecyclerView.ViewHolder {
final ItemSaleBinding binding;
public SaleViewHolder(@NonNull ItemSaleBinding binding) {
super(binding.getRoot());
this.binding = binding;
}
}
@NonNull
@Override
public SaleViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
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);
ItemSaleBinding binding = holder.binding;
binding.tvSaleId.setText("Sale #" + (s.getSaleId() != null ? s.getSaleId() : ""));
binding.tvSaleEmployee.setText("By: " + (s.getEmployeeName() != null ? s.getEmployeeName() : ""));
if (s.getCustomerName() != null && !s.getCustomerName().isEmpty()) {
binding.tvSaleCustomer.setText("Customer: " + s.getCustomerName());
binding.tvSaleCustomer.setVisibility(View.VISIBLE);
} else {
binding.tvSaleCustomer.setVisibility(View.GONE);
}
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() : "");
if (Boolean.TRUE.equals(s.getIsRefund())) {
binding.tvSaleRefundBadge.setVisibility(View.VISIBLE);
binding.tvSaleTotal.setTextColor(ContextCompat.getColor(holder.itemView.getContext(), R.color.status_adopted));
} else {
binding.tvSaleRefundBadge.setVisibility(View.GONE);
binding.tvSaleTotal.setTextColor(ContextCompat.getColor(holder.itemView.getContext(), R.color.status_available));
}
holder.itemView.setOnClickListener(v -> listener.onSaleClick(position));
}
@Override
public int getItemCount() {
return saleList.size();
}
}

View File

@@ -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();
}
}
}

View File

@@ -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();
}
}
}

View File

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

View File

@@ -0,0 +1,20 @@
package com.example.petstoremobile.api;
import com.example.petstoremobile.dtos.ActivityLogDTO;
import java.util.List;
import retrofit2.Call;
import retrofit2.http.GET;
import retrofit2.http.Query;
public interface ActivityLogApi {
@GET("api/v1/activity-logs")
Call<List<ActivityLogDTO>> getActivityLogs(
@Query("limit") int limit,
@Query("storeId") Long storeId,
@Query("role") String role,
@Query("search") String search
);
}

View File

@@ -0,0 +1,44 @@
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;
import retrofit2.http.Query;
public interface AdoptionApi {
@GET("api/v1/adoptions")
Call<PageResponse<AdoptionDTO>> getAllAdoptions(
@Query("page") int page,
@Query("size") int size,
@Query("q") String query,
@Query("status") String status,
@Query("storeId") Long storeId,
@Query("date") String date,
@Query("employeeId") Long employeeId,
@Query("sort") String sort);
@GET("api/v1/adoptions/{id}")
Call<AdoptionDTO> getAdoptionById(@Path("id") Long id);
@POST("api/v1/adoptions")
Call<AdoptionDTO> createAdoption(@Body AdoptionDTO adoption);
@PUT("api/v1/adoptions/{id}")
Call<AdoptionDTO> updateAdoption(@Path("id") Long id, @Body AdoptionDTO adoption);
@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);
}

View File

@@ -0,0 +1,44 @@
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;
import retrofit2.http.Query;
public interface AppointmentApi {
@GET("api/v1/appointments")
Call<PageResponse<AppointmentDTO>> getAllAppointments(
@Query("page") int page,
@Query("size") int size,
@Query("q") String query,
@Query("status") String status,
@Query("storeId") Long storeId,
@Query("date") String date,
@Query("employeeId") Long employeeId,
@Query("sort") String sort);
@GET("api/v1/appointments/{id}")
Call<AppointmentDTO> getAppointmentById(@Path("id") Long id);
@POST("api/v1/appointments")
Call<AppointmentDTO> createAppointment(@Body AppointmentDTO appointment);
@PUT("api/v1/appointments/{id}")
Call<AppointmentDTO> updateAppointment(@Path("id") Long id, @Body AppointmentDTO appointment);
@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);
}

View File

@@ -0,0 +1,15 @@
package com.example.petstoremobile.api;
import com.example.petstoremobile.dtos.CategoryDTO;
import com.example.petstoremobile.dtos.PageResponse;
import retrofit2.Call;
import retrofit2.http.GET;
import retrofit2.http.Query;
public interface CategoryApi {
@GET("api/v1/categories")
Call<PageResponse<CategoryDTO>> getAllCategories(
@Query("page") int page,
@Query("size") int size);
}

View File

@@ -2,13 +2,14 @@ package com.example.petstoremobile.api;
import com.example.petstoremobile.dtos.ConversationDTO;
import com.example.petstoremobile.dtos.MessageDTO;
import com.example.petstoremobile.dtos.UpdateConversationStatusRequest;
import java.util.List;
import retrofit2.Call;
import retrofit2.http.Body;
import retrofit2.http.GET;
import retrofit2.http.POST;
import retrofit2.http.PUT;
import retrofit2.http.Path;
//api calls to get conversations
@@ -20,4 +21,7 @@ public interface ChatApi {
@GET("api/v1/chat/conversations/{conversationId}")
Call<ConversationDTO> getConversationById(@Path("conversationId") Long conversationId);
@PUT("api/v1/chat/conversations/{conversationId}")
Call<ConversationDTO> updateConversationStatus(@Path("conversationId") Long conversationId, @Body UpdateConversationStatusRequest request);
}

View File

@@ -0,0 +1,44 @@
package com.example.petstoremobile.api;
import com.example.petstoremobile.dtos.CouponDTO;
import com.example.petstoremobile.dtos.PageResponse;
import java.util.List;
import retrofit2.Call;
import retrofit2.http.Body;
import retrofit2.http.DELETE;
import retrofit2.http.GET;
import retrofit2.http.POST;
import retrofit2.http.PUT;
import retrofit2.http.Path;
import retrofit2.http.Query;
public interface CouponApi {
@GET("api/v1/coupons")
Call<PageResponse<CouponDTO>> getAllCoupons(
@Query("page") int page,
@Query("size") int size,
@Query("active") Boolean active,
@Query("discountType") String discountType,
@Query("sort") String sort);
@GET("api/v1/coupons/{id}")
Call<CouponDTO> getCouponById(@Path("id") Long id);
@GET("api/v1/coupons/code/{code}")
Call<CouponDTO> getCouponByCode(@Path("code") String code);
@POST("api/v1/coupons")
Call<CouponDTO> createCoupon(@Body CouponDTO coupon);
@PUT("api/v1/coupons/{id}")
Call<CouponDTO> updateCoupon(@Path("id") Long id, @Body CouponDTO coupon);
@DELETE("api/v1/coupons/{id}")
Call<Void> deleteCoupon(@Path("id") Long id);
@DELETE("api/v1/coupons")
Call<Void> bulkDeleteCoupons(@Query("ids") List<Long> ids);
}

View File

@@ -1,16 +1,20 @@
package com.example.petstoremobile.api;
import com.example.petstoremobile.dtos.CustomerDTO;
import com.example.petstoremobile.dtos.DropdownDTO;
import com.example.petstoremobile.dtos.PageResponse;
import java.util.List;
import retrofit2.Call;
import retrofit2.http.Body;
import retrofit2.http.DELETE;
import retrofit2.http.GET;
import retrofit2.http.POST;
import retrofit2.http.PUT;
import retrofit2.http.Path;
import retrofit2.http.Query;
//api calls to get customers
public interface CustomerApi {
@GET("api/v1/customers")
@@ -18,4 +22,16 @@ public interface CustomerApi {
@GET("api/v1/customers/{customerId}")
Call<CustomerDTO> getCustomerById(@Path("customerId") Long customerId);
}
@PUT("api/v1/customers/{customerId}")
Call<CustomerDTO> updateCustomer(@Path("customerId") Long customerId, @Body CustomerDTO customer);
@DELETE("api/v1/customers/{customerId}")
Call<Void> deleteCustomer(@Path("customerId") Long customerId);
@POST("api/v1/auth/register")
Call<CustomerDTO> registerCustomer(@Body CustomerDTO customer);
@GET("api/v1/dropdowns/customers")
Call<List<DropdownDTO>> getCustomerDropdowns();
}

View File

@@ -0,0 +1,32 @@
package com.example.petstoremobile.api;
import com.example.petstoremobile.dtos.EmployeeDTO;
import com.example.petstoremobile.dtos.PageResponse;
import retrofit2.Call;
import retrofit2.http.*;
public interface EmployeeApi {
@GET("api/v1/employees")
Call<PageResponse<EmployeeDTO>> getAllEmployees(
@Query("page") int page,
@Query("size") int size);
@GET("api/v1/employees/{id}")
Call<EmployeeDTO> getEmployeeById(@Path("id") Long id);
@POST("api/v1/employees")
Call<EmployeeDTO> createEmployee(@Body EmployeeDTO employee);
@PUT("api/v1/employees/{id}")
Call<EmployeeDTO> updateEmployee(@Path("id") Long id, @Body EmployeeDTO employee);
@DELETE("api/v1/employees/{id}")
Call<Void> deleteEmployee(@Path("id") Long id);
}

View File

@@ -0,0 +1,46 @@
package com.example.petstoremobile.api;
import com.example.petstoremobile.dtos.BulkDeleteRequest;
import com.example.petstoremobile.dtos.InventoryDTO;
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;
import retrofit2.http.Query;
public interface InventoryApi {
@GET("api/v1/inventory")
Call<PageResponse<InventoryDTO>> getAllInventory(
@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}
@GET("api/v1/inventory/{id}")
Call<InventoryDTO> getInventoryById(@Path("id") Long id);
// POST /api/v1/inventory
@POST("api/v1/inventory")
Call<InventoryDTO> createInventory(@Body InventoryDTO request);
// PUT /api/v1/inventory/{id}
@PUT("api/v1/inventory/{id}")
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)
@HTTP(method = "DELETE", path = "api/v1/inventory", hasBody = true)
Call<Void> bulkDeleteInventory(@Body BulkDeleteRequest request);
}

View File

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

View File

@@ -1,26 +1,58 @@
package com.example.petstoremobile.api;
import com.example.petstoremobile.dtos.BulkDeleteRequest;
import com.example.petstoremobile.dtos.DropdownDTO;
import com.example.petstoremobile.dtos.PageResponse;
import com.example.petstoremobile.dtos.PetDTO;
import java.util.List;
import okhttp3.MultipartBody;
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;
import retrofit2.http.Part;
import retrofit2.http.Path;
import retrofit2.http.Query;
//api calls to CRUD pets
public interface PetApi {
// Get all pets
// endpoint for downloading the pet's image file
String PET_IMAGE_PATH = "api/v1/pets/%d/image";
// 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("customerId") Long customerId,
@Query("sort") String sort
);
@GET("api/v1/dropdowns/customers/{customerId}/pets")
Call<List<DropdownDTO>> getCustomerPets(@Path("customerId") Long customerId);
@GET("api/v1/dropdowns/adoption-pets")
Call<List<DropdownDTO>> getAdoptionPets(@Query("storeId") Long storeId);
@GET("api/v1/dropdowns/pets")
Call<List<DropdownDTO>> getPetDropdowns();
@GET("api/v1/dropdowns/pet-species")
Call<List<DropdownDTO>> getPetSpeciesDropdowns();
@GET("api/v1/dropdowns/pet-breeds")
Call<List<DropdownDTO>> getPetBreedsDropdowns(@Query("species") String species);
// Get pet by id
@GET("api/v1/pets/{id}")
Call<PetDTO> getPetById(@Path("id") Long id);
@@ -37,4 +69,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);
}

View File

@@ -0,0 +1,47 @@
package com.example.petstoremobile.api;
import com.example.petstoremobile.dtos.DropdownDTO;
import com.example.petstoremobile.dtos.PageResponse;
import com.example.petstoremobile.dtos.ProductDTO;
import okhttp3.MultipartBody;
import retrofit2.Call;
import retrofit2.http.*;
import java.util.List;
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("sort") String sort);
@GET("api/v1/products/{id}")
Call<ProductDTO> getProductById(@Path("id") Long id);
@POST("api/v1/products")
Call<ProductDTO> createProduct(@Body ProductDTO product);
@PUT("api/v1/products/{id}")
Call<ProductDTO> updateProduct(@Path("id") Long id, @Body ProductDTO product);
@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);
@GET("api/v1/dropdowns/products")
Call<List<DropdownDTO>> getProductDropdowns();
@GET("api/v1/dropdowns/categories")
Call<List<DropdownDTO>> getCategoryDropdowns();
}

View File

@@ -0,0 +1,40 @@
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;
import retrofit2.http.*;
public interface ProductSupplierApi {
@GET("api/v1/product-suppliers")
Call<PageResponse<ProductSupplierDTO>> getAllProductSuppliers(
@Query("page") int page,
@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);
@PUT("api/v1/product-suppliers/{productId}/{supplierId}")
Call<ProductSupplierDTO> updateProductSupplier(
@Path("productId") Long productId,
@Path("supplierId") Long supplierId,
@Body ProductSupplierDTO dto);
@DELETE("api/v1/product-suppliers/{productId}/{supplierId}")
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);
}

View File

@@ -0,0 +1,22 @@
package com.example.petstoremobile.api;
import com.example.petstoremobile.dtos.PageResponse;
import com.example.petstoremobile.dtos.PurchaseOrderDTO;
import retrofit2.Call;
import retrofit2.http.GET;
import retrofit2.http.Path;
import retrofit2.http.Query;
public interface PurchaseOrderApi {
@GET("api/v1/purchase-orders")
Call<PageResponse<PurchaseOrderDTO>> getAllPurchaseOrders(
@Query("page") int page,
@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);
}

View File

@@ -0,0 +1,25 @@
package com.example.petstoremobile.api;
import com.example.petstoremobile.dtos.RefundDTO;
import retrofit2.Call;
import retrofit2.http.*;
import java.util.List;
public interface RefundApi {
@GET("api/v1/refunds")
Call<List<RefundDTO>> getAllRefunds();
@GET("api/v1/refunds/{id}")
Call<RefundDTO> getRefundById(@Path("id") Long id);
@POST("api/v1/refunds")
Call<RefundDTO> createRefund(@Body RefundDTO refund);
@PUT("api/v1/refunds/{id}")
Call<RefundDTO> updateRefund(@Path("id") Long id, @Body RefundDTO refund);
@DELETE("api/v1/refunds/{id}")
Call<Void> deleteRefund(@Path("id") Long id);
}

View File

@@ -1,70 +0,0 @@
package com.example.petstoremobile.api;
import android.content.Context;
import com.example.petstoremobile.api.auth.AuthApi;
import com.example.petstoremobile.api.auth.AuthInterceptor;
import okhttp3.OkHttpClient;
import okhttp3.logging.HttpLoggingInterceptor;
import retrofit2.Retrofit;
import retrofit2.converter.gson.GsonConverterFactory;
//Retrofit client Used for API calls
public class RetrofitClient {
//base URL
public static final String BASE_URL = "http://10.0.2.2:8080"; //for emulator testing
// public static final String BASE_URL = "http://10.0.0.200:8080/"; //for hardware testing
private static Retrofit retrofit = null;
public static Retrofit getClient(Context context) {
//create an http logging using an interceptor
HttpLoggingInterceptor interceptor = new HttpLoggingInterceptor();
interceptor.setLevel(HttpLoggingInterceptor.Level.BODY);
OkHttpClient client = new OkHttpClient.Builder()
.addInterceptor(interceptor)
.addInterceptor(new AuthInterceptor(context))
.build();
//build the retrofit object with all needed properties
retrofit = new Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create()) //JSON converter
.client(client) //logging interceptor - OkHttpClient
.build();
return retrofit;
}
//associate the retrofit object with the API interface
public static PetApi getPetApi(Context context) {
return getClient(context).create(PetApi.class);
}
public static ServiceApi getServiceApi(Context context) {
return getClient(context).create(ServiceApi.class);
}
public static SupplierApi getSupplierApi(Context context) {
return getClient(context).create(SupplierApi.class);
}
public static AuthApi getAuthApi(Context context) {
return getClient(context).create(AuthApi.class);
}
public static ChatApi getChatApi(Context context) {
return getClient(context).create(ChatApi.class);
}
public static CustomerApi getCustomerApi(Context context) {
return getClient(context).create(CustomerApi.class);
}
public static MessageApi getMessageApi(Context context) {
return getClient(context).create(MessageApi.class);
}
}

View File

@@ -0,0 +1,31 @@
package com.example.petstoremobile.api;
import com.example.petstoremobile.dtos.PageResponse;
import com.example.petstoremobile.dtos.SaleDTO;
import retrofit2.Call;
import retrofit2.http.Body;
import retrofit2.http.GET;
import retrofit2.http.POST;
import retrofit2.http.Path;
import retrofit2.http.Query;
public interface SaleApi {
@GET("api/v1/sales")
Call<PageResponse<SaleDTO>> getAllSales(
@Query("page") int page,
@Query("size") int size,
@Query("q") String query,
@Query("paymentMethod") String paymentMethod,
@Query("storeId") Long storeId,
@Query("isRefund") Boolean isRefund,
@Query("customerId") Long customerId,
@Query("sort") String sort);
@GET("api/v1/sales/{id}")
Call<SaleDTO> getSaleById(@Path("id") Long id);
@POST("api/v1/sales")
Call<SaleDTO> createSale(@Body SaleDTO sale);
}

View File

@@ -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);
}

View File

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

View File

@@ -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);
}

View File

@@ -0,0 +1,15 @@
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 {
String AVATAR_PATH = "api/v1/users/%d/avatar/file";
@GET("api/v1/users")
Call<PageResponse<UserDTO>> getUsers(@Query("role") String role, @Query("page") int page, @Query("size") int size);
}

View File

@@ -1,17 +1,50 @@
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;
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;
import retrofit2.http.PUT;
import retrofit2.http.Part;
//Api for logging in and getting current user
public interface AuthApi {
// endpoint for downloading the current user's avatar file
String AVATAR_FILE_PATH = "api/v1/auth/me/avatar/file";
//login endpoint
@POST("api/v1/auth/login")
Call<AuthDTO.LoginResponse> login(@Body AuthDTO.LoginRequest loginRequest);
//get current user endpoint
@GET("api/v1/auth/me")
Call<AuthDTO.UserResponse> getCurrentUser();
Call<UserDTO> getMe();
//update current user endpoint
@PUT("api/v1/auth/me")
Call<UserDTO> updateMe(@Body Map<String, String> updates);
//upload avatar endpoint
@Multipart
@POST("api/v1/auth/me/avatar")
Call<AvatarUploadResponse> uploadAvatar(@Part MultipartBody.Part avatar);
//delete avatar endpoint
@DELETE("api/v1/auth/me/avatar")
Call<Void> deleteAvatar();
//forgot password endpoint
@POST("api/v1/auth/forgot-password")
Call<Void> forgotPassword(@Body Map<String, String> body);
}

View File

@@ -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

View File

@@ -3,28 +3,27 @@ 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";
private static final String ROLE_KEY = "role";
private static final String PREFS_NAME = "auth_prefs";
private static final String USER_ID_KEY = "user_id";
private static final String PRIMARY_STORE_ID_KEY = "primary_store_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()
@@ -56,6 +55,19 @@ public class TokenManager {
prefs.edit().putLong(USER_ID_KEY, userId).apply();
}
public void savePrimaryStoreId(Long storeId) {
if (storeId != null) {
prefs.edit().putLong(PRIMARY_STORE_ID_KEY, storeId).apply();
} else {
prefs.edit().remove(PRIMARY_STORE_ID_KEY).apply();
}
}
public Long getPrimaryStoreId() {
long id = prefs.getLong(PRIMARY_STORE_ID_KEY, -1L);
return id == -1L ? null : id;
}
//Check if logged in
public boolean isLoggedIn() {
return getToken() != null;
@@ -65,6 +77,4 @@ public class TokenManager {
public void clearLoginData() {
prefs.edit().clear().apply();
}
}
}

View File

@@ -0,0 +1,206 @@
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.HEADERS);
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);
}
@Provides
@Singleton
public static EmployeeApi provideEmployeeApi(Retrofit retrofit) {
return retrofit.create(EmployeeApi.class);
}
@Provides
@Singleton
public static RefundApi provideRefundApi(Retrofit retrofit) {
return retrofit.create(RefundApi.class);
}
@Provides
@Singleton
public static CouponApi provideCouponApi(Retrofit retrofit) {
return retrofit.create(CouponApi.class);
}
@Provides
@Singleton
public static ActivityLogApi provideActivityLogApi(Retrofit retrofit) {
return retrofit.create(ActivityLogApi.class);
}
}

View File

@@ -0,0 +1,31 @@
package com.example.petstoremobile.dtos;
public class ActivityLogDTO {
private Long logId;
private String activity;
private String fullName;
private String fullNameSnapshot;
private String logTimestamp;
private String role;
private String roleSnapshot;
private Long storeId;
private String storeName;
private String storeNameSnapshot;
private Long userId;
private String username;
private String usernameSnapshot;
public Long getLogId() { return logId; }
public String getActivity() { return activity; }
public String getFullName() { return fullName; }
public String getFullNameSnapshot() { return fullNameSnapshot; }
public String getLogTimestamp() { return logTimestamp; }
public String getRole() { return role; }
public String getRoleSnapshot() { return roleSnapshot; }
public Long getStoreId() { return storeId; }
public String getStoreName() { return storeName; }
public String getStoreNameSnapshot() { return storeNameSnapshot; }
public Long getUserId() { return userId; }
public String getUsername() { return username; }
public String getUsernameSnapshot() { return usernameSnapshot; }
}

View File

@@ -0,0 +1,76 @@
package com.example.petstoremobile.dtos;
import java.math.BigDecimal;
public class AdoptionDTO {
private Long adoptionId;
private Long petId;
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 String createdAt;
private String updatedAt;
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.adoptionFee = adoptionFee;
}
public Long getAdoptionId() { return adoptionId; }
public void setAdoptionId(Long adoptionId) { this.adoptionId = adoptionId; }
public Long getPetId() { return petId; }
public void setPetId(Long petId) { this.petId = petId; }
public String getPetName() { return petName; }
public void setPetName(String petName) { this.petName = petName; }
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 getEmployeeId() { return employeeId; }
public void setEmployeeId(Long employeeId) { this.employeeId = employeeId; }
public String getEmployeeName() { return employeeName; }
public void setEmployeeName(String employeeName) { this.employeeName = employeeName; }
public Long getSourceStoreId() { return sourceStoreId; }
public void setSourceStoreId(Long sourceStoreId) { this.sourceStoreId = sourceStoreId; }
public String getSourceStoreName() { return sourceStoreName; }
public void setSourceStoreName(String sourceStoreName) { this.sourceStoreName = sourceStoreName; }
public String getAdoptionDate() { return adoptionDate; }
public void setAdoptionDate(String adoptionDate) { this.adoptionDate = adoptionDate; }
public String getAdoptionStatus() { return adoptionStatus; }
public void setAdoptionStatus(String adoptionStatus) { this.adoptionStatus = adoptionStatus; }
public BigDecimal getAdoptionFee() { return adoptionFee; }
public void setAdoptionFee(BigDecimal adoptionFee) { this.adoptionFee = adoptionFee; }
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; }
}

View File

@@ -0,0 +1,80 @@
package com.example.petstoremobile.dtos;
public class AppointmentDTO {
private Long appointmentId;
private Long customerId;
private String customerName;
private Long storeId;
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 String petName;
private Long petId;
private String createdAt;
private String updatedAt;
public AppointmentDTO(Long customerId, Long storeId, Long serviceId,
String appointmentDate, String appointmentTime,
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.petId = petId;
}
public Long getAppointmentId() { return appointmentId; }
public Long getCustomerId() { return customerId; }
public String getCustomerName() { return customerName; }
public Long getStoreId() { return storeId; }
public String getStoreName() { return storeName; }
public Long getServiceId() { return serviceId; }
public String getServiceName() { return serviceName; }
public Long getEmployeeId() { return employeeId; }
public String getEmployeeName() { return employeeName; }
public String getAppointmentDate() { return appointmentDate; }
public String getAppointmentTime() { return appointmentTime; }
public String getAppointmentStatus() { return appointmentStatus; }
public String getPetName() { return petName; }
public Long getPetId() { return petId; }
public String getCreatedAt() { return createdAt; }
public String getUpdatedAt() { return updatedAt; }
public Long getPetID() { return petId; }
public String getServiceType() { return serviceName; }
public Long getServiceID() { return serviceId; }
public String getStatus() { return appointmentStatus; }
}

View File

@@ -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;
}
}

View File

@@ -0,0 +1,22 @@
package com.example.petstoremobile.dtos;
import java.util.List;
public class BulkDeleteRequest {
private List<String> ids;
public BulkDeleteRequest() {
}
public BulkDeleteRequest(List<String> ids) {
this.ids = ids;
}
public List<String> getIds() {
return ids;
}
public void setIds(List<String> ids) {
this.ids = ids;
}
}

View File

@@ -0,0 +1,29 @@
package com.example.petstoremobile.dtos;
public class CategoryDTO {
private Long categoryId;
private String categoryName;
private String categoryType;
private String createdAt;
private String updatedAt;
public Long getCategoryId() {
return categoryId;
}
public String getCategoryName() {
return categoryName;
}
public String getCategoryType() {
return categoryType;
}
public String getCreatedAt() {
return createdAt;
}
public String getUpdatedAt() {
return updatedAt;
}
}

View File

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

View File

@@ -0,0 +1,52 @@
package com.example.petstoremobile.dtos;
import java.math.BigDecimal;
public class CouponDTO {
private Long couponId;
private String couponCode;
private String discountType;
private BigDecimal discountValue;
private BigDecimal minOrderAmount;
private Boolean active;
private String startsAt;
private String endsAt;
private Integer usageLimit;
private String createdAt;
private String updatedAt;
public CouponDTO() {}
public Long getCouponId() { return couponId; }
public void setCouponId(Long couponId) { this.couponId = couponId; }
public String getCouponCode() { return couponCode; }
public void setCouponCode(String couponCode) { this.couponCode = couponCode; }
public String getDiscountType() { return discountType; }
public void setDiscountType(String discountType) { this.discountType = discountType; }
public BigDecimal getDiscountValue() { return discountValue; }
public void setDiscountValue(BigDecimal discountValue) { this.discountValue = discountValue; }
public BigDecimal getMinOrderAmount() { return minOrderAmount; }
public void setMinOrderAmount(BigDecimal minOrderAmount) { this.minOrderAmount = minOrderAmount; }
public Boolean getActive() { return active; }
public void setActive(Boolean active) { this.active = active; }
public String getStartsAt() { return startsAt; }
public void setStartsAt(String startsAt) { this.startsAt = startsAt; }
public String getEndsAt() { return endsAt; }
public void setEndsAt(String endsAt) { this.endsAt = endsAt; }
public Integer getUsageLimit() { return usageLimit; }
public void setUsageLimit(Integer usageLimit) { this.usageLimit = usageLimit; }
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; }
}

View File

@@ -3,57 +3,78 @@ package com.example.petstoremobile.dtos;
import com.google.gson.annotations.SerializedName;
public class CustomerDTO {
@SerializedName("customerId")
@SerializedName("id")
private Long customerId;
private String username;
private String firstName;
private String lastName;
private String fullName;
private String email;
private String phone;
private Boolean active;
private Integer loyaltyPoints;
private Long primaryStoreId;
private String createdAt;
private String updatedAt;
private String password;
private String role;
public CustomerDTO() {}
public Long getCustomerId() {
return customerId;
}
public void setCustomerId(Long customerId) {
this.customerId = customerId;
}
public String getFirstName() {
return firstName;
}
public void setFirstName(String firstName) {
public CustomerDTO(String username, String password, String firstName, String lastName,
String email, String phone) {
this.username = username;
this.password = password;
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 getPhone() {
return phone;
}
public void setPhone(String phone) {
this.phone = phone;
}
public Long getCustomerId() { return customerId; }
public void setCustomerId(Long customerId) { this.customerId = customerId; }
public String getUsername() { return username; }
public void setUsername(String username) { this.username = username; }
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 getFullName() {
return (firstName != null ? firstName : "") + " " + (lastName != null ? lastName : "");
if (fullName != null) return fullName;
String f = firstName != null ? firstName : "";
String l = lastName != null ? lastName : "";
return (f + " " + l).trim();
}
}
public void setFullName(String fullName) { this.fullName = fullName; }
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }
public String getPhone() { return phone; }
public void setPhone(String phone) { this.phone = phone; }
public Boolean getActive() { return active; }
public void setActive(Boolean active) { this.active = active; }
public Integer getLoyaltyPoints() { return loyaltyPoints; }
public void setLoyaltyPoints(Integer loyaltyPoints) { this.loyaltyPoints = loyaltyPoints; }
public Long getPrimaryStoreId() { return primaryStoreId; }
public void setPrimaryStoreId(Long primaryStoreId) { this.primaryStoreId = primaryStoreId; }
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 String getPassword() { return password; }
public void setPassword(String password) { this.password = password; }
public String getRole() { return role; }
public void setRole(String role) { this.role = role; }
}

View File

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

View File

@@ -0,0 +1,156 @@
package com.example.petstoremobile.dtos;
public class EmployeeDTO {
private Long id;
private String username;
private String firstName;
private String lastName;
private String fullName;
private String email;
private String phone;
private String role;
private String staffRole;
private Boolean active;
private Integer loyaltyPoints;
private Long primaryStoreId;
private String createdAt;
private String updatedAt;
private String password;
public EmployeeDTO() {}
public EmployeeDTO(String username, String password, String firstName, String lastName,
String email, String phone, String role, String staffRole, boolean active, Long primaryStoreId) {
this.username = username;
this.password = password;
this.firstName = firstName;
this.lastName = lastName;
this.email = email;
this.phone = phone;
this.role = role;
this.staffRole = staffRole;
this.active = active;
this.primaryStoreId = primaryStoreId;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
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 getFullName() {
return fullName;
}
public void setFullName(String fullName) {
this.fullName = fullName;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getPhone() {
return phone;
}
public void setPhone(String phone) {
this.phone = phone;
}
public String getRole() {
return role;
}
public void setRole(String role) {
this.role = role;
}
public String getStaffRole() {
return staffRole;
}
public void setStaffRole(String staffRole) {
this.staffRole = staffRole;
}
public Boolean getActive() {
return active;
}
public void setActive(Boolean active) {
this.active = active;
}
public Integer getLoyaltyPoints() {
return loyaltyPoints;
}
public void setLoyaltyPoints(Integer loyaltyPoints) {
this.loyaltyPoints = loyaltyPoints;
}
public Long getPrimaryStoreId() {
return primaryStoreId;
}
public void setPrimaryStoreId(Long primaryStoreId) {
this.primaryStoreId = primaryStoreId;
}
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 String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
}

View File

@@ -0,0 +1,15 @@
package com.example.petstoremobile.dtos;
//Used to get messages of any errors from the backend
public class ErrorResponse {
private String message;
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
}

View File

@@ -0,0 +1,96 @@
package com.example.petstoremobile.dtos;
public class InventoryDTO {
// Response fields (from backend InventoryResponse)
private Long inventoryId;
private Long prodId;
private String productName;
private String categoryName;
private Long storeId;
private String storeName;
private Integer quantity;
private String createdAt;
private String updatedAt;
public InventoryDTO() {
}
// Constructor for create/update requests (matches InventoryRequest)
public InventoryDTO(Long prodId, Long storeId, Integer quantity) {
this.prodId = prodId;
this.storeId = storeId;
this.quantity = quantity;
}
public Long getInventoryId() {
return inventoryId;
}
public void setInventoryId(Long inventoryId) {
this.inventoryId = inventoryId;
}
public Long getProdId() {
return prodId;
}
public void setProdId(Long prodId) {
this.prodId = prodId;
}
public String getProductName() {
return productName;
}
public void setProductName(String productName) {
this.productName = productName;
}
public String getCategoryName() {
return categoryName;
}
public void setCategoryName(String categoryName) {
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;
}
public void setQuantity(Integer quantity) {
this.quantity = quantity;
}
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;
}
}

View File

@@ -22,6 +22,18 @@ public class MessageDTO {
@SerializedName("isRead")
private Boolean isRead;
@SerializedName("attachmentUrl")
private String attachmentUrl;
@SerializedName("attachmentName")
private String attachmentName;
@SerializedName("attachmentMimeType")
private String attachmentMimeType;
@SerializedName("attachmentSizeBytes")
private Long attachmentSizeBytes;
public MessageDTO() {}
public Long getId() { return id; }
@@ -41,4 +53,16 @@ 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 getAttachmentMimeType() { return attachmentMimeType; }
public void setAttachmentMimeType(String attachmentMimeType) { this.attachmentMimeType = attachmentMimeType; }
public Long getAttachmentSizeBytes() { return attachmentSizeBytes; }
public void setAttachmentSizeBytes(Long attachmentSizeBytes) { this.attachmentSizeBytes = attachmentSizeBytes; }
}

View File

@@ -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; }
}

View File

@@ -0,0 +1,81 @@
package com.example.petstoremobile.dtos;
import java.math.BigDecimal;
public class ProductDTO {
private Long prodId;
private String prodName;
private Long categoryId;
private String categoryName;
private String prodDesc;
private BigDecimal prodPrice;
private String createdAt;
private String updatedAt;
public ProductDTO() {
}
// Constructor for create/update
public ProductDTO(String prodName, Long categoryId, String prodDesc, BigDecimal prodPrice) {
this.prodName = prodName;
this.categoryId = categoryId;
this.prodDesc = prodDesc;
this.prodPrice = prodPrice;
}
public Long getProdId() {
return prodId;
}
public void setProdId(Long prodId) {
this.prodId = prodId;
}
public String getProdName() {
return prodName;
}
public void setProdName(String prodName) {
this.prodName = prodName;
}
public Long getCategoryId() {
return categoryId;
}
public void setCategoryId(Long categoryId) {
this.categoryId = categoryId;
}
public String getCategoryName() {
return categoryName;
}
public void setCategoryName(String categoryName) {
this.categoryName = categoryName;
}
public String getProdDesc() {
return prodDesc;
}
public void setProdDesc(String prodDesc) {
this.prodDesc = prodDesc;
}
public BigDecimal getProdPrice() {
return prodPrice;
}
public void setProdPrice(BigDecimal prodPrice) {
this.prodPrice = prodPrice;
}
public String getCreatedAt() {
return createdAt;
}
public String getUpdatedAt() {
return updatedAt;
}
}

View File

@@ -0,0 +1,48 @@
package com.example.petstoremobile.dtos;
import java.math.BigDecimal;
public class ProductSupplierDTO {
private Long productId;
private String productName;
private Long supplierId;
private String supplierName;
private BigDecimal cost;
private String createdAt;
private String updatedAt;
// Constructor for create/update
public ProductSupplierDTO(Long productId, Long supplierId, BigDecimal cost) {
this.productId = productId;
this.supplierId = supplierId;
this.cost = cost;
}
public Long getProductId() {
return productId;
}
public String getProductName() {
return productName;
}
public Long getSupplierId() {
return supplierId;
}
public String getSupplierName() {
return supplierName;
}
public BigDecimal getCost() {
return cost;
}
public String getCreatedAt() {
return createdAt;
}
public String getUpdatedAt() {
return updatedAt;
}
}

View File

@@ -0,0 +1,49 @@
package com.example.petstoremobile.dtos;
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;
private String updatedAt;
public Long getPurchaseOrderId() {
return purchaseOrderId;
}
public Long getSupId() {
return supId;
}
public String getSupplierName() {
return supplierName;
}
public Long getStoreId() {
return storeId;
}
public String getStoreName() {
return storeName;
}
public String getOrderDate() {
return orderDate;
}
public String getStatus() {
return status;
}
public String getCreatedAt() {
return createdAt;
}
public String getUpdatedAt() {
return updatedAt;
}
}

View File

@@ -0,0 +1,58 @@
package com.example.petstoremobile.dtos;
import java.math.BigDecimal;
public class RefundDTO {
// Response fields
private Long id;
private Long saleId;
private Long customerId;
private BigDecimal amount;
private String reason;
private String status;
private String createdAt;
private String updatedAt;
// Constructor for create request
public RefundDTO(Long saleId, String reason) {
this.saleId = saleId;
this.reason = reason;
}
// Constructor for update request
public RefundDTO(String status) {
this.status = status;
}
public Long getId() {
return id;
}
public Long getSaleId() {
return saleId;
}
public Long getCustomerId() {
return customerId;
}
public BigDecimal getAmount() {
return amount;
}
public String getReason() {
return reason;
}
public String getStatus() {
return status;
}
public String getCreatedAt() {
return createdAt;
}
public String getUpdatedAt() {
return updatedAt;
}
}

View File

@@ -0,0 +1,190 @@
package com.example.petstoremobile.dtos;
import java.math.BigDecimal;
import java.util.List;
public class SaleDTO {
// Response fields
private Long saleId;
private String saleDate;
private Long employeeId;
private String employeeName;
private Long customerId;
private String customerName;
private Long storeId;
private String storeName;
private BigDecimal totalAmount;
private BigDecimal subtotalAmount;
private BigDecimal couponDiscountAmount;
private BigDecimal employeeDiscountAmount;
private BigDecimal loyaltyDiscountAmount;
private BigDecimal pointsDiscountAmount;
private Integer pointsUsed;
private String paymentMethod;
private String channel;
private Boolean isRefund;
private Long originalSaleId;
private Long cartId;
private Long couponId;
private Integer pointsEarned;
private List<SaleItemDTO> items;
private String createdAt;
// Constructor for create request
public SaleDTO(Long storeId, String paymentMethod, List<SaleItemDTO> items,
Boolean isRefund, Long originalSaleId, Long customerId) {
this.storeId = storeId;
this.paymentMethod = paymentMethod;
this.items = items;
this.isRefund = isRefund;
this.originalSaleId = originalSaleId;
this.customerId = customerId;
}
public Long getSaleId() {
return saleId;
}
public String getSaleDate() {
return saleDate;
}
public Long getEmployeeId() {
return employeeId;
}
public String getEmployeeName() {
return employeeName;
}
public Long getStoreId() {
return storeId;
}
public String getStoreName() {
return storeName;
}
public BigDecimal getTotalAmount() {
return totalAmount;
}
public BigDecimal getSubtotalAmount() {
return subtotalAmount;
}
public BigDecimal getCouponDiscountAmount() {
return couponDiscountAmount;
}
public BigDecimal getEmployeeDiscountAmount() {
return employeeDiscountAmount;
}
public BigDecimal getLoyaltyDiscountAmount() {
return loyaltyDiscountAmount;
}
public void setLoyaltyDiscountAmount(BigDecimal loyaltyDiscountAmount) {
this.loyaltyDiscountAmount = loyaltyDiscountAmount;
}
public Integer getPointsUsed() {
return pointsUsed;
}
public void setPointsUsed(Integer pointsUsed) {
this.pointsUsed = pointsUsed;
}
public String getPaymentMethod() {
return paymentMethod;
}
public String getChannel() {
return channel;
}
public Boolean getIsRefund() {
return isRefund;
}
public Long getOriginalSaleId() {
return originalSaleId;
}
public Long getCartId() {
return cartId;
}
public Long getCouponId() {
return couponId;
}
public void setCouponId(Long couponId) {
this.couponId = couponId;
}
public Integer getPointsEarned() {
return pointsEarned;
}
public List<SaleItemDTO> getItems() {
return items;
}
public String getCreatedAt() {
return createdAt;
}
public Long getCustomerId() {
return customerId;
}
public String getCustomerName() {
return customerName;
}
public BigDecimal getPointsDiscountAmount() {
return pointsDiscountAmount;
}
public void setPointsDiscountAmount(BigDecimal pointsDiscountAmount) {
this.pointsDiscountAmount = pointsDiscountAmount;
}
// Nested SaleItemDTO
public static class SaleItemDTO {
private Long saleItemId;
private Long prodId;
private String productName;
private Integer quantity;
private BigDecimal unitPrice;
// Constructor for request
public SaleItemDTO(Long prodId, Integer quantity) {
this.prodId = prodId;
this.quantity = quantity;
}
public Long getSaleItemId() {
return saleItemId;
}
public Long getProdId() {
return prodId;
}
public String getProductName() {
return productName;
}
public Integer getQuantity() {
return quantity;
}
public BigDecimal getUnitPrice() {
return unitPrice;
}
}
}

View File

@@ -0,0 +1,25 @@
package com.example.petstoremobile.dtos;
public class StoreDTO {
private Long storeId;
private String storeName;
private String address;
private String phone;
private String email;
private String createdAt;
private String updatedAt;
// Constructor for hardcoded fallback
public StoreDTO(Long storeId, String storeName) {
this.storeId = storeId;
this.storeName = storeName;
}
public Long getStoreId() { return storeId; }
public String getStoreName() { return storeName; }
public String getAddress() { return address; }
public String getPhone() { return phone; }
public String getEmail() { return email; }
public String getCreatedAt() { return createdAt; }
public String getUpdatedAt() { return updatedAt; }
}

View File

@@ -0,0 +1,17 @@
package com.example.petstoremobile.dtos;
public class UpdateConversationStatusRequest {
private String status;
public UpdateConversationStatusRequest(String status) {
this.status = status;
}
public String getStatus() {
return status;
}
public void setStatus(String status) {
this.status = status;
}
}

View File

@@ -0,0 +1,50 @@
package com.example.petstoremobile.dtos;
public class UserDTO {
private Long id;
private String username;
private String email;
private String fullName;
private String phone;
private String avatarUrl;
private String role;
private Long storeId;
private String storeName;
// Getters
public Long getId() {
return id;
}
public String getUsername() {
return username;
}
public String getEmail() {
return email;
}
public String getFullName() {
return fullName;
}
public String getPhone() {
return phone;
}
public String getAvatarUrl() {
return avatarUrl;
}
public String getRole() {
return role;
}
public Long getStoreId() {
return storeId;
}
public String getStoreName() {
return storeName;
}
}

View File

@@ -1,407 +1,551 @@
package com.example.petstoremobile.fragments;
import android.app.Activity;
import android.app.Dialog;
import android.content.ContentValues;
import android.content.Intent;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Environment;
import android.provider.MediaStore;
import android.util.Log;
import android.view.*;
import android.widget.*;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.inputmethod.EditorInfo;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
import android.widget.Toast;
import 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.DialogUtils;
import com.example.petstoremobile.utils.FileUtils;
import com.example.petstoremobile.utils.GlideUtils;
import com.example.petstoremobile.utils.Resource;
import com.example.petstoremobile.utils.UIUtils;
import com.example.petstoremobile.viewmodels.ChatListViewModel;
import com.example.petstoremobile.websocket.StompChatManager;
import java.util.*;
import java.util.stream.Collectors;
import retrofit2.*;
import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.List;
import javax.inject.Inject;
import javax.inject.Named;
import dagger.hilt.android.AndroidEntryPoint;
import okhttp3.MediaType;
import okhttp3.MultipartBody;
import okhttp3.RequestBody;
import okhttp3.ResponseBody;
@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 FragmentChatBinding binding;
private ChatListViewModel viewModel;
// Adapters
private ChatAdapter chatAdapter;
private ChatAdapter activeChatAdapter;
private ChatAdapter closedChatAdapter;
private MessageAdapter messageAdapter;
// Data
private final List<Chat> chatList = new ArrayList<>();
private final List<Chat> activeChatList = new ArrayList<>();
private final List<Chat> closedChatList = 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;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
viewModel = new ViewModelProvider(requireActivity()).get(ChatListViewModel.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);
}
}
);
}
@Override
public View onCreateView(@NonNull LayoutInflater inflater,
ViewGroup container, Bundle savedInstanceState) {
binding = FragmentChatBinding.inflate(inflater, container, false);
View view = inflater.inflate(R.layout.fragment_chat, container, false);
binding.btnHamburger.setOnClickListener(v -> binding.chatDrawerLayout.openDrawer(GravityCompat.START));
chatApi = RetrofitClient.getChatApi(requireContext());
customerApi = RetrofitClient.getCustomerApi(requireContext());
messageApi = RetrofitClient.getMessageApi(requireContext());
binding.etMessage.setOnEditorActionListener((v, actionId, event) -> {
if (actionId == EditorInfo.IME_ACTION_SEND || actionId == EditorInfo.IME_NULL) {
binding.btnSend.performClick();
return true;
}
return false;
});
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);
binding.btnSend.setOnClickListener(v -> {
if (pendingAttachmentUri != null) sendWithAttachment(pendingAttachmentUri);
else sendMessage();
});
ImageButton hamburger = view.findViewById(R.id.btnHamburger);
hamburger.setOnClickListener(v -> drawerLayout.openDrawer(GravityCompat.START));
btnSend.setOnClickListener(v -> sendMessage());
binding.btnAttach.setOnClickListener(v -> selectAttachment());
binding.btnRemoveAttachment.setOnClickListener(v -> removeAttachment());
binding.btnCloseChat.setOnClickListener(v -> closeChat());
setupDrawerToggles();
setupRecyclerViews();
observeViewModel();
loadInitialData();
return view;
return binding.getRoot();
}
private void setupDrawerToggles() {
binding.headerActiveChats.setOnClickListener(v -> {
if (binding.rvActiveChats.getVisibility() == View.VISIBLE) {
binding.rvActiveChats.setVisibility(View.GONE);
binding.ivActiveChevron.setImageResource(android.R.drawable.arrow_down_float);
} else {
binding.rvActiveChats.setVisibility(View.VISIBLE);
binding.ivActiveChevron.setImageResource(android.R.drawable.arrow_up_float);
}
});
binding.headerClosedChats.setOnClickListener(v -> {
if (binding.rvClosedChats.getVisibility() == View.VISIBLE) {
binding.rvClosedChats.setVisibility(View.GONE);
binding.ivClosedChevron.setImageResource(android.R.drawable.arrow_down_float);
} else {
binding.rvClosedChats.setVisibility(View.VISIBLE);
binding.ivClosedChevron.setImageResource(android.R.drawable.arrow_up_float);
}
});
}
private void setupRecyclerViews() {
// Set up Drawer menu to select conversation
chatAdapter = new ChatAdapter(chatList, this);
rvChatList.setLayoutManager(new LinearLayoutManager(getContext()));
rvChatList.setAdapter(chatAdapter);
activeChatAdapter = new ChatAdapter(activeChatList, this);
binding.rvActiveChats.setLayoutManager(new LinearLayoutManager(getContext()));
binding.rvActiveChats.setAdapter(activeChatAdapter);
closedChatAdapter = new ChatAdapter(closedChatList, this);
binding.rvClosedChats.setLayoutManager(new LinearLayoutManager(getContext()));
binding.rvClosedChats.setAdapter(closedChatAdapter);
// set up RecyclerView for selected chat to show messages
messageAdapter = new MessageAdapter(messageList, null);
messageAdapter.setBaseUrl(baseUrl);
messageAdapter.setOnAttachmentClickListener(message -> {
if (message.getAttachmentMimeType() != null && message.getAttachmentMimeType().startsWith("image/")) {
showFullScreenImage(message);
} else {
downloadFile(message);
}
});
LinearLayoutManager lm = new LinearLayoutManager(getContext());
lm.setStackFromEnd(true);
rvMessages.setLayoutManager(lm);
rvMessages.setAdapter(messageAdapter);
setConversationActive(false);
binding.rvMessages.setLayoutManager(lm);
binding.rvMessages.setItemAnimator(null);
binding.rvMessages.setAdapter(messageAdapter);
setConversationActive(false, null);
}
private void showFullScreenImage(Message message) {
if (baseUrl == null || message.getId() == null) return;
Dialog dialog = new Dialog(requireContext(), android.R.style.Theme_Black_NoTitleBar_Fullscreen);
dialog.setContentView(R.layout.dialog_full_screen_image);
ImageView imageView = dialog.findViewById(R.id.ivFullScreen);
ImageButton closeButton = dialog.findViewById(R.id.btnClose);
ImageButton downloadButton = dialog.findViewById(R.id.btnDownload);
String cleanBase = baseUrl.endsWith("/") ? baseUrl.substring(0, baseUrl.length() - 1) : baseUrl;
String downloadUrl = cleanBase + "/api/v1/chat/messages/" + message.getId() + "/attachment";
GlideUtils.loadImageWithToken(requireContext(), imageView, downloadUrl, tokenManager.getToken(), R.drawable.placeholder);
closeButton.setOnClickListener(v -> dialog.dismiss());
downloadButton.setOnClickListener(v -> downloadFile(message));
imageView.setOnClickListener(v -> dialog.dismiss());
dialog.show();
}
private void downloadFile(Message message) {
if (message.getId() == null) return;
DialogUtils.showConfirmDialog(requireContext(), "Download Attachment",
"Do you want to download \"" + message.getAttachmentName() + "\"?", () -> {
Toast.makeText(requireContext(), "Downloading " + message.getAttachmentName() + "...", Toast.LENGTH_SHORT).show();
viewModel.downloadAttachment(message.getId()).observe(getViewLifecycleOwner(), resource -> {
if (resource == null) return;
setLoading(resource.status == Resource.Status.LOADING);
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
saveFileToDownloads(resource.data, message.getAttachmentName(), message.getAttachmentMimeType());
} else if (resource.status == Resource.Status.ERROR) {
Toast.makeText(requireContext(), "Download failed: " + resource.message, Toast.LENGTH_SHORT).show();
}
});
});
}
private void saveFileToDownloads(ResponseBody body, String fileName, String mimeType) {
android.os.Handler mainHandler = new android.os.Handler(android.os.Looper.getMainLooper());
new Thread(() -> {
try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
ContentValues values = new ContentValues();
values.put(MediaStore.Downloads.DISPLAY_NAME, fileName);
values.put(MediaStore.Downloads.MIME_TYPE, mimeType);
values.put(MediaStore.Downloads.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS);
Uri uri = requireContext().getContentResolver().insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, values);
if (uri != null) {
try (OutputStream outputStream = requireContext().getContentResolver().openOutputStream(uri);
InputStream inputStream = body.byteStream()) {
byte[] buffer = new byte[4096];
int bytesRead;
while ((bytesRead = inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, bytesRead);
}
}
mainHandler.post(() -> Toast.makeText(requireContext(), "File saved to Downloads", Toast.LENGTH_SHORT).show());
}
} else {
File downloadsDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS);
File file = new File(downloadsDir, fileName);
try (OutputStream outputStream = new FileOutputStream(file);
InputStream inputStream = body.byteStream()) {
byte[] buffer = new byte[4096];
int bytesRead;
while ((bytesRead = inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, bytesRead);
}
}
mainHandler.post(() -> Toast.makeText(requireContext(), "File saved to Downloads: " + file.getAbsolutePath(), Toast.LENGTH_LONG).show());
}
body.close();
} catch (Exception e) {
Log.e(TAG, "Error saving file", e);
mainHandler.post(() -> Toast.makeText(requireContext(), "Error saving file", Toast.LENGTH_SHORT).show());
}
}).start();
}
private void observeViewModel() {
viewModel.getActiveChats().observe(getViewLifecycleOwner(), list -> {
activeChatList.clear();
activeChatList.addAll(list);
activeChatAdapter.notifyDataSetChanged();
updateTitleAndStateIfActive(list);
});
viewModel.getClosedChats().observe(getViewLifecycleOwner(), list -> {
closedChatList.clear();
closedChatList.addAll(list);
closedChatAdapter.notifyDataSetChanged();
updateTitleAndStateIfActive(list);
});
viewModel.getMessageList().observe(getViewLifecycleOwner(), list -> {
int prevSize = messageList.size();
messageList.clear();
messageList.addAll(list);
if (prevSize > 0 && list.size() == prevSize + 1) {
messageAdapter.notifyItemInserted(list.size() - 1);
} else {
messageAdapter.notifyDataSetChanged();
}
scrollToBottom();
});
viewModel.getIsLoading().observe(getViewLifecycleOwner(), this::setLoading);
}
private void setLoading(boolean loading) {
if (binding != null && binding.progressBar != null) {
binding.progressBar.setVisibility(loading ? View.VISIBLE : View.GONE);
}
}
private void updateTitleAndStateIfActive(List<Chat> list) {
if (activeConversationId != null) {
for (Chat chat : list) {
if (chat.getChatId().equals(String.valueOf(activeConversationId))) {
binding.tvChatTitle.setText(chat.getCustomerName());
setConversationActive(true, chat.getStatus());
break;
}
}
}
}
//Helper function to load token and user id then connect to websocket
private void loadInitialData() {
TokenManager tm = TokenManager.getInstance(requireContext());
String token = tm.getToken();
currentUserId = tm.getUserId();
String role = tm.getRole();
String token = tokenManager.getToken();
Long 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);
stompChatManager.connect();
} else {
Log.e(TAG, "No token found");
}
loadCustomers();
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");
} else {
activeConversationId = viewModel.getLastActiveConversationId();
}
viewModel.loadCustomers();
if (activeConversationId != null) {
if (stompChatManager != null) stompChatManager.subscribeToConversation(activeConversationId);
viewModel.loadMessageHistory(activeConversationId);
} else {
setConversationActive(false, null);
}
}
//Helper function to load customer names for it to be displayed on drawer menu
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) {
loadConversations();
}
});
}
//helper function to load conversations entities to display with customer names in drawer menu
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) {
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
@Override
public void onChatClick(Chat chat) {
activeConversationId = Long.parseLong(chat.getChatId());
setConversationActive(true);
drawerLayout.closeDrawer(GravityCompat.START);
viewModel.setLastActiveConversationId(activeConversationId);
if (stompChatManager != null) {
stompChatManager.subscribeToConversation(activeConversationId);
}
setConversationActive(true, chat.getStatus());
binding.tvChatTitle.setText(chat.getCustomerName());
binding.chatDrawerLayout.closeDrawer(GravityCompat.START);
loadMessageHistory(activeConversationId);
if (stompChatManager != null) stompChatManager.subscribeToConversation(activeConversationId);
viewModel.loadMessageHistory(activeConversationId);
}
//helper function to load messages for selected chat
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();
private void closeChat() {
if (activeConversationId == null) return;
DialogUtils.showConfirmDialog(requireContext(), "Close Chat",
"Are you sure you want to close this chat? This will notify the customer.", () -> {
viewModel.sendMessage(activeConversationId, "The Chat has been closed").observe(getViewLifecycleOwner(), resource -> {
if (resource != null && resource.status == Resource.Status.SUCCESS) {
viewModel.addMessageLocally(resource.data);
viewModel.closeConversation(activeConversationId).observe(getViewLifecycleOwner(), statusResource -> {
if (statusResource == null) return;
setLoading(statusResource.status == Resource.Status.LOADING);
if (statusResource.status == Resource.Status.SUCCESS) {
viewModel.loadConversations();
setConversationActive(true, "CLOSED");
} else if (statusResource.status == Resource.Status.ERROR) {
Toast.makeText(requireContext(), "Failed to close chat: " + statusResource.message, Toast.LENGTH_SHORT).show();
}
});
}
}
@Override
public void onFailure(@NonNull Call<List<MessageDTO>> call,
@NonNull Throwable t) {
Log.e(TAG, "Error loading messages", t);
});
});
}
private void sendMessage() {
if (activeConversationId == null) return;
String text = binding.etMessage.getText().toString().trim();
if (text.isEmpty()) return;
binding.etMessage.setText("");
viewModel.sendMessage(activeConversationId, text).observe(getViewLifecycleOwner(), resource -> {
if (resource == null) return;
setLoading(resource.status == Resource.Status.LOADING);
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
viewModel.addMessageLocally(resource.data);
viewModel.loadConversations();
}
});
}
//Helper function to send a message to the chat
private void sendMessage() {
//check if a chat is selected
private void selectAttachment() {
Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
intent.setType("*/*");
attachmentLauncher.launch(intent);
}
private void showAttachmentPreview(Uri uri) {
pendingAttachmentUri = uri;
binding.layoutAttachmentPreview.setVisibility(View.VISIBLE);
String mimeType = requireContext().getContentResolver().getType(uri);
binding.tvPreviewName.setText(FileUtils.getFileName(requireContext(), uri));
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);
}
}
private void removeAttachment() {
pendingAttachmentUri = null;
binding.layoutAttachmentPreview.setVisibility(View.GONE);
}
private void sendWithAttachment(Uri uri) {
if (activeConversationId == null) return;
//get the message from text field
String text = etMessage.getText().toString().trim();
if (text.isEmpty()) return;
//clear text field after sending
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);
}
});
}
// When a message is received updates the chat preview
@Override
public void onMessageReceived(MessageDTO dto) {
//if there is no active selected conversation or the message received is for another chat, then just update the preview of last message
if (activeConversationId == null || !activeConversationId.equals(dto.getConversationId())) {
updateConversationPreview(dto.getConversationId(), dto.getContent());
File file = FileUtils.getFileFromUri(requireContext(), uri);
if (file == null) {
Toast.makeText(requireContext(), "Failed to prepare file", Toast.LENGTH_SHORT).show();
return;
}
updateConversationPreview(dto.getConversationId(), dto.getContent());
if (currentUserId != null && currentUserId.equals(dto.getSenderId())) return;
String text = binding.etMessage.getText().toString().trim();
//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();
MultipartBody.Part contentPart = text.isEmpty()
? null
: MultipartBody.Part.createFormData("content", text);
String mimeType = requireContext().getContentResolver().getType(uri);
if (mimeType == null) mimeType = "application/octet-stream";
RequestBody filePartBody = RequestBody.create(file, MediaType.parse(mimeType));
MultipartBody.Part filePart = MultipartBody.Part.createFormData("file", file.getName(), filePartBody);
binding.etMessage.setText("");
removeAttachment();
viewModel.sendMessageWithAttachment(activeConversationId, contentPart, filePart).observe(getViewLifecycleOwner(), resource -> {
if (resource == null) return;
setLoading(resource.status == Resource.Status.LOADING);
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
viewModel.addMessageLocally(resource.data);
viewModel.loadConversations();
} else if (resource.status == Resource.Status.ERROR) {
Toast.makeText(requireContext(), "Failed to send attachment: " + resource.message, Toast.LENGTH_SHORT).show();
}
});
}
@Override
public void onMessageReceived(MessageDTO dto) {
requireActivity().runOnUiThread(() -> {
if (activeConversationId != null && activeConversationId.equals(dto.getConversationId())) {
if (!tokenManager.getUserId().equals(dto.getSenderId())) {
viewModel.addMessageLocally(dto);
}
}
viewModel.updateConversationLocally(new ConversationDTO(dto.getConversationId(), 0L, 0L, dto.getContent(), ""));
});
}
// When a new conversation is added, updates the chat preview
@Override
public void onConversationUpdated(ConversationDTO dto) {
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(
String.valueOf(dto.getId()),
name,
dto.getLastMessage(),
dto.getCustomerId(),
dto.getStaffId()
));
chatAdapter.notifyItemChanged(i);
updated = true;
break;
requireActivity().runOnUiThread(() -> {
viewModel.updateConversationLocally(dto);
if (activeConversationId != null && activeConversationId.equals(dto.getId())) {
setConversationActive(true, dto.getStatus());
binding.tvChatTitle.setText(viewModel.getCustomerName(dto.getCustomerId()));
}
}
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);
}
});
}
@Override
public void onSocketOpened() {
if (!isAdded()) {
return;
}
loadConversations();
if (activeConversationId != null) {
loadMessageHistory(activeConversationId);
}
if (!isAdded()) return;
requireActivity().runOnUiThread(() -> {
viewModel.loadConversations();
if (activeConversationId != null) viewModel.loadMessageHistory(activeConversationId);
});
}
@Override
public void onSocketClosed() {
if (!isAdded()) {
return;
}
loadConversations();
if (!isAdded()) return;
requireActivity().runOnUiThread(viewModel::loadConversations);
}
@Override
public void onSocketError() {
if (!isAdded()) {
return;
}
loadConversations();
if (activeConversationId != null) {
loadMessageHistory(activeConversationId);
}
if (!isAdded()) return;
requireActivity().runOnUiThread(() -> {
viewModel.loadConversations();
if (activeConversationId != null) viewModel.loadMessageHistory(activeConversationId);
});
}
// Helper function to convert DTO to message
private Message dtoToModel(MessageDTO dto) {
Message m = new Message();
m.setId(dto.getId());
m.setConversationId(dto.getConversationId());
m.setSenderId(dto.getSenderId());
m.setContent(dto.getContent());
m.setTimestamp(dto.getTimestamp());
m.setIsRead(dto.getIsRead());
return m;
}
//Helper function to scroll to bottom of the chat
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
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;
}
}
}
//Helper function to enable or disable the send button when there is no active chat
private void setConversationActive(boolean active) {
btnSend.setEnabled(active);
etMessage.setEnabled(active);
private void setConversationActive(boolean active, String status) {
boolean isClosed = "CLOSED".equalsIgnoreCase(status);
UIUtils.setViewsEnabled(active && !isClosed, binding.btnSend, binding.etMessage, binding.btnAttach);
binding.btnCloseChat.setVisibility(active && !isClosed ? View.VISIBLE : View.GONE);
if (!active) {
activeConversationId = null;
if (stompChatManager != null) {
stompChatManager.clearConversationSubscription();
}
ChatNotificationService.activeConversationIdInUi = null;
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(isClosed ? "This chat is closed" : "Type a message...");
ChatNotificationService.activeConversationIdInUi = activeConversationId;
}
}
// When fragment is destroyed, disconnect from websocket
@Override
public void onDestroyView() {
super.onDestroyView();
binding = null;
ChatNotificationService.activeConversationIdInUi = null;
if (stompChatManager != null) stompChatManager.disconnect();
}
}

View File

@@ -2,77 +2,72 @@ 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.databinding.FragmentListBinding;
import com.example.petstoremobile.fragments.listfragments.PetFragment;
import com.example.petstoremobile.fragments.listfragments.ServiceFragment;
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 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;
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);
binding = FragmentListBinding.inflate(inflater, container, false);
//get controls from the layout
drawerLayout = view.findViewById(R.id.drawerLayout);
drawerPets = view.findViewById(R.id.drawerPets);
drawerServices = view.findViewById(R.id.drawerServices);
drawerSuppliers = view.findViewById(R.id.drawerSuppliers);
drawerAdoptions = view.findViewById(R.id.drawerAdoptions);
drawerAppointments = view.findViewById(R.id.drawerAppointments);
drawerInventory = view.findViewById(R.id.drawerInventory);
drawerProducts = view.findViewById(R.id.drawerProducts);
//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());
// Check user role and restrict access for STAFF
String role = tokenManager.getRole();
if ("STAFF".equalsIgnoreCase(role)) {
binding.sectionAdmin.setVisibility(View.GONE);
} else if ("ADMIN".equalsIgnoreCase(role)) {
binding.sectionAdmin.setVisibility(View.VISIBLE);
binding.drawerStaff.setVisibility(View.VISIBLE);
} else {
// Default or other roles
binding.sectionAdmin.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
@@ -83,63 +78,58 @@ 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.drawerCustomers.setOnClickListener(v -> navigateTo(R.id.nav_customer));
binding.drawerAnalytics.setOnClickListener(v -> navigateTo(R.id.nav_analytics));
binding.drawerCoupons.setOnClickListener(v -> navigateTo(R.id.nav_coupon));
binding.drawerActivityLogs.setOnClickListener(v -> navigateTo(R.id.nav_activity_log));
//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();
});
//Appoinment
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();
});
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);
binding.drawerLayout.openDrawer(GravityCompat.START);
}
// helper function to load the fragment into the display
public void loadFragment(Fragment fragment) {
getChildFragmentManager()
.beginTransaction()
.replace(R.id.inner_fragment_container, fragment)
.addToBackStack(null)
.commit();
}
}
}

View File

@@ -1,165 +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.example.petstoremobile.R;
import com.example.petstoremobile.activities.MainActivity;
import com.example.petstoremobile.api.auth.AuthApi;
import com.example.petstoremobile.api.auth.TokenManager;
import com.example.petstoremobile.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.example.petstoremobile.utils.Resource;
import com.example.petstoremobile.utils.UIUtils;
import com.example.petstoremobile.viewmodels.AuthViewModel;
import java.io.File;
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;
/**
* 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 Button btnChangePhoto, btnEditEmail, btnEditPhone, btnLogout;
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();
imgProfile.setImageURI(selectedImage);
//TODO: SAVE CHANGED PHOTO TO DATABASE
}
}
);
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) {
//Clear the old image and set the new one
imgProfile.setImageURI(null);
imgProfile.setImageURI(photoUri);
//TODO: SAVE CHANGED PHOTO TO DATABASE
}
}
);
// 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();
}
});
}
//TODO: MAKE PROFILE VIEW DISPLAY PROFILE DATA FROM DATABASE
/**
* 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);
binding = FragmentProfileBinding.inflate(inflater, container, false);
//get all the controls from the view
imgProfile = view.findViewById(R.id.imgProfile);
tvProfileName = view.findViewById(R.id.tvProfileName);
tvProfileEmail = view.findViewById(R.id.tvProfileEmail);
tvProfilePhone = view.findViewById(R.id.tvProfilePhone);
tvProfileRole = view.findViewById(R.id.tvProfileRole);
btnChangePhoto = view.findViewById(R.id.btnChangePhoto);
btnEditEmail = view.findViewById(R.id.btnEditEmail);
btnEditPhone = view.findViewById(R.id.btnEditPhone);
btnLogout = view.findViewById(R.id.btnLogout);
//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();
//TODO: UPDATE PHOTO IN DATABASE
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);
@@ -170,19 +114,10 @@ public class ProfileFragment extends Fragment {
.setTitle("Edit Email")
.setView(input)
.setPositiveButton("Save", (dialog, which) -> {
String newEmail = input.getText().toString();
//if the new value is a valid email then set the email to the new value
if (android.util.Patterns.EMAIL_ADDRESS.matcher(newEmail).matches()) {
tvProfileEmail.setText(newEmail);
//TODO: UPDATE THE EMAIL IN DATABASE
}
else {
//tell the user to email is invalid
new AlertDialog.Builder(requireContext())
.setTitle("Error")
.setMessage("Email is invalid")
.setPositiveButton("OK", null)
.show();
if (InputValidator.isValidEmail(input)) {
updateProfileField("email", input.getText().toString());
} else {
Toast.makeText(requireContext(), "Email is invalid", Toast.LENGTH_SHORT).show();
}
})
.setNegativeButton("Cancel", null)
@@ -191,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
@@ -210,19 +144,10 @@ public class ProfileFragment extends Fragment {
.setTitle("Edit Phone Number")
.setView(input)
.setPositiveButton("Save", (dialog, which) -> {
String newPhone = input.getText().toString();
//if the new value is format: (XXX) XXX-XXXX then set the phone to the new value
if (newPhone.matches("\\(\\d{3}\\) \\d{3}-\\d{4}")) { //TODO MAKE VALIDATION CLASS INSTEAD FOR THIS
tvProfilePhone.setText(newPhone);
//TODO: UPDATE PHONE IN DATABASE
}
else {
//tell the user to email cannot be empty
new AlertDialog.Builder(requireContext())
.setTitle("Error")
.setMessage("Phone number is invalid. Format: (XXX) XXX-XXXX")
.setPositiveButton("OK", null)
.show();
if (InputValidator.isValidPhone(input)) {
updateProfileField("phone", input.getText().toString());
} else {
Toast.makeText(requireContext(), "Phone number is invalid", Toast.LENGTH_SHORT).show();
}
})
.setNegativeButton("Cancel", null)
@@ -230,26 +155,137 @@ public class ProfileFragment extends Fragment {
});
//Logout button
btnLogout.setOnClickListener(v -> {
TokenManager.getInstance(requireContext()).clearLoginData(); // clear the token for next login
binding.btnLogout.setOnClickListener(v -> {
// Stop notification service before logging out so notifications stop
android.content.Intent serviceIntent = new android.content.Intent(requireContext(), ChatNotificationService.class);
requireContext().stopService(serviceIntent);
// clear the token for next login
tokenManager.clearLoginData();
//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);
private void setLoading(boolean loading) {
if (binding != null && binding.progressBar != null) {
binding.progressBar.setVisibility(loading ? View.VISIBLE : View.GONE);
}
}
}
@Override
public void onDestroyView() {
super.onDestroyView();
binding = null;
}
/**
* Fetches current user profile data from the API and then updates the UI.
*/
private void loadProfileData() {
viewModel.getMe().observe(getViewLifecycleOwner(), resource -> {
if (resource == null) return;
setLoading(resource.status == Resource.Status.LOADING);
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
currentUser = resource.data;
//set the user data to the view
binding.tvProfileName.setText(currentUser.getFullName());
binding.tvProfileEmail.setText(currentUser.getEmail());
binding.tvProfilePhone.setText(currentUser.getPhone());
binding.tvProfileRole.setText(currentUser.getRole());
// get the avatar endpoint to load profile image and the token for authorization
String avatarUrl = baseUrl + AuthApi.AVATAR_FILE_PATH;
String token = tokenManager.getToken();
GlideUtils.loadImageWithToken(requireContext(), binding.imgProfile, avatarUrl, token, R.drawable.placeholder, new GlideUtils.ImageLoadListener() {
@Override
public void onResourceReady() {
hasImage = true;
}
@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();
}
});
}
/**
* Uploads the selected or captured image as the user's new avatar.
*/
private void uploadAvatar(Uri uri) {
try {
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("avatar", file.getName(), requestFile);
//Call the backend to upload the avatar
viewModel.uploadAvatar(body).observe(getViewLifecycleOwner(), resource -> {
if (resource == null) return;
setLoading(resource.status == Resource.Status.LOADING);
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) {
Log.e("UPLOAD_AVATAR", "Error: " + e.getMessage());
}
}
/**
* 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;
setLoading(resource.status == Resource.Status.LOADING);
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();
}
});
}
/**
* Updates a specific profile field (like email or phone) by sending a request to the API.
*/
private void updateProfileField(String fieldName, String value) {
Map<String, String> updates = new HashMap<>();
updates.put(fieldName, value);
viewModel.updateMe(updates).observe(getViewLifecycleOwner(), resource -> {
if (resource == null) return;
setLoading(resource.status == Resource.Status.LOADING);
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
currentUser = resource.data;
Toast.makeText(getContext(), "Profile updated successfully", Toast.LENGTH_SHORT).show();
// Update the view with the new data from backend
binding.tvProfileEmail.setText(currentUser.getEmail());
binding.tvProfilePhone.setText(currentUser.getPhone());
} else if (resource.status == Resource.Status.ERROR) {
Toast.makeText(getContext(), "Update failed: " + resource.message, Toast.LENGTH_SHORT).show();
}
});
}
}

View File

@@ -0,0 +1,122 @@
package com.example.petstoremobile.fragments.listfragments;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.ViewModelProvider;
import androidx.recyclerview.widget.LinearLayoutManager;
import android.widget.Toast;
import androidx.navigation.fragment.NavHostFragment;
import com.example.petstoremobile.adapters.ActivityLogAdapter;
import com.example.petstoremobile.api.auth.TokenManager;
import com.example.petstoremobile.databinding.FragmentActivityLogBinding;
import com.example.petstoremobile.dtos.ActivityLogDTO;
import com.example.petstoremobile.dtos.DropdownDTO;
import com.example.petstoremobile.utils.SpinnerUtils;
import com.example.petstoremobile.utils.UIUtils;
import com.example.petstoremobile.viewmodels.ActivityLogListViewModel;
import javax.inject.Inject;
import java.util.ArrayList;
import java.util.List;
import dagger.hilt.android.AndroidEntryPoint;
@AndroidEntryPoint
public class ActivityLogFragment extends Fragment {
private FragmentActivityLogBinding binding;
private ActivityLogListViewModel viewModel;
private ActivityLogAdapter adapter;
private final List<ActivityLogDTO> logList = new ArrayList<>();
private List<DropdownDTO> storeList = new ArrayList<>();
@Inject TokenManager tokenManager;
@Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
binding = FragmentActivityLogBinding.inflate(inflater, container, false);
if (!"ADMIN".equalsIgnoreCase(tokenManager.getRole())) {
Toast.makeText(requireContext(), "Access denied", Toast.LENGTH_SHORT).show();
NavHostFragment.findNavController(this).popBackStack();
return binding.getRoot();
}
viewModel = new ViewModelProvider(this).get(ActivityLogListViewModel.class);
setupRecyclerView();
setupFilters();
observeViewModel();
binding.swipeRefreshActivityLog.setOnRefreshListener(() -> viewModel.loadInitialData());
UIUtils.setupHamburgerMenu(binding.btnHamburgerActivityLog, this);
return binding.getRoot();
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
viewModel.loadInitialData();
}
private void setupRecyclerView() {
adapter = new ActivityLogAdapter(logList);
binding.recyclerViewActivityLog.setLayoutManager(new LinearLayoutManager(getContext()));
binding.recyclerViewActivityLog.setAdapter(adapter);
}
private void setupFilters() {
UIUtils.setupFilterToggle(binding.btnToggleFilter, binding.layoutFilter,
binding.etSearchLog, binding.spinnerRoleFilter, binding.spinnerStoreFilter);
UIUtils.attachSearch(binding.etSearchLog, () ->
viewModel.setSearchQuery(binding.etSearchLog.getText().toString()));
String[] roles = {"All Roles", "Admin", "Staff", "Customer"};
SpinnerUtils.setupStringFilterSpinner(requireContext(), binding.spinnerRoleFilter, roles, () ->
viewModel.setRoleFilter(binding.spinnerRoleFilter.getSelectedItem() != null
? binding.spinnerRoleFilter.getSelectedItem().toString() : "All Roles"));
SpinnerUtils.setupFilterSpinner(binding.spinnerStoreFilter, this::onStoreSelected);
}
private void onStoreSelected() {
int pos = binding.spinnerStoreFilter.getSelectedItemPosition();
Long storeId = (pos > 0 && !storeList.isEmpty()) ? storeList.get(pos - 1).getId() : null;
viewModel.setStoreFilter(storeId);
}
private void observeViewModel() {
viewModel.getLogs().observe(getViewLifecycleOwner(), list -> {
logList.clear();
logList.addAll(list);
adapter.notifyDataSetChanged();
});
viewModel.getStoreOptions().observe(getViewLifecycleOwner(), stores -> {
storeList = stores;
SpinnerUtils.populateWhiteSpinner(requireContext(), binding.spinnerStoreFilter,
stores, DropdownDTO::getLabel, "All Stores", -1L, DropdownDTO::getId);
});
viewModel.getIsLoading().observe(getViewLifecycleOwner(), loading ->
binding.swipeRefreshActivityLog.setRefreshing(loading));
}
@Override
public void onDestroyView() {
super.onDestroyView();
binding = null;
}
}

View File

@@ -1,163 +1,260 @@
package com.example.petstoremobile.fragments.listfragments;
// Added search/filter bar to filter adoptions by adopter name or pet name.
// Added pull-to-refresh using SwipeRefreshLayout.
import android.graphics.Color;
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 com.example.petstoremobile.R;
import com.example.petstoremobile.adapters.AdoptionAdapter;
import com.example.petstoremobile.fragments.ListFragment;
import com.example.petstoremobile.fragments.listfragments.detailfragments.AdoptionDetailFragment;
import com.example.petstoremobile.models.Adoption;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
import java.util.ArrayList;
import java.util.List;
import com.example.petstoremobile.api.auth.TokenManager;
import com.example.petstoremobile.databinding.FragmentAdoptionBinding;
import com.example.petstoremobile.dtos.AdoptionDTO;
import com.example.petstoremobile.dtos.StoreDTO;
import com.example.petstoremobile.utils.BulkDeleteHandler;
import com.example.petstoremobile.utils.Resource;
import com.example.petstoremobile.utils.SpinnerUtils;
import com.example.petstoremobile.utils.UIUtils;
import com.example.petstoremobile.viewmodels.AdoptionListViewModel;
import com.example.petstoremobile.utils.EventDecorator;
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 javax.inject.Inject;
import dagger.hilt.android.AndroidEntryPoint;
@AndroidEntryPoint
public class AdoptionFragment extends Fragment implements AdoptionAdapter.OnAdoptionClickListener {
private List<Adoption> adoptionList = new ArrayList<>();
private List<Adoption> filteredList = new ArrayList<>();
private FragmentAdoptionBinding binding;
private List<AdoptionDTO> adoptionList = new ArrayList<>();
private AdoptionAdapter adapter;
private SwipeRefreshLayout swipeRefreshLayout;
private EditText etSearch;
private ImageButton hamburger;
private AdoptionListViewModel viewModel;
private BulkDeleteHandler bulkDeleteHandler;
private CalendarDay selectedCalendarDay;
private boolean isMonthMode = false;
private final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd", Locale.getDefault());
@Inject TokenManager tokenManager;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
viewModel = new ViewModelProvider(this).get(AdoptionListViewModel.class);
}
@Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_adoption, container, false);
binding = FragmentAdoptionBinding.inflate(inflater, container, false);
hamburger = view.findViewById(R.id.btnHamburger);
setupRecyclerView();
setupSearch();
setupStatusFilter();
setupStoreFilter();
setupSwipeRefresh();
setupCalendar();
setupFilterToggle();
setupBulkDelete();
observeViewModel();
loadAdoptionData();
// Replace with actual API call when backend is ready
setupRecyclerView(view);
setupSearch(view);
setupSwipeRefresh(view);
binding.fabAddAdoption.setOnClickListener(v -> openDetail(-1));
FloatingActionButton fabAddAdoption = view.findViewById(R.id.fabAddAdoption);
fabAddAdoption.setOnClickListener(v -> openAdoptionDetails(-1));
UIUtils.setupHamburgerMenu(binding.btnHamburgerAdoption, this);
//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.btnToggleCalendarModeAdoption.setOnClickListener(v -> toggleCalendarMode());
return view;
return binding.getRoot();
}
// Filters adoption list by adopter name or pet name
private void setupSearch(View view) {
etSearch = view.findViewById(R.id.etSearchAdoption);
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) {
filterAdoptions(s.toString());
}
@Override public void afterTextChanged(Editable s) {}
private void observeViewModel() {
viewModel.getAdoptions().observe(getViewLifecycleOwner(), list -> {
adoptionList.clear();
adoptionList.addAll(list);
updateCalendarDecorators();
adapter.notifyDataSetChanged();
});
viewModel.getStores().observe(getViewLifecycleOwner(), list -> {
SpinnerUtils.populateWhiteSpinner(requireContext(), binding.spinnerStoreAdoption, list,
StoreDTO::getStoreName, "All Stores", -1L, StoreDTO::getStoreId);
});
viewModel.getIsLoading().observe(getViewLifecycleOwner(), loading -> {
binding.swipeRefreshAdoption.setRefreshing(loading);
});
}
private void filterAdoptions(String query) {
filteredList.clear();
if (query.isEmpty()) {
filteredList.addAll(adoptionList);
} else {
String lower = query.toLowerCase();
for (Adoption a : adoptionList) {
if (a.getAdopterName().toLowerCase().contains(lower)
|| a.getPetName().toLowerCase().contains(lower)
|| a.getStatus().toLowerCase().contains(lower)) {
filteredList.add(a);
}
}
}
adapter.notifyDataSetChanged();
}
private void setupSwipeRefresh(View view) {
swipeRefreshLayout = view.findViewById(R.id.swipeRefreshAdoption);
swipeRefreshLayout.setOnRefreshListener(() -> {
loadAdoptionData(); // TODO: Replace with actual API call
filterAdoptions(etSearch.getText().toString());
swipeRefreshLayout.setRefreshing(false);
});
}
private void openAdoptionDetails(int position) {
AdoptionDetailFragment detailFragment = new AdoptionDetailFragment();
Bundle args = new Bundle();
args.putInt("position", position);
if (position != -1) {
Adoption adoption = filteredList.get(position);
int realPosition = adoptionList.indexOf(adoption);
args.putInt("position", realPosition);
args.putInt("adoptionId", adoption.getAdoptionId());
args.putString("adopterName", adoption.getAdopterName());
args.putString("adopterEmail", adoption.getAdopterEmail());
args.putString("adopterPhone", adoption.getAdopterPhone());
args.putString("petName", adoption.getPetName());
args.putString("adoptionDate", adoption.getAdoptionDate());
args.putString("status", adoption.getStatus());
}
detailFragment.setArguments(args);
detailFragment.setAdoptionFragment(this);
ListFragment listFragment = (ListFragment) getParentFragment();
if (listFragment != null) listFragment.loadFragment(detailFragment);
}
public void onAdoptionSaved(int position, Adoption adoption) {
if (position == -1) {
adoptionList.add(adoption);
} else {
adoptionList.set(position, adoption);
}
filterAdoptions(etSearch.getText().toString());
}
public void onAdoptionDeleted(int position) {
adoptionList.remove(position);
filterAdoptions(etSearch.getText().toString());
private void setupBulkDelete() {
bulkDeleteHandler = new BulkDeleteHandler(
this,
binding.layoutBulkDelete,
binding.tvSelectionCount,
binding.btnBulkDelete,
adapter,
"adoption",
viewModel::bulkDeleteAdoptions,
this::loadAdoptions
);
}
@Override
public void onAdoptionClick(int position) {
openAdoptionDetails(position);
public void onResume() {
super.onResume();
loadAdoptions();
if (!isStaff()) viewModel.loadStores();
}
private void loadAdoptionData() {
adoptionList.clear();
adoptionList.add(new Adoption(1, "Sarah Connor", "sarah@email.com", "555-1234", "Luna", "2026-03-01", "Approved"));
adoptionList.add(new Adoption(2, "Tom Hardy", "tom@email.com", "555-5678", "Bella", "2026-03-05", "Pending"));
adoptionList.add(new Adoption(3, "Emily Clark", "emily@email.com", "555-9012", "Charlie", "2026-03-07", "Pending"));
adoptionList.add(new Adoption(4, "Mike Ross", "mike@email.com", "555-3456", "Milo", "2026-02-20", "Rejected"));
filteredList.clear();
filteredList.addAll(adoptionList);
private void toggleCalendarMode() {
isMonthMode = !isMonthMode;
binding.calendarViewAdoption.state().edit()
.setCalendarDisplayMode(isMonthMode ? CalendarMode.MONTHS : CalendarMode.WEEKS)
.commit();
}
private void setupRecyclerView(View view) {
RecyclerView recyclerView = view.findViewById(R.id.recyclerViewAdoptions);
adapter = new AdoptionAdapter(filteredList, this);
recyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
recyclerView.setAdapter(adapter);
private void setupFilterToggle() {
if (isStaff()) {
UIUtils.setupFilterToggle(binding.btnToggleFilterAdoption, binding.layoutFilterAdoption,
binding.etSearchAdoption, binding.spinnerStatusAdoption);
binding.spinnerStoreAdoption.setVisibility(View.GONE);
} else {
UIUtils.setupFilterToggle(binding.btnToggleFilterAdoption, binding.layoutFilterAdoption,
binding.etSearchAdoption, binding.spinnerStatusAdoption, binding.spinnerStoreAdoption);
}
}
private boolean isStaff() {
return "STAFF".equalsIgnoreCase(tokenManager.getRole());
}
private void setupCalendar() {
binding.calendarViewAdoption.setOnDateChangedListener((widget, date, selected) -> {
if (selected) {
if (date.equals(selectedCalendarDay)) {
selectedCalendarDay = null;
binding.calendarViewAdoption.clearSelection();
} else {
selectedCalendarDay = date;
}
} else {
selectedCalendarDay = null;
}
loadAdoptions();
});
}
private void updateCalendarDecorators() {
HashSet<CalendarDay> datesWithAdoptions = new HashSet<>();
for (AdoptionDTO adoption : adoptionList) {
try {
if (adoption.getAdoptionDate() != null) {
Date date = dateFormat.parse(adoption.getAdoptionDate());
if (date != null) {
Calendar cal = Calendar.getInstance();
cal.setTime(date);
datesWithAdoptions.add(CalendarDay.from(cal.get(Calendar.YEAR), cal.get(Calendar.MONTH) + 1, cal.get(Calendar.DAY_OF_MONTH)));
}
}
} catch (ParseException e) {
Log.e("AdoptionFragment", "Error parsing date: " + adoption.getAdoptionDate());
}
}
binding.calendarViewAdoption.removeDecorators();
binding.calendarViewAdoption.addDecorator(new EventDecorator(Color.RED, datesWithAdoptions));
}
private void setupRecyclerView() {
adapter = new AdoptionAdapter(adoptionList, this);
binding.recyclerViewAdoptions.setLayoutManager(new LinearLayoutManager(getContext()));
binding.recyclerViewAdoptions.setAdapter(adapter);
}
private void setupSearch() {
UIUtils.attachSearch(binding.etSearchAdoption, this::loadAdoptions);
}
private void setupStatusFilter() {
String[] statuses = {"All Statuses", "Completed", "Pending", "Missed", "Cancelled"};
SpinnerUtils.setupStringFilterSpinner(requireContext(), binding.spinnerStatusAdoption, statuses, this::loadAdoptions);
}
private void setupStoreFilter() {
SpinnerUtils.setupFilterSpinner(binding.spinnerStoreAdoption, this::loadAdoptions);
}
private void setupSwipeRefresh() {
binding.swipeRefreshAdoption.setOnRefreshListener(this::loadAdoptions);
}
private void loadAdoptions() {
String query = binding.etSearchAdoption.getText().toString().trim();
String status = binding.spinnerStatusAdoption.getSelectedItem() != null ? binding.spinnerStatusAdoption.getSelectedItem().toString() : "All Statuses";
Long storeId;
if (isStaff()) {
storeId = tokenManager.getPrimaryStoreId();
} else {
storeId = null;
List<StoreDTO> stores = viewModel.getStores().getValue();
if (binding.spinnerStoreAdoption.getSelectedItemPosition() > 0 && stores != null && !stores.isEmpty()) {
storeId = stores.get(binding.spinnerStoreAdoption.getSelectedItemPosition() - 1).getStoreId();
}
}
String selectedDateString = null;
if (selectedCalendarDay != null) {
selectedDateString = String.format(Locale.getDefault(), "%04d-%02d-%02d",
selectedCalendarDay.getYear(), selectedCalendarDay.getMonth(), selectedCalendarDay.getDay());
}
if (status.equals("All Statuses")) status = null;
else status = status.toUpperCase();
viewModel.loadAdoptions(true, query, status, storeId, selectedDateString, null);
}
private void openDetail(int position) {
Bundle args = new Bundle();
if (position != -1) {
AdoptionDTO a = adoptionList.get(position);
args.putLong("adoptionId", a.getAdoptionId());
}
NavHostFragment.findNavController(this).navigate(R.id.nav_adoption_detail, args);
}
@Override
public void onAdoptionClick(int position) { openDetail(position); }
@Override
public void onSelectionChanged(int selectedCount) {
if (bulkDeleteHandler != null) {
bulkDeleteHandler.onSelectionChanged(selectedCount);
}
}
@Override
public void onDestroyView() {
super.onDestroyView();
binding = null;
}
}

View File

@@ -0,0 +1,379 @@
package com.example.petstoremobile.fragments.listfragments;
import android.graphics.Color;
import android.os.Bundle;
import android.view.*;
import android.widget.*;
import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.ViewModelProvider;
import com.example.petstoremobile.api.auth.TokenManager;
import com.example.petstoremobile.databinding.FragmentAnalyticsBinding;
import com.example.petstoremobile.utils.SpinnerUtils;
import com.example.petstoremobile.utils.UIUtils;
import com.example.petstoremobile.viewmodels.AnalyticsViewModel;
import dagger.hilt.android.AndroidEntryPoint;
import javax.inject.Inject;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.*;
@AndroidEntryPoint
public class AnalyticsFragment extends Fragment {
@Inject
TokenManager tokenManager;
private FragmentAnalyticsBinding binding;
private AnalyticsViewModel viewModel;
private boolean filtersExpanded = false;
private static final String[] TOP_N_OPTIONS = {"5", "10", "15", "20"};
private static final int[] TOP_N_VALUES = { 5, 10, 15, 20 };
@Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
binding = FragmentAnalyticsBinding.inflate(inflater, container, false);
viewModel = new ViewModelProvider(this).get(AnalyticsViewModel.class);
setupFilterPanel();
setupViewModeToggle();
observeViewModel();
viewModel.loadAnalytics();
binding.btnRefreshAnalytics.setOnClickListener(v -> viewModel.loadAnalytics());
UIUtils.setupHamburgerMenu(binding.btnHamburgerAnalytics, this);
return binding.getRoot();
}
private static final int COLOR_SELECTED = 0xFF4ECDC4;
private static final int COLOR_UNSELECTED = 0xFFCBD5E1;
private void setupViewModeToggle() {
updateViewModeButtonStyles(viewModel.getViewMode());
binding.btnMyAnalytics.setOnClickListener(v -> {
viewModel.setViewMode("mine");
updateViewModeButtonStyles("mine");
updateStoreFilterVisibility("mine");
});
binding.btnStoreAnalytics.setOnClickListener(v -> {
viewModel.setViewMode("store");
updateViewModeButtonStyles("store");
updateStoreFilterVisibility("store");
});
}
private void updateViewModeButtonStyles(String mode) {
binding.btnMyAnalytics.setBackgroundTintList(
android.content.res.ColorStateList.valueOf(mode.equals("mine") ? COLOR_SELECTED : COLOR_UNSELECTED));
binding.btnStoreAnalytics.setBackgroundTintList(
android.content.res.ColorStateList.valueOf(mode.equals("store") ? COLOR_SELECTED : COLOR_UNSELECTED));
}
private void updateStoreFilterVisibility(String mode) {
boolean isAdmin = "ADMIN".equalsIgnoreCase(tokenManager.getRole());
int vis = (isAdmin && mode.equals("store")) ? View.VISIBLE : View.GONE;
binding.tvStoreFilterLabel.setVisibility(vis);
binding.spinnerFilterStore.setVisibility(vis);
}
// Filter Panel
private void setupFilterPanel() {
SpinnerUtils.setupStringSpinner(requireContext(), binding.spinnerTopN, TOP_N_OPTIONS);
// Toggle expand/collapse
binding.rowFilterHeader.setOnClickListener(v -> toggleFilters());
// Date pickers
binding.etFilterStartDate.setOnClickListener(v ->
UIUtils.showDatePicker(requireContext(), binding.etFilterStartDate, this::updateFilterSummary));
binding.etFilterEndDate.setOnClickListener(v ->
UIUtils.showDatePicker(requireContext(), binding.etFilterEndDate, this::updateFilterSummary));
// Quick presets
binding.btnPresetToday.setOnClickListener(v -> applyPreset(0, 0));
binding.btnPreset7D.setOnClickListener(v -> applyPreset(-6, 0));
binding.btnPreset30D.setOnClickListener(v -> applyPreset(-29, 0));
binding.btnPreset3M.setOnClickListener(v -> applyPreset(-89, 0));
binding.btnPreset1Y.setOnClickListener(v -> applyPreset(-364, 0));
binding.btnPresetAll.setOnClickListener(v -> {
binding.etFilterStartDate.setText("");
binding.etFilterEndDate.setText("");
updateFilterSummary();
});
binding.btnFilterApply.setOnClickListener(v -> applyFiltersFromUI());
binding.btnFilterReset.setOnClickListener(v -> resetFilters());
}
private void toggleFilters() {
filtersExpanded = !filtersExpanded;
binding.llFilterContent.setVisibility(filtersExpanded ? View.VISIBLE : View.GONE);
binding.tvFilterToggleIcon.setText(filtersExpanded ? "" : "");
}
private void applyPreset(int startOffset, int endOffset) {
binding.etFilterStartDate.setText(getDateString(startOffset));
binding.etFilterEndDate.setText(getDateString(endOffset));
updateFilterSummary();
applyFiltersFromUI();
}
private void applyFiltersFromUI() {
AnalyticsViewModel.FilterState filter = new AnalyticsViewModel.FilterState();
filter.startDate = binding.etFilterStartDate.getText().toString().trim();
filter.endDate = binding.etFilterEndDate.getText().toString().trim();
Object pm = binding.spinnerFilterPayment.getSelectedItem();
filter.paymentMethod = pm != null ? pm.toString() : "All";
int topNPos = binding.spinnerTopN.getSelectedItemPosition();
filter.topN = (topNPos >= 0 && topNPos < TOP_N_VALUES.length) ? TOP_N_VALUES[topNPos] : 5;
Object store = binding.spinnerFilterStore.getSelectedItem();
viewModel.setStoreFilter(store != null ? store.toString() : "All Stores");
updateFilterSummary();
viewModel.applyFilter(filter);
}
private void resetFilters() {
binding.etFilterStartDate.setText("");
binding.etFilterEndDate.setText("");
binding.spinnerTopN.setSelection(0);
SpinnerUtils.setSelectionByValue(binding.spinnerFilterPayment, "All");
SpinnerUtils.setSelectionByValue(binding.spinnerFilterStore, "All Stores");
updateFilterSummary();
viewModel.resetFilter();
}
private void updateFilterSummary() {
String start = binding.etFilterStartDate.getText().toString().trim();
String end = binding.etFilterEndDate.getText().toString().trim();
if (start.isEmpty() && end.isEmpty()) {
binding.tvFilterSummary.setText("All time");
} else if (start.isEmpty()) {
binding.tvFilterSummary.setText("Up to " + shortDate(end));
} else if (end.isEmpty()) {
binding.tvFilterSummary.setText("From " + shortDate(start));
} else {
binding.tvFilterSummary.setText(shortDate(start) + " " + shortDate(end));
}
}
private String shortDate(String date) {
return (date != null && date.length() >= 10) ? date.substring(5) : date;
}
private String getDateString(int offsetDays) {
Calendar c = Calendar.getInstance();
c.add(Calendar.DAY_OF_YEAR, offsetDays);
return String.format(Locale.US, "%04d-%02d-%02d",
c.get(Calendar.YEAR), c.get(Calendar.MONTH) + 1, c.get(Calendar.DAY_OF_MONTH));
}
// ViewModel Observation
private void observeViewModel() {
viewModel.getAnalyticsData().observe(getViewLifecycleOwner(), this::computeAndDisplay);
viewModel.getIsLoading().observe(getViewLifecycleOwner(), loading -> {
binding.progressBar.setVisibility(loading ? View.VISIBLE : View.GONE);
if (loading) {
binding.tvTotalRevenue.setText("Loading...");
binding.tvTotalTransactions.setText("...");
binding.tvAvgTransaction.setText("...");
binding.tvTotalItems.setText("...");
}
});
viewModel.getErrorMessage().observe(getViewLifecycleOwner(), error -> {
if (error != null) showError(error);
});
viewModel.getAvailablePaymentMethods().observe(getViewLifecycleOwner(), methods -> {
if (methods == null || methods.isEmpty()) return;
String currentSelection = binding.spinnerFilterPayment.getSelectedItem() != null
? binding.spinnerFilterPayment.getSelectedItem().toString() : "All";
SpinnerUtils.setupStringSpinner(requireContext(), binding.spinnerFilterPayment,
methods.toArray(new String[0]));
SpinnerUtils.setSelectionByValue(binding.spinnerFilterPayment, currentSelection);
});
viewModel.getAvailableStores().observe(getViewLifecycleOwner(), stores -> {
if (stores == null || stores.isEmpty()) return;
String currentSelection = binding.spinnerFilterStore.getSelectedItem() != null
? binding.spinnerFilterStore.getSelectedItem().toString() : "All Stores";
SpinnerUtils.setupStringSpinner(requireContext(), binding.spinnerFilterStore,
stores.toArray(new String[0]));
SpinnerUtils.setSelectionByValue(binding.spinnerFilterStore, currentSelection);
updateStoreFilterVisibility(viewModel.getViewMode());
});
}
@Override
public void onDestroyView() {
super.onDestroyView();
binding = null;
}
// Display
private void computeAndDisplay(AnalyticsViewModel.AnalyticsData data) {
if (data == null) return;
// Summary cards
binding.tvTotalRevenue.setText("$" + data.totalRevenue.setScale(2, RoundingMode.HALF_UP));
binding.tvTotalTransactions.setText(String.valueOf(data.totalTransactions));
binding.tvAvgTransaction.setText("$" + data.avgTransaction);
binding.tvTotalItems.setText(String.valueOf(data.totalItems));
// Top Revenue Products
binding.llTopRevenue.removeAllViews();
if (data.topRevenueProducts != null && !data.topRevenueProducts.isEmpty()) {
BigDecimal maxRev = data.topRevenueProducts.get(0).getValue();
if (maxRev.compareTo(BigDecimal.ZERO) == 0) maxRev = BigDecimal.ONE;
for (Map.Entry<String, BigDecimal> e : data.topRevenueProducts) {
addBarRow(binding.llTopRevenue, e.getKey(),
"$" + e.getValue().setScale(2, RoundingMode.HALF_UP),
e.getValue().floatValue() / maxRev.floatValue(), "#ff6b35");
}
} else {
addEmptyRow(binding.llTopRevenue, "No data");
}
// Top Quantity Products
binding.llTopQuantity.removeAllViews();
if (data.topQuantityProducts != null && !data.topQuantityProducts.isEmpty()) {
int maxQty = data.topQuantityProducts.get(0).getValue();
if (maxQty == 0) maxQty = 1;
for (Map.Entry<String, Integer> e : data.topQuantityProducts) {
addBarRow(binding.llTopQuantity, e.getKey(), e.getValue() + " units",
(float) e.getValue() / maxQty, "#4ecdc4");
}
} else {
addEmptyRow(binding.llTopQuantity, "No data");
}
// Payment Methods
binding.llPaymentMethods.removeAllViews();
if (data.paymentMethodStats != null && !data.paymentMethodStats.isEmpty()) {
int maxPay = data.paymentMethodStats.stream().mapToInt(Map.Entry::getValue).max().orElse(1);
String[] payColors = { "#1a759f", "#ff9f1c", "#577590", "#90be6d" };
int ci = 0;
for (Map.Entry<String, Integer> e : data.paymentMethodStats) {
addBarRow(binding.llPaymentMethods, e.getKey(),
e.getValue() + " transactions",
(float) e.getValue() / maxPay, payColors[ci++ % payColors.length]);
}
} else {
addEmptyRow(binding.llPaymentMethods, "No data");
}
// Employee Performance
boolean showEmployeeSection = viewModel.getViewMode().equals("store");
View empParent = (View) binding.llEmployeePerformance.getParent();
if (empParent != null) empParent.setVisibility(showEmployeeSection ? View.VISIBLE : View.GONE);
if (showEmployeeSection) {
binding.llEmployeePerformance.removeAllViews();
if (data.employeePerformance != null && !data.employeePerformance.isEmpty()) {
BigDecimal maxEmp = data.employeePerformance.get(0).getValue();
if (maxEmp.compareTo(BigDecimal.ZERO) == 0) maxEmp = BigDecimal.ONE;
for (Map.Entry<String, BigDecimal> e : data.employeePerformance) {
addBarRow(binding.llEmployeePerformance, e.getKey(),
"$" + e.getValue().setScale(2, RoundingMode.HALF_UP),
e.getValue().floatValue() / maxEmp.floatValue(), "#1a759f");
}
} else {
addEmptyRow(binding.llEmployeePerformance, "No data");
}
}
// Daily Revenue
binding.tvDailyRevenueTitle.setText(data.dailyRevenueTitle);
binding.llDailyRevenue.removeAllViews();
if (data.dailyRevenue != null && !data.dailyRevenue.isEmpty()) {
BigDecimal maxDaily = data.dailyRevenue.stream()
.map(Map.Entry::getValue).max(BigDecimal::compareTo).orElse(BigDecimal.ONE);
if (maxDaily.compareTo(BigDecimal.ZERO) == 0) maxDaily = BigDecimal.ONE;
for (Map.Entry<String, BigDecimal> e : data.dailyRevenue) {
String label = e.getKey().length() >= 10 ? e.getKey().substring(5) : e.getKey();
addBarRow(binding.llDailyRevenue, label,
"$" + e.getValue().setScale(2, RoundingMode.HALF_UP),
e.getValue().floatValue() / maxDaily.floatValue(), "#ff6b35");
}
} else {
addEmptyRow(binding.llDailyRevenue, "No data");
}
}
// Chart Helpers
private void addBarRow(LinearLayout parent, String label, String value, float ratio, String color) {
if (getContext() == null) return;
LinearLayout row = new LinearLayout(getContext());
row.setOrientation(LinearLayout.VERTICAL);
row.setPadding(0, 6, 0, 6);
LinearLayout labelRow = new LinearLayout(getContext());
labelRow.setOrientation(LinearLayout.HORIZONTAL);
TextView tvLabel = new TextView(getContext());
tvLabel.setLayoutParams(new LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f));
tvLabel.setText(label);
tvLabel.setTextColor(Color.parseColor("#444441"));
tvLabel.setTextSize(13f);
TextView tvValue = new TextView(getContext());
tvValue.setText(value);
tvValue.setTextColor(Color.parseColor("#444441"));
tvValue.setTextSize(13f);
tvValue.setTextAlignment(View.TEXT_ALIGNMENT_VIEW_END);
labelRow.addView(tvLabel);
labelRow.addView(tvValue);
LinearLayout barBg = new LinearLayout(getContext());
LinearLayout.LayoutParams bgParams = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, 12);
bgParams.setMargins(0, 4, 0, 0);
barBg.setLayoutParams(bgParams);
barBg.setBackgroundColor(Color.parseColor("#EEEEEE"));
float safeRatio = Math.max(0f, Math.min(1f, ratio));
View barFill = new View(getContext());
barFill.setLayoutParams(new LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.MATCH_PARENT, safeRatio));
barFill.setBackgroundColor(Color.parseColor(color));
barBg.addView(barFill);
View spacer = new View(getContext());
spacer.setLayoutParams(new LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.MATCH_PARENT, 1f - safeRatio));
barBg.addView(spacer);
row.addView(labelRow);
row.addView(barBg);
parent.addView(row);
}
private void addEmptyRow(LinearLayout parent, String message) {
if (getContext() == null) return;
TextView tv = new TextView(getContext());
tv.setText(message);
tv.setTextColor(Color.parseColor("#888780"));
tv.setTextSize(13f);
parent.addView(tv);
}
private void showError(String msg) {
if (getContext() == null || binding == null) return;
binding.tvTotalRevenue.setText("Error");
binding.tvTotalTransactions.setText("");
binding.tvAvgTransaction.setText("");
binding.tvTotalItems.setText("");
Toast.makeText(getContext(), msg, Toast.LENGTH_SHORT).show();
}
}

View File

@@ -1,144 +1,228 @@
package com.example.petstoremobile.fragments.listfragments;
// Added search/filter bar to filter appointments by customer name or service type.
// Added pull-to-refresh using SwipeRefreshLayout.
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 com.example.petstoremobile.R;
import com.example.petstoremobile.adapters.AppointmentAdapter;
import com.example.petstoremobile.fragments.ListFragment;
import com.example.petstoremobile.fragments.listfragments.detailfragments.AppointmentDetailFragment;
import com.example.petstoremobile.models.Appointment;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
import java.util.ArrayList;
import java.util.List;
import com.example.petstoremobile.api.auth.TokenManager;
import com.example.petstoremobile.databinding.FragmentAppointmentBinding;
import com.example.petstoremobile.dtos.AppointmentDTO;
import com.example.petstoremobile.dtos.StoreDTO;
import com.example.petstoremobile.utils.BulkDeleteHandler;
import com.example.petstoremobile.utils.Resource;
import com.example.petstoremobile.utils.SpinnerUtils;
import com.example.petstoremobile.utils.UIUtils;
import com.example.petstoremobile.viewmodels.AppointmentListViewModel;
import com.example.petstoremobile.utils.EventDecorator;
import com.example.petstoremobile.viewmodels.AuthViewModel;
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 javax.inject.Inject;
import dagger.hilt.android.AndroidEntryPoint;
@AndroidEntryPoint
public class AppointmentFragment extends Fragment implements AppointmentAdapter.OnAppointmentClickListener {
private List<Appointment> appointmentList = new ArrayList<>(); // full data list
private List<Appointment> filteredList = new ArrayList<>(); // filtered display list
private FragmentAppointmentBinding binding;
private List<AppointmentDTO> appointmentList = new ArrayList<>();
private AppointmentAdapter adapter;
private SwipeRefreshLayout swipeRefreshLayout;
private EditText etSearch;
private ImageButton hamburger;
private AppointmentListViewModel viewModel;
private AuthViewModel authViewModel;
private BulkDeleteHandler bulkDeleteHandler;
@Inject TokenManager tokenManager;
private CalendarDay selectedCalendarDay;
private boolean isMonthMode = false;
private Long currentUserId = null;
private final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd", Locale.getDefault());
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
viewModel = new ViewModelProvider(this).get(AppointmentListViewModel.class);
authViewModel = new ViewModelProvider(this).get(AuthViewModel.class);
}
@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);
hamburger = view.findViewById(R.id.btnHamburger);
setupRecyclerView();
setupSearch();
setupStatusFilter();
setupStoreFilter();
setupSwipeRefresh();
setupCalendar();
setupFilterToggle();
setupMyAppointmentFilter();
setupBulkDelete();
observeViewModel();
loadAppointmentData(); // TODO: Replace with actual API call when backend is ready
setupRecyclerView(view);
setupSearch(view);
setupSwipeRefresh(view);
binding.fabAddAppointment.setOnClickListener(v -> openAppointmentDetails(-1));
FloatingActionButton fabAddAppointment = view.findViewById(R.id.fabAddAppointment);
fabAddAppointment.setOnClickListener(v -> openAppointmentDetails(-1));
UIUtils.setupHamburgerMenu(binding.btnHamburger, this);
//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.btnToggleCalendarMode.setOnClickListener(v -> toggleCalendarMode());
return view;
loadCurrentUserInfo();
return binding.getRoot();
}
// Sets up the search bar to filter appointments by customer name or service type
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 observeViewModel() {
viewModel.getAppointments().observe(getViewLifecycleOwner(), list -> {
appointmentList.clear();
appointmentList.addAll(list);
updateCalendarDecorators();
adapter.notifyDataSetChanged();
});
viewModel.getStores().observe(getViewLifecycleOwner(), list -> {
SpinnerUtils.populateWhiteSpinner(requireContext(), binding.spinnerStore, list,
StoreDTO::getStoreName, "All Stores", -1L, StoreDTO::getStoreId);
});
viewModel.getIsLoading().observe(getViewLifecycleOwner(), loading -> {
binding.swipeRefreshAppointment.setRefreshing(loading);
});
}
// Filters the appointment list based on the search query
private void filterAppointments(String query) {
filteredList.clear();
if (query.isEmpty()) {
filteredList.addAll(appointmentList);
private void setupBulkDelete() {
bulkDeleteHandler = new BulkDeleteHandler(
this,
binding.layoutBulkDelete,
binding.tvSelectionCount,
binding.btnBulkDelete,
adapter,
"appointment",
viewModel::bulkDeleteAppointments,
this::loadAppointmentData
);
}
@Override
public void onResume() {
super.onResume();
loadAppointmentData();
if (!isStaff()) viewModel.loadStores();
}
private void toggleCalendarMode() {
isMonthMode = !isMonthMode;
binding.calendarView.state().edit()
.setCalendarDisplayMode(isMonthMode ? CalendarMode.MONTHS : CalendarMode.WEEKS)
.commit();
}
private void setupMyAppointmentFilter() {
binding.btnMyAppointments.setOnClickListener(v -> {
loadAppointmentData();
});
}
private void loadCurrentUserInfo() {
authViewModel.getMe().observe(getViewLifecycleOwner(), resource -> {
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
currentUserId = resource.data.getId();
}
});
}
private void setupFilterToggle() {
if (isStaff()) {
UIUtils.setupFilterToggle(binding.btnToggleFilter, binding.layoutFilter, binding.etSearchAppointment, binding.spinnerStatus);
binding.spinnerStore.setVisibility(View.GONE);
} else {
String lower = query.toLowerCase();
for (Appointment a : appointmentList) {
if (a.getCustomerName().toLowerCase().contains(lower)
|| a.getServiceType().toLowerCase().contains(lower)
|| a.getPetName().toLowerCase().contains(lower)) {
filteredList.add(a);
UIUtils.setupFilterToggle(binding.btnToggleFilter, binding.layoutFilter, binding.etSearchAppointment,
binding.spinnerStatus, binding.spinnerStore);
}
}
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();
});
}
private void updateCalendarDecorators() {
HashSet<CalendarDay> datesWithAppointments = new HashSet<>();
for (AppointmentDTO appointment : appointmentList) {
try {
Date date = dateFormat.parse(appointment.getAppointmentDate());
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();
binding.calendarView.removeDecorators();
binding.calendarView.addDecorator(new EventDecorator(Color.RED, datesWithAppointments));
}
// Sets up pull-to-refresh: reloads data when user swipes down
private void setupSwipeRefresh(View view) {
swipeRefreshLayout = view.findViewById(R.id.swipeRefreshAppointment);
swipeRefreshLayout.setOnRefreshListener(() -> {
loadAppointmentData(); // TODO: Replace with actual API call when backend is ready
filterAppointments(etSearch.getText().toString());
swipeRefreshLayout.setRefreshing(false);
});
private void setupSearch() {
UIUtils.attachSearch(binding.etSearchAppointment, this::loadAppointmentData);
}
private void setupStatusFilter() {
String[] statuses = {"All Statuses", "Booked", "Completed", "Cancelled", "Missed"};
SpinnerUtils.setupStringFilterSpinner(requireContext(), binding.spinnerStatus, statuses, this::loadAppointmentData);
}
private void setupStoreFilter() {
SpinnerUtils.setupFilterSpinner(binding.spinnerStore, this::loadAppointmentData);
}
private void setupSwipeRefresh() {
binding.swipeRefreshAppointment.setOnRefreshListener(this::loadAppointmentData);
}
private void openAppointmentDetails(int position) {
AppointmentDetailFragment detailFragment = new AppointmentDetailFragment();
Bundle args = new Bundle();
args.putInt("position", position);
if (position != -1) {
Appointment appointment = filteredList.get(position);
// Find the real position in the full list for save/delete callbacks
int realPosition = appointmentList.indexOf(appointment);
args.putInt("position", realPosition);
args.putInt("appointmentId", appointment.getAppointmentId());
args.putString("customerName", appointment.getCustomerName());
args.putString("petName", appointment.getPetName());
args.putString("serviceType", appointment.getServiceType());
args.putString("appointmentDate", appointment.getAppointmentDate());
args.putString("appointmentTime", appointment.getAppointmentTime());
args.putString("status", appointment.getStatus());
AppointmentDTO a = appointmentList.get(position);
args.putLong("appointmentId", a.getAppointmentId());
}
detailFragment.setArguments(args);
detailFragment.setAppointmentFragment(this);
ListFragment listFragment = (ListFragment) getParentFragment();
if (listFragment != null) listFragment.loadFragment(detailFragment);
}
public void onAppointmentSaved(int position, Appointment 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);
}
@Override
@@ -146,22 +230,58 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter.
openAppointmentDetails(position);
}
// Helper function to load hardcoded sample data
// Replace with API call
private void loadAppointmentData() {
appointmentList.clear();
appointmentList.add(new Appointment(1, "John Smith", "Buddy", "Grooming", "2026-03-10", "10:00 AM", "Confirmed"));
appointmentList.add(new Appointment(2, "Jane Doe", "Luna", "Vet Checkup", "2026-03-11", "02:00 PM", "Pending"));
appointmentList.add(new Appointment(3, "Bob Lee", "Max", "Training", "2026-03-12", "11:00 AM", "Confirmed"));
appointmentList.add(new Appointment(4, "Alice Brown", "Milo", "Grooming", "2026-03-13", "03:00 PM", "Cancelled"));
filteredList.clear();
filteredList.addAll(appointmentList);
@Override
public void onSelectionChanged(int count) {
if (bulkDeleteHandler != null) {
bulkDeleteHandler.onSelectionChanged(count);
}
}
private void setupRecyclerView(View view) {
RecyclerView recyclerView = view.findViewById(R.id.recyclerViewAppointments);
adapter = new AppointmentAdapter(filteredList, this); // adapter uses filteredList
recyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
recyclerView.setAdapter(adapter);
private boolean isStaff() {
return "STAFF".equalsIgnoreCase(tokenManager.getRole());
}
private void loadAppointmentData() {
String query = binding.etSearchAppointment.getText().toString().trim();
String status = binding.spinnerStatus.getSelectedItem() != null ? binding.spinnerStatus.getSelectedItem().toString() : "All Statuses";
Long storeId;
if (isStaff()) {
storeId = tokenManager.getPrimaryStoreId();
} else {
storeId = null;
List<StoreDTO> stores = viewModel.getStores().getValue();
if (binding.spinnerStore.getSelectedItemPosition() > 0 && stores != null && !stores.isEmpty()) {
storeId = stores.get(binding.spinnerStore.getSelectedItemPosition() - 1).getStoreId();
}
}
String selectedDateString = null;
if (selectedCalendarDay != null) {
selectedDateString = String.format(Locale.getDefault(), "%04d-%02d-%02d",
selectedCalendarDay.getYear(), selectedCalendarDay.getMonth(), selectedCalendarDay.getDay());
}
Long employeeId = null;
if (binding.btnMyAppointments.isChecked()) {
employeeId = currentUserId;
}
if (status.equals("All Statuses")) status = null;
else status = status.toUpperCase();
viewModel.loadAppointments(query, status, storeId, selectedDateString, employeeId);
}
private void setupRecyclerView() {
adapter = new AppointmentAdapter(appointmentList, this);
binding.recyclerViewAppointments.setLayoutManager(new LinearLayoutManager(getContext()));
binding.recyclerViewAppointments.setAdapter(adapter);
}
@Override
public void onDestroyView() {
super.onDestroyView();
binding = null;
}
}

View File

@@ -0,0 +1,145 @@
package com.example.petstoremobile.fragments.listfragments;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.ViewModelProvider;
import androidx.navigation.Navigation;
import androidx.recyclerview.widget.LinearLayoutManager;
import com.example.petstoremobile.R;
import com.example.petstoremobile.adapters.CouponAdapter;
import com.example.petstoremobile.databinding.FragmentCouponBinding;
import com.example.petstoremobile.dtos.CouponDTO;
import com.example.petstoremobile.utils.Resource;
import com.example.petstoremobile.utils.SpinnerUtils;
import com.example.petstoremobile.utils.UIUtils;
import com.example.petstoremobile.viewmodels.CouponListViewModel;
import java.util.ArrayList;
import java.util.List;
import dagger.hilt.android.AndroidEntryPoint;
@AndroidEntryPoint
public class CouponFragment extends Fragment implements CouponAdapter.OnCouponClickListener {
private FragmentCouponBinding binding;
private CouponListViewModel viewModel;
private CouponAdapter adapter;
private final List<CouponDTO> couponList = new ArrayList<>();
@Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
binding = FragmentCouponBinding.inflate(inflater, container, false);
viewModel = new ViewModelProvider(this).get(CouponListViewModel.class);
setupRecyclerView();
setupSearch();
setupStatusFilter();
setupTypeFilter();
setupSwipeRefresh();
observeViewModel();
viewModel.loadCoupons(0, 100, null, null, null);
binding.fabAddCoupon.setOnClickListener(v -> openDetail(-1));
binding.btnBulkDeleteCoupons.setOnClickListener(v -> confirmBulkDelete());
UIUtils.setupHamburgerMenu(binding.btnHamburgerCoupon, this);
UIUtils.setupFilterToggle(binding.btnToggleFilterCoupon, binding.layoutFilterCoupon, binding.etSearchCoupon,
binding.spinnerTypeCoupon, binding.spinnerStatusCoupon);
return binding.getRoot();
}
private void observeViewModel() {
viewModel.getCoupons().observe(getViewLifecycleOwner(), list -> {
couponList.clear();
couponList.addAll(list);
adapter.notifyDataSetChanged();
});
viewModel.getIsLoading().observe(getViewLifecycleOwner(), loading -> {
binding.swipeRefreshCoupon.setRefreshing(loading);
});
}
private void setupRecyclerView() {
adapter = new CouponAdapter(couponList, this);
binding.recyclerViewCoupon.setLayoutManager(new LinearLayoutManager(getContext()));
binding.recyclerViewCoupon.setAdapter(adapter);
}
private void setupStatusFilter() {
String[] statuses = {"All Statuses", "Active", "Inactive"};
SpinnerUtils.setupStringFilterSpinner(requireContext(), binding.spinnerStatusCoupon, statuses, this::applyFilters);
}
private void setupTypeFilter() {
String[] types = {"All Types", "FIXED", "PERCENT"};
SpinnerUtils.setupStringFilterSpinner(requireContext(), binding.spinnerTypeCoupon, types, this::applyFilters);
}
private void setupSearch() {
UIUtils.attachSearch(binding.etSearchCoupon, this::applyFilters);
}
private void setupSwipeRefresh() {
binding.swipeRefreshCoupon.setOnRefreshListener(this::applyFilters);
}
private void applyFilters() {
String statusStr = binding.spinnerStatusCoupon.getSelectedItem() != null ?
binding.spinnerStatusCoupon.getSelectedItem().toString() : "All Statuses";
Boolean active = null;
if (statusStr.equals("Active")) active = true;
else if (statusStr.equals("Inactive")) active = false;
String typeStr = binding.spinnerTypeCoupon.getSelectedItem() != null ?
binding.spinnerTypeCoupon.getSelectedItem().toString() : "All Types";
String discountType = typeStr.equals("All Types") ? null : typeStr;
viewModel.loadCoupons(0, 100, active, discountType, null);
}
private void openDetail(long id) {
Bundle args = new Bundle();
args.putLong("couponId", id);
Navigation.findNavController(requireView()).navigate(R.id.couponDetailFragment, args);
}
@Override
public void onCouponClick(CouponDTO coupon) {
openDetail(coupon.getCouponId());
}
@Override
public void onSelectionChanged(int count) {
binding.btnBulkDeleteCoupons.setVisibility(count > 0 ? View.VISIBLE : View.GONE);
}
private void confirmBulkDelete() {
new AlertDialog.Builder(requireContext())
.setTitle("Confirm Bulk Delete")
.setMessage("Are you sure you want to delete the selected coupons?")
.setPositiveButton("Delete", (dialog, which) -> {
List<Long> ids = new ArrayList<>(adapter.getSelectedIds());
viewModel.bulkDeleteCoupons(ids).observe(getViewLifecycleOwner(), resource -> {
if (resource.status == Resource.Status.SUCCESS) {
adapter.setSelectionMode(false);
applyFilters();
} else if (resource.status == Resource.Status.ERROR) {
UIUtils.showToast(requireContext(), resource.message);
}
});
})
.setNegativeButton("Cancel", null)
.show();
}
}

View File

@@ -0,0 +1,125 @@
package com.example.petstoremobile.fragments.listfragments;
import android.os.Bundle;
import android.view.*;
import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.ViewModelProvider;
import androidx.navigation.fragment.NavHostFragment;
import androidx.recyclerview.widget.LinearLayoutManager;
import com.example.petstoremobile.R;
import com.example.petstoremobile.adapters.CustomerAdapter;
import com.example.petstoremobile.api.auth.TokenManager;
import com.example.petstoremobile.databinding.FragmentCustomerBinding;
import com.example.petstoremobile.dtos.CustomerDTO;
import com.example.petstoremobile.utils.SpinnerUtils;
import com.example.petstoremobile.utils.UIUtils;
import com.example.petstoremobile.viewmodels.CustomerListViewModel;
import dagger.hilt.android.AndroidEntryPoint;
import java.util.*;
import javax.inject.Inject;
import javax.inject.Named;
@AndroidEntryPoint
public class CustomerFragment extends Fragment implements CustomerAdapter.OnCustomerClickListener {
private FragmentCustomerBinding binding;
private CustomerListViewModel viewModel;
private List<CustomerDTO> customerList = new ArrayList<>();
private CustomerAdapter adapter;
@Inject @Named("baseUrl") String baseUrl;
@Inject TokenManager tokenManager;
@Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
binding = FragmentCustomerBinding.inflate(inflater, container, false);
viewModel = new ViewModelProvider(this).get(CustomerListViewModel.class);
setupRecyclerView();
setupSearch();
setupStatusFilter();
setupSwipeRefresh();
observeViewModel();
viewModel.loadCustomers();
binding.fabAddCustomer.setOnClickListener(v -> openDetail(-1));
UIUtils.setupHamburgerMenu(binding.btnHamburgerCustomer, this);
UIUtils.setupFilterToggle(binding.btnToggleFilterCustomer, binding.layoutFilterCustomer,
binding.etSearchCustomer, binding.spinnerStatusCustomer);
return binding.getRoot();
}
private void observeViewModel() {
viewModel.getFilteredCustomers().observe(getViewLifecycleOwner(), list -> {
customerList.clear();
customerList.addAll(list);
adapter.notifyDataSetChanged();
});
viewModel.getIsLoading().observe(getViewLifecycleOwner(), loading -> {
binding.swipeRefreshCustomer.setRefreshing(loading);
});
}
private void setupRecyclerView() {
adapter = new CustomerAdapter(customerList, this);
adapter.setBaseUrl(baseUrl);
adapter.setToken(tokenManager.getToken());
binding.recyclerViewCustomer.setLayoutManager(new LinearLayoutManager(getContext()));
binding.recyclerViewCustomer.setAdapter(adapter);
}
private void setupStatusFilter() {
String[] statuses = {"All Statuses", "Active", "Inactive"};
SpinnerUtils.setupStringFilterSpinner(requireContext(), binding.spinnerStatusCustomer, statuses, this::applyFilters);
}
private void setupSearch() {
UIUtils.attachSearch(binding.etSearchCustomer, this::applyFilters);
}
private void applyFilters() {
String query = binding.etSearchCustomer.getText().toString().trim();
String status = binding.spinnerStatusCustomer.getSelectedItem() != null ?
binding.spinnerStatusCustomer.getSelectedItem().toString() : "All Statuses";
viewModel.filter(query, status);
}
private void setupSwipeRefresh() {
binding.swipeRefreshCustomer.setOnRefreshListener(viewModel::loadCustomers);
}
private void openDetail(int position) {
Bundle args = new Bundle();
if (position != -1) {
CustomerDTO c = customerList.get(position);
args.putLong("customerId", c.getCustomerId() != null ? c.getCustomerId() : -1);
args.putString("username", c.getUsername() != null ? c.getUsername() : "");
args.putString("firstName", c.getFirstName() != null ? c.getFirstName() : "");
args.putString("lastName", c.getLastName() != null ? c.getLastName() : "");
args.putString("email", c.getEmail() != null ? c.getEmail() : "");
args.putString("phone", c.getPhone() != null ? c.getPhone() : "");
args.putBoolean("active", Boolean.TRUE.equals(c.getActive()));
args.putInt("loyaltyPoints", c.getLoyaltyPoints() != null ? c.getLoyaltyPoints() : 0);
args.putBoolean("isEditing", true);
}
NavHostFragment.findNavController(this).navigate(R.id.nav_customer_detail, args);
}
@Override
public void onCustomerClick(int position) {
openDetail(position);
}
@Override
public void onDestroyView() {
super.onDestroyView();
binding = null;
}
}

View File

@@ -1,162 +1,202 @@
package com.example.petstoremobile.fragments.listfragments;
// Added search/filter bar to filter inventory by item name or category.
// Added pull-to-refresh using SwipeRefreshLayout.
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.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.InventoryAdapter;
import com.example.petstoremobile.fragments.ListFragment;
import com.example.petstoremobile.fragments.listfragments.detailfragments.InventoryDetailFragment;
import com.example.petstoremobile.models.Inventory;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
import com.example.petstoremobile.api.auth.TokenManager;
import com.example.petstoremobile.databinding.FragmentInventoryBinding;
import com.example.petstoremobile.dtos.InventoryDTO;
import com.example.petstoremobile.dtos.StoreDTO;
import com.example.petstoremobile.utils.BulkDeleteHandler;
import com.example.petstoremobile.utils.UIUtils;
import com.example.petstoremobile.viewmodels.InventoryListViewModel;
import com.example.petstoremobile.utils.SpinnerUtils;
import java.util.ArrayList;
import java.util.List;
import javax.inject.Inject;
import dagger.hilt.android.AndroidEntryPoint;
@AndroidEntryPoint
public class InventoryFragment extends Fragment implements InventoryAdapter.OnInventoryClickListener {
private List<Inventory> inventoryList = new ArrayList<>();
private List<Inventory> filteredList = new ArrayList<>();
private FragmentInventoryBinding binding;
private final List<InventoryDTO> inventoryList = new ArrayList<>();
private InventoryAdapter adapter;
private SwipeRefreshLayout swipeRefreshLayout;
private EditText etSearch;
private ImageButton hamburger;
private InventoryListViewModel viewModel;
private BulkDeleteHandler bulkDeleteHandler;
@Inject TokenManager tokenManager;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
viewModel = new ViewModelProvider(this).get(InventoryListViewModel.class);
}
@Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_inventory, container, false);
binding = FragmentInventoryBinding.inflate(inflater, container, false);
hamburger = view.findViewById(R.id.btnHamburger);
setupRecyclerView();
setupSearch();
setupStoreFilter();
setupSwipeRefresh();
setupFilterToggle();
setupBulkDelete();
observeViewModel();
loadInventory(true);
loadInventoryData(); // TODO: Replace with actual API call when backend is ready
setupRecyclerView(view);
setupSearch(view);
setupSwipeRefresh(view);
binding.fabAddInventory.setOnClickListener(v -> openDetail(null));
FloatingActionButton fabAddInventory = view.findViewById(R.id.fabAddInventory);
fabAddInventory.setOnClickListener(v -> openInventoryDetails(-1));
UIUtils.setupHamburgerMenu(binding.btnHamburger, this);
//Make the hamburger button open the drawer from listFragment
hamburger.setOnClickListener(v -> {
ListFragment listFragment = (ListFragment) getParentFragment();
//if list fragment is found then use its helper function to open the drawer
if (listFragment != null) {
listFragment.openDrawer();
}
});
return view;
return binding.getRoot();
}
// Filters inventory list by item name or category
private void setupSearch(View view) {
etSearch = view.findViewById(R.id.etSearchInventory);
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) {
filterInventory(s.toString());
}
@Override public void afterTextChanged(Editable s) {}
private void observeViewModel() {
viewModel.getInventory().observe(getViewLifecycleOwner(), list -> {
inventoryList.clear();
inventoryList.addAll(list);
adapter.notifyDataSetChanged();
});
viewModel.getStores().observe(getViewLifecycleOwner(), list -> {
SpinnerUtils.populateWhiteSpinner(requireContext(), binding.spinnerStore, list,
StoreDTO::getStoreName, "All Stores", -1L, StoreDTO::getStoreId);
});
viewModel.getIsLoading().observe(getViewLifecycleOwner(), loading -> {
binding.swipeRefreshInventory.setRefreshing(loading);
});
}
private void filterInventory(String query) {
filteredList.clear();
if (query.isEmpty()) {
filteredList.addAll(inventoryList);
private void setupBulkDelete() {
bulkDeleteHandler = new BulkDeleteHandler(
this,
binding.layoutBulkDelete,
binding.tvSelectionCount,
binding.btnBulkDelete,
adapter,
"inventory item",
viewModel::bulkDeleteInventory,
() -> loadInventory(true)
);
}
@Override
public void onResume() {
super.onResume();
if (!isStaff()) viewModel.loadStores();
}
@Override
public void onDestroyView() {
super.onDestroyView();
binding = null;
}
private void setupFilterToggle() {
if (isStaff()) {
UIUtils.setupFilterToggle(binding.btnToggleFilter, binding.layoutFilter, binding.etSearchInventory);
binding.spinnerStore.setVisibility(View.GONE);
} else {
String lower = query.toLowerCase();
for (Inventory i : inventoryList) {
if (i.getItemName().toLowerCase().contains(lower)
|| i.getCategory().toLowerCase().contains(lower)
|| i.getSupplier().toLowerCase().contains(lower)) {
filteredList.add(i);
UIUtils.setupFilterToggle(binding.btnToggleFilter, binding.layoutFilter, binding.etSearchInventory, binding.spinnerStore);
}
}
private boolean isStaff() {
return "STAFF".equalsIgnoreCase(tokenManager.getRole());
}
private void setupSearch() {
UIUtils.attachSearch(binding.etSearchInventory, () -> loadInventory(true));
}
private void setupStoreFilter() {
SpinnerUtils.setupFilterSpinner(binding.spinnerStore, () -> loadInventory(true));
}
private void setupRecyclerView() {
adapter = new InventoryAdapter(inventoryList, this);
binding.recyclerViewInventory.setLayoutManager(new LinearLayoutManager(getContext()));
binding.recyclerViewInventory.setAdapter(adapter);
binding.recyclerViewInventory.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
if (dy <= 0) return;
LinearLayoutManager lm = (LinearLayoutManager) binding.recyclerViewInventory.getLayoutManager();
if (lm == null) return;
int visible = lm.getChildCount();
int total = lm.getItemCount();
int firstVis = lm.findFirstVisibleItemPosition();
Boolean isLoading = viewModel.getIsLoading().getValue();
if ((isLoading == null || !isLoading) && !viewModel.isLastPage() && (visible + firstVis) >= total - 3) {
loadInventory(false);
}
}
}
adapter.notifyDataSetChanged();
}
private void setupSwipeRefresh(View view) {
swipeRefreshLayout = view.findViewById(R.id.swipeRefreshInventory);
swipeRefreshLayout.setOnRefreshListener(() -> {
loadInventoryData(); // TODO: Replace with actual API call
filterInventory(etSearch.getText().toString());
swipeRefreshLayout.setRefreshing(false);
});
}
private void openInventoryDetails(int position) {
InventoryDetailFragment detailFragment = new InventoryDetailFragment();
Bundle args = new Bundle();
args.putInt("position", position);
if (position != -1) {
Inventory inventory = filteredList.get(position);
int realPosition = inventoryList.indexOf(inventory);
args.putInt("position", realPosition);
args.putInt("inventoryId", inventory.getInventoryId());
args.putString("itemName", inventory.getItemName());
args.putString("category", inventory.getCategory());
args.putInt("quantity", inventory.getQuantity());
args.putDouble("unitPrice", inventory.getUnitPrice());
args.putString("supplier", inventory.getSupplier());
}
detailFragment.setArguments(args);
detailFragment.setInventoryFragment(this);
ListFragment listFragment = (ListFragment) getParentFragment();
if (listFragment != null) listFragment.loadFragment(detailFragment);
private void setupSwipeRefresh() {
binding.swipeRefreshInventory.setOnRefreshListener(() -> loadInventory(true));
}
public void onInventorySaved(int position, Inventory inventory) {
if (position == -1) {
inventoryList.add(inventory);
private void loadInventory(boolean reset) {
String query = binding.etSearchInventory != null ? binding.etSearchInventory.getText().toString().trim() : "";
if (query.isEmpty()) query = null;
Long storeId;
if (isStaff()) {
storeId = tokenManager.getPrimaryStoreId();
} else {
inventoryList.set(position, inventory);
storeId = null;
List<StoreDTO> stores = viewModel.getStores().getValue();
if (binding.spinnerStore.getSelectedItemPosition() > 0 && stores != null && !stores.isEmpty()) {
storeId = stores.get(binding.spinnerStore.getSelectedItemPosition() - 1).getStoreId();
}
}
filterInventory(etSearch.getText().toString());
viewModel.loadInventory(reset, query, storeId);
}
public void onInventoryDeleted(int position) {
inventoryList.remove(position);
filterInventory(etSearch.getText().toString());
private void openDetail(InventoryDTO inv) {
Bundle args = new Bundle();
if (inv != null) {
args.putLong("inventoryId", inv.getInventoryId());
}
NavHostFragment.findNavController(this).navigate(R.id.nav_inventory_detail, args);
}
@Override
public void onInventoryClick(int position) {
openInventoryDetails(position);
if (position >= 0 && position < inventoryList.size()) {
openDetail(inventoryList.get(position));
}
}
private void loadInventoryData() {
inventoryList.clear();
inventoryList.add(new Inventory(1, "Dog Food - Large", "Food", 50, 25.99, "PetSupplies Co."));
inventoryList.add(new Inventory(2, "Cat Litter", "Hygiene", 30, 12.99, "CleanPaws Ltd."));
inventoryList.add(new Inventory(3, "Dog Leash", "Accessories", 4, 15.99, "PetGear Inc."));
inventoryList.add(new Inventory(4, "Bird Cage - Medium", "Housing", 8, 79.99, "BirdWorld"));
inventoryList.add(new Inventory(5, "Flea Treatment", "Medicine", 2, 34.99, "VetCare Supply"));
filteredList.clear();
filteredList.addAll(inventoryList);
}
private void setupRecyclerView(View view) {
RecyclerView recyclerView = view.findViewById(R.id.recyclerViewInventory);
adapter = new InventoryAdapter(filteredList, this);
recyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
recyclerView.setAdapter(adapter);
@Override
public void onSelectionChanged(int selectedCount) {
if (bulkDeleteHandler != null) {
bulkDeleteHandler.onSelectionChanged(selectedCount);
}
}
}

View File

@@ -2,201 +2,210 @@ 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;
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.PetAdapter;
import com.example.petstoremobile.api.PetApi;
import com.example.petstoremobile.api.RetrofitClient;
import com.example.petstoremobile.dtos.PageResponse;
import com.example.petstoremobile.api.auth.TokenManager;
import com.example.petstoremobile.databinding.FragmentPetBinding;
import com.example.petstoremobile.dtos.PetDTO;
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.dtos.StoreDTO;
import com.example.petstoremobile.utils.BulkDeleteHandler;
import com.example.petstoremobile.utils.Resource;
import com.example.petstoremobile.utils.SpinnerUtils;
import com.example.petstoremobile.utils.UIUtils;
import com.example.petstoremobile.viewmodels.PetListViewModel;
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 PetAdapter adapter;
private PetApi api;
private SwipeRefreshLayout swipeRefreshLayout;
private EditText etSearch;
private PetListViewModel viewModel;
private BulkDeleteHandler bulkDeleteHandler;
@Inject @Named("baseUrl") String baseUrl;
@Inject TokenManager tokenManager;
//load pet view
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
viewModel = new ViewModelProvider(this).get(PetListViewModel.class);
}
@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();
observeViewModel();
hamburger = view.findViewById(R.id.btnHamburger);
binding.fabAddPet.setOnClickListener(v -> openPetDetails());
setupRecyclerView(view);
setupSearch(view);
setupSwipeRefresh(view);
UIUtils.setupHamburgerMenu(binding.btnHamburger, this);
return binding.getRoot();
}
private void observeViewModel() {
viewModel.getPets().observe(getViewLifecycleOwner(), list -> {
petList.clear();
petList.addAll(list);
adapter.notifyDataSetChanged();
});
viewModel.getStores().observe(getViewLifecycleOwner(), list -> {
SpinnerUtils.populateWhiteSpinner(requireContext(), binding.spinnerStore, list,
StoreDTO::getStoreName, "All Stores", -1L, StoreDTO::getStoreId);
});
viewModel.getIsLoading().observe(getViewLifecycleOwner(), loading -> {
binding.swipeRefreshPet.setRefreshing(loading);
});
viewModel.getSpeciesOptions().observe(getViewLifecycleOwner(), options -> {
String[] arr = options.toArray(new String[0]);
SpinnerUtils.setupStringFilterSpinner(requireContext(), binding.spinnerSpecies, arr, this::loadPetData);
});
}
private void setupBulkDelete() {
bulkDeleteHandler = new BulkDeleteHandler(
this,
binding.layoutBulkDelete,
binding.tvSelectionCount,
binding.btnBulkDelete,
adapter,
"pet",
viewModel::bulkDeletePets,
this::loadPetData
);
}
@Override
public void onResume() {
super.onResume();
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();
}
});
return view;
viewModel.loadSpecies();
if (!isStaff()) viewModel.loadStores();
}
private void setupSearch(View view) {
etSearch = view.findViewById(R.id.etSearchPet);
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) {
filterPets(s.toString());
}
@Override public void afterTextChanged(Editable s) {}
});
}
private void filterPets(String query) {
filteredList.clear();
if (query.isEmpty()) {
filteredList.addAll(petList);
private void setupFilterToggle() {
if (isStaff()) {
UIUtils.setupFilterToggle(binding.btnToggleFilter, binding.layoutFilter, binding.etSearchPet,
binding.spinnerStatus, binding.spinnerSpecies);
binding.spinnerStore.setVisibility(View.GONE);
} else {
String lower = query.toLowerCase();
for (PetDTO p : petList) {
if (p.getPetName().toLowerCase().contains(lower)
|| p.getPetSpecies().toLowerCase().contains(lower)
|| p.getPetBreed().toLowerCase().contains(lower)) {
filteredList.add(p);
}
UIUtils.setupFilterToggle(binding.btnToggleFilter, binding.layoutFilter, binding.etSearchPet,
binding.spinnerStatus, binding.spinnerSpecies, binding.spinnerStore);
}
}
private boolean isStaff() {
return "STAFF".equalsIgnoreCase(tokenManager.getRole());
}
private void setupSearch() {
UIUtils.attachSearch(binding.etSearchPet, this::loadPetData);
}
private void setupStatusFilter() {
String[] statuses = {"All Statuses", "Available", "Adopted", "Owned", "Pending"};
SpinnerUtils.setupStringFilterSpinner(requireContext(), binding.spinnerStatus, statuses, this::loadPetData);
}
private void setupSpeciesFilter() {
String[] initial = {"All Species"};
SpinnerUtils.setupStringFilterSpinner(requireContext(), binding.spinnerSpecies, initial, this::loadPetData);
}
private void setupStoreFilter() {
SpinnerUtils.setupFilterSpinner(binding.spinnerStore, this::loadPetData);
}
private void setupSwipeRefresh() {
binding.swipeRefreshPet.setOnRefreshListener(this::loadPetData);
}
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;
if (isStaff()) {
storeId = tokenManager.getPrimaryStoreId();
} else {
storeId = null;
List<StoreDTO> stores = viewModel.getStores().getValue();
if (binding.spinnerStore.getSelectedItemPosition() > 0 && stores != null && !stores.isEmpty()) {
storeId = stores.get(binding.spinnerStore.getSelectedItemPosition() - 1).getStoreId();
}
}
adapter.notifyDataSetChanged();
viewModel.loadPets(query, status, species, storeId);
}
private void setupSwipeRefresh(View view) {
swipeRefreshLayout = view.findViewById(R.id.swipeRefreshPet);
swipeRefreshLayout.setOnRefreshListener(() -> {
loadPetData();
});
private void setupRecyclerView() {
adapter = new PetAdapter(petList, this);
adapter.setBaseUrl(baseUrl);
adapter.setToken(tokenManager.getToken());
binding.recyclerViewPets.setLayoutManager(new LinearLayoutManager(getContext()));
binding.recyclerViewPets.setAdapter(adapter);
}
//Open pet profile
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);
}
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(etSearch.getText().toString());
} else {
Log.e("onResponse: ", response.message());
}
}
@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());
}
});
}
//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);
@Override
public void onDestroyView() {
super.onDestroyView();
binding = null;
}
}
}

View File

@@ -1,140 +1,138 @@
package com.example.petstoremobile.fragments.listfragments;
// Added search/filter bar to filter products by name or category.
// Added pull-to-refresh using SwipeRefreshLayout.
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.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.EditText;
import android.widget.ImageButton;
import com.example.petstoremobile.R;
import com.example.petstoremobile.adapters.ProductAdapter;
import com.example.petstoremobile.fragments.ListFragment;
import com.example.petstoremobile.fragments.listfragments.detailfragments.ProductDetailFragment;
import com.example.petstoremobile.models.Product;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
import com.example.petstoremobile.databinding.FragmentProductBinding;
import com.example.petstoremobile.dtos.DropdownDTO;
import com.example.petstoremobile.dtos.ProductDTO;
import com.example.petstoremobile.utils.SpinnerUtils;
import com.example.petstoremobile.utils.UIUtils;
import com.example.petstoremobile.viewmodels.ProductListViewModel;
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 List<Product> productList = new ArrayList<>();
private List<Product> filteredList = new ArrayList<>();
private FragmentProductBinding binding;
private List<ProductDTO> productList = new ArrayList<>();
private ProductAdapter adapter;
private SwipeRefreshLayout swipeRefreshLayout;
private EditText etSearch;
private ImageButton hamburger;
private ProductListViewModel viewModel;
@Inject @Named("baseUrl") String baseUrl;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
viewModel = new ViewModelProvider(this).get(ProductListViewModel.class);
}
@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);
hamburger = view.findViewById(R.id.btnHamburger);
setupRecyclerView();
setupSearch();
setupCategoryFilter();
setupSwipeRefresh();
setupFilterToggle();
observeViewModel();
loadProductData(); // TODO: Replace with actual API call when backend is ready
setupRecyclerView(view);
setupSearch(view);
setupSwipeRefresh(view);
binding.fabAddProduct.setOnClickListener(v -> openProductDetails(-1));
FloatingActionButton fabAddProduct = view.findViewById(R.id.fabAddProduct);
fabAddProduct.setOnClickListener(v -> openProductDetails(-1));
UIUtils.setupHamburgerMenu(binding.btnHamburgerProduct, this);
//Make the hamburger button open the drawer from listFragment
hamburger.setOnClickListener(v -> {
ListFragment listFragment = (ListFragment) getParentFragment();
//if list fragment is found then use its helper function to open the drawer
if (listFragment != null) {
listFragment.openDrawer();
}
});
return view;
return binding.getRoot();
}
// Filters products by name, description, or category
private void setupSearch(View view) {
etSearch = view.findViewById(R.id.etSearchProduct);
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) {
filterProducts(s.toString());
}
@Override public void afterTextChanged(Editable s) {}
private void observeViewModel() {
viewModel.getProducts().observe(getViewLifecycleOwner(), list -> {
productList.clear();
productList.addAll(list);
adapter.notifyDataSetChanged();
});
viewModel.getCategories().observe(getViewLifecycleOwner(), list -> {
SpinnerUtils.populateWhiteSpinner(requireContext(), binding.spinnerCategory, list,
DropdownDTO::getLabel, "All Categories", -1L, DropdownDTO::getId);
});
viewModel.getIsLoading().observe(getViewLifecycleOwner(), loading -> {
binding.swipeRefreshProduct.setRefreshing(loading);
});
}
private void filterProducts(String query) {
filteredList.clear();
if (query.isEmpty()) {
filteredList.addAll(productList);
} else {
String lower = query.toLowerCase();
for (Product p : productList) {
if (p.getProductName().toLowerCase().contains(lower)
|| p.getCategory().toLowerCase().contains(lower)
|| p.getProductDesc().toLowerCase().contains(lower)) {
filteredList.add(p);
}
}
@Override
public void onResume() {
super.onResume();
loadProductData();
viewModel.loadCategories();
}
private void setupFilterToggle() {
UIUtils.setupFilterToggle(binding.btnToggleFilter, binding.layoutFilter,
binding.etSearchProduct, binding.spinnerCategory);
}
private void setupSearch() {
UIUtils.attachSearch(binding.etSearchProduct, this::loadProductData);
}
private void setupCategoryFilter() {
SpinnerUtils.setupFilterSpinner(binding.spinnerCategory, this::loadProductData);
}
private void setupSwipeRefresh() {
binding.swipeRefreshProduct.setOnRefreshListener(this::loadProductData);
}
private void loadProductData() {
String query = binding.etSearchProduct.getText().toString().trim();
if (query.isEmpty()) query = null;
Long categoryId = null;
List<DropdownDTO> categories = viewModel.getCategories().getValue();
if (binding.spinnerCategory.getSelectedItemPosition() > 0 && categories != null && !categories.isEmpty()) {
categoryId = categories.get(binding.spinnerCategory.getSelectedItemPosition() - 1).getId();
}
adapter.notifyDataSetChanged();
viewModel.loadProducts(query, categoryId);
}
private void setupSwipeRefresh(View view) {
swipeRefreshLayout = view.findViewById(R.id.swipeRefreshProduct);
swipeRefreshLayout.setOnRefreshListener(() -> {
loadProductData(); // TODO: Replace with actual API call
filterProducts(etSearch.getText().toString());
swipeRefreshLayout.setRefreshing(false);
});
private void setupRecyclerView() {
adapter = new ProductAdapter(productList, this);
adapter.setBaseUrl(baseUrl);
binding.recyclerViewProducts.setLayoutManager(new LinearLayoutManager(getContext()));
binding.recyclerViewProducts.setAdapter(adapter);
}
private void openProductDetails(int position) {
ProductDetailFragment detailFragment = new ProductDetailFragment();
Bundle args = new Bundle();
args.putInt("position", position);
if (position != -1) {
Product product = filteredList.get(position);
int realPosition = productList.indexOf(product);
args.putInt("position", realPosition);
args.putInt("productId", product.getProductId());
args.putString("productName", product.getProductName());
args.putString("productDesc", product.getProductDesc());
args.putString("category", product.getCategory());
args.putDouble("productPrice", product.getProductPrice());
args.putInt("stockQuantity", product.getStockQuantity());
ProductDTO product = productList.get(position);
args.putLong("prodId", product.getProdId());
}
detailFragment.setArguments(args);
detailFragment.setProductFragment(this);
ListFragment listFragment = (ListFragment) getParentFragment();
if (listFragment != null) listFragment.loadFragment(detailFragment);
}
public void onProductSaved(int position, Product product) {
if (position == -1) {
productList.add(product);
} else {
productList.set(position, product);
}
filterProducts(etSearch.getText().toString());
}
public void onProductDeleted(int position) {
productList.remove(position);
filterProducts(etSearch.getText().toString());
NavHostFragment.findNavController(this).navigate(R.id.nav_product_detail, args);
}
@Override
@@ -142,21 +140,9 @@ public class ProductFragment extends Fragment implements ProductAdapter.OnProduc
openProductDetails(position);
}
private void loadProductData() {
productList.clear();
productList.add(new Product(1, "Premium Dog Food", "High protein dry food for adult dogs", "Food", 45.99, 25));
productList.add(new Product(2, "Cat Toy Bundle", "Set of 5 interactive toys", "Toys", 19.99, 40));
productList.add(new Product(3, "Pet Shampoo", "Gentle formula for all breeds", "Grooming", 12.99, 60));
productList.add(new Product(4, "Dog Bed - Large", "Memory foam orthopedic bed", "Bedding", 89.99, 10));
productList.add(new Product(5, "Aquarium Starter Kit", "20-gallon tank with filter and light", "Aquatic", 129.99, 5));
filteredList.clear();
filteredList.addAll(productList);
}
private void setupRecyclerView(View view) {
RecyclerView recyclerView = view.findViewById(R.id.recyclerViewProducts);
adapter = new ProductAdapter(filteredList, this);
recyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
recyclerView.setAdapter(adapter);
@Override
public void onDestroyView() {
super.onDestroyView();
binding = null;
}
}

View File

@@ -0,0 +1,182 @@
package com.example.petstoremobile.fragments.listfragments;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
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 com.example.petstoremobile.R;
import com.example.petstoremobile.adapters.ProductSupplierAdapter;
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.utils.BulkDeleteHandler;
import com.example.petstoremobile.utils.SpinnerUtils;
import com.example.petstoremobile.utils.UIUtils;
import com.example.petstoremobile.viewmodels.ProductSupplierListViewModel;
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 ProductSupplierAdapter adapter;
private ProductSupplierListViewModel viewModel;
private BulkDeleteHandler bulkDeleteHandler;
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
viewModel = new ViewModelProvider(this).get(ProductSupplierListViewModel.class);
}
@Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
binding = FragmentProductSupplierBinding.inflate(inflater, container, false);
setupRecyclerView();
setupSearch();
setupProductFilter();
setupSupplierFilter();
setupSwipeRefresh();
setupFilterToggle();
setupBulkDelete();
observeViewModel();
binding.fabAddPS.setOnClickListener(v -> openDetail(-1));
UIUtils.setupHamburgerMenu(binding.btnHamburgerPS, this);
return binding.getRoot();
}
private void observeViewModel() {
viewModel.getProductSuppliers().observe(getViewLifecycleOwner(), list -> {
psList.clear();
psList.addAll(list);
adapter.notifyDataSetChanged();
});
viewModel.getProducts().observe(getViewLifecycleOwner(), list -> {
SpinnerUtils.populateWhiteSpinner(requireContext(), binding.spinnerProduct, list,
ProductDTO::getProdName, "All Products", -1L, ProductDTO::getProdId);
});
viewModel.getSuppliers().observe(getViewLifecycleOwner(), list -> {
SpinnerUtils.populateWhiteSpinner(requireContext(), binding.spinnerSupplier, list,
SupplierDTO::getSupCompany, "All Suppliers", -1L, SupplierDTO::getSupId);
});
viewModel.getIsLoading().observe(getViewLifecycleOwner(), loading -> {
binding.swipeRefreshPS.setRefreshing(loading);
});
}
private void setupBulkDelete() {
bulkDeleteHandler = new BulkDeleteHandler(
this,
binding.layoutBulkDelete,
binding.tvSelectionCount,
binding.btnBulkDelete,
adapter,
"relationship",
viewModel::bulkDeleteProductSuppliers,
this::loadData
);
}
@Override
public void onResume() {
super.onResume();
loadData();
viewModel.loadFilterData();
}
@Override
public void onDestroyView() {
super.onDestroyView();
binding = null;
}
private void setupFilterToggle() {
UIUtils.setupFilterToggle(binding.btnToggleFilter, binding.layoutFilter, binding.etSearchPS,
binding.spinnerProduct, binding.spinnerSupplier);
}
private void setupRecyclerView() {
adapter = new ProductSupplierAdapter(psList, this);
binding.recyclerViewPS.setLayoutManager(new LinearLayoutManager(getContext()));
binding.recyclerViewPS.setAdapter(adapter);
}
private void setupSearch() {
UIUtils.attachSearch(binding.etSearchPS, this::loadData);
}
private void setupProductFilter() {
SpinnerUtils.setupFilterSpinner(binding.spinnerProduct, this::loadData);
}
private void setupSupplierFilter() {
SpinnerUtils.setupFilterSpinner(binding.spinnerSupplier, this::loadData);
}
private void setupSwipeRefresh() {
binding.swipeRefreshPS.setOnRefreshListener(this::loadData);
}
private void loadData() {
String query = binding.etSearchPS.getText().toString().trim();
if (query.isEmpty()) query = null;
Long productId = null;
List<ProductDTO> products = viewModel.getProducts().getValue();
if (binding.spinnerProduct.getSelectedItemPosition() > 0 && products != null && !products.isEmpty()) {
productId = products.get(binding.spinnerProduct.getSelectedItemPosition() - 1).getProdId();
}
Long supplierId = null;
List<SupplierDTO> suppliers = viewModel.getSuppliers().getValue();
if (binding.spinnerSupplier.getSelectedItemPosition() > 0 && suppliers != null && !suppliers.isEmpty()) {
supplierId = suppliers.get(binding.spinnerSupplier.getSelectedItemPosition() - 1).getSupId();
}
viewModel.loadProductSuppliers(query, productId, supplierId);
}
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);
}
@Override
public void onProductSupplierClick(int position) { openDetail(position); }
@Override
public void onSelectionChanged(int count) {
if (bulkDeleteHandler != null) {
bulkDeleteHandler.onSelectionChanged(count);
}
}
}

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