diff --git a/android/.gitignore b/android/.gitignore
new file mode 100644
index 00000000..faf530b2
--- /dev/null
+++ b/android/.gitignore
@@ -0,0 +1,10 @@
+*.iml
+.gradle
+/local.properties
+/.idea/
+.DS_Store
+/build
+/captures
+.externalNativeBuild
+.cxx
+local.properties
diff --git a/android/app/.gitignore b/android/app/.gitignore
new file mode 100644
index 00000000..42afabfd
--- /dev/null
+++ b/android/app/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts
new file mode 100644
index 00000000..d1cc3c30
--- /dev/null
+++ b/android/app/build.gradle.kts
@@ -0,0 +1,62 @@
+plugins {
+ alias(libs.plugins.android.application)
+}
+
+android {
+ namespace = "com.example.petstoremobile"
+ compileSdk = 36
+
+ defaultConfig {
+ applicationId = "com.example.petstoremobile"
+ minSdk = 24
+ targetSdk = 36
+ versionCode = 1
+ versionName = "1.0"
+
+ testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
+ }
+
+ buildTypes {
+ release {
+ isMinifyEnabled = false
+ proguardFiles(
+ getDefaultProguardFile("proguard-android-optimize.txt"),
+ "proguard-rules.pro"
+ )
+ }
+ }
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_11
+ targetCompatibility = JavaVersion.VERSION_11
+ }
+}
+
+dependencies {
+ 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("com.github.NaikSoftware:StompProtocolAndroid:1.6.6")
+ implementation("io.reactivex.rxjava2:rxjava:2.2.21")
+ implementation("io.reactivex.rxjava2:rxandroid:2.1.1")
+
+ testImplementation(libs.junit)
+ androidTestImplementation(libs.ext.junit)
+ androidTestImplementation(libs.espresso.core)
+}
\ No newline at end of file
diff --git a/android/app/proguard-rules.pro b/android/app/proguard-rules.pro
new file mode 100644
index 00000000..481bb434
--- /dev/null
+++ b/android/app/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
\ No newline at end of file
diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
new file mode 100644
index 00000000..633ea6ba
--- /dev/null
+++ b/android/app/src/main/AndroidManifest.xml
@@ -0,0 +1,52 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/android/app/src/main/java/com/example/petstoremobile/PetStoreApplication.java b/android/app/src/main/java/com/example/petstoremobile/PetStoreApplication.java
new file mode 100644
index 00000000..6134d221
--- /dev/null
+++ b/android/app/src/main/java/com/example/petstoremobile/PetStoreApplication.java
@@ -0,0 +1,13 @@
+package com.example.petstoremobile;
+
+import android.app.Application;
+import com.example.petstoremobile.api.auth.TokenManager;
+
+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();
+ }
+}
\ No newline at end of file
diff --git a/android/app/src/main/java/com/example/petstoremobile/activities/HomeActivity.java b/android/app/src/main/java/com/example/petstoremobile/activities/HomeActivity.java
new file mode 100644
index 00000000..1dee2f66
--- /dev/null
+++ b/android/app/src/main/java/com/example/petstoremobile/activities/HomeActivity.java
@@ -0,0 +1,69 @@
+package com.example.petstoremobile.activities;
+
+import android.os.Bundle;
+
+import androidx.activity.EdgeToEdge;
+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 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;
+
+public class HomeActivity extends AppCompatActivity {
+
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ EdgeToEdge.enable(this);
+ setContentView(R.layout.activity_home);
+
+ ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.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);
+ }
+
+ //when an item in the bar is selected, load the corresponding fragment
+ bottomNav.setOnItemSelectedListener(item -> {
+
+ if (item.getItemId() == R.id.nav_list) {
+ loadFragment(new ListFragment());
+ return true;
+ } else if (item.getItemId() == R.id.nav_chat) {
+ loadFragment(new ChatFragment());
+ return true;
+ } else if (item.getItemId() == R.id.nav_profile) {
+ loadFragment(new ProfileFragment());
+ return true;
+ }
+ return false;
+ });
+ }
+
+ //helper function to load a fragment
+ private void loadFragment(Fragment fragment) {
+ getSupportFragmentManager()
+ .beginTransaction()
+ .replace(R.id.fragment_container, fragment)
+ .commit();
+ }
+}
\ No newline at end of file
diff --git a/android/app/src/main/java/com/example/petstoremobile/activities/MainActivity.java b/android/app/src/main/java/com/example/petstoremobile/activities/MainActivity.java
new file mode 100644
index 00000000..6bc65f23
--- /dev/null
+++ b/android/app/src/main/java/com/example/petstoremobile/activities/MainActivity.java
@@ -0,0 +1,132 @@
+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.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.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 retrofit2.Call;
+import retrofit2.Callback;
+import retrofit2.Response;
+
+//The login screen activity
+public class MainActivity extends AppCompatActivity {
+
+ private EditText etUser;
+ private EditText etPassword;
+ private Button btnLogin;
+ private TextView tvLoginStatus;
+
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ 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;
+ }
+
+ EdgeToEdge.enable(this);
+ setContentView(R.layout.activity_main);
+
+ ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.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("");
+
+ //Set click listener for login button
+ btnLogin.setOnClickListener(v -> {
+ //Get username and password from text fields
+ String username = etUser.getText().toString();
+ String password = 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");
+ return;
+ }
+
+ AuthApi authApi = RetrofitClient.getAuthApi(this);
+
+ //Call login from api and get response
+ authApi.login(new AuthDTO.LoginRequest(username,password)).enqueue(new Callback() {
+ @Override
+ public void onResponse(Call call, Response 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() {
+ @Override
+ public void onResponse(Call call,
+ Response 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 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 call, Throwable t) {
+ Toast.makeText(MainActivity.this, "Login failed", Toast.LENGTH_SHORT).show();
+ tvLoginStatus.setText("Login failed");
+ }
+ });
+ });
+ }
+}
\ No newline at end of file
diff --git a/android/app/src/main/java/com/example/petstoremobile/adapters/AdoptionAdapter.java b/android/app/src/main/java/com/example/petstoremobile/adapters/AdoptionAdapter.java
new file mode 100644
index 00000000..98e88256
--- /dev/null
+++ b/android/app/src/main/java/com/example/petstoremobile/adapters/AdoptionAdapter.java
@@ -0,0 +1,79 @@
+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 java.util.List;
+
+public class AdoptionAdapter extends RecyclerView.Adapter {
+
+ private List adoptionList;
+ private OnAdoptionClickListener adoptionClickListener;
+
+ // Interface for adoption click on recycler view
+ public interface OnAdoptionClickListener {
+ void onAdoptionClick(int position);
+ }
+
+ // Constructor
+ public AdoptionAdapter(List adoptionList, OnAdoptionClickListener adoptionClickListener) {
+ this.adoptionList = adoptionList;
+ this.adoptionClickListener = adoptionClickListener;
+ }
+
+ // Get the controls of each row in recycler view
+ public static class AdoptionViewHolder extends RecyclerView.ViewHolder {
+ TextView tvAdopterName, tvPetName, tvAdoptionDate, tvAdoptionStatus;
+
+ 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);
+ }
+ }
+
+ // 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);
+ }
+
+ // Populate the row with adoption data
+ @Override
+ public void onBindViewHolder(@NonNull AdoptionViewHolder holder, int position) {
+ Adoption adoption = adoptionList.get(position);
+
+ holder.tvAdopterName.setText(adoption.getAdopterName());
+ holder.tvPetName.setText("Pet: " + adoption.getPetName());
+ holder.tvAdoptionDate.setText("Date: " + adoption.getAdoptionDate());
+ holder.tvAdoptionStatus.setText(adoption.getStatus());
+
+ // 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"));
+ }
+
+ // When a row is clicked, open the detail view
+ holder.itemView.setOnClickListener(v -> adoptionClickListener.onAdoptionClick(position));
+ }
+
+ @Override
+ public int getItemCount() {
+ return adoptionList.size();
+ }
+}
diff --git a/android/app/src/main/java/com/example/petstoremobile/adapters/AppointmentAdapter.java b/android/app/src/main/java/com/example/petstoremobile/adapters/AppointmentAdapter.java
new file mode 100644
index 00000000..7924c12b
--- /dev/null
+++ b/android/app/src/main/java/com/example/petstoremobile/adapters/AppointmentAdapter.java
@@ -0,0 +1,86 @@
+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 java.util.List;
+
+public class AppointmentAdapter extends RecyclerView.Adapter {
+
+ private List appointmentList;
+ private OnAppointmentClickListener appointmentClickListener;
+
+ // Interface for appointment click on recycler view
+ public interface OnAppointmentClickListener {
+ void onAppointmentClick(int position);
+ }
+
+ // Constructor
+ public AppointmentAdapter(List appointmentList, OnAppointmentClickListener appointmentClickListener) {
+ this.appointmentList = appointmentList;
+ this.appointmentClickListener = appointmentClickListener;
+ }
+
+ // Get the controls of each row in recycler view
+ public static class AppointmentViewHolder extends RecyclerView.ViewHolder {
+ TextView tvCustomerName, tvPetName, tvServiceType, tvDateTime, tvAppointmentStatus;
+
+ 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);
+ }
+ }
+
+ // 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);
+ }
+
+ // Populate the row with appointment data
+ @Override
+ public void onBindViewHolder(@NonNull AppointmentViewHolder holder, int position) {
+ Appointment appointment = appointmentList.get(position);
+
+ 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());
+
+ // Set the status color depending on appointment status
+ switch (appointment.getStatus()) {
+ case "Confirmed":
+ holder.tvAppointmentStatus.setBackgroundColor(Color.parseColor("#4CAF50"));
+ break;
+ case "Pending":
+ holder.tvAppointmentStatus.setBackgroundColor(Color.parseColor("#FF9800"));
+ break;
+ default:
+ holder.tvAppointmentStatus.setBackgroundColor(Color.parseColor("#F44336"));
+ break;
+ }
+
+ // When a row is clicked, open the detail view
+ holder.itemView.setOnClickListener(v -> appointmentClickListener.onAppointmentClick(position));
+ }
+
+ @Override
+ public int getItemCount() {
+ return appointmentList.size();
+ }
+}
diff --git a/android/app/src/main/java/com/example/petstoremobile/adapters/ChatAdapter.java b/android/app/src/main/java/com/example/petstoremobile/adapters/ChatAdapter.java
new file mode 100644
index 00000000..93e6d902
--- /dev/null
+++ b/android/app/src/main/java/com/example/petstoremobile/adapters/ChatAdapter.java
@@ -0,0 +1,59 @@
+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.models.Chat;
+
+import java.util.List;
+
+public class ChatAdapter extends RecyclerView.Adapter {
+
+ private List chatList;
+ private OnChatClickListener listener;
+
+ public interface OnChatClickListener {
+ void onChatClick(Chat chat);
+ }
+
+ public ChatAdapter(List chatList, OnChatClickListener listener) {
+ this.chatList = chatList;
+ this.listener = listener;
+ }
+
+ @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);
+ }
+
+ @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.itemView.setOnClickListener(v -> listener.onChatClick(chat));
+ }
+
+ @Override
+ public int getItemCount() {
+ return chatList.size();
+ }
+
+ public static class ChatViewHolder extends RecyclerView.ViewHolder {
+ TextView tvCustomerName, tvLastMessage;
+
+ public ChatViewHolder(@NonNull View itemView) {
+ super(itemView);
+ tvCustomerName = itemView.findViewById(R.id.tvCustomerName);
+ tvLastMessage = itemView.findViewById(R.id.tvLastMessage);
+ }
+ }
+}
\ No newline at end of file
diff --git a/android/app/src/main/java/com/example/petstoremobile/adapters/InventoryAdapter.java b/android/app/src/main/java/com/example/petstoremobile/adapters/InventoryAdapter.java
new file mode 100644
index 00000000..7ae36dc5
--- /dev/null
+++ b/android/app/src/main/java/com/example/petstoremobile/adapters/InventoryAdapter.java
@@ -0,0 +1,79 @@
+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 java.util.List;
+
+public class InventoryAdapter extends RecyclerView.Adapter {
+
+ private List inventoryList;
+ private OnInventoryClickListener inventoryClickListener;
+
+ // Interface for inventory click on recycler view
+ public interface OnInventoryClickListener {
+ void onInventoryClick(int position);
+ }
+
+ // Constructor
+ public InventoryAdapter(List inventoryList, OnInventoryClickListener inventoryClickListener) {
+ this.inventoryList = inventoryList;
+ this.inventoryClickListener = inventoryClickListener;
+ }
+
+ // Get the controls of each row in recycler view
+ public static class InventoryViewHolder extends RecyclerView.ViewHolder {
+ TextView tvItemName, tvCategory, tvQuantity, tvUnitPrice, tvSupplier;
+
+ 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);
+ }
+ }
+
+ // 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);
+ }
+
+ // Populate the row with inventory data
+ @Override
+ public void onBindViewHolder(@NonNull InventoryViewHolder holder, int position) {
+ Inventory inventory = inventoryList.get(position);
+
+ 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());
+
+ // Highlight low stock items in red
+ if (inventory.getQuantity() <= 5) {
+ holder.tvQuantity.setTextColor(Color.parseColor("#F44336"));
+ } else {
+ holder.tvQuantity.setTextColor(Color.parseColor("#4CAF50"));
+ }
+
+ // When a row is clicked, open the detail view
+ holder.itemView.setOnClickListener(v -> inventoryClickListener.onInventoryClick(position));
+ }
+
+ @Override
+ public int getItemCount() {
+ return inventoryList.size();
+ }
+}
diff --git a/android/app/src/main/java/com/example/petstoremobile/adapters/MessageAdapter.java b/android/app/src/main/java/com/example/petstoremobile/adapters/MessageAdapter.java
new file mode 100644
index 00000000..0354a1fd
--- /dev/null
+++ b/android/app/src/main/java/com/example/petstoremobile/adapters/MessageAdapter.java
@@ -0,0 +1,78 @@
+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.models.Message;
+import java.util.List;
+
+public class MessageAdapter extends RecyclerView.Adapter {
+
+ private static final int TYPE_SENT = 1;
+ private static final int TYPE_RECEIVED = 2;
+
+ private final List messages;
+ private Long currentUserId;
+
+ public MessageAdapter(List messages, Long currentUserId) {
+ this.messages = messages;
+ this.currentUserId = currentUserId;
+ }
+
+ public void setCurrentUserId(Long id) {
+ this.currentUserId = id;
+ notifyDataSetChanged();
+ }
+
+ @Override
+ public int getItemViewType(int position) {
+ Message m = messages.get(position);
+ if (currentUserId != null && currentUserId.equals(m.getSenderId())) {
+ return TYPE_SENT;
+ }
+ return TYPE_RECEIVED;
+ }
+
+ @NonNull @Override
+ 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);
+ } else {
+ View v = inf.inflate(R.layout.item_message_received, parent, false);
+ return new ReceivedHolder(v);
+ }
+ }
+
+ @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);
+ }
+
+ @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
+ }
+ 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
+ }
+ void bind(Message m) { tvMessage.setText(m.getContent()); }
+ }
+}
\ No newline at end of file
diff --git a/android/app/src/main/java/com/example/petstoremobile/adapters/PetAdapter.java b/android/app/src/main/java/com/example/petstoremobile/adapters/PetAdapter.java
new file mode 100644
index 00000000..c78cc7cc
--- /dev/null
+++ b/android/app/src/main/java/com/example/petstoremobile/adapters/PetAdapter.java
@@ -0,0 +1,85 @@
+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.dtos.PetDTO;
+import java.util.List;
+
+public class PetAdapter extends RecyclerView.Adapter {
+
+ private List petList;
+ private OnPetClickListener petClickListener;
+
+ // Interface for pet click on recycler view
+ public interface OnPetClickListener {
+ void onPetClick(int position);
+ }
+
+ //Constructor
+ public PetAdapter(List petList, OnPetClickListener petClickListener) {
+ this.petList = petList;
+ this.petClickListener = petClickListener;
+ }
+
+ // Get the controls of each row in recycler view
+ public static class PetViewHolder extends RecyclerView.ViewHolder {
+ TextView tvPetName, tvPetSpeciesBreed, tvPetAge, tvPetPrice, tvPetStatus;
+
+ 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);
+ }
+ }
+
+ // Create a new row view
+ @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);
+ }
+
+ //populate the row with pet data
+ @Override
+ public void onBindViewHolder(@NonNull PetViewHolder holder, int position) {
+ PetDTO pet = petList.get(position);
+
+ holder.tvPetName.setText(pet.getPetName());
+ holder.tvPetSpeciesBreed.setText(pet.getPetSpecies() + " - " + pet.getPetBreed());
+ holder.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());
+ }
+
+ holder.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"));
+ } else {
+ holder.tvPetStatus.setBackgroundColor(Color.parseColor("#F44336"));
+ }
+
+ //when a row is clicked, open the detail view
+ holder.itemView.setOnClickListener(v -> petClickListener.onPetClick(position));
+ }
+
+ @Override
+ public int getItemCount() {
+ return petList.size();
+ }
+}
\ No newline at end of file
diff --git a/android/app/src/main/java/com/example/petstoremobile/adapters/ProductAdapter.java b/android/app/src/main/java/com/example/petstoremobile/adapters/ProductAdapter.java
new file mode 100644
index 00000000..a4e5e770
--- /dev/null
+++ b/android/app/src/main/java/com/example/petstoremobile/adapters/ProductAdapter.java
@@ -0,0 +1,72 @@
+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.models.Product;
+import java.util.List;
+
+public class ProductAdapter extends RecyclerView.Adapter {
+
+ private List productList;
+ private OnProductClickListener productClickListener;
+
+ // Interface for product click on recycler view
+ public interface OnProductClickListener {
+ void onProductClick(int position);
+ }
+
+ // Constructor
+ public ProductAdapter(List productList, OnProductClickListener productClickListener) {
+ this.productList = productList;
+ this.productClickListener = productClickListener;
+ }
+
+ // Get the controls of each row in recycler view
+ public static class ProductViewHolder extends RecyclerView.ViewHolder {
+ TextView tvProductName, tvProductDesc, tvCategory, tvProductPrice, tvStockQuantity;
+
+ 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);
+ }
+ }
+
+ // 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);
+ }
+
+ // Populate the row with product data
+ @Override
+ public void onBindViewHolder(@NonNull ProductViewHolder holder, int position) {
+ Product product = productList.get(position);
+
+ 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());
+
+ // When a row is clicked, open the detail view
+ holder.itemView.setOnClickListener(v -> productClickListener.onProductClick(position));
+ }
+
+ @Override
+ public int getItemCount() {
+ return productList.size();
+ }
+}
+
diff --git a/android/app/src/main/java/com/example/petstoremobile/adapters/ServiceAdapter.java b/android/app/src/main/java/com/example/petstoremobile/adapters/ServiceAdapter.java
new file mode 100644
index 00000000..e3cc6d1c
--- /dev/null
+++ b/android/app/src/main/java/com/example/petstoremobile/adapters/ServiceAdapter.java
@@ -0,0 +1,68 @@
+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.ServiceDTO;
+import java.util.List;
+
+public class ServiceAdapter extends RecyclerView.Adapter {
+
+ private List serviceList;
+ private OnServiceClickListener serviceClickListener;
+
+ // Interface for service click on recycler view
+ public interface OnServiceClickListener {
+ void onServiceClick(int position);
+ }
+
+ //Constructor
+ public ServiceAdapter(List serviceList, OnServiceClickListener serviceClickListener) {
+ this.serviceList = serviceList;
+ this.serviceClickListener = serviceClickListener;
+ }
+
+ // Get the controls of each row in recycler view
+ public static class ServiceViewHolder extends RecyclerView.ViewHolder {
+ TextView tvServiceName, tvServiceDesc, tvServiceDuration, tvServicePrice;
+
+ 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);
+ }
+ }
+
+ // Create a new row view
+ @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);
+ }
+
+ //populate the row with service data
+ @Override
+ public void onBindViewHolder(@NonNull ServiceViewHolder holder, int position) {
+ ServiceDTO service = serviceList.get(position);
+
+ 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()));
+
+ //when a row is clicked, open the detail view
+ holder.itemView.setOnClickListener(v -> serviceClickListener.onServiceClick(position));
+ }
+
+ @Override
+ public int getItemCount() {
+ return serviceList.size();
+ }
+}
\ No newline at end of file
diff --git a/android/app/src/main/java/com/example/petstoremobile/adapters/SupplierAdapter.java b/android/app/src/main/java/com/example/petstoremobile/adapters/SupplierAdapter.java
new file mode 100644
index 00000000..e134f5b2
--- /dev/null
+++ b/android/app/src/main/java/com/example/petstoremobile/adapters/SupplierAdapter.java
@@ -0,0 +1,68 @@
+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.SupplierDTO;
+import java.util.List;
+
+public class SupplierAdapter extends RecyclerView.Adapter {
+
+ private List supplierList;
+ private OnSupplierClickListener supplierClickListener;
+
+ // Interface for supplier click on recycler view
+ public interface OnSupplierClickListener {
+ void onSupplierClick(int position);
+ }
+
+ //Constructor
+ public SupplierAdapter(List supplierList, OnSupplierClickListener supplierClickListener) {
+ this.supplierList = supplierList;
+ this.supplierClickListener = supplierClickListener;
+ }
+
+ // Get the controls of each row in recycler view
+ public static class SupplierViewHolder extends RecyclerView.ViewHolder {
+ TextView tvSupCompany, tvSupContactName, tvSupEmail, tvSupPhone;
+
+ 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);
+ }
+ }
+
+ // Create a new row view
+ @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);
+ }
+
+ //populate the row with supplier data
+ @Override
+ public void onBindViewHolder(@NonNull SupplierViewHolder holder, int position) {
+ SupplierDTO supplier = supplierList.get(position);
+
+ holder.tvSupCompany.setText(supplier.getSupCompany());
+ holder.tvSupContactName.setText(supplier.getSupContactFirstName() + " " + supplier.getSupContactLastName());
+ holder.tvSupEmail.setText(supplier.getSupEmail());
+ holder.tvSupPhone.setText(supplier.getSupPhone());
+
+ //when a row is clicked, open the detail view
+ holder.itemView.setOnClickListener(v -> supplierClickListener.onSupplierClick(position));
+ }
+
+ @Override
+ public int getItemCount() {
+ return supplierList.size();
+ }
+}
\ No newline at end of file
diff --git a/android/app/src/main/java/com/example/petstoremobile/api/ChatApi.java b/android/app/src/main/java/com/example/petstoremobile/api/ChatApi.java
new file mode 100644
index 00000000..63f5ac51
--- /dev/null
+++ b/android/app/src/main/java/com/example/petstoremobile/api/ChatApi.java
@@ -0,0 +1,23 @@
+package com.example.petstoremobile.api;
+
+import com.example.petstoremobile.dtos.ConversationDTO;
+import com.example.petstoremobile.dtos.MessageDTO;
+
+import java.util.List;
+
+import retrofit2.Call;
+import retrofit2.http.Body;
+import retrofit2.http.GET;
+import retrofit2.http.POST;
+import retrofit2.http.Path;
+
+//api calls to get conversations
+public interface ChatApi {
+
+ @GET("api/v1/chat/conversations")
+ Call> getAllConversations();
+
+ @GET("api/v1/chat/conversations/{conversationId}")
+ Call getConversationById(@Path("conversationId") Long conversationId);
+
+}
\ No newline at end of file
diff --git a/android/app/src/main/java/com/example/petstoremobile/api/CustomerApi.java b/android/app/src/main/java/com/example/petstoremobile/api/CustomerApi.java
new file mode 100644
index 00000000..02700075
--- /dev/null
+++ b/android/app/src/main/java/com/example/petstoremobile/api/CustomerApi.java
@@ -0,0 +1,21 @@
+package com.example.petstoremobile.api;
+
+import com.example.petstoremobile.dtos.CustomerDTO;
+import com.example.petstoremobile.dtos.PageResponse;
+
+import java.util.List;
+
+import retrofit2.Call;
+import retrofit2.http.GET;
+import retrofit2.http.Path;
+import retrofit2.http.Query;
+
+//api calls to get customers
+public interface CustomerApi {
+
+ @GET("api/v1/customers")
+ Call> getAllCustomers(@Query("page") int page, @Query("size") int size);
+
+ @GET("api/v1/customers/{customerId}")
+ Call getCustomerById(@Path("customerId") Long customerId);
+}
\ No newline at end of file
diff --git a/android/app/src/main/java/com/example/petstoremobile/api/MessageApi.java b/android/app/src/main/java/com/example/petstoremobile/api/MessageApi.java
new file mode 100644
index 00000000..13df781f
--- /dev/null
+++ b/android/app/src/main/java/com/example/petstoremobile/api/MessageApi.java
@@ -0,0 +1,20 @@
+package com.example.petstoremobile.api;
+
+import com.example.petstoremobile.dtos.MessageDTO;
+import com.example.petstoremobile.dtos.SendMessageRequest;
+import java.util.List;
+import retrofit2.Call;
+import retrofit2.http.Body;
+import retrofit2.http.GET;
+import retrofit2.http.POST;
+import retrofit2.http.Path;
+
+//api calls to get and send messages
+public interface MessageApi {
+
+ @GET("api/v1/chat/conversations/{id}/messages")
+ Call> getMessages(@Path("id") Long conversationId);
+
+ @POST("api/v1/chat/conversations/{id}/messages")
+ Call sendMessage(@Path("id") Long conversationId, @Body SendMessageRequest request);
+}
\ No newline at end of file
diff --git a/android/app/src/main/java/com/example/petstoremobile/api/PetApi.java b/android/app/src/main/java/com/example/petstoremobile/api/PetApi.java
new file mode 100644
index 00000000..35fccfbc
--- /dev/null
+++ b/android/app/src/main/java/com/example/petstoremobile/api/PetApi.java
@@ -0,0 +1,40 @@
+package com.example.petstoremobile.api;
+
+import com.example.petstoremobile.dtos.PageResponse;
+import com.example.petstoremobile.dtos.PetDTO;
+
+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 CRUD pets
+public interface PetApi {
+ // Get all pets
+ @GET("api/v1/pets")
+ Call> getAllPets(
+ @Query("page") int page,
+ @Query("size") int size
+ );
+
+ // Get pet by id
+ @GET("api/v1/pets/{id}")
+ Call getPetById(@Path("id") Long id);
+
+ // Create pet
+ @POST("api/v1/pets")
+ Call createPet(@Body PetDTO pet);
+
+ // Update pet
+ @PUT("api/v1/pets/{id}")
+ Call updatePet(@Path("id") Long id, @Body PetDTO pet);
+
+ // Delete pet
+ @DELETE("api/v1/pets/{id}")
+ Call deletePet(@Path("id") Long id);
+
+}
diff --git a/android/app/src/main/java/com/example/petstoremobile/api/RetrofitClient.java b/android/app/src/main/java/com/example/petstoremobile/api/RetrofitClient.java
new file mode 100644
index 00000000..8a7f48bc
--- /dev/null
+++ b/android/app/src/main/java/com/example/petstoremobile/api/RetrofitClient.java
@@ -0,0 +1,70 @@
+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);
+ }
+
+}
\ No newline at end of file
diff --git a/android/app/src/main/java/com/example/petstoremobile/api/ServiceApi.java b/android/app/src/main/java/com/example/petstoremobile/api/ServiceApi.java
new file mode 100644
index 00000000..a8e4ed32
--- /dev/null
+++ b/android/app/src/main/java/com/example/petstoremobile/api/ServiceApi.java
@@ -0,0 +1,39 @@
+package com.example.petstoremobile.api;
+
+import com.example.petstoremobile.dtos.PageResponse;
+import com.example.petstoremobile.dtos.ServiceDTO;
+
+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 CRUD services
+public interface ServiceApi {
+ // Get all services
+ @GET("api/v1/services")
+ Call> getAllServices(
+ @Query("page") int page,
+ @Query("size") int size
+ );
+
+ // Get service by id
+ @GET("api/v1/services/{id}")
+ Call getServiceById(@Path("id") Long id);
+
+ // Create service
+ @POST("api/v1/services")
+ Call createService(@Body ServiceDTO service);
+
+ // Update service
+ @PUT("api/v1/services/{id}")
+ Call updateService(@Path("id") Long id, @Body ServiceDTO service);
+
+ // Delete service
+ @DELETE("api/v1/services/{id}")
+ Call deleteService(@Path("id") Long id);
+}
diff --git a/android/app/src/main/java/com/example/petstoremobile/api/SupplierApi.java b/android/app/src/main/java/com/example/petstoremobile/api/SupplierApi.java
new file mode 100644
index 00000000..47d4e1e3
--- /dev/null
+++ b/android/app/src/main/java/com/example/petstoremobile/api/SupplierApi.java
@@ -0,0 +1,39 @@
+package com.example.petstoremobile.api;
+
+import com.example.petstoremobile.dtos.PageResponse;
+import com.example.petstoremobile.dtos.SupplierDTO;
+
+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 CRUD suppliers
+public interface SupplierApi {
+ // Get all suppliers
+ @GET("api/v1/suppliers")
+ Call> getAllSuppliers(
+ @Query("page") int page,
+ @Query("size") int size
+ );
+
+ // Get supplier by id
+ @GET("api/v1/suppliers/{id}")
+ Call getSupplierById(@Path("id") Long id);
+
+ // Create supplier
+ @POST("api/v1/suppliers")
+ Call createSupplier(@Body SupplierDTO supplier);
+
+ // Update supplier
+ @PUT("api/v1/suppliers/{id}")
+ Call updateSupplier(@Path("id") Long id, @Body SupplierDTO supplier);
+
+ // Delete supplier
+ @DELETE("api/v1/suppliers/{id}")
+ Call deleteSupplier(@Path("id") Long id);
+}
diff --git a/android/app/src/main/java/com/example/petstoremobile/api/auth/AuthApi.java b/android/app/src/main/java/com/example/petstoremobile/api/auth/AuthApi.java
new file mode 100644
index 00000000..cf82623f
--- /dev/null
+++ b/android/app/src/main/java/com/example/petstoremobile/api/auth/AuthApi.java
@@ -0,0 +1,17 @@
+package com.example.petstoremobile.api.auth;
+
+import com.example.petstoremobile.dtos.AuthDTO;
+
+import retrofit2.Call;
+import retrofit2.http.Body;
+import retrofit2.http.GET;
+import retrofit2.http.POST;
+
+//Api for logging in and getting current user
+public interface AuthApi {
+ @POST("api/v1/auth/login")
+ Call login(@Body AuthDTO.LoginRequest loginRequest);
+
+ @GET("api/v1/auth/me")
+ Call getCurrentUser();
+}
diff --git a/android/app/src/main/java/com/example/petstoremobile/api/auth/AuthInterceptor.java b/android/app/src/main/java/com/example/petstoremobile/api/auth/AuthInterceptor.java
new file mode 100644
index 00000000..dd17fffd
--- /dev/null
+++ b/android/app/src/main/java/com/example/petstoremobile/api/auth/AuthInterceptor.java
@@ -0,0 +1,43 @@
+package com.example.petstoremobile.api.auth;
+
+import android.content.Context;
+
+import androidx.annotation.NonNull;
+
+import java.io.IOException;
+
+import okhttp3.Interceptor;
+import okhttp3.Request;
+import okhttp3.Response;
+
+//Used to get the token from the backend for authenticated api calls
+public class AuthInterceptor implements Interceptor {
+
+ private final TokenManager tokenManager;
+
+ public AuthInterceptor(Context context) {
+ this.tokenManager = TokenManager.getInstance(context);
+ }
+
+ @NonNull
+ @Override
+ public Response intercept(@NonNull Chain chain) throws IOException {
+ String token = tokenManager.getToken();
+ String url = chain.request().url().toString();
+
+ if (url.contains("auth/login") || url.contains("auth/register")) {
+ return chain.proceed(chain.request());
+ }
+
+ //If we have a token then add it to the request
+ if (token != null) {
+ Request request = chain.request().newBuilder()
+ .addHeader("Authorization", "Bearer " + token)
+ .build();
+ return chain.proceed(request);
+ }
+
+ //If no token then just pass the request
+ return chain.proceed(chain.request());
+ }
+}
diff --git a/android/app/src/main/java/com/example/petstoremobile/api/auth/TokenManager.java b/android/app/src/main/java/com/example/petstoremobile/api/auth/TokenManager.java
new file mode 100644
index 00000000..b0f90508
--- /dev/null
+++ b/android/app/src/main/java/com/example/petstoremobile/api/auth/TokenManager.java
@@ -0,0 +1,70 @@
+package com.example.petstoremobile.api.auth;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+
+//Store login token in shared preferences
+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 TokenManager instance;
+ private SharedPreferences prefs;
+
+ private TokenManager(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()
+ .putString(TOKEN_KEY, token)
+ .putString(USERNAME_KEY, username)
+ .putString(ROLE_KEY, role)
+ .apply();
+ }
+
+ //Getters
+ public String getToken() {
+ return prefs.getString(TOKEN_KEY, null);
+ }
+
+ public String getUsername() {
+ return prefs.getString(USERNAME_KEY, null);
+ }
+
+ public String getRole() {
+ return prefs.getString(ROLE_KEY, null);
+ }
+
+ public Long getUserId() {
+ long id = prefs.getLong(USER_ID_KEY, -1L);
+ return id == -1L ? null : id;
+ }
+
+ public void saveUserId(Long userId) {
+ prefs.edit().putLong(USER_ID_KEY, userId).apply();
+ }
+
+ //Check if logged in
+ public boolean isLoggedIn() {
+ return getToken() != null;
+ }
+
+ //Clear login data
+ public void clearLoginData() {
+ prefs.edit().clear().apply();
+ }
+
+
+}
diff --git a/android/app/src/main/java/com/example/petstoremobile/dtos/AuthDTO.java b/android/app/src/main/java/com/example/petstoremobile/dtos/AuthDTO.java
new file mode 100644
index 00000000..6aecdbc3
--- /dev/null
+++ b/android/app/src/main/java/com/example/petstoremobile/dtos/AuthDTO.java
@@ -0,0 +1,49 @@
+package com.example.petstoremobile.dtos;
+
+//Used to send login data to the backend
+public class AuthDTO {
+ public static class LoginRequest {
+ private String username;
+ private String password;
+
+ public LoginRequest(String username, String password) {
+ this.username = username;
+ this.password = password;
+ }
+
+ public String getUsername() { return username; }
+ public String getPassword() { return password; }
+ }
+
+ //Used to receive login data from the backend
+ public static class LoginResponse {
+ private String token;
+ private String username;
+ private String role;
+
+ public String getToken() { return token; }
+ public String getUsername() { return username; }
+ public String getRole() { return role; }
+ }
+
+ //Used to get logged in profile
+ public static class UserResponse {
+ private Long id;
+ private String username;
+ private String email;
+ private String fullName;
+ private String avatarUrl;
+ private String role;
+ private Long storeId;
+ private String storeName;
+
+ public Long getId() { return id; }
+ public String getUsername() { return username; }
+ public String getEmail() { return email; }
+ public String getFullName() { return fullName; }
+ public String getAvatarUrl() { return avatarUrl; }
+ public String getRole() { return role; }
+ public Long getStoreId() { return storeId; }
+ public String getStoreName() { return storeName; }
+ }
+}
diff --git a/android/app/src/main/java/com/example/petstoremobile/dtos/ConversationDTO.java b/android/app/src/main/java/com/example/petstoremobile/dtos/ConversationDTO.java
new file mode 100644
index 00000000..316aa467
--- /dev/null
+++ b/android/app/src/main/java/com/example/petstoremobile/dtos/ConversationDTO.java
@@ -0,0 +1,78 @@
+package com.example.petstoremobile.dtos;
+
+public class ConversationDTO {
+ private Long id;
+ private Long customerId;
+ private Long staffId;
+ private String status;
+ private String mode;
+ private String lastMessage;
+ private String createdAt;
+ private String updatedAt;
+
+ public ConversationDTO() {}
+
+ public Long getId() {
+ return id;
+ }
+
+ public void setId(Long id) {
+ this.id = id;
+ }
+
+ public Long getCustomerId() {
+ return customerId;
+ }
+
+ public void setCustomerId(Long customerId) {
+ this.customerId = customerId;
+ }
+
+ public Long getStaffId() {
+ return staffId;
+ }
+
+ public void setStaffId(Long staffId) {
+ this.staffId = staffId;
+ }
+
+ public String getStatus() {
+ return status;
+ }
+
+ public void setStatus(String status) {
+ this.status = status;
+ }
+
+ public String getMode() {
+ return mode;
+ }
+
+ public void setMode(String mode) {
+ this.mode = mode;
+ }
+
+ public String getLastMessage() {
+ return lastMessage;
+ }
+
+ public void setLastMessage(String lastMessage) {
+ this.lastMessage = lastMessage;
+ }
+
+ 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;
+ }
+}
\ No newline at end of file
diff --git a/android/app/src/main/java/com/example/petstoremobile/dtos/CustomerDTO.java b/android/app/src/main/java/com/example/petstoremobile/dtos/CustomerDTO.java
new file mode 100644
index 00000000..1a135a6d
--- /dev/null
+++ b/android/app/src/main/java/com/example/petstoremobile/dtos/CustomerDTO.java
@@ -0,0 +1,59 @@
+package com.example.petstoremobile.dtos;
+
+import com.google.gson.annotations.SerializedName;
+
+public class CustomerDTO {
+ @SerializedName("customerId")
+ private Long customerId;
+
+ private String firstName;
+ private String lastName;
+ private String email;
+ private String phone;
+
+ 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) {
+ 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 String getFullName() {
+ return (firstName != null ? firstName : "") + " " + (lastName != null ? lastName : "");
+ }
+}
\ No newline at end of file
diff --git a/android/app/src/main/java/com/example/petstoremobile/dtos/MessageDTO.java b/android/app/src/main/java/com/example/petstoremobile/dtos/MessageDTO.java
new file mode 100644
index 00000000..1080b600
--- /dev/null
+++ b/android/app/src/main/java/com/example/petstoremobile/dtos/MessageDTO.java
@@ -0,0 +1,44 @@
+package com.example.petstoremobile.dtos;
+
+import com.google.gson.annotations.SerializedName;
+
+public class MessageDTO {
+
+ @SerializedName("id")
+ private Long id;
+
+ @SerializedName("conversationId")
+ private Long conversationId;
+
+ @SerializedName("senderId")
+ private Long senderId;
+
+ @SerializedName("content")
+ private String content;
+
+ @SerializedName("timestamp")
+ private String timestamp;
+
+ @SerializedName("isRead")
+ private Boolean isRead;
+
+ public MessageDTO() {}
+
+ public Long getId() { return id; }
+ public void setId(Long id) { this.id = id; }
+
+ public Long getConversationId() { return conversationId; }
+ public void setConversationId(Long conversationId) { this.conversationId = conversationId; }
+
+ public Long getSenderId() { return senderId; }
+ public void setSenderId(Long senderId) { this.senderId = senderId; }
+
+ public String getContent() { return content; }
+ public void setContent(String content) { this.content = content; }
+
+ public String getTimestamp() { return timestamp; }
+ public void setTimestamp(String timestamp) { this.timestamp = timestamp; }
+
+ public Boolean getIsRead() { return isRead; }
+ public void setIsRead(Boolean isRead) { this.isRead = isRead; }
+}
\ No newline at end of file
diff --git a/android/app/src/main/java/com/example/petstoremobile/dtos/PageResponse.java b/android/app/src/main/java/com/example/petstoremobile/dtos/PageResponse.java
new file mode 100644
index 00000000..7237105e
--- /dev/null
+++ b/android/app/src/main/java/com/example/petstoremobile/dtos/PageResponse.java
@@ -0,0 +1,32 @@
+package com.example.petstoremobile.dtos;
+
+import java.util.List;
+
+//Used to get data from the API
+public class PageResponse {
+ private List content;
+ private int totalPages;
+ private long totalElements;
+ private int number;
+ private int size;
+ private boolean last;
+
+ public List getContent() {
+ return content;
+ }
+ public int getTotalPages() {
+ return totalPages;
+ }
+ public long getTotalElements() {
+ return totalElements;
+ }
+ public int getNumber() {
+ return number;
+ }
+ public int getSize() {
+ return size;
+ }
+ public boolean isLast() {
+ return last;
+ }
+}
\ No newline at end of file
diff --git a/android/app/src/main/java/com/example/petstoremobile/dtos/PetDTO.java b/android/app/src/main/java/com/example/petstoremobile/dtos/PetDTO.java
new file mode 100644
index 00000000..d76a8509
--- /dev/null
+++ b/android/app/src/main/java/com/example/petstoremobile/dtos/PetDTO.java
@@ -0,0 +1,40 @@
+package com.example.petstoremobile.dtos;
+
+public class PetDTO {
+ private Long petId;
+ private String petName;
+ private String petSpecies;
+ private String petBreed;
+ private Integer petAge;
+ private String petStatus;
+ private String petPrice;
+ private String createdAt;
+ private String updatedAt;
+
+ 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 String getPetSpecies() { return petSpecies; }
+ public void setPetSpecies(String petSpecies) { this.petSpecies = petSpecies; }
+
+ public String getPetBreed() { return petBreed; }
+ public void setPetBreed(String petBreed) { this.petBreed = petBreed; }
+
+ public Integer getPetAge() { return petAge; }
+ public void setPetAge(Integer petAge) { this.petAge = petAge; }
+
+ 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 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; }
+}
diff --git a/android/app/src/main/java/com/example/petstoremobile/dtos/SendMessageRequest.java b/android/app/src/main/java/com/example/petstoremobile/dtos/SendMessageRequest.java
new file mode 100644
index 00000000..7a521de3
--- /dev/null
+++ b/android/app/src/main/java/com/example/petstoremobile/dtos/SendMessageRequest.java
@@ -0,0 +1,13 @@
+package com.example.petstoremobile.dtos;
+
+public class SendMessageRequest {
+
+ private String content;
+
+ public SendMessageRequest(String content) {
+ this.content = content;
+ }
+
+ public String getContent() { return content; }
+ public void setContent(String content) { this.content = content; }
+}
\ No newline at end of file
diff --git a/android/app/src/main/java/com/example/petstoremobile/dtos/ServiceDTO.java b/android/app/src/main/java/com/example/petstoremobile/dtos/ServiceDTO.java
new file mode 100644
index 00000000..56e44371
--- /dev/null
+++ b/android/app/src/main/java/com/example/petstoremobile/dtos/ServiceDTO.java
@@ -0,0 +1,32 @@
+package com.example.petstoremobile.dtos;
+
+public class ServiceDTO {
+ private Long serviceId;
+ private String serviceName;
+ private String serviceDesc;
+ private Integer serviceDuration;
+ private Double servicePrice;
+ private String createdAt;
+ private String updatedAt;
+
+ public Long getServiceId() { return serviceId; }
+ public void setServiceId(Long serviceId) { this.serviceId = serviceId; }
+
+ public String getServiceName() { return serviceName; }
+ public void setServiceName(String serviceName) { this.serviceName = serviceName; }
+
+ public String getServiceDesc() { return serviceDesc; }
+ public void setServiceDesc(String serviceDesc) { this.serviceDesc = serviceDesc; }
+
+ public Integer getServiceDuration() { return serviceDuration; }
+ public void setServiceDuration(Integer serviceDuration) { this.serviceDuration = serviceDuration; }
+
+ public Double getServicePrice() { return servicePrice; }
+ public void setServicePrice(Double servicePrice) { this.servicePrice = servicePrice; }
+
+ 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; }
+}
diff --git a/android/app/src/main/java/com/example/petstoremobile/dtos/SupplierDTO.java b/android/app/src/main/java/com/example/petstoremobile/dtos/SupplierDTO.java
new file mode 100644
index 00000000..e34816c1
--- /dev/null
+++ b/android/app/src/main/java/com/example/petstoremobile/dtos/SupplierDTO.java
@@ -0,0 +1,36 @@
+package com.example.petstoremobile.dtos;
+
+public class SupplierDTO {
+ private Long supId;
+ private String supCompany;
+ private String supContactFirstName;
+ private String supContactLastName;
+ private String supEmail;
+ private String supPhone;
+ private String createdAt;
+ private String updatedAt;
+
+ public Long getSupId() { return supId; }
+ public void setSupId(Long supId) { this.supId = supId; }
+
+ public String getSupCompany() { return supCompany; }
+ public void setSupCompany(String supCompany) { this.supCompany = supCompany; }
+
+ public String getSupContactFirstName() { return supContactFirstName; }
+ public void setSupContactFirstName(String supContactFirstName) { this.supContactFirstName = supContactFirstName; }
+
+ public String getSupContactLastName() { return supContactLastName; }
+ public void setSupContactLastName(String supContactLastName) { this.supContactLastName = supContactLastName; }
+
+ public String getSupEmail() { return supEmail; }
+ public void setSupEmail(String supEmail) { this.supEmail = supEmail; }
+
+ public String getSupPhone() { return supPhone; }
+ public void setSupPhone(String supPhone) { this.supPhone = supPhone; }
+
+ 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; }
+}
diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/ChatFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/ChatFragment.java
new file mode 100644
index 00000000..b73fdc6b
--- /dev/null
+++ b/android/app/src/main/java/com/example/petstoremobile/fragments/ChatFragment.java
@@ -0,0 +1,407 @@
+package com.example.petstoremobile.fragments;
+
+import android.os.Bundle;
+import android.util.Log;
+import android.view.*;
+import android.widget.*;
+import androidx.annotation.NonNull;
+import androidx.core.view.GravityCompat;
+import androidx.drawerlayout.widget.DrawerLayout;
+import androidx.fragment.app.Fragment;
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+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.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.websocket.StompChatManager;
+import java.util.*;
+import java.util.stream.Collectors;
+import retrofit2.*;
+
+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;
+
+ // Adapters
+ private ChatAdapter chatAdapter;
+ private MessageAdapter messageAdapter;
+
+ // Data
+ private final List chatList = new ArrayList<>();
+ private final List messageList = new ArrayList<>();
+ private final Map customerNames = new HashMap<>();
+
+ // APIs
+ private ChatApi chatApi;
+ private CustomerApi customerApi;
+ private MessageApi messageApi;
+
+ // chat
+ private Long currentUserId;
+ private Long activeConversationId;
+ private StompChatManager stompChatManager;
+
+ @Override
+ public View onCreateView(@NonNull LayoutInflater inflater,
+ ViewGroup container, Bundle savedInstanceState) {
+
+ View view = inflater.inflate(R.layout.fragment_chat, container, false);
+
+ chatApi = RetrofitClient.getChatApi(requireContext());
+ customerApi = RetrofitClient.getCustomerApi(requireContext());
+ messageApi = RetrofitClient.getMessageApi(requireContext());
+
+ 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);
+
+ ImageButton hamburger = view.findViewById(R.id.btnHamburger);
+ hamburger.setOnClickListener(v -> drawerLayout.openDrawer(GravityCompat.START));
+ btnSend.setOnClickListener(v -> sendMessage());
+
+ setupRecyclerViews();
+ loadInitialData();
+
+ return view;
+ }
+
+ private void setupRecyclerViews() {
+ // Set up Drawer menu to select conversation
+ chatAdapter = new ChatAdapter(chatList, this);
+ rvChatList.setLayoutManager(new LinearLayoutManager(getContext()));
+ rvChatList.setAdapter(chatAdapter);
+
+ // set up RecyclerView for selected chat to show messages
+ messageAdapter = new MessageAdapter(messageList, null);
+ LinearLayoutManager lm = new LinearLayoutManager(getContext());
+ lm.setStackFromEnd(true);
+ rvMessages.setLayoutManager(lm);
+ rvMessages.setAdapter(messageAdapter);
+ setConversationActive(false);
+ }
+
+ //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();
+
+ messageAdapter.setCurrentUserId(currentUserId);
+
+ // if token exist then connect to websocket
+ if (token != null) {
+ stompChatManager = new StompChatManager(token, role);
+ stompChatManager.setMessageListener(this);
+ stompChatManager.setConversationListener(this);
+ stompChatManager.setConnectionListener(this);
+ stompChatManager.connect();
+ } else {
+ Log.e(TAG, "No token found");
+ }
+
+ loadCustomers();
+ }
+
+ //Helper function to load customer names for it to be displayed on drawer menu
+ private void loadCustomers() {
+ customerApi.getAllCustomers(0, 100).enqueue(new Callback>() {
+ @Override
+ public void onResponse(@NonNull Call> call,
+ @NonNull Response> 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> 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>() {
+ @Override
+ public void onResponse(@NonNull Call> call,
+ @NonNull Response> response) {
+ if (response.isSuccessful() && response.body() != null) {
+ chatList.clear();
+ List 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> 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);
+
+ if (stompChatManager != null) {
+ stompChatManager.subscribeToConversation(activeConversationId);
+ }
+
+ loadMessageHistory(activeConversationId);
+ }
+
+ //helper function to load messages for selected chat
+ private void loadMessageHistory(Long conversationId) {
+ messageApi.getMessages(conversationId).enqueue(new Callback>() {
+ @Override
+ public void onResponse(@NonNull Call> call,
+ @NonNull Response> response) {
+ if (response.isSuccessful() && response.body() != null) {
+ messageList.clear();
+ for (MessageDTO dto : response.body()) {
+ messageList.add(dtoToModel(dto));
+ }
+ messageAdapter.notifyDataSetChanged();
+ scrollToBottom();
+ }
+ }
+ @Override
+ public void onFailure(@NonNull Call> call,
+ @NonNull Throwable t) {
+ Log.e(TAG, "Error loading messages", t);
+ }
+ });
+ }
+
+ //Helper function to send a message to the chat
+ private void sendMessage() {
+ //check if a chat is selected
+ if (activeConversationId == null) return;
+
+ //get the message from text field
+ String text = etMessage.getText().toString().trim();
+ 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() {
+ @Override
+ public void onResponse(@NonNull Call call,
+ @NonNull Response 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 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());
+ return;
+ }
+ updateConversationPreview(dto.getConversationId(), dto.getContent());
+
+ if (currentUserId != null && currentUserId.equals(dto.getSenderId())) return;
+
+ //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();
+ }
+
+ // 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;
+ }
+ }
+
+ 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);
+ }
+ }
+
+ @Override
+ public void onSocketClosed() {
+ if (!isAdded()) {
+ return;
+ }
+ loadConversations();
+ }
+
+ @Override
+ public void onSocketError() {
+ if (!isAdded()) {
+ return;
+ }
+ loadConversations();
+ if (activeConversationId != null) {
+ 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));
+ }
+ }
+
+ // 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);
+ if (!active) {
+ activeConversationId = null;
+ if (stompChatManager != null) {
+ stompChatManager.clearConversationSubscription();
+ }
+ messageList.clear();
+ messageAdapter.notifyDataSetChanged();
+ etMessage.setText("");
+ etMessage.setHint("Select a chat to start messaging");
+ } else {
+ etMessage.setHint("Type a message...");
+ }
+ }
+
+ // When fragment is destroyed, disconnect from websocket
+ @Override
+ public void onDestroyView() {
+ super.onDestroyView();
+ if (stompChatManager != null) stompChatManager.disconnect();
+ }
+}
diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/ListFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/ListFragment.java
new file mode 100644
index 00000000..cd07df63
--- /dev/null
+++ b/android/app/src/main/java/com/example/petstoremobile/fragments/ListFragment.java
@@ -0,0 +1,145 @@
+package com.example.petstoremobile.fragments;
+
+import android.os.Bundle;
+
+import androidx.core.view.GravityCompat;
+import androidx.drawerlayout.widget.DrawerLayout;
+import androidx.fragment.app.Fragment;
+
+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.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;
+
+//The Fragment for the displaying the list of entities to be viewed
+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;
+
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ View view = inflater.inflate(R.layout.fragment_list, container, false);
+
+ //get controls from the layout
+ drawerLayout = view.findViewById(R.id.drawerLayout);
+ drawerPets = view.findViewById(R.id.drawerPets);
+ drawerServices = view.findViewById(R.id.drawerServices);
+ drawerSuppliers = view.findViewById(R.id.drawerSuppliers);
+ drawerAdoptions = view.findViewById(R.id.drawerAdoptions);
+ drawerAppointments = view.findViewById(R.id.drawerAppointments);
+ drawerInventory = view.findViewById(R.id.drawerInventory);
+ drawerProducts = view.findViewById(R.id.drawerProducts);
+
+ //needed to disable touches on the innerContainer while the drawer is open
+ touchBlocker = view.findViewById(R.id.touchBlocker);
+
+ //Display pets fragment by default
+ if (savedInstanceState == null) {
+ loadFragment(new PetFragment());
+ }
+
+ //add Listeners to the drawer so user won't be able to interact with the innerContainer (the list fragments)
+ //while the drawer is open
+ 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);
+ }
+
+ //When the drawer is closed, enable touches again
+ @Override
+ public void onDrawerClosed(View drawerView) {
+ touchBlocker.setVisibility(View.GONE);
+ touchBlocker.setClickable(false);
+ }
+
+ //unused methods
+ @Override
+ public void onDrawerSlide(View drawerView, float slideOffset) {}
+ @Override
+ public void onDrawerStateChanged(int newState) {}
+ });
+
+ // Click listeners for each drawer
+ //Pets
+ drawerPets.setOnClickListener(v -> {
+ loadFragment(new PetFragment());
+ drawerLayout.closeDrawers();
+ });
+
+ //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;
+ }
+
+ //helper function to open the drawer
+ public void openDrawer() {
+ drawerLayout.openDrawer(GravityCompat.START);
+ }
+
+ // helper function to load the fragment into the display
+ public void loadFragment(Fragment fragment) {
+ getChildFragmentManager()
+ .beginTransaction()
+ .replace(R.id.inner_fragment_container, fragment)
+ .addToBackStack(null)
+ .commit();
+ }
+}
\ No newline at end of file
diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/ProfileFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/ProfileFragment.java
new file mode 100644
index 00000000..2bb92e4c
--- /dev/null
+++ b/android/app/src/main/java/com/example/petstoremobile/fragments/ProfileFragment.java
@@ -0,0 +1,255 @@
+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.appcompat.app.AlertDialog;
+import androidx.core.content.ContextCompat;
+import androidx.core.content.FileProvider;
+import androidx.fragment.app.Fragment;
+
+import android.provider.MediaStore;
+import android.text.InputType;
+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 com.example.petstoremobile.R;
+import com.example.petstoremobile.activities.MainActivity;
+import com.example.petstoremobile.api.auth.TokenManager;
+
+import java.io.File;
+
+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;
+
+ //Initialize the launchers for camera and gallery
+ private ActivityResultLauncher galleryLauncher;
+ private ActivityResultLauncher cameraLauncher;
+ private ActivityResultLauncher permissionLauncher;
+
+ //Called when the fragment is created, sets up the launchers is set profile image
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ // 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
+ }
+ }
+ );
+
+ // 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();
+ }
+ }
+ );
+ }
+
+ //TODO: MAKE PROFILE VIEW DISPLAY PROFILE DATA FROM DATABASE
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ View view = inflater.inflate(R.layout.fragment_profile, container, false);
+
+ //get all the controls from the view
+ imgProfile = view.findViewById(R.id.imgProfile);
+ tvProfileName = view.findViewById(R.id.tvProfileName);
+ tvProfileEmail = view.findViewById(R.id.tvProfileEmail);
+ tvProfilePhone = view.findViewById(R.id.tvProfilePhone);
+ tvProfileRole = view.findViewById(R.id.tvProfileRole);
+ btnChangePhoto = view.findViewById(R.id.btnChangePhoto);
+ btnEditEmail = view.findViewById(R.id.btnEditEmail);
+ btnEditPhone = view.findViewById(R.id.btnEditPhone);
+ btnLogout = view.findViewById(R.id.btnLogout);
+
+ //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
+ });
+
+ //Edit email button
+ //When clicked open a dialog to change email
+ 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());
+
+ //set input type to email
+ input.setInputType(android.text.InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS);
+
+
+ //Show alert dialog to user to enter new email
+ new AlertDialog.Builder(requireContext())
+ .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();
+ }
+ })
+ .setNegativeButton("Cancel", null)
+ .show();
+ });
+
+ //Edit phone button
+ //When clicked open a dialog to change phone
+ 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());
+
+ //set input type to phone number
+ input.setInputType(InputType.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)});
+
+
+ //Show alert dialog to user to enter new phone
+ new AlertDialog.Builder(requireContext())
+ .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();
+ }
+ })
+ .setNegativeButton("Cancel", null)
+ .show();
+ });
+
+ //Logout button
+ btnLogout.setOnClickListener(v -> {
+ TokenManager.getInstance(requireContext()).clearLoginData(); // clear the token for next login
+ //get the intent to the main activity and clear the back stack so the back button won't allow the user to go back to the previous screen
+ Intent intent = new Intent(getActivity(), MainActivity.class);
+ intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK);
+ //start the activity to go to login page and finish the current activity
+ startActivity(intent);
+ requireActivity().finish();
+ });
+
+ return view;
+ }
+
+ //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);
+ }
+}
\ No newline at end of file
diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AdoptionFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AdoptionFragment.java
new file mode 100644
index 00000000..f72d6836
--- /dev/null
+++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AdoptionFragment.java
@@ -0,0 +1,163 @@
+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.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 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;
+
+public class AdoptionFragment extends Fragment implements AdoptionAdapter.OnAdoptionClickListener {
+
+ private List adoptionList = new ArrayList<>();
+ private List filteredList = new ArrayList<>();
+ private AdoptionAdapter adapter;
+ private SwipeRefreshLayout swipeRefreshLayout;
+ private EditText etSearch;
+ private ImageButton hamburger;
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ View view = inflater.inflate(R.layout.fragment_adoption, container, false);
+
+ hamburger = view.findViewById(R.id.btnHamburger);
+
+ loadAdoptionData();
+ // Replace with actual API call when backend is ready
+ setupRecyclerView(view);
+ setupSearch(view);
+ setupSwipeRefresh(view);
+
+ FloatingActionButton fabAddAdoption = view.findViewById(R.id.fabAddAdoption);
+ fabAddAdoption.setOnClickListener(v -> openAdoptionDetails(-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;
+ }
+
+ // 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 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());
+ }
+
+ @Override
+ public void onAdoptionClick(int position) {
+ openAdoptionDetails(position);
+ }
+
+ 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 setupRecyclerView(View view) {
+ RecyclerView recyclerView = view.findViewById(R.id.recyclerViewAdoptions);
+ adapter = new AdoptionAdapter(filteredList, this);
+ recyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
+ recyclerView.setAdapter(adapter);
+ }
+}
diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AppointmentFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AppointmentFragment.java
new file mode 100644
index 00000000..09ad489b
--- /dev/null
+++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AppointmentFragment.java
@@ -0,0 +1,167 @@
+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.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 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;
+
+public class AppointmentFragment extends Fragment implements AppointmentAdapter.OnAppointmentClickListener {
+
+ private List appointmentList = new ArrayList<>(); // full data list
+ private List filteredList = new ArrayList<>(); // filtered display list
+ private AppointmentAdapter adapter;
+ private SwipeRefreshLayout swipeRefreshLayout;
+ private EditText etSearch;
+ private ImageButton hamburger;
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ View view = inflater.inflate(R.layout.fragment_appointment, container, false);
+
+ hamburger = view.findViewById(R.id.btnHamburger);
+
+ loadAppointmentData(); // TODO: Replace with actual API call when backend is ready
+ setupRecyclerView(view);
+ setupSearch(view);
+ setupSwipeRefresh(view);
+
+ FloatingActionButton fabAddAppointment = view.findViewById(R.id.fabAddAppointment);
+ fabAddAppointment.setOnClickListener(v -> openAppointmentDetails(-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;
+ }
+
+ // 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) {}
+ });
+ }
+
+ // Filters the appointment list based on the search query
+ private void filterAppointments(String query) {
+ filteredList.clear();
+ if (query.isEmpty()) {
+ filteredList.addAll(appointmentList);
+ } 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);
+ }
+ }
+ }
+ adapter.notifyDataSetChanged();
+ }
+
+ // 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 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());
+ }
+
+ 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());
+ }
+
+ @Override
+ public void onAppointmentClick(int position) {
+ 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);
+ }
+
+ 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);
+ }
+}
diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/InventoryFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/InventoryFragment.java
new file mode 100644
index 00000000..38546e83
--- /dev/null
+++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/InventoryFragment.java
@@ -0,0 +1,162 @@
+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 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 java.util.ArrayList;
+import java.util.List;
+
+public class InventoryFragment extends Fragment implements InventoryAdapter.OnInventoryClickListener {
+
+ private List inventoryList = new ArrayList<>();
+ private List filteredList = new ArrayList<>();
+ private InventoryAdapter adapter;
+ private SwipeRefreshLayout swipeRefreshLayout;
+ private EditText etSearch;
+ private ImageButton hamburger;
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ View view = inflater.inflate(R.layout.fragment_inventory, container, false);
+
+ hamburger = view.findViewById(R.id.btnHamburger);
+
+ loadInventoryData(); // TODO: Replace with actual API call when backend is ready
+ setupRecyclerView(view);
+ setupSearch(view);
+ setupSwipeRefresh(view);
+
+ FloatingActionButton fabAddInventory = view.findViewById(R.id.fabAddInventory);
+ fabAddInventory.setOnClickListener(v -> openInventoryDetails(-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;
+ }
+
+ // 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 filterInventory(String query) {
+ filteredList.clear();
+ if (query.isEmpty()) {
+ filteredList.addAll(inventoryList);
+ } 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);
+ }
+ }
+ }
+ 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);
+ }
+
+ public void onInventorySaved(int position, Inventory inventory) {
+ if (position == -1) {
+ inventoryList.add(inventory);
+ } else {
+ inventoryList.set(position, inventory);
+ }
+ filterInventory(etSearch.getText().toString());
+ }
+
+ public void onInventoryDeleted(int position) {
+ inventoryList.remove(position);
+ filterInventory(etSearch.getText().toString());
+ }
+
+ @Override
+ public void onInventoryClick(int position) {
+ openInventoryDetails(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);
+ }
+}
diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/PetFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/PetFragment.java
new file mode 100644
index 00000000..6448b5f3
--- /dev/null
+++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/PetFragment.java
@@ -0,0 +1,202 @@
+package com.example.petstoremobile.fragments.listfragments;
+
+import android.os.Bundle;
+
+import androidx.fragment.app.Fragment;
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
+
+import android.text.Editable;
+import android.text.TextWatcher;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.EditText;
+import android.widget.ImageButton;
+import android.widget.Toast;
+
+import 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.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 java.util.ArrayList;
+import java.util.List;
+
+import retrofit2.Call;
+import retrofit2.Callback;
+import retrofit2.Response;
+
+public class PetFragment extends Fragment implements PetAdapter.OnPetClickListener {
+ private List petList = new ArrayList<>();
+ private List filteredList = new ArrayList<>();
+ private ImageButton hamburger;
+ private PetAdapter adapter;
+ private PetApi api;
+ private SwipeRefreshLayout swipeRefreshLayout;
+ private EditText etSearch;
+
+ //load pet view
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ View view = inflater.inflate(R.layout.fragment_pet, container, false);
+
+ //get retrofit
+ api = RetrofitClient.getPetApi(requireContext());
+
+ hamburger = view.findViewById(R.id.btnHamburger);
+
+ setupRecyclerView(view);
+ setupSearch(view);
+ setupSwipeRefresh(view);
+ loadPetData();
+
+
+ //Add button to opens the add dialog
+ FloatingActionButton fabAddPet = view.findViewById(R.id.fabAddPet);
+ fabAddPet.setOnClickListener(v -> openPetDetails(-1));
+
+ //Make the hamburger button open the drawer from listFragment
+ hamburger.setOnClickListener(v -> {
+ ListFragment listFragment = (ListFragment) getParentFragment();
+ //if list fragment is found then use its helper function to open the drawer
+ if (listFragment != null) {
+ listFragment.openDrawer();
+ }
+ });
+
+ return view;
+ }
+
+ 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);
+ } 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);
+ }
+ }
+ }
+ adapter.notifyDataSetChanged();
+ }
+
+ private void setupSwipeRefresh(View view) {
+ swipeRefreshLayout = view.findViewById(R.id.swipeRefreshPet);
+ swipeRefreshLayout.setOnRefreshListener(() -> {
+ loadPetData();
+ });
+ }
+
+ //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);
+ }
+ }
+
+ //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);
+ }
+ }
+
+ // 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);
+ }
+ api.getAllPets(0, 100).enqueue(new Callback>() {
+ @Override
+ public void onResponse(Call> call, Response> 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> 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);
+ }
+}
\ No newline at end of file
diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ProductFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ProductFragment.java
new file mode 100644
index 00000000..65e14d4b
--- /dev/null
+++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ProductFragment.java
@@ -0,0 +1,162 @@
+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.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 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 java.util.ArrayList;
+import java.util.List;
+
+public class ProductFragment extends Fragment implements ProductAdapter.OnProductClickListener {
+
+ private List productList = new ArrayList<>();
+ private List filteredList = new ArrayList<>();
+ private ProductAdapter adapter;
+ private SwipeRefreshLayout swipeRefreshLayout;
+ private EditText etSearch;
+ private ImageButton hamburger;
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ View view = inflater.inflate(R.layout.fragment_product, container, false);
+
+ hamburger = view.findViewById(R.id.btnHamburger);
+
+ loadProductData(); // TODO: Replace with actual API call when backend is ready
+ setupRecyclerView(view);
+ setupSearch(view);
+ setupSwipeRefresh(view);
+
+ FloatingActionButton fabAddProduct = view.findViewById(R.id.fabAddProduct);
+ fabAddProduct.setOnClickListener(v -> openProductDetails(-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;
+ }
+
+ // 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 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);
+ }
+ }
+ }
+ adapter.notifyDataSetChanged();
+ }
+
+ 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 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());
+ }
+
+ 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());
+ }
+
+ @Override
+ public void onProductClick(int position) {
+ 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);
+ }
+}
diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ServiceFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ServiceFragment.java
new file mode 100644
index 00000000..1f204114
--- /dev/null
+++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ServiceFragment.java
@@ -0,0 +1,189 @@
+package com.example.petstoremobile.fragments.listfragments;
+
+import android.os.Bundle;
+
+import androidx.fragment.app.Fragment;
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
+
+import android.text.Editable;
+import android.text.TextWatcher;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.EditText;
+import android.widget.ImageButton;
+import android.widget.Toast;
+
+import com.example.petstoremobile.R;
+import com.example.petstoremobile.adapters.ServiceAdapter;
+import com.example.petstoremobile.api.RetrofitClient;
+import com.example.petstoremobile.api.ServiceApi;
+import com.example.petstoremobile.dtos.PageResponse;
+import com.example.petstoremobile.dtos.ServiceDTO;
+import com.example.petstoremobile.fragments.ListFragment;
+import com.example.petstoremobile.fragments.listfragments.detailfragments.ServiceDetailFragment;
+import com.google.android.material.floatingactionbutton.FloatingActionButton;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import retrofit2.Call;
+import retrofit2.Callback;
+import retrofit2.Response;
+
+public class ServiceFragment extends Fragment implements ServiceAdapter.OnServiceClickListener {
+
+ private List serviceList = new ArrayList<>();
+ private List filteredList = new ArrayList<>();
+ private ServiceAdapter adapter;
+ private ImageButton hamburger;
+ private ServiceApi api;
+ private SwipeRefreshLayout swipeRefreshLayout;
+ private EditText etSearch;
+
+ //load service view
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ View view = inflater.inflate(R.layout.fragment_service, container, false);
+
+ api = RetrofitClient.getServiceApi(requireContext());
+ hamburger = view.findViewById(R.id.btnHamburger);
+
+ setupRecyclerView(view);
+ setupSearch(view);
+ setupSwipeRefresh(view);
+ loadServiceData();
+
+ //Add button to opens the add dialog
+ FloatingActionButton fabAddService = view.findViewById(R.id.fabAddService);
+ fabAddService.setOnClickListener(v -> openServiceDetails(-1));
+
+ //Make the hamburger button open the drawer from listFragment
+ hamburger.setOnClickListener(v -> {
+ ListFragment listFragment = (ListFragment) getParentFragment();
+ //if list fragment is found then use its helper function to open the drawer
+ if (listFragment != null) {
+ listFragment.openDrawer();
+ }
+ });
+
+ return view;
+ }
+
+ private void setupSearch(View view) {
+ etSearch = view.findViewById(R.id.etSearchService);
+ 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) {
+ filterServices(s.toString());
+ }
+ @Override public void afterTextChanged(Editable s) {}
+ });
+ }
+
+ private void filterServices(String query) {
+ filteredList.clear();
+ if (query.isEmpty()) {
+ filteredList.addAll(serviceList);
+ } else {
+ String lower = query.toLowerCase();
+ for (ServiceDTO s : serviceList) {
+ if (s.getServiceName().toLowerCase().contains(lower)
+ || s.getServiceDesc().toLowerCase().contains(lower)) {
+ filteredList.add(s);
+ }
+ }
+ }
+ adapter.notifyDataSetChanged();
+ }
+
+ private void setupSwipeRefresh(View view) {
+ swipeRefreshLayout = view.findViewById(R.id.swipeRefreshService);
+ swipeRefreshLayout.setOnRefreshListener(() -> {
+ loadServiceData();
+ });
+ }
+
+ //Open the service detail view depending on the mode
+ private void openServiceDetails(int position) {
+ ServiceDetailFragment detailFragment = new ServiceDetailFragment();
+
+ //Make a bundle to pass data to the detail fragment
+ Bundle args = new Bundle();
+ args.putInt("position", position);
+
+ //if editing a service, add the service data to the bundle
+ if (position != -1) {
+ ServiceDTO service = filteredList.get(position);
+ args.putInt("serviceId", service.getServiceId().intValue());
+ args.putString("serviceName", service.getServiceName());
+ args.putString("serviceDesc", service.getServiceDesc());
+ args.putInt("serviceDuration", service.getServiceDuration());
+ args.putDouble("servicePrice", service.getServicePrice());
+ }
+
+ //send the bundle to the detail fragment to display
+ detailFragment.setArguments(args);
+ //set the service fragment to the parent so we refer back to service view when save or delete is done
+ detailFragment.setServiceFragment(this);
+
+ //get ListFragment to load the the detail view
+ ListFragment listFragment = (ListFragment) getParentFragment();
+ if (listFragment != null) {
+ listFragment.loadFragment(detailFragment);
+ }
+ }
+
+ // Called by ServiceAdapter when a row is clicked to open the details view
+ @Override
+ public void onServiceClick(int position) {
+ openServiceDetails(position);
+ }
+
+ // Helper function to get a list of all services from the backend
+ private void loadServiceData() {
+ if (swipeRefreshLayout != null) {
+ swipeRefreshLayout.setRefreshing(true);
+ }
+ api.getAllServices(0, 100).enqueue(new Callback>() {
+ @Override
+ public void onResponse(Call> call, Response> response) {
+ if (swipeRefreshLayout != null) {
+ swipeRefreshLayout.setRefreshing(false);
+ }
+ if (response.isSuccessful() && response.body() != null) {
+ serviceList.clear();
+ serviceList.addAll(response.body().getContent());
+ filterServices(etSearch.getText().toString());
+
+ } else {
+ Log.e("onResponse: ", response.message());
+ }
+ }
+
+ @Override
+ public void onFailure(Call> call, Throwable t) {
+ if (swipeRefreshLayout != null) {
+ swipeRefreshLayout.setRefreshing(false);
+ }
+ if (getContext() != null) {
+ Toast.makeText(getContext(),
+ "Failed to load services", Toast.LENGTH_SHORT).show();
+ }
+ Log.e("onFailure: ", t.getMessage());
+ }
+ });
+ }
+
+ //set up the recyclerview and adapter
+ private void setupRecyclerView(View view) {
+ RecyclerView recyclerView = view.findViewById(R.id.recyclerViewServices);
+ adapter = new ServiceAdapter(filteredList, this);
+ recyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
+ recyclerView.setAdapter(adapter);
+ }
+}
\ No newline at end of file
diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/SupplierFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/SupplierFragment.java
new file mode 100644
index 00000000..0d75da78
--- /dev/null
+++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/SupplierFragment.java
@@ -0,0 +1,192 @@
+package com.example.petstoremobile.fragments.listfragments;
+
+import android.os.Bundle;
+
+import androidx.fragment.app.Fragment;
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
+
+import android.text.Editable;
+import android.text.TextWatcher;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.EditText;
+import android.widget.ImageButton;
+import android.widget.Toast;
+
+import com.example.petstoremobile.R;
+import com.example.petstoremobile.adapters.SupplierAdapter;
+import com.example.petstoremobile.api.RetrofitClient;
+import com.example.petstoremobile.api.SupplierApi;
+import com.example.petstoremobile.dtos.PageResponse;
+import com.example.petstoremobile.dtos.SupplierDTO;
+import com.example.petstoremobile.fragments.ListFragment;
+import com.example.petstoremobile.fragments.listfragments.detailfragments.SupplierDetailFragment;
+import com.google.android.material.floatingactionbutton.FloatingActionButton;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import retrofit2.Call;
+import retrofit2.Callback;
+import retrofit2.Response;
+
+public class SupplierFragment extends Fragment implements SupplierAdapter.OnSupplierClickListener {
+
+ private List supplierList = new ArrayList<>();
+ private List filteredList = new ArrayList<>();
+ private SupplierAdapter adapter;
+ private ImageButton hamburger;
+ private SupplierApi api;
+ private SwipeRefreshLayout swipeRefreshLayout;
+ private EditText etSearch;
+
+ //load supplier view
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ View view = inflater.inflate(R.layout.fragment_supplier, container, false);
+
+ api = RetrofitClient.getSupplierApi(requireContext());
+ hamburger = view.findViewById(R.id.btnHamburger);
+
+ setupRecyclerView(view);
+ setupSearch(view);
+ setupSwipeRefresh(view);
+ loadSupplierData();
+
+ //Add button to opens the add dialog
+ FloatingActionButton fabAddSupplier = view.findViewById(R.id.fabAddSupplier);
+ fabAddSupplier.setOnClickListener(v -> openSupplierDetails(-1));
+
+ //Make the hamburger button open the drawer from listFragment
+ hamburger.setOnClickListener(v -> {
+ ListFragment listFragment = (ListFragment) getParentFragment();
+ //if list fragment is found then use its helper function to open the drawer
+ if (listFragment != null) {
+ listFragment.openDrawer();
+ }
+ });
+
+ return view;
+ }
+
+ private void setupSearch(View view) {
+ etSearch = view.findViewById(R.id.etSearchSupplier);
+ 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) {
+ filterSuppliers(s.toString());
+ }
+ @Override public void afterTextChanged(Editable s) {}
+ });
+ }
+
+ private void filterSuppliers(String query) {
+ filteredList.clear();
+ if (query.isEmpty()) {
+ filteredList.addAll(supplierList);
+ } else {
+ String lower = query.toLowerCase();
+ for (SupplierDTO s : supplierList) {
+ if (s.getSupCompany().toLowerCase().contains(lower)
+ || s.getSupContactFirstName().toLowerCase().contains(lower)
+ || s.getSupContactLastName().toLowerCase().contains(lower)) {
+ filteredList.add(s);
+ }
+ }
+ }
+ adapter.notifyDataSetChanged();
+ }
+
+ private void setupSwipeRefresh(View view) {
+ swipeRefreshLayout = view.findViewById(R.id.swipeRefreshSupplier);
+ swipeRefreshLayout.setOnRefreshListener(() -> {
+ loadSupplierData();
+ });
+ }
+
+ //Open the supplier detail view depending on the mode
+ private void openSupplierDetails(int position) {
+ SupplierDetailFragment detailFragment = new SupplierDetailFragment();
+
+ //Make a bundle to pass data to the detail fragment
+ Bundle args = new Bundle();
+ args.putInt("position", position);
+
+ //if editing a supplier, add the supplier data to the bundle
+ if (position != -1) {
+ SupplierDTO supplier = filteredList.get(position);
+ args.putInt("supId", supplier.getSupId().intValue());
+ args.putString("supCompany", supplier.getSupCompany());
+ args.putString("supContactFirstName", supplier.getSupContactFirstName());
+ args.putString("supContactLastName", supplier.getSupContactLastName());
+ args.putString("supEmail", supplier.getSupEmail());
+ args.putString("supPhone", supplier.getSupPhone());
+ }
+
+ //send the bundle to the detail fragment to display
+ detailFragment.setArguments(args);
+ //set the supplier fragment to the parent so we refer back to supplier view when save or delete is done
+ detailFragment.setSupplierFragment(this);
+
+ //get ListFragment to load the the detail view
+ ListFragment listFragment = (ListFragment) getParentFragment();
+ if (listFragment != null) {
+ listFragment.loadFragment(detailFragment);
+ }
+ }
+
+
+ // Called by SupplierAdapter when a row is clicked to open the details view
+ @Override
+ public void onSupplierClick(int position) {
+ openSupplierDetails(position);
+ }
+
+ // Helper function to get a list of all suppliers from the backend
+ private void loadSupplierData() {
+ if (swipeRefreshLayout != null) {
+ swipeRefreshLayout.setRefreshing(true);
+ }
+ api.getAllSuppliers(0, 100).enqueue(new Callback>() {
+ @Override
+ public void onResponse(Call> call, Response> response) {
+ if (swipeRefreshLayout != null) {
+ swipeRefreshLayout.setRefreshing(false);
+ }
+ if (response.isSuccessful() && response.body() != null) {
+ supplierList.clear();
+ supplierList.addAll(response.body().getContent());
+ filterSuppliers(etSearch.getText().toString());
+
+ } else {
+ Log.e("onResponse: ", response.message());
+ }
+ }
+
+ @Override
+ public void onFailure(Call> call, Throwable t) {
+ if (swipeRefreshLayout != null) {
+ swipeRefreshLayout.setRefreshing(false);
+ }
+ if (getContext() != null) {
+ Toast.makeText(getContext(),
+ "Failed to load suppliers", Toast.LENGTH_SHORT).show();
+ }
+ Log.e("onFailure: ", t.getMessage());
+ }
+ });
+ }
+
+ //set up the recyclerview and adapter
+ private void setupRecyclerView(View view) {
+ RecyclerView recyclerView = view.findViewById(R.id.recyclerViewSuppliers);
+ adapter = new SupplierAdapter(filteredList, this);
+ recyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
+ recyclerView.setAdapter(adapter);
+ }
+}
\ No newline at end of file
diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/AdoptionDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/AdoptionDetailFragment.java
new file mode 100644
index 00000000..32c06996
--- /dev/null
+++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/AdoptionDetailFragment.java
@@ -0,0 +1,172 @@
+package com.example.petstoremobile.fragments.listfragments.detailfragments;
+
+
+// Uses InputValidator for detailed field validation and ActivityLogger to log all changes.
+
+import android.os.Bundle;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.core.content.ContextCompat;
+import androidx.fragment.app.Fragment;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+import android.widget.Button;
+import android.widget.EditText;
+import android.widget.Spinner;
+import android.widget.TextView;
+import android.widget.Toast;
+import com.example.petstoremobile.R;
+import com.example.petstoremobile.fragments.ListFragment;
+import com.example.petstoremobile.fragments.listfragments.AdoptionFragment;
+import com.example.petstoremobile.models.Adoption;
+import com.example.petstoremobile.utils.ActivityLogger;
+import com.example.petstoremobile.utils.InputValidator;
+
+public class AdoptionDetailFragment extends Fragment {
+
+ private TextView tvMode, tvAdoptionId;
+ private EditText etAdopterName, etAdopterEmail, etAdopterPhone, etPetName, etAdoptionDate;
+ private Spinner spinnerAdoptionStatus;
+ private Button btnSaveAdoption, btnDeleteAdoption, btnBack;
+ private int adoptionId;
+ private int position;
+ private boolean isEditing = false;
+ private AdoptionFragment adoptionFragment;
+
+ // Set the adoption fragment as parent so we refer back when save or delete is done
+ public void setAdoptionFragment(AdoptionFragment fragment) {
+ this.adoptionFragment = fragment;
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ View view = inflater.inflate(R.layout.fragment_adoption_detail, container, false);
+
+ initViews(view);
+ setupSpinner();
+ handleArguments();
+
+ btnBack.setOnClickListener(v -> {
+ ListFragment listFragment = (ListFragment) getParentFragment();
+ if (listFragment != null) listFragment.getChildFragmentManager().popBackStack();
+ });
+ btnSaveAdoption.setOnClickListener(v -> saveAdoption());
+ btnDeleteAdoption.setOnClickListener(v -> deleteAdoption());
+
+ return view;
+ }
+
+ // Validates all fields using InputValidator, then saves the adoption record
+ private void saveAdoption() {
+ if (!InputValidator.isNotEmpty(etAdopterName, "Adopter Name")) return;
+ if (!InputValidator.isValidEmail(etAdopterEmail)) return;
+ if (!InputValidator.isValidPhone(etAdopterPhone)) return;
+ if (!InputValidator.isNotEmpty(etPetName, "Pet Name")) return;
+ if (!InputValidator.isValidDate(etAdoptionDate)) return;
+
+ String adopterName = etAdopterName.getText().toString().trim();
+ String adopterEmail = etAdopterEmail.getText().toString().trim();
+ String adopterPhone = etAdopterPhone.getText().toString().trim();
+ String petName = etPetName.getText().toString().trim();
+ String adoptionDate = etAdoptionDate.getText().toString().trim();
+ String status = spinnerAdoptionStatus.getSelectedItem().toString();
+
+ try {
+ if (isEditing) {
+ // TODO: Replace with actual API PUT call when backend is ready
+ Adoption updated = new Adoption(adoptionId, adopterName, adopterEmail, adopterPhone, petName, adoptionDate, status);
+ if (adoptionFragment != null) adoptionFragment.onAdoptionSaved(position, updated);
+ ActivityLogger.logChange(requireContext(), "Adoption", "UPDATED", adoptionId);
+ Toast.makeText(getContext(), "Adoption record updated.", Toast.LENGTH_SHORT).show();
+ } else {
+ // TODO: Replace with actual API POST call when backend is ready
+ Adoption newAdoption = new Adoption(0, adopterName, adopterEmail, adopterPhone, petName, adoptionDate, status);
+ if (adoptionFragment != null) adoptionFragment.onAdoptionSaved(-1, newAdoption);
+ ActivityLogger.log(requireContext(), "Added new Adoption record for: " + adopterName + " adopting " + petName);
+ Toast.makeText(getContext(), "Adoption record added.", Toast.LENGTH_SHORT).show();
+ }
+ ListFragment listFragment = (ListFragment) getParentFragment();
+ if (listFragment != null) listFragment.getChildFragmentManager().popBackStack();
+ } catch (Exception e) {
+ ActivityLogger.logException(requireContext(), "AdoptionDetailFragment.saveAdoption", e);
+ Toast.makeText(getContext(), "Error saving adoption record.", Toast.LENGTH_SHORT).show();
+ }
+ }
+
+ // Deletes the adoption record and logs the action
+ private void deleteAdoption() {
+ try {
+ // TODO: Replace with actual API DELETE call when backend is ready
+ if (adoptionFragment != null) adoptionFragment.onAdoptionDeleted(position);
+ ActivityLogger.logChange(requireContext(), "Adoption", "DELETED", adoptionId);
+ Toast.makeText(getContext(), "Adoption record deleted.", Toast.LENGTH_SHORT).show();
+ ListFragment listFragment = (ListFragment) getParentFragment();
+ if (listFragment != null) listFragment.getChildFragmentManager().popBackStack();
+ } catch (Exception e) {
+ ActivityLogger.logException(requireContext(), "AdoptionDetailFragment.deleteAdoption", e);
+ Toast.makeText(getContext(), "Error deleting adoption record.", Toast.LENGTH_SHORT).show();
+ }
+ }
+
+ private void handleArguments() {
+ if (getArguments() != null && getArguments().containsKey("adoptionId")) {
+ isEditing = true;
+ adoptionId = getArguments().getInt("adoptionId");
+ position = getArguments().getInt("position");
+ tvMode.setText("Edit Adoption");
+ tvAdoptionId.setText("ID: " + adoptionId);
+ etAdopterName.setText(getArguments().getString("adopterName"));
+ etAdopterEmail.setText(getArguments().getString("adopterEmail"));
+ etAdopterPhone.setText(getArguments().getString("adopterPhone"));
+ etPetName.setText(getArguments().getString("petName"));
+ etAdoptionDate.setText(getArguments().getString("adoptionDate"));
+ String status = getArguments().getString("status");
+ if ("Approved".equals(status)) spinnerAdoptionStatus.setSelection(0);
+ else if ("Pending".equals(status)) spinnerAdoptionStatus.setSelection(1);
+ else spinnerAdoptionStatus.setSelection(2);
+ btnDeleteAdoption.setVisibility(View.VISIBLE);
+ } else {
+ isEditing = false;
+ tvMode.setText("Add Adoption");
+ tvAdoptionId.setVisibility(View.GONE);
+ btnDeleteAdoption.setVisibility(View.GONE);
+ btnSaveAdoption.setText("Add");
+ }
+ }
+
+ private void initViews(View view) {
+ tvMode = view.findViewById(R.id.tvAdoptionMode);
+ tvAdoptionId = view.findViewById(R.id.tvAdoptionId);
+ etAdopterName = view.findViewById(R.id.etAdopterName);
+ etAdopterEmail = view.findViewById(R.id.etAdopterEmail);
+ etAdopterPhone = view.findViewById(R.id.etAdopterPhone);
+ etPetName = view.findViewById(R.id.etAdoptionPetName);
+ etAdoptionDate = view.findViewById(R.id.etAdoptionDate);
+ spinnerAdoptionStatus = view.findViewById(R.id.spinnerAdoptionStatus);
+ btnSaveAdoption = view.findViewById(R.id.btnSaveAdoption);
+ btnDeleteAdoption = view.findViewById(R.id.btnDeleteAdoption);
+ btnBack = view.findViewById(R.id.btnAdoptionBack);
+ }
+
+ private void setupSpinner() {
+ ArrayAdapter adapter = new ArrayAdapter(requireContext(),
+ android.R.layout.simple_spinner_item,
+ new String[]{"Approved", "Pending", "Rejected"}) {
+
+ //Override the getView method for the spinner to make the text color darker for more readability
+ @NonNull
+ @Override
+ public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) {
+ View view = super.getView(position, convertView, parent);
+ ((TextView) view).setTextColor(ContextCompat.getColor(requireContext(), R.color.text_dark));
+ return view;
+ }
+ };
+ adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
+ spinnerAdoptionStatus.setAdapter(adapter);
+ }
+}
diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/AppointmentDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/AppointmentDetailFragment.java
new file mode 100644
index 00000000..bfd2d6b5
--- /dev/null
+++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/AppointmentDetailFragment.java
@@ -0,0 +1,178 @@
+package com.example.petstoremobile.fragments.listfragments.detailfragments;
+
+
+// Uses InputValidator for detailed field validation and ActivityLogger to log all changes.
+
+import android.content.res.Configuration;
+import android.os.Bundle;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.core.content.ContextCompat;
+import androidx.fragment.app.Fragment;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+import android.widget.Button;
+import android.widget.EditText;
+import android.widget.Spinner;
+import android.widget.TextView;
+import android.widget.Toast;
+import com.example.petstoremobile.R;
+import com.example.petstoremobile.fragments.ListFragment;
+import com.example.petstoremobile.fragments.listfragments.AppointmentFragment;
+import com.example.petstoremobile.models.Appointment;
+import com.example.petstoremobile.utils.ActivityLogger;
+import com.example.petstoremobile.utils.InputValidator;
+
+public class AppointmentDetailFragment extends Fragment {
+
+ private TextView tvMode, tvAppointmentId;
+ private EditText etCustomerName, etPetName, etServiceType, etAppointmentDate, etAppointmentTime;
+ private Spinner spinnerStatus;
+ private Button btnSaveAppointment, btnDeleteAppointment, btnBack;
+ private int appointmentId;
+ private int position;
+ private boolean isEditing = false;
+ private AppointmentFragment appointmentFragment;
+
+ // Set the appointment fragment as parent so we refer back when save or delete is done
+ public void setAppointmentFragment(AppointmentFragment fragment) {
+ this.appointmentFragment = fragment;
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ View view = inflater.inflate(R.layout.fragment_appointment_detail, container, false);
+
+ initViews(view);
+ setupSpinner();
+ handleArguments();
+
+ btnBack.setOnClickListener(v -> {
+ ListFragment listFragment = (ListFragment) getParentFragment();
+ if (listFragment != null) {
+ listFragment.getChildFragmentManager().popBackStack();
+ }
+ });
+ btnSaveAppointment.setOnClickListener(v -> saveAppointment());
+ btnDeleteAppointment.setOnClickListener(v -> deleteAppointment());
+
+ return view;
+ }
+
+ // Validates all fields using InputValidator, then saves the appointment
+ private void saveAppointment() {
+ // Validate all inputs using InputValidator utility
+ if (!InputValidator.isNotEmpty(etCustomerName, "Customer Name")) return;
+ if (!InputValidator.isNotEmpty(etPetName, "Pet Name")) return;
+ if (!InputValidator.isNotEmpty(etServiceType, "Service Type")) return;
+ if (!InputValidator.isValidDate(etAppointmentDate)) return;
+ if (!InputValidator.isValidTime(etAppointmentTime)) return;
+
+ String customerName = etCustomerName.getText().toString().trim();
+ String petName = etPetName.getText().toString().trim();
+ String serviceType = etServiceType.getText().toString().trim();
+ String date = etAppointmentDate.getText().toString().trim();
+ String time = etAppointmentTime.getText().toString().trim();
+ String status = spinnerStatus.getSelectedItem().toString();
+
+ try {
+ if (isEditing) {
+ // TODO: Replace with actual API PUT call when backend is ready
+ Appointment updated = new Appointment(appointmentId, customerName, petName, serviceType, date, time, status);
+ if (appointmentFragment != null) appointmentFragment.onAppointmentSaved(position, updated);
+ ActivityLogger.logChange(requireContext(), "Appointment", "UPDATED", appointmentId);
+ Toast.makeText(getContext(), "Appointment updated.", Toast.LENGTH_SHORT).show();
+ } else {
+ // TODO: Replace with actual API POST call when backend is ready
+ Appointment newAppt = new Appointment(0, customerName, petName, serviceType, date, time, status);
+ if (appointmentFragment != null) appointmentFragment.onAppointmentSaved(-1, newAppt);
+ ActivityLogger.log(requireContext(), "Added new Appointment for customer: " + customerName);
+ Toast.makeText(getContext(), "Appointment added.", Toast.LENGTH_SHORT).show();
+ }
+ // Go back to list
+ ListFragment listFragment = (ListFragment) getParentFragment();
+ if (listFragment != null) listFragment.getChildFragmentManager().popBackStack();
+ } catch (Exception e) {
+ ActivityLogger.logException(requireContext(), "AppointmentDetailFragment.saveAppointment", e);
+ Toast.makeText(getContext(), "Error saving appointment.", Toast.LENGTH_SHORT).show();
+ }
+ }
+
+ // Deletes the appointment and logs the action
+ private void deleteAppointment() {
+ try {
+ // TODO: Replace with actual API DELETE call when backend is ready
+ if (appointmentFragment != null) appointmentFragment.onAppointmentDeleted(position);
+ ActivityLogger.logChange(requireContext(), "Appointment", "DELETED", appointmentId);
+ Toast.makeText(getContext(), "Appointment deleted.", Toast.LENGTH_SHORT).show();
+ ListFragment listFragment = (ListFragment) getParentFragment();
+ if (listFragment != null) listFragment.getChildFragmentManager().popBackStack();
+ } catch (Exception e) {
+ ActivityLogger.logException(requireContext(), "AppointmentDetailFragment.deleteAppointment", e);
+ Toast.makeText(getContext(), "Error deleting appointment.", Toast.LENGTH_SHORT).show();
+ }
+ }
+
+ // Determines if the fragment is in add or edit mode and populates fields accordingly
+ private void handleArguments() {
+ if (getArguments() != null && getArguments().containsKey("appointmentId")) {
+ isEditing = true;
+ appointmentId = getArguments().getInt("appointmentId");
+ position = getArguments().getInt("position");
+ tvMode.setText("Edit Appointment");
+ tvAppointmentId.setText("ID: " + appointmentId);
+ etCustomerName.setText(getArguments().getString("customerName"));
+ etPetName.setText(getArguments().getString("petName"));
+ etServiceType.setText(getArguments().getString("serviceType"));
+ etAppointmentDate.setText(getArguments().getString("appointmentDate"));
+ etAppointmentTime.setText(getArguments().getString("appointmentTime"));
+ String status = getArguments().getString("status");
+ if ("Confirmed".equals(status)) spinnerStatus.setSelection(0);
+ else if ("Pending".equals(status)) spinnerStatus.setSelection(1);
+ else spinnerStatus.setSelection(2);
+ btnDeleteAppointment.setVisibility(View.VISIBLE);
+ } else {
+ isEditing = false;
+ tvMode.setText("Add Appointment");
+ tvAppointmentId.setVisibility(View.GONE);
+ btnDeleteAppointment.setVisibility(View.GONE);
+ btnSaveAppointment.setText("Add");
+ }
+ }
+
+ private void initViews(View view) {
+ tvMode = view.findViewById(R.id.tvApptMode);
+ tvAppointmentId = view.findViewById(R.id.tvAppointmentId);
+ etCustomerName = view.findViewById(R.id.etCustomerName);
+ etPetName = view.findViewById(R.id.etApptPetName);
+ etServiceType = view.findViewById(R.id.etServiceType);
+ etAppointmentDate = view.findViewById(R.id.etAppointmentDate);
+ etAppointmentTime = view.findViewById(R.id.etAppointmentTime);
+ spinnerStatus = view.findViewById(R.id.spinnerAppointmentStatus);
+ btnSaveAppointment = view.findViewById(R.id.btnSaveAppointment);
+ btnDeleteAppointment = view.findViewById(R.id.btnDeleteAppointment);
+ btnBack = view.findViewById(R.id.btnApptBack);
+ }
+
+ private void setupSpinner() {
+ ArrayAdapter adapter = new ArrayAdapter(requireContext(),
+ android.R.layout.simple_spinner_item,
+ new String[]{"Confirmed", "Pending", "Cancelled"}) {
+
+ //Override the getView method for the spinner to make the text color darker for more readability
+ @NonNull
+ @Override
+ public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) {
+ View view = super.getView(position, convertView, parent);
+ ((TextView) view).setTextColor(ContextCompat.getColor(requireContext(), R.color.text_dark));
+ return view;
+ }
+ };
+ adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
+ spinnerStatus.setAdapter(adapter);
+ }
+}
diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/InventoryDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/InventoryDetailFragment.java
new file mode 100644
index 00000000..175feb91
--- /dev/null
+++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/InventoryDetailFragment.java
@@ -0,0 +1,139 @@
+package com.example.petstoremobile.fragments.listfragments.detailfragments;
+
+// Uses InputValidator for detailed field validation and ActivityLogger to log all changes.
+
+import android.os.Bundle;
+import androidx.fragment.app.Fragment;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Button;
+import android.widget.EditText;
+import android.widget.TextView;
+import android.widget.Toast;
+import com.example.petstoremobile.R;
+import com.example.petstoremobile.fragments.ListFragment;
+import com.example.petstoremobile.fragments.listfragments.InventoryFragment;
+import com.example.petstoremobile.models.Inventory;
+import com.example.petstoremobile.utils.ActivityLogger;
+import com.example.petstoremobile.utils.InputValidator;
+
+public class InventoryDetailFragment extends Fragment {
+
+ private TextView tvMode, tvInventoryId;
+ private EditText etItemName, etCategory, etQuantity, etUnitPrice, etSupplier;
+ private Button btnSaveInventory, btnDeleteInventory, btnBack;
+ private int inventoryId;
+ private int position;
+ private boolean isEditing = false;
+ private InventoryFragment inventoryFragment;
+
+ // Set the inventory fragment as parent so we refer back when save or delete is done
+ public void setInventoryFragment(InventoryFragment fragment) {
+ this.inventoryFragment = fragment;
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ View view = inflater.inflate(R.layout.fragment_inventory_detail, container, false);
+
+ initViews(view);
+ handleArguments();
+
+ btnBack.setOnClickListener(v -> {
+ ListFragment listFragment = (ListFragment) getParentFragment();
+ if (listFragment != null) listFragment.getChildFragmentManager().popBackStack();
+ });
+ btnSaveInventory.setOnClickListener(v -> saveInventory());
+ btnDeleteInventory.setOnClickListener(v -> deleteInventory());
+
+ return view;
+ }
+
+ // Validates all fields using InputValidator, then saves the inventory item
+ private void saveInventory() {
+ if (!InputValidator.isNotEmpty(etItemName, "Item Name")) return;
+ if (!InputValidator.isNotEmpty(etCategory, "Category")) return;
+ if (!InputValidator.isPositiveInteger(etQuantity, "Quantity")) return;
+ if (!InputValidator.isPositiveDecimal(etUnitPrice, "Unit Price")) return;
+ if (!InputValidator.isNotEmpty(etSupplier, "Supplier")) return;
+
+ String itemName = etItemName.getText().toString().trim();
+ String category = etCategory.getText().toString().trim();
+ int quantity = Integer.parseInt(etQuantity.getText().toString().trim());
+ double unitPrice = Double.parseDouble(etUnitPrice.getText().toString().trim());
+ String supplier = etSupplier.getText().toString().trim();
+
+ try {
+ if (isEditing) {
+ // TODO: Replace with actual API PUT call when backend is ready
+ Inventory updated = new Inventory(inventoryId, itemName, category, quantity, unitPrice, supplier);
+ if (inventoryFragment != null) inventoryFragment.onInventorySaved(position, updated);
+ ActivityLogger.logChange(requireContext(), "Inventory", "UPDATED", inventoryId);
+ Toast.makeText(getContext(), "Inventory item updated.", Toast.LENGTH_SHORT).show();
+ } else {
+ // TODO: Replace with actual API POST call when backend is ready
+ Inventory newItem = new Inventory(0, itemName, category, quantity, unitPrice, supplier);
+ if (inventoryFragment != null) inventoryFragment.onInventorySaved(-1, newItem);
+ ActivityLogger.log(requireContext(), "Added new Inventory item: " + itemName);
+ Toast.makeText(getContext(), "Inventory item added.", Toast.LENGTH_SHORT).show();
+ }
+ ListFragment listFragment = (ListFragment) getParentFragment();
+ if (listFragment != null) listFragment.getChildFragmentManager().popBackStack();
+ } catch (Exception e) {
+ ActivityLogger.logException(requireContext(), "InventoryDetailFragment.saveInventory", e);
+ Toast.makeText(getContext(), "Error saving inventory item.", Toast.LENGTH_SHORT).show();
+ }
+ }
+
+ // Deletes the inventory item and logs the action
+ private void deleteInventory() {
+ try {
+ // TODO: Replace with actual API DELETE call when backend is ready
+ if (inventoryFragment != null) inventoryFragment.onInventoryDeleted(position);
+ ActivityLogger.logChange(requireContext(), "Inventory", "DELETED", inventoryId);
+ Toast.makeText(getContext(), "Inventory item deleted.", Toast.LENGTH_SHORT).show();
+ ListFragment listFragment = (ListFragment) getParentFragment();
+ if (listFragment != null) listFragment.getChildFragmentManager().popBackStack();
+ } catch (Exception e) {
+ ActivityLogger.logException(requireContext(), "InventoryDetailFragment.deleteInventory", e);
+ Toast.makeText(getContext(), "Error deleting inventory item.", Toast.LENGTH_SHORT).show();
+ }
+ }
+
+ private void handleArguments() {
+ if (getArguments() != null && getArguments().containsKey("inventoryId")) {
+ isEditing = true;
+ inventoryId = getArguments().getInt("inventoryId");
+ position = getArguments().getInt("position");
+ tvMode.setText("Edit Inventory Item");
+ tvInventoryId.setText("ID: " + inventoryId);
+ etItemName.setText(getArguments().getString("itemName"));
+ etCategory.setText(getArguments().getString("category"));
+ etQuantity.setText(String.valueOf(getArguments().getInt("quantity")));
+ etUnitPrice.setText(String.valueOf(getArguments().getDouble("unitPrice")));
+ etSupplier.setText(getArguments().getString("supplier"));
+ btnDeleteInventory.setVisibility(View.VISIBLE);
+ } else {
+ isEditing = false;
+ tvMode.setText("Add Inventory Item");
+ tvInventoryId.setVisibility(View.GONE);
+ btnDeleteInventory.setVisibility(View.GONE);
+ btnSaveInventory.setText("Add");
+ }
+ }
+
+ private void initViews(View view) {
+ tvMode = view.findViewById(R.id.tvInventoryMode);
+ tvInventoryId = view.findViewById(R.id.tvInventoryId);
+ etItemName = view.findViewById(R.id.etItemName);
+ etCategory = view.findViewById(R.id.etInventoryCategory);
+ etQuantity = view.findViewById(R.id.etQuantity);
+ etUnitPrice = view.findViewById(R.id.etUnitPrice);
+ etSupplier = view.findViewById(R.id.etInventorySupplier);
+ btnSaveInventory = view.findViewById(R.id.btnSaveInventory);
+ btnDeleteInventory = view.findViewById(R.id.btnDeleteInventory);
+ btnBack = view.findViewById(R.id.btnInventoryBack);
+ }
+}
diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/PetDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/PetDetailFragment.java
new file mode 100644
index 00000000..6906bc73
--- /dev/null
+++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/PetDetailFragment.java
@@ -0,0 +1,245 @@
+package com.example.petstoremobile.fragments.listfragments.detailfragments;
+
+import android.os.Bundle;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.appcompat.app.AlertDialog;
+import androidx.core.content.ContextCompat;
+import androidx.fragment.app.Fragment;
+
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+import android.widget.Button;
+import android.widget.EditText;
+import android.widget.Spinner;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import com.example.petstoremobile.R;
+import com.example.petstoremobile.api.PetApi;
+import com.example.petstoremobile.api.RetrofitClient;
+import com.example.petstoremobile.dtos.PetDTO;
+import com.example.petstoremobile.fragments.ListFragment;
+import com.example.petstoremobile.fragments.listfragments.PetFragment;
+
+import retrofit2.Call;
+import retrofit2.Callback;
+import retrofit2.Response;
+
+public class PetDetailFragment extends Fragment {
+
+ private TextView tvMode, tvPetId;
+ private EditText etPetName, etPetSpecies, etPetBreed, etPetAge, etPetPrice;
+ private Spinner spinnerPetStatus;
+ private Button btnSavePet, btnDeletePet, btnBack;
+ private int petId;
+ private boolean isEditing = false;
+ private PetFragment petFragment;
+
+ //set the pet fragment to the parent so we refer back to pet view when save or delete is done
+ public void setPetFragment(PetFragment fragment) {
+ this.petFragment = fragment;
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ View view = inflater.inflate(R.layout.fragment_pet_detail, container, false);
+
+ //set up spinner and get controls from layout and display the view depending on the mode
+ initViews(view);
+ setupSpinner();
+ handleArguments();
+
+ //set button click listeners
+ btnBack.setOnClickListener(v -> navigateBack());
+ btnSavePet.setOnClickListener(v -> savePet());
+ btnDeletePet.setOnClickListener(v -> deletePet());
+
+ return view;
+ }
+
+ //Method to Update or Add a pet
+ private void savePet() {
+ //get all the values from the fields
+ String name = etPetName.getText().toString().trim();
+ String species = etPetSpecies.getText().toString().trim();
+ String breed = etPetBreed.getText().toString().trim();
+ String ageStr = etPetAge.getText().toString().trim();
+ String priceStr = etPetPrice.getText().toString().trim();
+ String status = spinnerPetStatus.getSelectedItem().toString();
+
+ //check if all the fields are filled
+ if (name.isEmpty() || species.isEmpty() || breed.isEmpty() || ageStr.isEmpty() || priceStr.isEmpty()) {
+ Toast.makeText(getContext(), "Please fill in all fields", Toast.LENGTH_SHORT).show();
+ return;
+ }
+
+ //create a pet object to send to the API
+ PetDTO petDTO = new PetDTO();
+ petDTO.setPetName(name);
+ petDTO.setPetSpecies(species);
+ petDTO.setPetBreed(breed);
+ petDTO.setPetAge(Integer.parseInt(ageStr));
+ petDTO.setPetPrice(priceStr);
+ petDTO.setPetStatus(status);
+
+ PetApi petApi = RetrofitClient.getPetApi(requireContext());
+
+ //check if the pet is being edited or added
+ if (isEditing) {
+ // Update existing pet
+ petDTO.setPetId((long) petId);
+ petApi.updatePet((long) petId, petDTO).enqueue(new Callback() {
+ @Override
+ public void onResponse(Call call, Response response) {
+ if (response.isSuccessful()) {
+ Toast.makeText(getContext(), "Pet updated successfully!", Toast.LENGTH_SHORT).show();
+ navigateBack();
+ } else {
+ Toast.makeText(getContext(), "Failed to update pet: " + response.code(), Toast.LENGTH_SHORT).show();
+ }
+ }
+
+ @Override
+ public void onFailure(Call call, Throwable t) {
+ Log.e("PetDetailFragment", "Error updating pet", t);
+ Toast.makeText(getContext(), "Error: " + t.getMessage(), Toast.LENGTH_SHORT).show();
+ }
+ });
+ } else {
+ // Add new pet
+ petApi.createPet(petDTO).enqueue(new Callback() {
+ @Override
+ public void onResponse(Call call, Response response) {
+ if (response.isSuccessful()) {
+ Toast.makeText(getContext(), "Pet added successfully!", Toast.LENGTH_SHORT).show();
+ navigateBack();
+ } else {
+ Toast.makeText(getContext(), "Failed to add pet: " + response.code(), Toast.LENGTH_SHORT).show();
+ }
+ }
+
+ @Override
+ public void onFailure(Call call, Throwable t) {
+ Log.e("PetDetailFragment", "Error adding pet", t);
+ Toast.makeText(getContext(), "Error: " + t.getMessage(), Toast.LENGTH_SHORT).show();
+ }
+ });
+ }
+ }
+
+ //Method to Delete a pet
+ private void deletePet() {
+ //Alert the user to confirm the delete
+ new AlertDialog.Builder(requireContext())
+ .setTitle("Delete Pet")
+ .setMessage("Are you sure you want to delete " + etPetName.getText().toString() + "?")
+ .setPositiveButton("Delete", (dialog, which) -> {
+ PetApi petApi = RetrofitClient.getPetApi(requireContext());
+ //if they say yes then delete the pet
+ petApi.deletePet((long) petId).enqueue(new Callback() {
+ @Override
+ public void onResponse(Call call, Response response) {
+ if (response.isSuccessful()) {
+ Toast.makeText(getContext(), "Pet deleted successfully!", Toast.LENGTH_SHORT).show();
+ navigateBack();
+ } else {
+ Toast.makeText(getContext(), "Failed to delete pet: " + response.code(), Toast.LENGTH_SHORT).show();
+ }
+ }
+
+ @Override
+ public void onFailure(Call call, Throwable t) {
+ Log.e("PetDetailFragment", "Error deleting pet", t);
+ Toast.makeText(getContext(), "Error: " + t.getMessage(), Toast.LENGTH_SHORT).show();
+ }
+ });
+ })
+ .setNegativeButton("Cancel", null)
+ .show();
+ }
+
+ //Helper method to navigate back to the list
+ private void navigateBack() {
+ ListFragment listFragment = (ListFragment) getParentFragment();
+ if (listFragment != null) {
+ // If editing pop back twice to get back to PetDetail Fragment instead of PetProfileFragment
+ if (isEditing) {
+ listFragment.getChildFragmentManager().popBackStack();
+ }
+ listFragment.getChildFragmentManager().popBackStack();
+ }
+ }
+
+ //helper function to check if pet is being edited or added and show the view accordingly
+ private void handleArguments() {
+ // Pet is being edited if the bundle contains a petId
+ if (getArguments() != null && getArguments().containsKey("petId")) {
+ // Get pet data from arguments and populate fields
+ isEditing = true;
+ petId = getArguments().getInt("petId");
+ tvMode.setText("Edit Pet");
+ tvPetId.setText("ID: " + petId);
+ etPetName.setText(getArguments().getString("petName"));
+ etPetSpecies.setText(getArguments().getString("petSpecies"));
+ etPetBreed.setText(getArguments().getString("petBreed"));
+ etPetAge.setText(String.valueOf(getArguments().getInt("petAge")));
+ etPetPrice.setText(String.valueOf(getArguments().getDouble("petPrice")));
+ String status = getArguments().getString("petStatus");
+ if ("Available".equals(status)) {
+ spinnerPetStatus.setSelection(0);
+ } else {
+ spinnerPetStatus.setSelection(1);
+ }
+ btnDeletePet.setVisibility(View.VISIBLE);
+ } else {
+ // Pet is being added
+ // Set default values for add a new pet
+ isEditing = false;
+ tvMode.setText("Add Pet");
+ tvPetId.setVisibility(View.GONE);
+ btnDeletePet.setVisibility(View.GONE);
+ btnSavePet.setText("Add");
+ }
+ }
+
+ //helper function to get controls from layout
+ private void initViews(View view) {
+ tvMode = view.findViewById(R.id.tvMode);
+ tvPetId = view.findViewById(R.id.tvPetId);
+ etPetName = view.findViewById(R.id.etPetName);
+ etPetSpecies = view.findViewById(R.id.etPetSpecies);
+ etPetBreed = view.findViewById(R.id.etPetBreed);
+ etPetAge = view.findViewById(R.id.etPetAge);
+ etPetPrice = view.findViewById(R.id.etPetPrice);
+ spinnerPetStatus = view.findViewById(R.id.spinnerPetStatus);
+ btnSavePet = view.findViewById(R.id.btnSavePet);
+ btnDeletePet = view.findViewById(R.id.btnDeletePet);
+ btnBack = view.findViewById(R.id.btnBack);
+ }
+
+ //helper function to set up the spinner menu for pet status
+ private void setupSpinner() {
+ ArrayAdapter adapter = new ArrayAdapter(requireContext(),
+ android.R.layout.simple_spinner_item,
+ new String[]{"Available", "Adopted"}) {
+
+ //Override the getView method for the spinner to make the text color darker for more readability
+ @NonNull
+ @Override
+ public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) {
+ View view = super.getView(position, convertView, parent);
+ ((TextView) view).setTextColor(ContextCompat.getColor(requireContext(), R.color.text_dark));
+ return view;
+ }
+ };
+ adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
+ spinnerPetStatus.setAdapter(adapter);
+ }
+
+}
diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ProductDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ProductDetailFragment.java
new file mode 100644
index 00000000..4179c4f5
--- /dev/null
+++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ProductDetailFragment.java
@@ -0,0 +1,139 @@
+package com.example.petstoremobile.fragments.listfragments.detailfragments;
+
+// Uses InputValidator for detailed field validation and ActivityLogger to log all changes.
+
+import android.os.Bundle;
+import androidx.fragment.app.Fragment;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Button;
+import android.widget.EditText;
+import android.widget.TextView;
+import android.widget.Toast;
+import com.example.petstoremobile.R;
+import com.example.petstoremobile.fragments.ListFragment;
+import com.example.petstoremobile.fragments.listfragments.ProductFragment;
+import com.example.petstoremobile.models.Product;
+import com.example.petstoremobile.utils.ActivityLogger;
+import com.example.petstoremobile.utils.InputValidator;
+
+public class ProductDetailFragment extends Fragment {
+
+ private TextView tvMode, tvProductId;
+ private EditText etProductName, etProductDesc, etCategory, etProductPrice, etStockQuantity;
+ private Button btnSaveProduct, btnDeleteProduct, btnBack;
+ private int productId;
+ private int position;
+ private boolean isEditing = false;
+ private ProductFragment productFragment;
+
+ // Set the product fragment as parent so we refer back when save or delete is done
+ public void setProductFragment(ProductFragment fragment) {
+ this.productFragment = fragment;
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ View view = inflater.inflate(R.layout.fragment_product_detail, container, false);
+
+ initViews(view);
+ handleArguments();
+
+ btnBack.setOnClickListener(v -> {
+ ListFragment listFragment = (ListFragment) getParentFragment();
+ if (listFragment != null) listFragment.getChildFragmentManager().popBackStack();
+ });
+ btnSaveProduct.setOnClickListener(v -> saveProduct());
+ btnDeleteProduct.setOnClickListener(v -> deleteProduct());
+
+ return view;
+ }
+
+ // Validates all fields using InputValidator, then saves the product
+ private void saveProduct() {
+ if (!InputValidator.isNotEmpty(etProductName, "Product Name")) return;
+ if (!InputValidator.isNotEmpty(etProductDesc, "Description")) return;
+ if (!InputValidator.isNotEmpty(etCategory, "Category")) return;
+ if (!InputValidator.isPositiveDecimal(etProductPrice, "Price")) return;
+ if (!InputValidator.isPositiveInteger(etStockQuantity, "Stock Quantity")) return;
+
+ String productName = etProductName.getText().toString().trim();
+ String productDesc = etProductDesc.getText().toString().trim();
+ String category = etCategory.getText().toString().trim();
+ double productPrice = Double.parseDouble(etProductPrice.getText().toString().trim());
+ int stockQuantity = Integer.parseInt(etStockQuantity.getText().toString().trim());
+
+ try {
+ if (isEditing) {
+ // TODO: Replace with actual API PUT call when backend is ready
+ Product updated = new Product(productId, productName, productDesc, category, productPrice, stockQuantity);
+ if (productFragment != null) productFragment.onProductSaved(position, updated);
+ ActivityLogger.logChange(requireContext(), "Product", "UPDATED", productId);
+ Toast.makeText(getContext(), "Product updated.", Toast.LENGTH_SHORT).show();
+ } else {
+ // TODO: Replace with actual API POST call when backend is ready
+ Product newProduct = new Product(0, productName, productDesc, category, productPrice, stockQuantity);
+ if (productFragment != null) productFragment.onProductSaved(-1, newProduct);
+ ActivityLogger.log(requireContext(), "Added new Product: " + productName);
+ Toast.makeText(getContext(), "Product added.", Toast.LENGTH_SHORT).show();
+ }
+ ListFragment listFragment = (ListFragment) getParentFragment();
+ if (listFragment != null) listFragment.getChildFragmentManager().popBackStack();
+ } catch (Exception e) {
+ ActivityLogger.logException(requireContext(), "ProductDetailFragment.saveProduct", e);
+ Toast.makeText(getContext(), "Error saving product.", Toast.LENGTH_SHORT).show();
+ }
+ }
+
+ // Deletes the product and logs the action
+ private void deleteProduct() {
+ try {
+ // TODO: Replace with actual API DELETE call when backend is ready
+ if (productFragment != null) productFragment.onProductDeleted(position);
+ ActivityLogger.logChange(requireContext(), "Product", "DELETED", productId);
+ Toast.makeText(getContext(), "Product deleted.", Toast.LENGTH_SHORT).show();
+ ListFragment listFragment = (ListFragment) getParentFragment();
+ if (listFragment != null) listFragment.getChildFragmentManager().popBackStack();
+ } catch (Exception e) {
+ ActivityLogger.logException(requireContext(), "ProductDetailFragment.deleteProduct", e);
+ Toast.makeText(getContext(), "Error deleting product.", Toast.LENGTH_SHORT).show();
+ }
+ }
+
+ private void handleArguments() {
+ if (getArguments() != null && getArguments().containsKey("productId")) {
+ isEditing = true;
+ productId = getArguments().getInt("productId");
+ position = getArguments().getInt("position");
+ tvMode.setText("Edit Product");
+ tvProductId.setText("ID: " + productId);
+ etProductName.setText(getArguments().getString("productName"));
+ etProductDesc.setText(getArguments().getString("productDesc"));
+ etCategory.setText(getArguments().getString("category"));
+ etProductPrice.setText(String.valueOf(getArguments().getDouble("productPrice")));
+ etStockQuantity.setText(String.valueOf(getArguments().getInt("stockQuantity")));
+ btnDeleteProduct.setVisibility(View.VISIBLE);
+ } else {
+ isEditing = false;
+ tvMode.setText("Add Product");
+ tvProductId.setVisibility(View.GONE);
+ btnDeleteProduct.setVisibility(View.GONE);
+ btnSaveProduct.setText("Add");
+ }
+ }
+
+ private void initViews(View view) {
+ tvMode = view.findViewById(R.id.tvProductMode);
+ tvProductId = view.findViewById(R.id.tvProductId);
+ etProductName = view.findViewById(R.id.etProductName);
+ etProductDesc = view.findViewById(R.id.etProductDesc);
+ etCategory = view.findViewById(R.id.etProductCategory);
+ etProductPrice = view.findViewById(R.id.etProductPrice);
+ etStockQuantity = view.findViewById(R.id.etStockQuantity);
+ btnSaveProduct = view.findViewById(R.id.btnSaveProduct);
+ btnDeleteProduct = view.findViewById(R.id.btnDeleteProduct);
+ btnBack = view.findViewById(R.id.btnProductBack);
+ }
+}
diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ServiceDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ServiceDetailFragment.java
new file mode 100644
index 00000000..2defbb69
--- /dev/null
+++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ServiceDetailFragment.java
@@ -0,0 +1,200 @@
+package com.example.petstoremobile.fragments.listfragments.detailfragments;
+
+import android.os.Bundle;
+
+import androidx.appcompat.app.AlertDialog;
+import androidx.fragment.app.Fragment;
+
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Button;
+import android.widget.EditText;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import com.example.petstoremobile.R;
+import com.example.petstoremobile.api.RetrofitClient;
+import com.example.petstoremobile.api.ServiceApi;
+import com.example.petstoremobile.dtos.ServiceDTO;
+import com.example.petstoremobile.fragments.ListFragment;
+import com.example.petstoremobile.fragments.listfragments.ServiceFragment;
+
+import retrofit2.Call;
+import retrofit2.Callback;
+import retrofit2.Response;
+
+public class ServiceDetailFragment extends Fragment {
+
+ private TextView tvMode, tvServiceId;
+ private EditText etServiceName, etServiceDesc, etServiceDuration, etServicePrice;
+ private Button btnSaveService, btnDeleteService, btnBack;
+ private int serviceId;
+ private boolean isEditing = false;
+ private ServiceFragment serviceFragment;
+
+ //set the service fragment to the parent so we refer back to service view when save or delete is done
+ public void setServiceFragment(ServiceFragment fragment) {
+ this.serviceFragment = fragment;
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ View view = inflater.inflate(R.layout.fragment_service_detail, container, false);
+
+ //get controls from layout and display the view depending on the mode
+ initViews(view);
+ handleArguments();
+
+ //set button click listeners
+ btnBack.setOnClickListener(v -> navigateBack());
+ btnSaveService.setOnClickListener(v -> saveService());
+ btnDeleteService.setOnClickListener(v -> deleteService());
+
+ return view;
+ }
+
+ //Method to Update or Add a service
+ private void saveService() {
+ //get all the values from the fields
+ String name = etServiceName.getText().toString().trim();
+ String desc = etServiceDesc.getText().toString().trim();
+ String durationStr = etServiceDuration.getText().toString().trim();
+ String priceStr = etServicePrice.getText().toString().trim();
+
+ //check if all the fields are filled (desc is optional)
+ if (name.isEmpty() || durationStr.isEmpty() || priceStr.isEmpty()) {
+ Toast.makeText(getContext(), "Please fill in all fields", Toast.LENGTH_SHORT).show();
+ return;
+ }
+
+ //create a service object to send to the API
+ ServiceDTO serviceDTO = new ServiceDTO();
+ serviceDTO.setServiceName(name);
+ serviceDTO.setServiceDesc(desc);
+ serviceDTO.setServiceDuration(Integer.parseInt(durationStr));
+ serviceDTO.setServicePrice(Double.parseDouble(priceStr));
+
+ ServiceApi serviceApi = RetrofitClient.getServiceApi(requireContext());
+
+ //check if the service is being edited or added
+ if (isEditing) {
+ // Update existing service
+ serviceDTO.setServiceId((long) serviceId);
+ serviceApi.updateService((long) serviceId, serviceDTO).enqueue(new Callback() {
+ @Override
+ public void onResponse(Call call, Response response) {
+ if (response.isSuccessful()) {
+ Toast.makeText(getContext(), "Service updated successfully!", Toast.LENGTH_SHORT).show();
+ navigateBack();
+ } else {
+ Toast.makeText(getContext(), "Failed to update service: " + response.code(), Toast.LENGTH_SHORT).show();
+ }
+ }
+
+ @Override
+ public void onFailure(Call call, Throwable t) {
+ Log.e("ServiceDetailFragment", "Error updating service", t);
+ Toast.makeText(getContext(), "Error: " + t.getMessage(), Toast.LENGTH_SHORT).show();
+ }
+ });
+ } else {
+ // Add new service
+ serviceApi.createService(serviceDTO).enqueue(new Callback() {
+ @Override
+ public void onResponse(Call call, Response response) {
+ if (response.isSuccessful()) {
+ Toast.makeText(getContext(), "Service added successfully!", Toast.LENGTH_SHORT).show();
+ navigateBack();
+ } else {
+ Toast.makeText(getContext(), "Failed to add service: " + response.code(), Toast.LENGTH_SHORT).show();
+ }
+ }
+
+ @Override
+ public void onFailure(Call call, Throwable t) {
+ Log.e("ServiceDetailFragment", "Error adding service", t);
+ Toast.makeText(getContext(), "Error: " + t.getMessage(), Toast.LENGTH_SHORT).show();
+ }
+ });
+ }
+ }
+
+ //Method to Delete a service
+ private void deleteService() {
+ //Alert the user to confirm the delete
+ new AlertDialog.Builder(requireContext())
+ .setTitle("Delete Service")
+ .setMessage("Are you sure you want to delete " + etServiceName.getText().toString() + "?")
+ .setPositiveButton("Delete", (dialog, which) -> {
+ ServiceApi serviceApi = RetrofitClient.getServiceApi(requireContext());
+ serviceApi.deleteService((long) serviceId).enqueue(new Callback() {
+ @Override
+ public void onResponse(Call call, Response response) {
+ if (response.isSuccessful()) {
+ Toast.makeText(getContext(), "Service deleted successfully!", Toast.LENGTH_SHORT).show();
+ navigateBack();
+ } else {
+ Toast.makeText(getContext(), "Failed to delete service: " + response.code(), Toast.LENGTH_SHORT).show();
+ }
+ }
+
+ @Override
+ public void onFailure(Call call, Throwable t) {
+ Log.e("ServiceDetailFragment", "Error deleting service", t);
+ Toast.makeText(getContext(), "Error: " + t.getMessage(), Toast.LENGTH_SHORT).show();
+ }
+ });
+ })
+ .setNegativeButton("Cancel", null)
+ .show();
+ }
+
+ //Helper method to navigate back to the list
+ private void navigateBack() {
+ ListFragment listFragment = (ListFragment) getParentFragment();
+ if (listFragment != null) {
+ listFragment.getChildFragmentManager().popBackStack();
+ }
+ }
+
+ //helper function to check if service is being edited or added and show the view accordingly
+ private void handleArguments() {
+ // Service is being edited if the bundle contains a serviceId
+ if (getArguments() != null && getArguments().containsKey("serviceId")) {
+ // Get service data from arguments and populate fields
+ isEditing = true;
+ serviceId = getArguments().getInt("serviceId");
+ tvMode.setText("Edit Service");
+ tvServiceId.setText("ID: " + serviceId);
+ etServiceName.setText(getArguments().getString("serviceName"));
+ etServiceDesc.setText(getArguments().getString("serviceDesc"));
+ etServiceDuration.setText(String.valueOf(getArguments().getInt("serviceDuration")));
+ etServicePrice.setText(String.valueOf(getArguments().getDouble("servicePrice")));
+ btnDeleteService.setVisibility(View.VISIBLE);
+ } else {
+ // Service is being added
+ // Set default values for add a new service
+ isEditing = false;
+ tvMode.setText("Add Service");
+ tvServiceId.setVisibility(View.GONE);
+ btnDeleteService.setVisibility(View.GONE);
+ btnSaveService.setText("Add");
+ }
+ }
+
+ //helper function to get controls from layout
+ private void initViews(View view) {
+ tvMode = view.findViewById(R.id.tvMode);
+ tvServiceId = view.findViewById(R.id.tvServiceId);
+ etServiceName = view.findViewById(R.id.etServiceName);
+ etServiceDesc = view.findViewById(R.id.etServiceDesc);
+ etServiceDuration = view.findViewById(R.id.etServiceDuration);
+ etServicePrice = view.findViewById(R.id.etServicePrice);
+ btnSaveService = view.findViewById(R.id.btnSaveService);
+ btnDeleteService = view.findViewById(R.id.btnDeleteService);
+ btnBack = view.findViewById(R.id.btnBack);
+ }
+}
diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/SupplierDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/SupplierDetailFragment.java
new file mode 100644
index 00000000..8537d6c2
--- /dev/null
+++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/SupplierDetailFragment.java
@@ -0,0 +1,204 @@
+package com.example.petstoremobile.fragments.listfragments.detailfragments;
+
+import android.os.Bundle;
+
+import androidx.appcompat.app.AlertDialog;
+import androidx.fragment.app.Fragment;
+
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Button;
+import android.widget.EditText;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import com.example.petstoremobile.R;
+import com.example.petstoremobile.api.RetrofitClient;
+import com.example.petstoremobile.api.SupplierApi;
+import com.example.petstoremobile.dtos.SupplierDTO;
+import com.example.petstoremobile.fragments.ListFragment;
+import com.example.petstoremobile.fragments.listfragments.SupplierFragment;
+
+import retrofit2.Call;
+import retrofit2.Callback;
+import retrofit2.Response;
+
+public class SupplierDetailFragment extends Fragment {
+
+ private TextView tvMode, tvSupId;
+ private EditText etSupCompany, etSupContactFirstName, etSupContactLastName, etSupEmail, etSupPhone;
+ private Button btnSaveSupplier, btnDeleteSupplier, btnBack;
+ private int supId;
+ private boolean isEditing = false;
+ private SupplierFragment supplierFragment;
+
+ //set the supplier fragment to the parent so we refer back to supplier view when save or delete is done
+ public void setSupplierFragment(SupplierFragment fragment) {
+ this.supplierFragment = fragment;
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ View view = inflater.inflate(R.layout.fragment_supplier_detail, container, false);
+
+ //get controls from layout and display the view depending on the mode
+ initViews(view);
+ handleArguments();
+
+ //set button click listeners
+ btnBack.setOnClickListener(v -> navigateBack());
+ btnSaveSupplier.setOnClickListener(v -> saveSupplier());
+ btnDeleteSupplier.setOnClickListener(v -> deleteSupplier());
+
+ return view;
+ }
+
+ //Method to Update or Add a supplier
+ private void saveSupplier() {
+ //get all the values from the fields
+ String company = etSupCompany.getText().toString().trim();
+ String firstName = etSupContactFirstName.getText().toString().trim();
+ String lastName = etSupContactLastName.getText().toString().trim();
+ String email = etSupEmail.getText().toString().trim();
+ String phone = etSupPhone.getText().toString().trim();
+
+ //check if all the fields are filled
+ if (company.isEmpty() || firstName.isEmpty() || lastName.isEmpty() || email.isEmpty() || phone.isEmpty()) {
+ Toast.makeText(getContext(), "Please fill in all fields", Toast.LENGTH_SHORT).show();
+ return;
+ }
+
+ //create a supplier object to send to the API
+ SupplierDTO supplierDTO = new SupplierDTO();
+ supplierDTO.setSupCompany(company);
+ supplierDTO.setSupContactFirstName(firstName);
+ supplierDTO.setSupContactLastName(lastName);
+ supplierDTO.setSupEmail(email);
+ supplierDTO.setSupPhone(phone);
+
+ SupplierApi supplierApi = RetrofitClient.getSupplierApi(requireContext());
+
+ //check if the supplier is being edited or added
+ if (isEditing) {
+ // Update existing supplier
+ supplierDTO.setSupId((long) supId);
+ supplierApi.updateSupplier((long) supId, supplierDTO).enqueue(new Callback() {
+ @Override
+ public void onResponse(Call call, Response response) {
+ if (response.isSuccessful()) {
+ Toast.makeText(getContext(), "Supplier updated successfully!", Toast.LENGTH_SHORT).show();
+ navigateBack();
+ } else {
+ Toast.makeText(getContext(), "Failed to update supplier: " + response.code(), Toast.LENGTH_SHORT).show();
+ }
+ }
+
+ @Override
+ public void onFailure(Call call, Throwable t) {
+ Log.e("SupplierDetailFragment", "Error updating supplier", t);
+ Toast.makeText(getContext(), "Error: " + t.getMessage(), Toast.LENGTH_SHORT).show();
+ }
+ });
+ } else {
+ // Add new supplier
+ supplierApi.createSupplier(supplierDTO).enqueue(new Callback() {
+ @Override
+ public void onResponse(Call call, Response response) {
+ if (response.isSuccessful()) {
+ Toast.makeText(getContext(), "Supplier added successfully!", Toast.LENGTH_SHORT).show();
+ navigateBack();
+ } else {
+ Toast.makeText(getContext(), "Failed to add supplier: " + response.code(), Toast.LENGTH_SHORT).show();
+ }
+ }
+
+ @Override
+ public void onFailure(Call call, Throwable t) {
+ Log.e("SupplierDetailFragment", "Error adding supplier", t);
+ Toast.makeText(getContext(), "Error: " + t.getMessage(), Toast.LENGTH_SHORT).show();
+ }
+ });
+ }
+ }
+
+ //Method to Delete a supplier
+ private void deleteSupplier() {
+ //Alert the user to confirm the delete
+ new AlertDialog.Builder(requireContext())
+ .setTitle("Delete Supplier")
+ .setMessage("Are you sure you want to delete " + etSupCompany.getText().toString() + "?")
+ .setPositiveButton("Delete", (dialog, which) -> {
+ SupplierApi supplierApi = RetrofitClient.getSupplierApi(requireContext());
+ supplierApi.deleteSupplier((long) supId).enqueue(new Callback() {
+ @Override
+ public void onResponse(Call call, Response response) {
+ if (response.isSuccessful()) {
+ Toast.makeText(getContext(), "Supplier deleted successfully!", Toast.LENGTH_SHORT).show();
+ navigateBack();
+ } else {
+ Toast.makeText(getContext(), "Failed to delete supplier: " + response.code(), Toast.LENGTH_SHORT).show();
+ }
+ }
+
+ @Override
+ public void onFailure(Call call, Throwable t) {
+ Log.e("SupplierDetailFragment", "Error deleting supplier", t);
+ Toast.makeText(getContext(), "Error: " + t.getMessage(), Toast.LENGTH_SHORT).show();
+ }
+ });
+ })
+ .setNegativeButton("Cancel", null)
+ .show();
+ }
+
+ //Helper method to navigate back to the list
+ private void navigateBack() {
+ ListFragment listFragment = (ListFragment) getParentFragment();
+ if (listFragment != null) {
+ listFragment.getChildFragmentManager().popBackStack();
+ }
+ }
+
+ //helper function to check if supplier is being edited or added and show the view accordingly
+ private void handleArguments() {
+ // Supplier is being edited if the bundle contains a supId
+ if (getArguments() != null && getArguments().containsKey("supId")) {
+ // Get supplier data from arguments and populate fields
+ isEditing = true;
+ supId = getArguments().getInt("supId");
+ tvMode.setText("Edit Supplier");
+ tvSupId.setText("ID: " + supId);
+ etSupCompany.setText(getArguments().getString("supCompany"));
+ etSupContactFirstName.setText(getArguments().getString("supContactFirstName"));
+ etSupContactLastName.setText(getArguments().getString("supContactLastName"));
+ etSupEmail.setText(getArguments().getString("supEmail"));
+ etSupPhone.setText(getArguments().getString("supPhone"));
+ btnDeleteSupplier.setVisibility(View.VISIBLE);
+ } else {
+ // Supplier is being added
+ // Set default values for add a new supplier
+ isEditing = false;
+ tvMode.setText("Add Supplier");
+ tvSupId.setVisibility(View.GONE);
+ btnDeleteSupplier.setVisibility(View.GONE);
+ btnSaveSupplier.setText("Add");
+ }
+ }
+
+ //helper function to get controls from layout
+ private void initViews(View view) {
+ tvMode = view.findViewById(R.id.tvMode);
+ tvSupId = view.findViewById(R.id.tvSupId);
+ etSupCompany = view.findViewById(R.id.etSupCompany);
+ etSupContactFirstName = view.findViewById(R.id.etSupContactFirstName);
+ etSupContactLastName = view.findViewById(R.id.etSupContactLastName);
+ etSupEmail = view.findViewById(R.id.etSupEmail);
+ etSupPhone = view.findViewById(R.id.etSupPhone);
+ btnSaveSupplier = view.findViewById(R.id.btnSaveSupplier);
+ btnDeleteSupplier = view.findViewById(R.id.btnDeleteSupplier);
+ btnBack = view.findViewById(R.id.btnBack);
+ }
+}
diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/listprofilefragments/PetProfileFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/listprofilefragments/PetProfileFragment.java
new file mode 100644
index 00000000..280789e8
--- /dev/null
+++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/listprofilefragments/PetProfileFragment.java
@@ -0,0 +1,177 @@
+package com.example.petstoremobile.fragments.listfragments.listprofilefragments;
+
+import android.Manifest;
+import android.app.Activity;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.graphics.Color;
+import android.net.Uri;
+import android.os.Bundle;
+
+import androidx.activity.result.ActivityResultLauncher;
+import androidx.activity.result.contract.ActivityResultContracts;
+import androidx.appcompat.app.AlertDialog;
+import androidx.core.content.ContextCompat;
+import androidx.core.content.FileProvider;
+import androidx.fragment.app.Fragment;
+
+import android.provider.MediaStore;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Button;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import com.example.petstoremobile.R;
+import com.example.petstoremobile.fragments.ListFragment;
+import com.example.petstoremobile.fragments.listfragments.detailfragments.PetDetailFragment;
+
+import java.io.File;
+import java.util.Locale;
+
+public class PetProfileFragment extends Fragment {
+
+ private TextView tvPetName, tvPetSpecies, tvPetBreed, tvPetAge, tvPetPrice;
+ private Button btnBack, btnEditPet, btnChangePhoto;
+ private ImageView imgPet;
+ private Uri photoUri;
+
+ // launchers for camera and gallery
+ private ActivityResultLauncher galleryLauncher;
+ private ActivityResultLauncher cameraLauncher;
+ private ActivityResultLauncher permissionLauncher;
+
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ // Launcher to open gallery to select image
+ galleryLauncher = registerForActivityResult(
+ new ActivityResultContracts.StartActivityForResult(),
+ result -> {
+ if (result.getResultCode() == Activity.RESULT_OK && result.getData() != null) {
+ Uri selectedImage = result.getData().getData();
+ imgPet.setImageURI(selectedImage);
+ // TODO: SAVE CHANGED PHOTO TO DATABASE
+ }
+ }
+ );
+
+ // Launcher for camera to open and capture image
+ cameraLauncher = registerForActivityResult(
+ new ActivityResultContracts.TakePicture(),
+ success -> {
+ if (success) {
+ imgPet.setImageURI(null);
+ imgPet.setImageURI(photoUri);
+ // TODO: SAVE CHANGED PHOTO TO DATABASE
+ }
+ }
+ );
+
+ // Launcher to request camera permission
+ permissionLauncher = registerForActivityResult(
+ new ActivityResultContracts.RequestPermission(),
+ granted -> {
+ if (granted) {
+ launchCamera();
+ } else {
+ new AlertDialog.Builder(requireContext())
+ .setTitle("Permission Required")
+ .setMessage("Please grant camera permission to use this feature")
+ .setPositiveButton("Open Settings", (dialog, which) -> {
+ Intent intent = new Intent(android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
+ intent.setData(Uri.fromParts("package", requireContext().getPackageName(), null));
+ startActivity(intent);
+ })
+ .setNegativeButton("Cancel", null)
+ .show();
+ }
+ }
+ );
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ View view = inflater.inflate(R.layout.fragment_pet_profile, container, false);
+
+ // Initialize views
+ tvPetName = view.findViewById(R.id.tvPetName);
+ tvPetSpecies = view.findViewById(R.id.tvPetSpecies);
+ tvPetBreed = view.findViewById(R.id.tvPetBreed);
+ tvPetAge = view.findViewById(R.id.tvPetAge);
+ tvPetPrice = view.findViewById(R.id.tvPetPrice);
+ btnBack = view.findViewById(R.id.btnBack);
+ btnEditPet = view.findViewById(R.id.btnEditPet);
+ btnChangePhoto = view.findViewById(R.id.btnChangePhoto);
+ imgPet = view.findViewById(R.id.imgPet);
+
+
+ // Set pet details to display
+ if (getArguments() != null) {
+ tvPetName.setText(getArguments().getString("petName"));
+ tvPetSpecies.setText(getArguments().getString("petSpecies"));
+ tvPetBreed.setText(getArguments().getString("petBreed"));
+ tvPetAge.setText(String.format(Locale.getDefault(), "%d yr(s)", getArguments().getInt("petAge")));
+ tvPetPrice.setText(String.format(Locale.getDefault(), "$%.2f", getArguments().getDouble("petPrice")));
+ }
+
+ //set button click listeners
+ btnBack.setOnClickListener(v -> {
+ //get the list fragment and pop the back stack to return to the previous view (PetFragment)
+ ListFragment listFragment = (ListFragment) getParentFragment();
+ if (listFragment != null) {
+ listFragment.getChildFragmentManager().popBackStack();
+ }
+ });
+
+ //Make the edit button go to the pet detail view
+ btnEditPet.setOnClickListener(v -> {
+ if (getArguments() == null) return;
+
+ PetDetailFragment detailFragment = new PetDetailFragment();
+ //send the bundle to the pet detail fragment
+ detailFragment.setArguments(getArguments());
+
+ //get ListFragment to load the the detail view
+ ListFragment listFragment = (ListFragment) getParentFragment();
+ if (listFragment != null) {
+ listFragment.loadFragment(detailFragment);
+ }
+ });
+
+ //Make change photo button ask user to select a new photo
+ btnChangePhoto.setOnClickListener(v -> {
+ new AlertDialog.Builder(requireContext())
+ .setTitle("Change Pet Photo")
+ .setItems(new String[]{"Take Photo", "Choose from Gallery"}, (dialog, which) -> {
+ if (which == 0) {
+ // Choose Camera
+ //Checks if the user has granted the camera permission already
+ if (ContextCompat.checkSelfPermission(requireContext(), Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED) {
+ //if the permission is already granted then launch the camera
+ launchCamera();
+ } else {
+ //otherwise request the permission
+ permissionLauncher.launch(Manifest.permission.CAMERA);
+ }
+ } else {
+ Intent intent = new Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI);
+ galleryLauncher.launch(intent);
+ }
+ })
+ .show();
+ });
+
+ return view;
+ }
+
+ private void launchCamera() {
+ File photoFile = new File(requireContext().getCacheDir(), "pet_photo.jpg");
+ photoUri = FileProvider.getUriForFile(requireContext(), requireContext().getPackageName() + ".fileprovider", photoFile);
+ cameraLauncher.launch(photoUri);
+ }
+}
\ No newline at end of file
diff --git a/android/app/src/main/java/com/example/petstoremobile/models/Adoption.java b/android/app/src/main/java/com/example/petstoremobile/models/Adoption.java
new file mode 100644
index 00000000..e227bc9b
--- /dev/null
+++ b/android/app/src/main/java/com/example/petstoremobile/models/Adoption.java
@@ -0,0 +1,79 @@
+package com.example.petstoremobile.models;
+
+public class Adoption {
+ private int adoptionId;
+ private String adopterName;
+ private String adopterEmail;
+ private String adopterPhone;
+ private String petName;
+ private String adoptionDate;
+ private String status;
+
+ // Constructor
+ public Adoption(int adoptionId, String adopterName, String adopterEmail, String adopterPhone, String petName, String adoptionDate, String status) {
+ this.adoptionId = adoptionId;
+ this.adopterName = adopterName;
+ this.adopterEmail = adopterEmail;
+ this.adopterPhone = adopterPhone;
+ this.petName = petName;
+ this.adoptionDate = adoptionDate;
+ this.status = status;
+ }
+
+ // Getters and setters
+ public int getAdoptionId() {
+ return adoptionId;
+ }
+
+ public void setAdoptionId(int adoptionId) {
+ this.adoptionId = adoptionId;
+ }
+
+ public String getAdopterName() {
+ return adopterName;
+ }
+
+ public void setAdopterName(String adopterName) {
+ this.adopterName = adopterName;
+ }
+
+ public String getAdopterEmail() {
+ return adopterEmail;
+ }
+
+ public void setAdopterEmail(String adopterEmail) {
+ this.adopterEmail = adopterEmail;
+ }
+
+ public String getAdopterPhone() {
+ return adopterPhone;
+ }
+
+ public void setAdopterPhone(String adopterPhone) {
+ this.adopterPhone = adopterPhone;
+ }
+
+ public String getPetName() {
+ return petName;
+ }
+
+ public void setPetName(String petName) {
+ this.petName = petName;
+ }
+
+ public String getAdoptionDate() {
+ return adoptionDate;
+ }
+
+ public void setAdoptionDate(String adoptionDate) {
+ this.adoptionDate = adoptionDate;
+ }
+
+ public String getStatus() {
+ return status;
+ }
+
+ public void setStatus(String status) {
+ this.status = status;
+ }
+}
diff --git a/android/app/src/main/java/com/example/petstoremobile/models/Appointment.java b/android/app/src/main/java/com/example/petstoremobile/models/Appointment.java
new file mode 100644
index 00000000..38e8da10
--- /dev/null
+++ b/android/app/src/main/java/com/example/petstoremobile/models/Appointment.java
@@ -0,0 +1,76 @@
+package com.example.petstoremobile.models;
+
+
+public class Appointment {
+ private int appointmentId;
+ private String customerName;
+ private String petName;
+ private String serviceType;
+ private String appointmentDate;
+ private String appointmentTime;
+ private String status;
+
+ // Constructor
+ public Appointment(int appointmentId, String customerName, String petName, String serviceType, String appointmentDate, String appointmentTime, String status) {
+ this.appointmentId = appointmentId;
+ this.customerName = customerName;
+ this.petName = petName;
+ this.serviceType = serviceType;
+ this.appointmentDate = appointmentDate;
+ this.appointmentTime = appointmentTime;
+ this.status = status;
+ }
+
+ // Getters and setters
+ public int getAppointmentId() {
+ return appointmentId;
+ }
+
+ public String getCustomerName() {
+ return customerName;
+ }
+
+ public void setCustomerName(String customerName) {
+ this.customerName = customerName;
+ }
+
+ public String getPetName() {
+ return petName;
+ }
+
+ public void setPetName(String petName) {
+ this.petName = petName;
+ }
+
+ public String getServiceType() {
+ return serviceType;
+ }
+
+ public void setServiceType(String serviceType) {
+ this.serviceType = serviceType;
+ }
+
+ public String getAppointmentDate() {
+ return appointmentDate;
+ }
+
+ public void setAppointmentDate(String appointmentDate) {
+ this.appointmentDate = appointmentDate;
+ }
+
+ public String getAppointmentTime() {
+ return appointmentTime;
+ }
+
+ public void setAppointmentTime(String appointmentTime) {
+ this.appointmentTime = appointmentTime;
+ }
+
+ public String getStatus() {
+ return status;
+ }
+
+ public void setStatus(String status) {
+ this.status = status;
+ }
+}
diff --git a/android/app/src/main/java/com/example/petstoremobile/models/Chat.java b/android/app/src/main/java/com/example/petstoremobile/models/Chat.java
new file mode 100644
index 00000000..f3a9a4eb
--- /dev/null
+++ b/android/app/src/main/java/com/example/petstoremobile/models/Chat.java
@@ -0,0 +1,37 @@
+package com.example.petstoremobile.models;
+
+public class Chat {
+ private String chatId;
+ private String customerName;
+ private String lastMessage;
+ private Long customerId;
+ private Long staffId;
+
+ public Chat(String chatId, String customerName, String lastMessage, Long customerId, Long staffId) {
+ this.chatId = chatId;
+ this.customerName = customerName;
+ this.lastMessage = lastMessage;
+ this.customerId = customerId;
+ this.staffId = staffId;
+ }
+
+ public String getChatId() {
+ return chatId;
+ }
+
+ public String getCustomerName() {
+ return customerName;
+ }
+
+ public String getLastMessage() {
+ return lastMessage;
+ }
+
+ public Long getCustomerId() {
+ return customerId;
+ }
+
+ public Long getStaffId() {
+ return staffId;
+ }
+}
\ No newline at end of file
diff --git a/android/app/src/main/java/com/example/petstoremobile/models/Inventory.java b/android/app/src/main/java/com/example/petstoremobile/models/Inventory.java
new file mode 100644
index 00000000..7aacd2df
--- /dev/null
+++ b/android/app/src/main/java/com/example/petstoremobile/models/Inventory.java
@@ -0,0 +1,66 @@
+package com.example.petstoremobile.models;
+
+
+public class Inventory {
+ private int inventoryId;
+ private String itemName;
+ private String category;
+ private int quantity;
+ private double unitPrice;
+ private String supplier;
+
+ // Constructor
+ public Inventory(int inventoryId, String itemName, String category, int quantity, double unitPrice, String supplier) {
+ this.inventoryId = inventoryId;
+ this.itemName = itemName;
+ this.category = category;
+ this.quantity = quantity;
+ this.unitPrice = unitPrice;
+ this.supplier = supplier;
+ }
+
+ // Getters and setters
+ public int getInventoryId() {
+ return inventoryId;
+ }
+
+ public String getItemName() {
+ return itemName;
+ }
+
+ public void setItemName(String itemName) {
+ this.itemName = itemName;
+ }
+
+ public String getCategory() {
+ return category;
+ }
+
+ public void setCategory(String category) {
+ this.category = category;
+ }
+
+ public int getQuantity() {
+ return quantity;
+ }
+
+ public void setQuantity(int quantity) {
+ this.quantity = quantity;
+ }
+
+ public double getUnitPrice() {
+ return unitPrice;
+ }
+
+ public void setUnitPrice(double unitPrice) {
+ this.unitPrice = unitPrice;
+ }
+
+ public String getSupplier() {
+ return supplier;
+ }
+
+ public void setSupplier(String supplier) {
+ this.supplier = supplier;
+ }
+}
diff --git a/android/app/src/main/java/com/example/petstoremobile/models/Message.java b/android/app/src/main/java/com/example/petstoremobile/models/Message.java
new file mode 100644
index 00000000..18ec549a
--- /dev/null
+++ b/android/app/src/main/java/com/example/petstoremobile/models/Message.java
@@ -0,0 +1,36 @@
+package com.example.petstoremobile.models;
+
+public class Message {
+ private Long id;
+ private Long conversationId;
+ private Long senderId;
+ private String content;
+ private String timestamp;
+ private Boolean isRead;
+
+ public Message() {}
+
+ public Message(Long conversationId, Long senderId, String content) {
+ this.conversationId = conversationId;
+ this.senderId = senderId;
+ this.content = content;
+ }
+
+ public Long getId() { return id; }
+ public void setId(Long id) { this.id = id; }
+
+ public Long getConversationId() { return conversationId; }
+ public void setConversationId(Long conversationId) { this.conversationId = conversationId; }
+
+ public Long getSenderId() { return senderId; }
+ public void setSenderId(Long senderId) { this.senderId = senderId; }
+
+ public String getContent() { return content; }
+ public void setContent(String content) { this.content = content; }
+
+ public String getTimestamp() { return timestamp; }
+ public void setTimestamp(String timestamp) { this.timestamp = timestamp; }
+
+ public Boolean getIsRead() { return isRead; }
+ public void setIsRead(Boolean isRead) { this.isRead = isRead; }
+}
\ No newline at end of file
diff --git a/android/app/src/main/java/com/example/petstoremobile/models/Product.java b/android/app/src/main/java/com/example/petstoremobile/models/Product.java
new file mode 100644
index 00000000..90a56eab
--- /dev/null
+++ b/android/app/src/main/java/com/example/petstoremobile/models/Product.java
@@ -0,0 +1,66 @@
+package com.example.petstoremobile.models;
+
+
+public class Product {
+ private int productId;
+ private String productName;
+ private String productDesc;
+ private String category;
+ private double productPrice;
+ private int stockQuantity;
+
+ // Constructor
+ public Product(int productId, String productName, String productDesc, String category, double productPrice, int stockQuantity) {
+ this.productId = productId;
+ this.productName = productName;
+ this.productDesc = productDesc;
+ this.category = category;
+ this.productPrice = productPrice;
+ this.stockQuantity = stockQuantity;
+ }
+
+ // Getters and setters
+ public int getProductId() {
+ return productId;
+ }
+
+ public String getProductName() {
+ return productName;
+ }
+
+ public void setProductName(String productName) {
+ this.productName = productName;
+ }
+
+ public String getProductDesc() {
+ return productDesc;
+ }
+
+ public void setProductDesc(String productDesc) {
+ this.productDesc = productDesc;
+ }
+
+ public String getCategory() {
+ return category;
+ }
+
+ public void setCategory(String category) {
+ this.category = category;
+ }
+
+ public double getProductPrice() {
+ return productPrice;
+ }
+
+ public void setProductPrice(double productPrice) {
+ this.productPrice = productPrice;
+ }
+
+ public int getStockQuantity() {
+ return stockQuantity;
+ }
+
+ public void setStockQuantity(int stockQuantity) {
+ this.stockQuantity = stockQuantity;
+ }
+}
diff --git a/android/app/src/main/java/com/example/petstoremobile/utils/ActivityLogger.java b/android/app/src/main/java/com/example/petstoremobile/utils/ActivityLogger.java
new file mode 100644
index 00000000..f474508f
--- /dev/null
+++ b/android/app/src/main/java/com/example/petstoremobile/utils/ActivityLogger.java
@@ -0,0 +1,45 @@
+package com.example.petstoremobile.utils;
+
+import android.content.Context;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Locale;
+
+public class ActivityLogger {
+
+ private static final String LOG_FILE = "log.txt";
+
+ // Logs a general message with a timestamp
+ public static void log(Context context, String message) {
+ String timestamp = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault()).format(new Date());
+ String entry = timestamp + " | INFO | " + message + "\n";
+ writeToFile(context, entry);
+ }
+
+ // Logs a database change (ADD, UPDATE, DELETE) with entity type and ID
+ public static void logChange(Context context, String entity, String action, int id) {
+ String timestamp = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault()).format(new Date());
+ String entry = timestamp + " | DB CHANGE | " + action + " " + entity + " ID: " + id + "\n";
+ writeToFile(context, entry);
+ }
+
+ // Logs an exception with location info
+ public static void logException(Context context, String location, Exception e) {
+ String timestamp = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault()).format(new Date());
+ String entry = timestamp + " | ERROR | " + location + ": " + e.getMessage() + "\n";
+ writeToFile(context, entry);
+ }
+
+ // Writes the log entry to log.txt in internal storage
+ private static void writeToFile(Context context, String entry) {
+ try {
+ FileWriter fw = new FileWriter(context.getFilesDir() + "/" + LOG_FILE, true);
+ fw.write(entry);
+ fw.close();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+}
diff --git a/android/app/src/main/java/com/example/petstoremobile/utils/InputValidator.java b/android/app/src/main/java/com/example/petstoremobile/utils/InputValidator.java
new file mode 100644
index 00000000..8182912b
--- /dev/null
+++ b/android/app/src/main/java/com/example/petstoremobile/utils/InputValidator.java
@@ -0,0 +1,97 @@
+package com.example.petstoremobile.utils;
+
+import android.widget.EditText;
+
+public class InputValidator {
+
+ // Checks if an EditText field is not empty
+ public static boolean isNotEmpty(EditText field, String fieldName) {
+ if (field.getText().toString().trim().isEmpty()) {
+ field.setError(fieldName + " is required");
+ field.requestFocus();
+ return false;
+ }
+ return true;
+ }
+
+ // Checks if the value is a positive integer
+ public static boolean isPositiveInteger(EditText field, String fieldName) {
+ String value = field.getText().toString().trim();
+ try {
+ int num = Integer.parseInt(value);
+ if (num < 0) {
+ field.setError(fieldName + " must be a positive number");
+ field.requestFocus();
+ return false;
+ }
+ return true;
+ } catch (NumberFormatException e) {
+ field.setError(fieldName + " must be a whole number");
+ field.requestFocus();
+ return false;
+ }
+ }
+
+ // Checks if the value is a positive decimal number
+ public static boolean isPositiveDecimal(EditText field, String fieldName) {
+ String value = field.getText().toString().trim();
+ try {
+ double num = Double.parseDouble(value);
+ if (num < 0) {
+ field.setError(fieldName + " must be a positive number");
+ field.requestFocus();
+ return false;
+ }
+ return true;
+ } catch (NumberFormatException e) {
+ field.setError(fieldName + " must be a number");
+ field.requestFocus();
+ return false;
+ }
+ }
+
+ // Checks if the email address is valid
+ public static boolean isValidEmail(EditText field) {
+ String email = field.getText().toString().trim();
+ if (email.isEmpty() || !android.util.Patterns.EMAIL_ADDRESS.matcher(email).matches()) {
+ field.setError("Enter a valid email address");
+ field.requestFocus();
+ return false;
+ }
+ return true;
+ }
+
+ // Checks if the phone number is valid (digits, spaces, dashes, brackets allowed)
+ public static boolean isValidPhone(EditText field) {
+ String phone = field.getText().toString().trim();
+ if (phone.isEmpty() || !phone.matches("[0-9\\-\\s\\(\\)\\+]+")) {
+ field.setError("Enter a valid phone number");
+ field.requestFocus();
+ return false;
+ }
+ return true;
+ }
+
+ // Checks if the date is in YYYY-MM-DD format
+ public static boolean isValidDate(EditText field) {
+ String date = field.getText().toString().trim();
+ if (date.isEmpty() || !date.matches("\\d{4}-\\d{2}-\\d{2}")) {
+ field.setError("Date must be in YYYY-MM-DD format");
+ field.requestFocus();
+ return false;
+ }
+ return true;
+ }
+
+ // Checks if the time format is valid (e.g. 10:00 AM)
+ public static boolean isValidTime(EditText field) {
+ String time = field.getText().toString().trim();
+ if (time.isEmpty() || !time.matches("\\d{1,2}:\\d{2}\\s?(AM|PM|am|pm)?")) {
+ field.setError("Enter a valid time (e.g. 10:00 AM)");
+ field.requestFocus();
+ return false;
+ }
+ return true;
+ }
+}
+
diff --git a/android/app/src/main/java/com/example/petstoremobile/websocket/StompChatManager.java b/android/app/src/main/java/com/example/petstoremobile/websocket/StompChatManager.java
new file mode 100644
index 00000000..af4264db
--- /dev/null
+++ b/android/app/src/main/java/com/example/petstoremobile/websocket/StompChatManager.java
@@ -0,0 +1,295 @@
+package com.example.petstoremobile.websocket;
+
+import android.os.Handler;
+import android.os.Looper;
+import android.util.Log;
+import com.example.petstoremobile.api.RetrofitClient;
+import com.example.petstoremobile.dtos.ConversationDTO;
+import com.example.petstoremobile.dtos.MessageDTO;
+import com.google.gson.Gson;
+import java.util.HashMap;
+import java.util.Collections;
+import java.util.Locale;
+import java.util.Map;
+import io.reactivex.android.schedulers.AndroidSchedulers;
+import io.reactivex.disposables.CompositeDisposable;
+import io.reactivex.disposables.Disposable;
+import io.reactivex.schedulers.Schedulers;
+import ua.naiksoftware.stomp.Stomp;
+import ua.naiksoftware.stomp.StompClient;
+import ua.naiksoftware.stomp.dto.StompHeader;
+
+//Used to handle the websocket connection for the chat
+public class StompChatManager {
+
+ private static final String TAG = "StompChatManager";
+
+ //Interface for when a message is received
+ public interface MessageListener {
+ void onMessageReceived(MessageDTO message);
+ }
+
+ //Interface for when a conversation is created or updated
+ public interface ConversationListener {
+ void onConversationUpdated(ConversationDTO conversation);
+ }
+
+ //Interface for when the websocket connection is opened, closed, or has an error
+ public interface ConnectionListener {
+ void onSocketOpened();
+ void onSocketClosed();
+ void onSocketError();
+ }
+
+ private StompClient stompClient;
+ private final CompositeDisposable compositeDisposable = new CompositeDisposable();
+ private Disposable topicDisposable;
+ private Disposable conversationsDisposable;
+ private Disposable userConversationsDisposable;
+ private Disposable errorQueueDisposable;
+ private final Gson gson = new Gson();
+ private final Handler reconnectHandler = new Handler(Looper.getMainLooper());
+ private MessageListener messageListener;
+ private ConversationListener conversationListener;
+ private ConnectionListener connectionListener;
+ private final String authToken;
+ private final String role;
+ private boolean isConnected;
+ private boolean isConnecting;
+ private boolean manualDisconnect;
+ private Long pendingConversationId;
+
+ public StompChatManager(String authToken, String role) {
+ this.authToken = authToken;
+ this.role = role == null ? "" : role.trim().toUpperCase(Locale.ROOT);
+ }
+
+ public void setMessageListener(MessageListener listener) {
+ this.messageListener = listener;
+ }
+
+ public void setConversationListener(ConversationListener listener) {
+ this.conversationListener = listener;
+ }
+
+ public void setConnectionListener(ConnectionListener listener) {
+ this.connectionListener = listener;
+ }
+
+ // Set up a stomp connection
+ public void connect() {
+ if (authToken == null || authToken.isBlank()) {
+ Log.e(TAG, "Cannot connect websocket without token");
+ return;
+ }
+ if (isConnected || isConnecting) {
+ return;
+ }
+ manualDisconnect = false;
+ isConnecting = true;
+ reconnectHandler.removeCallbacksAndMessages(null);
+ String webSocketUrl = buildWebSocketUrl();
+
+ Map headers = new HashMap<>();
+ headers.put("Authorization", "Bearer " + authToken);
+
+ stompClient = Stomp.over(Stomp.ConnectionProvider.OKHTTP, webSocketUrl, headers);
+
+ compositeDisposable.add(
+ stompClient.lifecycle()
+ .subscribeOn(Schedulers.io())
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe(event -> {
+ switch (event.getType()) {
+ case OPENED:
+ isConnected = true;
+ isConnecting = false;
+ Log.d(TAG, "WebSocket opened");
+ if (connectionListener != null) {
+ connectionListener.onSocketOpened();
+ }
+ subscribeToErrorQueue();
+ subscribeToConversationFeeds();
+ if (pendingConversationId != null) {
+ subscribeToTopic(pendingConversationId);
+ }
+ break;
+ case CLOSED:
+ isConnected = false;
+ isConnecting = false;
+ Log.d(TAG, "WebSocket closed");
+ if (connectionListener != null) {
+ connectionListener.onSocketClosed();
+ }
+ scheduleReconnect();
+ break;
+ case ERROR:
+ isConnected = false;
+ isConnecting = false;
+ Log.e(TAG, "WebSocket error: " + event.getException());
+ if (connectionListener != null) {
+ connectionListener.onSocketError();
+ }
+ scheduleReconnect();
+ break;
+ }
+ })
+ );
+
+ stompClient.connect(Collections.singletonList(
+ new StompHeader("Authorization", "Bearer " + authToken)
+ ));
+ }
+
+ // Subscribes to updates for a specific conversation
+ public void subscribeToConversation(Long conversationId) {
+ pendingConversationId = conversationId;
+ if (!isConnected || stompClient == null) {
+ Log.d(TAG, "Delaying subscription until socket opens for conversation " + conversationId);
+ connect();
+ return;
+ }
+ subscribeToTopic(conversationId);
+ }
+
+ // Clears the current conversation subscription
+ public void clearConversationSubscription() {
+ pendingConversationId = null;
+ if (topicDisposable != null && !topicDisposable.isDisposed()) {
+ topicDisposable.dispose();
+ topicDisposable = null;
+ }
+ }
+
+ //helper function to subscribe to a specific conversation topic
+ private void subscribeToTopic(Long conversationId) {
+ if (topicDisposable != null && !topicDisposable.isDisposed()) {
+ topicDisposable.dispose();
+ }
+
+ String topic = "/topic/chat/conversations/" + conversationId;
+ Log.d(TAG, "Subscribing to topic " + topic);
+
+ topicDisposable = stompClient.topic(topic)
+ .subscribeOn(Schedulers.io())
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe(
+ stompMessage -> {
+ MessageDTO message = gson.fromJson(
+ stompMessage.getPayload(), MessageDTO.class);
+ if (messageListener != null) {
+ messageListener.onMessageReceived(message);
+ }
+ },
+ throwable -> Log.e(TAG, "Topic error", throwable)
+ );
+
+ compositeDisposable.add(topicDisposable);
+ }
+
+ // Listens for conversation updates and refresh the chat list
+ private void subscribeToConversationFeeds() {
+ if (conversationsDisposable != null && !conversationsDisposable.isDisposed()) {
+ conversationsDisposable.dispose();
+ }
+ if (userConversationsDisposable != null && !userConversationsDisposable.isDisposed()) {
+ userConversationsDisposable.dispose();
+ }
+
+ userConversationsDisposable = stompClient.topic("/user/queue/chat/conversations")
+ .subscribeOn(Schedulers.io())
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe(
+ stompMessage -> {
+ Log.d(TAG, "Queue conversation update: " + stompMessage.getPayload());
+ ConversationDTO conversation = gson.fromJson(
+ stompMessage.getPayload(), ConversationDTO.class);
+ if (conversationListener != null) {
+ conversationListener.onConversationUpdated(conversation);
+ }
+ },
+ throwable -> Log.e(TAG, "Conversation queue error", throwable)
+ );
+
+ compositeDisposable.add(userConversationsDisposable);
+
+ if (isCustomer()) {
+ return;
+ }
+
+ conversationsDisposable = stompClient.topic("/topic/chat/conversations")
+ .subscribeOn(Schedulers.io())
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe(
+ stompMessage -> {
+ Log.d(TAG, "Broadcast conversation update: " + stompMessage.getPayload());
+ ConversationDTO conversation = gson.fromJson(
+ stompMessage.getPayload(), ConversationDTO.class);
+ if (conversationListener != null) {
+ conversationListener.onConversationUpdated(conversation);
+ }
+ },
+ throwable -> Log.e(TAG, "Conversation topic error", throwable)
+ );
+
+ compositeDisposable.add(conversationsDisposable);
+ }
+
+ // Log any error from stomp
+ private void subscribeToErrorQueue() {
+ if (errorQueueDisposable != null && !errorQueueDisposable.isDisposed()) {
+ errorQueueDisposable.dispose();
+ }
+
+ errorQueueDisposable = stompClient.topic("/user/queue/chat/errors")
+ .subscribeOn(Schedulers.io())
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe(
+ stompMessage -> Log.e(TAG, "WebSocket queue error payload: " + stompMessage.getPayload()),
+ throwable -> Log.e(TAG, "WebSocket error queue subscription failed", throwable)
+ );
+
+ compositeDisposable.add(errorQueueDisposable);
+ }
+
+ // Disconnects the stomp connection
+ public void disconnect() {
+ manualDisconnect = true;
+ isConnected = false;
+ isConnecting = false;
+ pendingConversationId = null;
+ reconnectHandler.removeCallbacksAndMessages(null);
+ compositeDisposable.clear();
+ if (stompClient != null) {
+ stompClient.disconnect();
+ }
+ }
+
+ // Make the URL for the websocket connection
+ private String buildWebSocketUrl() {
+ String baseUrl = RetrofitClient.BASE_URL.endsWith("/")
+ ? RetrofitClient.BASE_URL.substring(0, RetrofitClient.BASE_URL.length() - 1)
+ : RetrofitClient.BASE_URL;
+ if (baseUrl.startsWith("https://")) {
+ return "wss://" + baseUrl.substring("https://".length()) + "/ws/chat";
+ }
+ if (baseUrl.startsWith("http://")) {
+ return "ws://" + baseUrl.substring("http://".length()) + "/ws/chat";
+ }
+ return baseUrl + "/ws/chat";
+ }
+
+ // Helper to check if the current user is a customer
+ private boolean isCustomer() {
+ return "CUSTOMER".equals(role);
+ }
+
+ // if connection drops, try to reconnect after 1 second
+ private void scheduleReconnect() {
+ if (manualDisconnect) {
+ return;
+ }
+ reconnectHandler.removeCallbacksAndMessages(null);
+ reconnectHandler.postDelayed(this::connect, 1000);
+ }
+}
diff --git a/android/app/src/main/res/color/bottom_nav_colors.xml b/android/app/src/main/res/color/bottom_nav_colors.xml
new file mode 100644
index 00000000..c170f0da
--- /dev/null
+++ b/android/app/src/main/res/color/bottom_nav_colors.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/android/app/src/main/res/drawable/baseline_menu_36.xml b/android/app/src/main/res/drawable/baseline_menu_36.xml
new file mode 100644
index 00000000..f25be2a9
--- /dev/null
+++ b/android/app/src/main/res/drawable/baseline_menu_36.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/android/app/src/main/res/drawable/bg_message_received.xml b/android/app/src/main/res/drawable/bg_message_received.xml
new file mode 100644
index 00000000..7cc459d5
--- /dev/null
+++ b/android/app/src/main/res/drawable/bg_message_received.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/android/app/src/main/res/drawable/bg_message_sent.xml b/android/app/src/main/res/drawable/bg_message_sent.xml
new file mode 100644
index 00000000..a2013587
--- /dev/null
+++ b/android/app/src/main/res/drawable/bg_message_sent.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/android/app/src/main/res/drawable/circle_image.xml b/android/app/src/main/res/drawable/circle_image.xml
new file mode 100644
index 00000000..8635e87b
--- /dev/null
+++ b/android/app/src/main/res/drawable/circle_image.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/android/app/src/main/res/drawable/ic_launcher_background.xml b/android/app/src/main/res/drawable/ic_launcher_background.xml
new file mode 100644
index 00000000..07d5da9c
--- /dev/null
+++ b/android/app/src/main/res/drawable/ic_launcher_background.xml
@@ -0,0 +1,170 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/android/app/src/main/res/drawable/ic_launcher_foreground.xml b/android/app/src/main/res/drawable/ic_launcher_foreground.xml
new file mode 100644
index 00000000..2b068d11
--- /dev/null
+++ b/android/app/src/main/res/drawable/ic_launcher_foreground.xml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/android/app/src/main/res/drawable/petstore_logo.png b/android/app/src/main/res/drawable/petstore_logo.png
new file mode 100644
index 00000000..131282c1
Binary files /dev/null and b/android/app/src/main/res/drawable/petstore_logo.png differ
diff --git a/android/app/src/main/res/drawable/placeholder.png b/android/app/src/main/res/drawable/placeholder.png
new file mode 100644
index 00000000..e7bdbefd
Binary files /dev/null and b/android/app/src/main/res/drawable/placeholder.png differ
diff --git a/android/app/src/main/res/drawable/rounded_card.xml b/android/app/src/main/res/drawable/rounded_card.xml
new file mode 100644
index 00000000..125ee629
--- /dev/null
+++ b/android/app/src/main/res/drawable/rounded_card.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/android/app/src/main/res/layout/activity_home.xml b/android/app/src/main/res/layout/activity_home.xml
new file mode 100644
index 00000000..c7b9f3ea
--- /dev/null
+++ b/android/app/src/main/res/layout/activity_home.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/android/app/src/main/res/layout/activity_main.xml b/android/app/src/main/res/layout/activity_main.xml
new file mode 100644
index 00000000..9322e775
--- /dev/null
+++ b/android/app/src/main/res/layout/activity_main.xml
@@ -0,0 +1,113 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/android/app/src/main/res/layout/fragment_adoption.xml b/android/app/src/main/res/layout/fragment_adoption.xml
new file mode 100644
index 00000000..705b6fa2
--- /dev/null
+++ b/android/app/src/main/res/layout/fragment_adoption.xml
@@ -0,0 +1,80 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/android/app/src/main/res/layout/fragment_adoption_detail.xml b/android/app/src/main/res/layout/fragment_adoption_detail.xml
new file mode 100644
index 00000000..dc6ba4c6
--- /dev/null
+++ b/android/app/src/main/res/layout/fragment_adoption_detail.xml
@@ -0,0 +1,201 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/android/app/src/main/res/layout/fragment_appointment.xml b/android/app/src/main/res/layout/fragment_appointment.xml
new file mode 100644
index 00000000..a8da443e
--- /dev/null
+++ b/android/app/src/main/res/layout/fragment_appointment.xml
@@ -0,0 +1,80 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/android/app/src/main/res/layout/fragment_appointment_detail.xml b/android/app/src/main/res/layout/fragment_appointment_detail.xml
new file mode 100644
index 00000000..3396f459
--- /dev/null
+++ b/android/app/src/main/res/layout/fragment_appointment_detail.xml
@@ -0,0 +1,197 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/android/app/src/main/res/layout/fragment_chat.xml b/android/app/src/main/res/layout/fragment_chat.xml
new file mode 100644
index 00000000..e56e283f
--- /dev/null
+++ b/android/app/src/main/res/layout/fragment_chat.xml
@@ -0,0 +1,102 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/android/app/src/main/res/layout/fragment_inventory.xml b/android/app/src/main/res/layout/fragment_inventory.xml
new file mode 100644
index 00000000..7773c8da
--- /dev/null
+++ b/android/app/src/main/res/layout/fragment_inventory.xml
@@ -0,0 +1,80 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/android/app/src/main/res/layout/fragment_inventory_detail.xml b/android/app/src/main/res/layout/fragment_inventory_detail.xml
new file mode 100644
index 00000000..7b2b995c
--- /dev/null
+++ b/android/app/src/main/res/layout/fragment_inventory_detail.xml
@@ -0,0 +1,182 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/android/app/src/main/res/layout/fragment_list.xml b/android/app/src/main/res/layout/fragment_list.xml
new file mode 100644
index 00000000..95d17697
--- /dev/null
+++ b/android/app/src/main/res/layout/fragment_list.xml
@@ -0,0 +1,214 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/android/app/src/main/res/layout/fragment_pet.xml b/android/app/src/main/res/layout/fragment_pet.xml
new file mode 100644
index 00000000..2ad2d22d
--- /dev/null
+++ b/android/app/src/main/res/layout/fragment_pet.xml
@@ -0,0 +1,80 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/android/app/src/main/res/layout/fragment_pet_detail.xml b/android/app/src/main/res/layout/fragment_pet_detail.xml
new file mode 100644
index 00000000..48558349
--- /dev/null
+++ b/android/app/src/main/res/layout/fragment_pet_detail.xml
@@ -0,0 +1,201 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/android/app/src/main/res/layout/fragment_pet_profile.xml b/android/app/src/main/res/layout/fragment_pet_profile.xml
new file mode 100644
index 00000000..b750bd29
--- /dev/null
+++ b/android/app/src/main/res/layout/fragment_pet_profile.xml
@@ -0,0 +1,244 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/android/app/src/main/res/layout/fragment_product.xml b/android/app/src/main/res/layout/fragment_product.xml
new file mode 100644
index 00000000..5afd0352
--- /dev/null
+++ b/android/app/src/main/res/layout/fragment_product.xml
@@ -0,0 +1,80 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/android/app/src/main/res/layout/fragment_product_detail.xml b/android/app/src/main/res/layout/fragment_product_detail.xml
new file mode 100644
index 00000000..4a41efcc
--- /dev/null
+++ b/android/app/src/main/res/layout/fragment_product_detail.xml
@@ -0,0 +1,183 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/android/app/src/main/res/layout/fragment_profile.xml b/android/app/src/main/res/layout/fragment_profile.xml
new file mode 100644
index 00000000..ad7279ac
--- /dev/null
+++ b/android/app/src/main/res/layout/fragment_profile.xml
@@ -0,0 +1,200 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/android/app/src/main/res/layout/fragment_service.xml b/android/app/src/main/res/layout/fragment_service.xml
new file mode 100644
index 00000000..a74f261a
--- /dev/null
+++ b/android/app/src/main/res/layout/fragment_service.xml
@@ -0,0 +1,80 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/android/app/src/main/res/layout/fragment_service_detail.xml b/android/app/src/main/res/layout/fragment_service_detail.xml
new file mode 100644
index 00000000..0f021422
--- /dev/null
+++ b/android/app/src/main/res/layout/fragment_service_detail.xml
@@ -0,0 +1,171 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/android/app/src/main/res/layout/fragment_supplier.xml b/android/app/src/main/res/layout/fragment_supplier.xml
new file mode 100644
index 00000000..9036b6ea
--- /dev/null
+++ b/android/app/src/main/res/layout/fragment_supplier.xml
@@ -0,0 +1,80 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/android/app/src/main/res/layout/fragment_supplier_detail.xml b/android/app/src/main/res/layout/fragment_supplier_detail.xml
new file mode 100644
index 00000000..2a4a616c
--- /dev/null
+++ b/android/app/src/main/res/layout/fragment_supplier_detail.xml
@@ -0,0 +1,182 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/android/app/src/main/res/layout/item_adoption.xml b/android/app/src/main/res/layout/item_adoption.xml
new file mode 100644
index 00000000..9cf3cd58
--- /dev/null
+++ b/android/app/src/main/res/layout/item_adoption.xml
@@ -0,0 +1,64 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/android/app/src/main/res/layout/item_appointment.xml b/android/app/src/main/res/layout/item_appointment.xml
new file mode 100644
index 00000000..82e6ab2e
--- /dev/null
+++ b/android/app/src/main/res/layout/item_appointment.xml
@@ -0,0 +1,74 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/android/app/src/main/res/layout/item_chat.xml b/android/app/src/main/res/layout/item_chat.xml
new file mode 100644
index 00000000..11c02e69
--- /dev/null
+++ b/android/app/src/main/res/layout/item_chat.xml
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/android/app/src/main/res/layout/item_inventory.xml b/android/app/src/main/res/layout/item_inventory.xml
new file mode 100644
index 00000000..4858202a
--- /dev/null
+++ b/android/app/src/main/res/layout/item_inventory.xml
@@ -0,0 +1,81 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/android/app/src/main/res/layout/item_message_received.xml b/android/app/src/main/res/layout/item_message_received.xml
new file mode 100644
index 00000000..1320c88d
--- /dev/null
+++ b/android/app/src/main/res/layout/item_message_received.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/android/app/src/main/res/layout/item_message_sent.xml b/android/app/src/main/res/layout/item_message_sent.xml
new file mode 100644
index 00000000..ab99c033
--- /dev/null
+++ b/android/app/src/main/res/layout/item_message_sent.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/android/app/src/main/res/layout/item_pet.xml b/android/app/src/main/res/layout/item_pet.xml
new file mode 100644
index 00000000..d93bf881
--- /dev/null
+++ b/android/app/src/main/res/layout/item_pet.xml
@@ -0,0 +1,89 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/android/app/src/main/res/layout/item_product.xml b/android/app/src/main/res/layout/item_product.xml
new file mode 100644
index 00000000..86bf243d
--- /dev/null
+++ b/android/app/src/main/res/layout/item_product.xml
@@ -0,0 +1,84 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/android/app/src/main/res/layout/item_service.xml b/android/app/src/main/res/layout/item_service.xml
new file mode 100644
index 00000000..775203e5
--- /dev/null
+++ b/android/app/src/main/res/layout/item_service.xml
@@ -0,0 +1,67 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/android/app/src/main/res/layout/item_supplier.xml b/android/app/src/main/res/layout/item_supplier.xml
new file mode 100644
index 00000000..d2a07491
--- /dev/null
+++ b/android/app/src/main/res/layout/item_supplier.xml
@@ -0,0 +1,66 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/android/app/src/main/res/menu/bottom_nav_menu.xml b/android/app/src/main/res/menu/bottom_nav_menu.xml
new file mode 100644
index 00000000..a6dccb5c
--- /dev/null
+++ b/android/app/src/main/res/menu/bottom_nav_menu.xml
@@ -0,0 +1,19 @@
+
+
\ No newline at end of file
diff --git a/android/app/src/main/res/menu/menu_main.xml b/android/app/src/main/res/menu/menu_main.xml
new file mode 100644
index 00000000..f5ee13b0
--- /dev/null
+++ b/android/app/src/main/res/menu/menu_main.xml
@@ -0,0 +1,32 @@
+
+
\ No newline at end of file
diff --git a/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100644
index 00000000..6f3b755b
--- /dev/null
+++ b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
new file mode 100644
index 00000000..6f3b755b
--- /dev/null
+++ b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/android/app/src/main/res/mipmap-hdpi/ic_launcher.webp
new file mode 100644
index 00000000..c209e78e
Binary files /dev/null and b/android/app/src/main/res/mipmap-hdpi/ic_launcher.webp differ
diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
new file mode 100644
index 00000000..b2dfe3d1
Binary files /dev/null and b/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ
diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/android/app/src/main/res/mipmap-mdpi/ic_launcher.webp
new file mode 100644
index 00000000..4f0f1d64
Binary files /dev/null and b/android/app/src/main/res/mipmap-mdpi/ic_launcher.webp differ
diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
new file mode 100644
index 00000000..62b611da
Binary files /dev/null and b/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ
diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp
new file mode 100644
index 00000000..948a3070
Binary files /dev/null and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp differ
diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
new file mode 100644
index 00000000..1b9a6956
Binary files /dev/null and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ
diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
new file mode 100644
index 00000000..28d4b77f
Binary files /dev/null and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ
diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
new file mode 100644
index 00000000..9287f508
Binary files /dev/null and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ
diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
new file mode 100644
index 00000000..aa7d6427
Binary files /dev/null and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ
diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
new file mode 100644
index 00000000..9126ae37
Binary files /dev/null and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ
diff --git a/android/app/src/main/res/values-night/themes.xml b/android/app/src/main/res/values-night/themes.xml
new file mode 100644
index 00000000..d198919e
--- /dev/null
+++ b/android/app/src/main/res/values-night/themes.xml
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/android/app/src/main/res/values/colors.xml b/android/app/src/main/res/values/colors.xml
new file mode 100644
index 00000000..24665e48
--- /dev/null
+++ b/android/app/src/main/res/values/colors.xml
@@ -0,0 +1,14 @@
+
+
+ #2C3E50
+ #E74C3C
+ #34495E
+ #F5F5F5
+ #FFFFFF
+ #BDC3C7
+ #2C3E50
+ #3D5166
+ #2ECC71
+ #E74C3C
+ #3498DB
+
\ No newline at end of file
diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml
new file mode 100644
index 00000000..ee132b12
--- /dev/null
+++ b/android/app/src/main/res/values/strings.xml
@@ -0,0 +1,5 @@
+
+ Leons Pet Store
+
+ Hello blank fragment
+
\ No newline at end of file
diff --git a/android/app/src/main/res/values/themes.xml b/android/app/src/main/res/values/themes.xml
new file mode 100644
index 00000000..5ac5ff57
--- /dev/null
+++ b/android/app/src/main/res/values/themes.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/android/app/src/main/res/xml/backup_rules.xml b/android/app/src/main/res/xml/backup_rules.xml
new file mode 100644
index 00000000..4df92558
--- /dev/null
+++ b/android/app/src/main/res/xml/backup_rules.xml
@@ -0,0 +1,13 @@
+
+
+
+
\ No newline at end of file
diff --git a/android/app/src/main/res/xml/data_extraction_rules.xml b/android/app/src/main/res/xml/data_extraction_rules.xml
new file mode 100644
index 00000000..9ee9997b
--- /dev/null
+++ b/android/app/src/main/res/xml/data_extraction_rules.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/android/app/src/main/res/xml/file_paths.xml b/android/app/src/main/res/xml/file_paths.xml
new file mode 100644
index 00000000..63a0f72f
--- /dev/null
+++ b/android/app/src/main/res/xml/file_paths.xml
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/android/app/src/main/res/xml/network_security_config.xml b/android/app/src/main/res/xml/network_security_config.xml
new file mode 100644
index 00000000..f18e1f04
--- /dev/null
+++ b/android/app/src/main/res/xml/network_security_config.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/android/build.gradle.kts b/android/build.gradle.kts
new file mode 100644
index 00000000..37562787
--- /dev/null
+++ b/android/build.gradle.kts
@@ -0,0 +1,4 @@
+// Top-level build file where you can add configuration options common to all sub-projects/modules.
+plugins {
+ alias(libs.plugins.android.application) apply false
+}
\ No newline at end of file
diff --git a/android/gradle.properties b/android/gradle.properties
new file mode 100644
index 00000000..4387edc2
--- /dev/null
+++ b/android/gradle.properties
@@ -0,0 +1,21 @@
+# Project-wide Gradle settings.
+# IDE (e.g. Android Studio) users:
+# Gradle settings configured through the IDE *will override*
+# any settings specified in this file.
+# For more details on how to configure your build environment visit
+# http://www.gradle.org/docs/current/userguide/build_environment.html
+# Specifies the JVM arguments used for the daemon process.
+# The setting is particularly useful for tweaking memory settings.
+org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
+# When configured, Gradle will run in incubating parallel mode.
+# This option should only be used with decoupled projects. For more details, visit
+# https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects
+# org.gradle.parallel=true
+# AndroidX package structure to make it clearer which packages are bundled with the
+# Android operating system, and which are packaged with your app's APK
+# https://developer.android.com/topic/libraries/support-library/androidx-rn
+android.useAndroidX=true
+# Enables namespacing of each library's R class so that its R class includes only the
+# resources declared in the library itself and none from the library's dependencies,
+# thereby reducing the size of the R class for that library
+android.nonTransitiveRClass=true
\ No newline at end of file
diff --git a/android/gradle/gradle-daemon-jvm.properties b/android/gradle/gradle-daemon-jvm.properties
new file mode 100644
index 00000000..7fc22e3b
--- /dev/null
+++ b/android/gradle/gradle-daemon-jvm.properties
@@ -0,0 +1,13 @@
+#This file is generated by updateDaemonJvm
+toolchainUrl.FREE_BSD.AARCH64=https\://api.foojay.io/disco/v3.0/ids/536afcd1dff540251f85e5d2c80458cf/redirect
+toolchainUrl.FREE_BSD.X86_64=https\://api.foojay.io/disco/v3.0/ids/398ffe3949748bfb1d5636f023d228fd/redirect
+toolchainUrl.LINUX.AARCH64=https\://api.foojay.io/disco/v3.0/ids/536afcd1dff540251f85e5d2c80458cf/redirect
+toolchainUrl.LINUX.X86_64=https\://api.foojay.io/disco/v3.0/ids/398ffe3949748bfb1d5636f023d228fd/redirect
+toolchainUrl.MAC_OS.AARCH64=https\://api.foojay.io/disco/v3.0/ids/10fc3bf1ee0001078a473afe6e43cfdb/redirect
+toolchainUrl.MAC_OS.X86_64=https\://api.foojay.io/disco/v3.0/ids/658299a896470fbb3103ba3a430ee227/redirect
+toolchainUrl.UNIX.AARCH64=https\://api.foojay.io/disco/v3.0/ids/56a19bc915b9ba2eb62ba7554c61b919/redirect
+toolchainUrl.UNIX.X86_64=https\://api.foojay.io/disco/v3.0/ids/398ffe3949748bfb1d5636f023d228fd/redirect
+toolchainUrl.WINDOWS.AARCH64=https\://api.foojay.io/disco/v3.0/ids/39846e8427e64a3824c13e399d7d813c/redirect
+toolchainUrl.WINDOWS.X86_64=https\://api.foojay.io/disco/v3.0/ids/932015f6361ccaead0c6d9b8717ed96e/redirect
+toolchainVendor=JETBRAINS
+toolchainVersion=21
diff --git a/android/gradle/libs.versions.toml b/android/gradle/libs.versions.toml
new file mode 100644
index 00000000..18b8ebb8
--- /dev/null
+++ b/android/gradle/libs.versions.toml
@@ -0,0 +1,24 @@
+[versions]
+agp = "8.9.1"
+junit = "4.13.2"
+junitVersion = "1.3.0"
+espressoCore = "3.7.0"
+appcompat = "1.7.1"
+material = "1.13.0"
+activity = "1.12.4"
+constraintlayout = "2.2.1"
+swiperefreshlayout = "1.2.0"
+
+[libraries]
+junit = { group = "junit", name = "junit", version.ref = "junit" }
+ext-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
+espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
+appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" }
+material = { group = "com.google.android.material", name = "material", version.ref = "material" }
+activity = { group = "androidx.activity", name = "activity", version.ref = "activity" }
+constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "constraintlayout" }
+swiperefreshlayout = { group = "androidx.swiperefreshlayout", name = "swiperefreshlayout", version.ref = "swiperefreshlayout" }
+
+[plugins]
+android-application = { id = "com.android.application", version.ref = "agp" }
+
diff --git a/android/gradle/wrapper/gradle-wrapper.jar b/android/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 00000000..8bdaf60c
Binary files /dev/null and b/android/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 00000000..7e694390
--- /dev/null
+++ b/android/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,9 @@
+#Sun Mar 01 14:36:37 MST 2026
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+distributionSha256Sum=a17ddd85a26b6a7f5ddb71ff8b05fc5104c0202c6e64782429790c933686c806
+distributionUrl=https\://services.gradle.org/distributions/gradle-9.1.0-bin.zip
+networkTimeout=10000
+validateDistributionUrl=true
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
diff --git a/android/gradlew b/android/gradlew
new file mode 100755
index 00000000..ef07e016
--- /dev/null
+++ b/android/gradlew
@@ -0,0 +1,251 @@
+#!/bin/sh
+
+#
+# Copyright © 2015 the original authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# SPDX-License-Identifier: Apache-2.0
+#
+
+##############################################################################
+#
+# Gradle start up script for POSIX generated by Gradle.
+#
+# Important for running:
+#
+# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
+# noncompliant, but you have some other compliant shell such as ksh or
+# bash, then to run this script, type that shell name before the whole
+# command line, like:
+#
+# ksh Gradle
+#
+# Busybox and similar reduced shells will NOT work, because this script
+# requires all of these POSIX shell features:
+# * functions;
+# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
+# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
+# * compound commands having a testable exit status, especially «case»;
+# * various built-in commands including «command», «set», and «ulimit».
+#
+# Important for patching:
+#
+# (2) This script targets any POSIX shell, so it avoids extensions provided
+# by Bash, Ksh, etc; in particular arrays are avoided.
+#
+# The "traditional" practice of packing multiple parameters into a
+# space-separated string is a well documented source of bugs and security
+# problems, so this is (mostly) avoided, by progressively accumulating
+# options in "$@", and eventually passing that to Java.
+#
+# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
+# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
+# see the in-line comments for details.
+#
+# There are tweaks for specific operating systems such as AIX, CygWin,
+# Darwin, MinGW, and NonStop.
+#
+# (3) This script is generated from the Groovy template
+# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
+# within the Gradle project.
+#
+# You can find Gradle at https://github.com/gradle/gradle/.
+#
+##############################################################################
+
+# Attempt to set APP_HOME
+
+# Resolve links: $0 may be a link
+app_path=$0
+
+# Need this for daisy-chained symlinks.
+while
+ APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
+ [ -h "$app_path" ]
+do
+ ls=$( ls -ld "$app_path" )
+ link=${ls#*' -> '}
+ case $link in #(
+ /*) app_path=$link ;; #(
+ *) app_path=$APP_HOME$link ;;
+ esac
+done
+
+# This is normally unused
+# shellcheck disable=SC2034
+APP_BASE_NAME=${0##*/}
+# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
+APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD=maximum
+
+warn () {
+ echo "$*"
+} >&2
+
+die () {
+ echo
+ echo "$*"
+ echo
+ exit 1
+} >&2
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "$( uname )" in #(
+ CYGWIN* ) cygwin=true ;; #(
+ Darwin* ) darwin=true ;; #(
+ MSYS* | MINGW* ) msys=true ;; #(
+ NONSTOP* ) nonstop=true ;;
+esac
+
+CLASSPATH="\\\"\\\""
+
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD=$JAVA_HOME/jre/sh/java
+ else
+ JAVACMD=$JAVA_HOME/bin/java
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD=java
+ if ! command -v java >/dev/null 2>&1
+ then
+ die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+fi
+
+# Increase the maximum file descriptors if we can.
+if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
+ case $MAX_FD in #(
+ max*)
+ # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
+ # shellcheck disable=SC2039,SC3045
+ MAX_FD=$( ulimit -H -n ) ||
+ warn "Could not query maximum file descriptor limit"
+ esac
+ case $MAX_FD in #(
+ '' | soft) :;; #(
+ *)
+ # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
+ # shellcheck disable=SC2039,SC3045
+ ulimit -n "$MAX_FD" ||
+ warn "Could not set maximum file descriptor limit to $MAX_FD"
+ esac
+fi
+
+# Collect all arguments for the java command, stacking in reverse order:
+# * args from the command line
+# * the main class name
+# * -classpath
+# * -D...appname settings
+# * --module-path (only if needed)
+# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
+
+# For Cygwin or MSYS, switch paths to Windows format before running java
+if "$cygwin" || "$msys" ; then
+ APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
+ CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
+
+ JAVACMD=$( cygpath --unix "$JAVACMD" )
+
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ for arg do
+ if
+ case $arg in #(
+ -*) false ;; # don't mess with options #(
+ /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
+ [ -e "$t" ] ;; #(
+ *) false ;;
+ esac
+ then
+ arg=$( cygpath --path --ignore --mixed "$arg" )
+ fi
+ # Roll the args list around exactly as many times as the number of
+ # args, so each arg winds up back in the position where it started, but
+ # possibly modified.
+ #
+ # NB: a `for` loop captures its iteration list before it begins, so
+ # changing the positional parameters here affects neither the number of
+ # iterations, nor the values presented in `arg`.
+ shift # remove old arg
+ set -- "$@" "$arg" # push replacement arg
+ done
+fi
+
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+
+# Collect all arguments for the java command:
+# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
+# and any embedded shellness will be escaped.
+# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
+# treated as '${Hostname}' itself on the command line.
+
+set -- \
+ "-Dorg.gradle.appname=$APP_BASE_NAME" \
+ -classpath "$CLASSPATH" \
+ -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
+ "$@"
+
+# Stop when "xargs" is not available.
+if ! command -v xargs >/dev/null 2>&1
+then
+ die "xargs is not available"
+fi
+
+# Use "xargs" to parse quoted args.
+#
+# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
+#
+# In Bash we could simply go:
+#
+# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
+# set -- "${ARGS[@]}" "$@"
+#
+# but POSIX shell has neither arrays nor command substitution, so instead we
+# post-process each arg (as a line of input to sed) to backslash-escape any
+# character that might be a shell metacharacter, then use eval to reverse
+# that process (while maintaining the separation between arguments), and wrap
+# the whole thing up as a single "set" statement.
+#
+# This will of course break if any of these variables contains a newline or
+# an unmatched quote.
+#
+
+eval "set -- $(
+ printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
+ xargs -n1 |
+ sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
+ tr '\n' ' '
+ )" '"$@"'
+
+exec "$JAVACMD" "$@"
diff --git a/android/gradlew.bat b/android/gradlew.bat
new file mode 100644
index 00000000..db3a6ac2
--- /dev/null
+++ b/android/gradlew.bat
@@ -0,0 +1,94 @@
+@rem
+@rem Copyright 2015 the original author or authors.
+@rem
+@rem Licensed under the Apache License, Version 2.0 (the "License");
+@rem you may not use this file except in compliance with the License.
+@rem You may obtain a copy of the License at
+@rem
+@rem https://www.apache.org/licenses/LICENSE-2.0
+@rem
+@rem Unless required by applicable law or agreed to in writing, software
+@rem distributed under the License is distributed on an "AS IS" BASIS,
+@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+@rem See the License for the specific language governing permissions and
+@rem limitations under the License.
+@rem
+@rem SPDX-License-Identifier: Apache-2.0
+@rem
+
+@if "%DEBUG%"=="" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%"=="" set DIRNAME=.
+@rem This is normally unused
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Resolve any "." and ".." in APP_HOME to make it shorter.
+for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if %ERRORLEVEL% equ 0 goto execute
+
+echo. 1>&2
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
+echo. 1>&2
+echo Please set the JAVA_HOME variable in your environment to match the 1>&2
+echo location of your Java installation. 1>&2
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto execute
+
+echo. 1>&2
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
+echo. 1>&2
+echo Please set the JAVA_HOME variable in your environment to match the 1>&2
+echo location of your Java installation. 1>&2
+
+goto fail
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=
+
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
+
+:end
+@rem End local scope for the variables with windows NT shell
+if %ERRORLEVEL% equ 0 goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+set EXIT_CODE=%ERRORLEVEL%
+if %EXIT_CODE% equ 0 set EXIT_CODE=1
+if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
+exit /b %EXIT_CODE%
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/android/local.properties.template b/android/local.properties.template
new file mode 100644
index 00000000..788baf64
--- /dev/null
+++ b/android/local.properties.template
@@ -0,0 +1,11 @@
+## This file must be *NOT* checked into Version Control Systems,
+# as it contains information specific to your local configuration.
+#
+# Location of the Android SDK. This is typically:
+# - Windows: C:\Users\YourUsername\AppData\Local\Android\Sdk
+# - Mac: /Users/YourUsername/Library/Android/sdk
+# - Linux: /home/YourUsername/Android/Sdk
+#
+# Copy this file to local.properties and update the path below:
+
+sdk.dir=/path/to/your/android/sdk
diff --git a/android/settings.gradle.kts b/android/settings.gradle.kts
new file mode 100644
index 00000000..0a2384fb
--- /dev/null
+++ b/android/settings.gradle.kts
@@ -0,0 +1,28 @@
+pluginManagement {
+ repositories {
+ google {
+ content {
+ includeGroupByRegex("com\\.android.*")
+ includeGroupByRegex("com\\.google.*")
+ includeGroupByRegex("androidx.*")
+ }
+ }
+ mavenCentral()
+ gradlePluginPortal()
+ }
+}
+plugins {
+ id("org.gradle.toolchains.foojay-resolver-convention") version "1.0.0"
+}
+dependencyResolutionManagement {
+ repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
+ repositories {
+ google()
+ mavenCentral()
+ maven { url = uri("https://jitpack.io") }
+ }
+
+}
+
+rootProject.name = "PetStoreMobile"
+include(":app")