diff --git a/.idea/.gitignore b/.idea/.gitignore
new file mode 100644
index 00000000..ab1f4164
--- /dev/null
+++ b/.idea/.gitignore
@@ -0,0 +1,10 @@
+# Default ignored files
+/shelf/
+/workspace.xml
+# Ignored default folder with query files
+/queries/
+# Datasource local storage ignored files
+/dataSources/
+/dataSources.local.xml
+# Editor-based HTTP Client requests
+/httpRequests/
diff --git a/.idea/group-2-threaded-project-petshop.iml b/.idea/group-2-threaded-project-petshop.iml
new file mode 100644
index 00000000..d6ebd480
--- /dev/null
+++ b/.idea/group-2-threaded-project-petshop.iml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
new file mode 100644
index 00000000..4b151abf
--- /dev/null
+++ b/.idea/misc.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/modules.xml b/.idea/modules.xml
new file mode 100644
index 00000000..e9d7459a
--- /dev/null
+++ b/.idea/modules.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
new file mode 100644
index 00000000..35eb1ddf
--- /dev/null
+++ b/.idea/vcs.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts
index fa8c88a7..e3955e2f 100644
--- a/android/app/build.gradle.kts
+++ b/android/app/build.gradle.kts
@@ -1,7 +1,23 @@
+import java.util.Properties
+
plugins {
alias(libs.plugins.android.application)
}
+val localProperties = Properties().apply {
+ val file = rootProject.file("local.properties")
+ if (file.exists()) {
+ file.inputStream().use { load(it) }
+ }
+}
+
+fun quoted(value: String): String = "\"$value\""
+
+val emulatorBackendUrl =
+ (localProperties.getProperty("petstore.backend.emulatorUrl") ?: "http://10.0.2.2:8080/").trim()
+val deviceBackendUrl =
+ (localProperties.getProperty("petstore.backend.deviceUrl") ?: "http://10.0.0.200:8080/").trim()
+
android {
namespace = "com.example.petstoremobile"
compileSdk = 36
@@ -14,6 +30,13 @@ android {
versionName = "1.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
+
+ buildConfigField("String", "EMULATOR_BACKEND_URL", quoted(emulatorBackendUrl))
+ buildConfigField("String", "DEVICE_BACKEND_URL", quoted(deviceBackendUrl))
+ }
+
+ buildFeatures {
+ buildConfig = true
}
buildTypes {
@@ -39,7 +62,7 @@ dependencies {
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:logging-interceptor:4.12.0")
implementation("com.squareup.okhttp3:okhttp:4.12.0")
implementation("com.google.android.material:material:1.11.0")
@@ -62,4 +85,4 @@ dependencies {
testImplementation(libs.junit)
androidTestImplementation(libs.ext.junit)
androidTestImplementation(libs.espresso.core)
-}
\ 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
index bc646e26..aea1f427 100644
--- a/android/app/src/main/java/com/example/petstoremobile/activities/HomeActivity.java
+++ b/android/app/src/main/java/com/example/petstoremobile/activities/HomeActivity.java
@@ -38,8 +38,8 @@ public class HomeActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
EdgeToEdge.enable(this);
+ super.onCreate(savedInstanceState);
setContentView(R.layout.activity_home);
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> {
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
index 242000c8..94a51ac3 100644
--- a/android/app/src/main/java/com/example/petstoremobile/activities/MainActivity.java
+++ b/android/app/src/main/java/com/example/petstoremobile/activities/MainActivity.java
@@ -36,6 +36,7 @@ public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
+ EdgeToEdge.enable(this);
super.onCreate(savedInstanceState);
// Check if user is already logged in
@@ -52,7 +53,6 @@ public class MainActivity extends AppCompatActivity {
}
}
- EdgeToEdge.enable(this);
setContentView(R.layout.activity_main);
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> {
@@ -132,15 +132,43 @@ public class MainActivity extends AppCompatActivity {
}
});
} else {
- Toast.makeText(MainActivity.this, "Login failed", Toast.LENGTH_SHORT).show();
- tvLoginStatus.setText("Login failed");
+ String errorMessage;
+ switch (response.code()) {
+ case 401:
+ errorMessage = "Invalid username or password";
+ break;
+ case 500:
+ errorMessage = "Server error. Please try again later.";
+ break;
+ case 503:
+ errorMessage = "Service unavailable. Backend may be starting up.";
+ break;
+ default:
+ errorMessage = "Login failed (Error " + response.code() + ")";
+ }
+ Toast.makeText(MainActivity.this, errorMessage, Toast.LENGTH_LONG).show();
+ tvLoginStatus.setText(errorMessage);
}
}
@Override
public void onFailure(Call call, Throwable t) {
- Toast.makeText(MainActivity.this, "Login failed", Toast.LENGTH_SHORT).show();
- tvLoginStatus.setText("Login failed");
+ Log.e("MainActivity", "Login request failed", t);
+
+ String errorMessage;
+ if (t instanceof java.net.ConnectException ||
+ t instanceof java.net.SocketTimeoutException ||
+ t instanceof java.net.UnknownHostException) {
+ errorMessage = "Cannot connect to server at " + RetrofitClient.BASE_URL +
+ ". Please check if the backend is running.";
+ } else if (t instanceof java.io.IOException) {
+ errorMessage = "Network error. Please check your connection.";
+ } else {
+ errorMessage = "Login failed: " + t.getMessage();
+ }
+
+ Toast.makeText(MainActivity.this, errorMessage, Toast.LENGTH_LONG).show();
+ tvLoginStatus.setText(errorMessage);
}
});
});
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
index 98e88256..c6bd678c 100644
--- a/android/app/src/main/java/com/example/petstoremobile/adapters/AdoptionAdapter.java
+++ b/android/app/src/main/java/com/example/petstoremobile/adapters/AdoptionAdapter.java
@@ -1,79 +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.view.*;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import com.example.petstoremobile.R;
-import com.example.petstoremobile.models.Adoption;
+import com.example.petstoremobile.dtos.AdoptionDTO;
import java.util.List;
public class AdoptionAdapter extends RecyclerView.Adapter {
- private List adoptionList;
- private OnAdoptionClickListener adoptionClickListener;
+ private List adoptionList;
+ private OnAdoptionClickListener listener;
- // Interface for adoption click on recycler view
public interface OnAdoptionClickListener {
void onAdoptionClick(int position);
}
- // Constructor
- public AdoptionAdapter(List adoptionList, OnAdoptionClickListener adoptionClickListener) {
+ public AdoptionAdapter(List adoptionList, OnAdoptionClickListener listener) {
this.adoptionList = adoptionList;
- this.adoptionClickListener = adoptionClickListener;
+ this.listener = listener;
}
- // Get the controls of each row in recycler view
public static class AdoptionViewHolder extends RecyclerView.ViewHolder {
- TextView tvAdopterName, tvPetName, tvAdoptionDate, tvAdoptionStatus;
+ TextView tvCustomerName, tvPetName, tvDate, tvFee, tvStatus;
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);
+ tvCustomerName = v.findViewById(R.id.tvAdoptionCustomerName);
+ tvPetName = v.findViewById(R.id.tvAdoptionPetName);
+ tvDate = v.findViewById(R.id.tvAdoptionDate);
+ tvFee = v.findViewById(R.id.tvAdoptionFee);
+ tvStatus = 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);
+ 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);
+ AdoptionDTO a = 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());
+ holder.tvCustomerName.setText(a.getCustomerName() != null ? a.getCustomerName() : "");
+ holder.tvPetName.setText("Pet: " + (a.getPetName() != null ? a.getPetName() : ""));
+ holder.tvDate.setText("Date: " + (a.getAdoptionDate() != null ? a.getAdoptionDate() : ""));
+ holder.tvFee.setText(a.getAdoptionFee() != null ? "$" + a.getAdoptionFee() : "");
- // Set the status color depending on adoption status
- if (adoption.getStatus().equals("Approved")) {
- holder.tvAdoptionStatus.setBackgroundColor(Color.parseColor("#4CAF50"));
- } else if (adoption.getStatus().equals("Pending")) {
- holder.tvAdoptionStatus.setBackgroundColor(Color.parseColor("#FF9800"));
- } else {
- holder.tvAdoptionStatus.setBackgroundColor(Color.parseColor("#F44336"));
+ String status = a.getAdoptionStatus() != null ? a.getAdoptionStatus() : "";
+ holder.tvStatus.setText(status);
+
+ switch (status) {
+ case "Approved":
+ holder.tvStatus.setBackgroundColor(Color.parseColor("#4CAF50"));
+ break;
+ case "Pending":
+ holder.tvStatus.setBackgroundColor(Color.parseColor("#FF9800"));
+ break;
+ case "Rejected":
+ holder.tvStatus.setBackgroundColor(Color.parseColor("#F44336"));
+ break;
+ default:
+ holder.tvStatus.setBackgroundColor(Color.parseColor("#9E9E9E"));
+ break;
}
- // When a row is clicked, open the detail view
- holder.itemView.setOnClickListener(v -> adoptionClickListener.onAdoptionClick(position));
+ holder.itemView.setOnClickListener(v -> listener.onAdoptionClick(position));
}
@Override
- public int getItemCount() {
- return adoptionList.size();
- }
-}
+ public int getItemCount() { return adoptionList.size(); }
+}
\ No newline at end of file
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
index 7924c12b..9960b5b6 100644
--- a/android/app/src/main/java/com/example/petstoremobile/adapters/AppointmentAdapter.java
+++ b/android/app/src/main/java/com/example/petstoremobile/adapters/AppointmentAdapter.java
@@ -1,7 +1,5 @@
package com.example.petstoremobile.adapters;
-
-
import android.graphics.Color;
import android.view.LayoutInflater;
import android.view.View;
@@ -10,26 +8,24 @@ import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import com.example.petstoremobile.R;
-import com.example.petstoremobile.models.Appointment;
+import com.example.petstoremobile.dtos.AppointmentDTO;
import java.util.List;
public class AppointmentAdapter extends RecyclerView.Adapter {
- private List appointmentList;
+ 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) {
+ 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;
@@ -43,7 +39,6 @@ public class AppointmentAdapter extends RecyclerView.Adapter appointmentClickListener.onAppointmentClick(position));
}
diff --git a/android/app/src/main/java/com/example/petstoremobile/adapters/BlackTextArrayAdapter.java b/android/app/src/main/java/com/example/petstoremobile/adapters/BlackTextArrayAdapter.java
new file mode 100644
index 00000000..22af59de
--- /dev/null
+++ b/android/app/src/main/java/com/example/petstoremobile/adapters/BlackTextArrayAdapter.java
@@ -0,0 +1,44 @@
+package com.example.petstoremobile.adapters;
+
+import android.content.Context;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+import android.widget.TextView;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.core.content.ContextCompat;
+import com.example.petstoremobile.R;
+import java.util.List;
+
+// A class that overrides the arrayAdapter so the text color changes based on theme
+public class BlackTextArrayAdapter extends ArrayAdapter {
+ public BlackTextArrayAdapter(@NonNull Context context, int resource, @NonNull T[] objects) {
+ super(context, resource, objects);
+ }
+
+ public BlackTextArrayAdapter(@NonNull Context context, int resource, @NonNull List objects) {
+ super(context, resource, objects);
+ }
+
+ @NonNull
+ @Override
+ public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) {
+ View view = super.getView(position, convertView, parent);
+ view.setBackgroundColor(ContextCompat.getColor(getContext(), R.color.white));
+ if (view instanceof TextView) {
+ ((TextView) view).setTextColor(ContextCompat.getColor(getContext(), R.color.spinner_text));
+ }
+ return view;
+ }
+
+ @Override
+ public View getDropDownView(int position, @Nullable View convertView, @NonNull ViewGroup parent) {
+ View view = super.getDropDownView(position, convertView, parent);
+ view.setBackgroundColor(ContextCompat.getColor(getContext(), R.color.white));
+ if (view instanceof TextView) {
+ ((TextView) view).setTextColor(ContextCompat.getColor(getContext(), R.color.spinner_text));
+ }
+ return view;
+ }
+}
\ 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
index 7ae36dc5..1e455cc6 100644
--- a/android/app/src/main/java/com/example/petstoremobile/adapters/InventoryAdapter.java
+++ b/android/app/src/main/java/com/example/petstoremobile/adapters/InventoryAdapter.java
@@ -1,79 +1,150 @@
package com.example.petstoremobile.adapters;
-
import android.graphics.Color;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
+import android.widget.CheckBox;
import android.widget.TextView;
+
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
+
import com.example.petstoremobile.R;
-import com.example.petstoremobile.models.Inventory;
+import com.example.petstoremobile.dtos.InventoryDTO;
+
+import java.util.ArrayList;
import java.util.List;
public class InventoryAdapter extends RecyclerView.Adapter {
- private List inventoryList;
- private OnInventoryClickListener inventoryClickListener;
+ private final List inventoryList;
+ private final OnInventoryClickListener clickListener;
+ private final List selectedIds = new ArrayList<>();
+ private boolean selectionMode = false;
- // Interface for inventory click on recycler view
public interface OnInventoryClickListener {
void onInventoryClick(int position);
+
+ void onSelectionChanged(int selectedCount);
}
- // Constructor
- public InventoryAdapter(List inventoryList, OnInventoryClickListener inventoryClickListener) {
+ public InventoryAdapter(List inventoryList, OnInventoryClickListener clickListener) {
this.inventoryList = inventoryList;
- this.inventoryClickListener = inventoryClickListener;
+ this.clickListener = clickListener;
}
- // Get the controls of each row in recycler view
public static class InventoryViewHolder extends RecyclerView.ViewHolder {
- TextView tvItemName, tvCategory, tvQuantity, tvUnitPrice, tvSupplier;
+ // Matches desktop table columns: Inventory ID, Product ID, Product Name,
+ // Quantity
+ TextView tvInventoryId, tvProdId, tvProductName, tvQuantity;
+ CheckBox checkBox;
public InventoryViewHolder(@NonNull View v) {
super(v);
- tvItemName = v.findViewById(R.id.tvItemName);
- tvCategory = v.findViewById(R.id.tvCategory);
+ tvInventoryId = v.findViewById(R.id.tvInventoryId);
+ tvProdId = v.findViewById(R.id.tvProdId);
+ tvProductName = v.findViewById(R.id.tvProductName);
tvQuantity = v.findViewById(R.id.tvQuantity);
- tvUnitPrice = v.findViewById(R.id.tvUnitPrice);
- tvSupplier = v.findViewById(R.id.tvInvSupplier);
+ checkBox = v.findViewById(R.id.cbSelectInventory);
}
}
- // 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);
+ 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);
+ InventoryDTO inv = 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());
+ // Column: Inventory ID
+ String invIdStr = inv.getInventoryId() != null ? String.valueOf(inv.getInventoryId()) : "—";
+ holder.tvInventoryId.setText("Inv ID: " + invIdStr);
- // Highlight low stock items in red
- if (inventory.getQuantity() <= 5) {
+ // Column: Product ID
+ String prodIdStr = inv.getProdId() != null ? String.valueOf(inv.getProdId()) : "—";
+ holder.tvProdId.setText("Prod ID: " + prodIdStr);
+
+ // Column: Product Name
+ holder.tvProductName.setText(inv.getProductName() != null ? inv.getProductName() : "—");
+
+ // Column: Quantity
+ int qty = inv.getQuantity() != null ? inv.getQuantity() : 0;
+ holder.tvQuantity.setText(String.valueOf(qty));
+
+ // Low stock = red, normal = green (like desktop reorder concept)
+ if (qty <= 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));
+ // Bulk delete selection mode
+ if (selectionMode) {
+ holder.checkBox.setVisibility(View.VISIBLE);
+ holder.checkBox.setChecked(inv.getInventoryId() != null
+ && selectedIds.contains(inv.getInventoryId()));
+ } else {
+ holder.checkBox.setVisibility(View.GONE);
+ holder.checkBox.setChecked(false);
+ }
+
+ holder.itemView.setOnClickListener(v -> {
+ if (selectionMode) {
+ toggleSelection(inv.getInventoryId(), holder.checkBox);
+ } else {
+ clickListener.onInventoryClick(holder.getAdapterPosition());
+ }
+ });
+
+ holder.itemView.setOnLongClickListener(v -> {
+ if (!selectionMode) {
+ selectionMode = true;
+ toggleSelection(inv.getInventoryId(), holder.checkBox);
+ notifyDataSetChanged();
+ }
+ return true;
+ });
+ }
+
+ private void toggleSelection(Long id, CheckBox checkBox) {
+ if (id == null)
+ return;
+ if (selectedIds.contains(id)) {
+ selectedIds.remove(id);
+ checkBox.setChecked(false);
+ } else {
+ selectedIds.add(id);
+ checkBox.setChecked(true);
+ }
+ clickListener.onSelectionChanged(selectedIds.size());
+ if (selectedIds.isEmpty()) {
+ selectionMode = false;
+ notifyDataSetChanged();
+ }
+ }
+
+ public List getSelectedIds() {
+ return new ArrayList<>(selectedIds);
+ }
+
+ public void clearSelection() {
+ selectedIds.clear();
+ selectionMode = false;
+ notifyDataSetChanged();
+ }
+
+ public boolean isInSelectionMode() {
+ return selectionMode;
}
@Override
public int getItemCount() {
return inventoryList.size();
}
-}
+}
\ 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
index c78cc7cc..166b0d1d 100644
--- a/android/app/src/main/java/com/example/petstoremobile/adapters/PetAdapter.java
+++ b/android/app/src/main/java/com/example/petstoremobile/adapters/PetAdapter.java
@@ -4,10 +4,16 @@ import android.graphics.Color;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
+import android.widget.ImageView;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
+
+import com.bumptech.glide.Glide;
+import com.bumptech.glide.load.engine.DiskCacheStrategy;
import com.example.petstoremobile.R;
+import com.example.petstoremobile.api.PetApi;
+import com.example.petstoremobile.api.RetrofitClient;
import com.example.petstoremobile.dtos.PetDTO;
import java.util.List;
@@ -30,6 +36,7 @@ public class PetAdapter extends RecyclerView.Adapter {
// Get the controls of each row in recycler view
public static class PetViewHolder extends RecyclerView.ViewHolder {
TextView tvPetName, tvPetSpeciesBreed, tvPetAge, tvPetPrice, tvPetStatus;
+ ImageView ivPetProfile;
public PetViewHolder(@NonNull View v) {
super(v);
@@ -38,6 +45,7 @@ public class PetAdapter extends RecyclerView.Adapter {
tvPetAge = v.findViewById(R.id.tvPetAge);
tvPetPrice = v.findViewById(R.id.tvPetPrice);
tvPetStatus = v.findViewById(R.id.tvPetStatus);
+ ivPetProfile = v.findViewById(R.id.ivPetProfile);
}
}
@@ -74,6 +82,17 @@ public class PetAdapter extends RecyclerView.Adapter {
holder.tvPetStatus.setBackgroundColor(Color.parseColor("#F44336"));
}
+ // Load pet image using Glide with circle crop
+ String imageUrl = RetrofitClient.BASE_URL + String.format(PetApi.PET_IMAGE_PATH, pet.getPetId());
+ Glide.with(holder.itemView.getContext())
+ .load(imageUrl)
+ .circleCrop()
+ .diskCacheStrategy(DiskCacheStrategy.NONE)
+ .skipMemoryCache(true)
+ .placeholder(R.drawable.placeholder)
+ .error(R.drawable.placeholder)
+ .into(holder.ivPetProfile);
+
//when a row is clicked, open the detail view
holder.itemView.setOnClickListener(v -> petClickListener.onPetClick(position));
}
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
index a4e5e770..300b7e57 100644
--- a/android/app/src/main/java/com/example/petstoremobile/adapters/ProductAdapter.java
+++ b/android/app/src/main/java/com/example/petstoremobile/adapters/ProductAdapter.java
@@ -1,72 +1,57 @@
package com.example.petstoremobile.adapters;
-
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
+import android.view.*;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import com.example.petstoremobile.R;
-import com.example.petstoremobile.models.Product;
+import com.example.petstoremobile.dtos.ProductDTO;
import java.util.List;
public class ProductAdapter extends RecyclerView.Adapter {
- private List productList;
- private OnProductClickListener productClickListener;
+ private List productList;
+ private OnProductClickListener listener;
- // Interface for product click on recycler view
public interface OnProductClickListener {
void onProductClick(int position);
}
- // Constructor
- public ProductAdapter(List productList, OnProductClickListener productClickListener) {
+ public ProductAdapter(List productList, OnProductClickListener listener) {
this.productList = productList;
- this.productClickListener = productClickListener;
+ this.listener = listener;
}
- // Get the controls of each row in recycler view
public static class ProductViewHolder extends RecyclerView.ViewHolder {
- TextView tvProductName, tvProductDesc, tvCategory, tvProductPrice, tvStockQuantity;
+ TextView tvName, tvCategory, tvDesc, tvPrice;
public ProductViewHolder(@NonNull View v) {
super(v);
- tvProductName = v.findViewById(R.id.tvProductName);
- tvProductDesc = v.findViewById(R.id.tvProductDesc);
+ tvName = v.findViewById(R.id.tvProductName);
tvCategory = v.findViewById(R.id.tvProductCategory);
- tvProductPrice = v.findViewById(R.id.tvProductPrice);
- tvStockQuantity = v.findViewById(R.id.tvStockQuantity);
+ tvDesc = v.findViewById(R.id.tvProductDesc);
+ tvPrice = v.findViewById(R.id.tvProductPrice);
}
}
- // 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);
+ 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));
+ ProductDTO p = productList.get(position);
+ holder.tvName.setText(p.getProdName() != null ? p.getProdName() : "");
+ holder.tvCategory.setText("Category: " + (p.getCategoryName() != null ? p.getCategoryName() : ""));
+ holder.tvDesc.setText(p.getProdDesc() != null ? p.getProdDesc() : "");
+ holder.tvPrice.setText(p.getProdPrice() != null ? "$" + p.getProdPrice() : "");
+ holder.itemView.setOnClickListener(v -> listener.onProductClick(position));
}
@Override
- public int getItemCount() {
- return productList.size();
- }
-}
-
+ public int getItemCount() { return productList.size(); }
+}
\ No newline at end of file
diff --git a/android/app/src/main/java/com/example/petstoremobile/adapters/ProductSupplierAdapter.java b/android/app/src/main/java/com/example/petstoremobile/adapters/ProductSupplierAdapter.java
new file mode 100644
index 00000000..4c6377e1
--- /dev/null
+++ b/android/app/src/main/java/com/example/petstoremobile/adapters/ProductSupplierAdapter.java
@@ -0,0 +1,55 @@
+package com.example.petstoremobile.adapters;
+
+import android.view.*;
+import android.widget.TextView;
+import androidx.annotation.NonNull;
+import androidx.recyclerview.widget.RecyclerView;
+import com.example.petstoremobile.R;
+import com.example.petstoremobile.dtos.ProductSupplierDTO;
+import java.util.List;
+
+public class ProductSupplierAdapter extends RecyclerView.Adapter {
+
+ private List list;
+ private OnProductSupplierClickListener listener;
+
+ public interface OnProductSupplierClickListener {
+ void onProductSupplierClick(int position);
+ }
+
+ public ProductSupplierAdapter(List list, OnProductSupplierClickListener listener) {
+ this.list = list;
+ this.listener = listener;
+ }
+
+ public static class PSViewHolder extends RecyclerView.ViewHolder {
+ TextView tvProductName, tvSupplierName, tvCost;
+
+ public PSViewHolder(@NonNull View v) {
+ super(v);
+ tvProductName = v.findViewById(R.id.tvPSProductName);
+ tvSupplierName = v.findViewById(R.id.tvPSSupplierName);
+ tvCost = v.findViewById(R.id.tvPSCost);
+ }
+ }
+
+ @NonNull
+ @Override
+ public PSViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
+ View v = LayoutInflater.from(parent.getContext())
+ .inflate(R.layout.item_product_supplier, parent, false);
+ return new PSViewHolder(v);
+ }
+
+ @Override
+ public void onBindViewHolder(@NonNull PSViewHolder holder, int position) {
+ ProductSupplierDTO ps = list.get(position);
+ holder.tvProductName.setText(ps.getProductName() != null ? ps.getProductName() : "");
+ holder.tvSupplierName.setText("Supplier: " + (ps.getSupplierName() != null ? ps.getSupplierName() : ""));
+ holder.tvCost.setText(ps.getCost() != null ? "Cost: $" + ps.getCost() : "");
+ holder.itemView.setOnClickListener(v -> listener.onProductSupplierClick(position));
+ }
+
+ @Override
+ public int getItemCount() { return list.size(); }
+}
\ No newline at end of file
diff --git a/android/app/src/main/java/com/example/petstoremobile/adapters/PurchaseOrderAdapter.java b/android/app/src/main/java/com/example/petstoremobile/adapters/PurchaseOrderAdapter.java
new file mode 100644
index 00000000..2d66e672
--- /dev/null
+++ b/android/app/src/main/java/com/example/petstoremobile/adapters/PurchaseOrderAdapter.java
@@ -0,0 +1,78 @@
+package com.example.petstoremobile.adapters;
+
+import android.graphics.Color;
+import android.view.*;
+import android.widget.TextView;
+import androidx.annotation.NonNull;
+import androidx.recyclerview.widget.RecyclerView;
+import com.example.petstoremobile.R;
+import com.example.petstoremobile.dtos.PurchaseOrderDTO;
+import java.util.List;
+
+public class PurchaseOrderAdapter extends RecyclerView.Adapter {
+
+ private List list;
+ private OnPurchaseOrderClickListener listener;
+
+ public interface OnPurchaseOrderClickListener {
+ void onPurchaseOrderClick(int position);
+ }
+
+ public PurchaseOrderAdapter(List list, OnPurchaseOrderClickListener listener) {
+ this.list = list;
+ this.listener = listener;
+ }
+
+ public static class POViewHolder extends RecyclerView.ViewHolder {
+ TextView tvId, tvSupplier, tvDate, tvStatus;
+
+ public POViewHolder(@NonNull View v) {
+ super(v);
+ tvId = v.findViewById(R.id.tvPOId);
+ tvSupplier = v.findViewById(R.id.tvPOSupplier);
+ tvDate = v.findViewById(R.id.tvPODate);
+ tvStatus = v.findViewById(R.id.tvPOStatus);
+ }
+ }
+
+ @NonNull
+ @Override
+ public POViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
+ View v = LayoutInflater.from(parent.getContext())
+ .inflate(R.layout.item_purchase_order, parent, false);
+ return new POViewHolder(v);
+ }
+
+ @Override
+ public void onBindViewHolder(@NonNull POViewHolder holder, int position) {
+ PurchaseOrderDTO po = list.get(position);
+ holder.tvId.setText("PO #" + (po.getPurchaseOrderId() != null ? po.getPurchaseOrderId() : ""));
+ holder.tvSupplier.setText("Supplier: " + (po.getSupplierName() != null ? po.getSupplierName() : ""));
+ holder.tvDate.setText("Date: " + (po.getOrderDate() != null ? po.getOrderDate() : ""));
+
+ String status = po.getStatus() != null ? po.getStatus() : "";
+ holder.tvStatus.setText(status);
+
+ switch (status) {
+ case "Completed":
+ holder.tvStatus.setBackgroundColor(Color.parseColor("#4CAF50"));
+ break;
+ case "Pending":
+ holder.tvStatus.setBackgroundColor(Color.parseColor("#FF9800"));
+ break;
+ case "Cancelled":
+ holder.tvStatus.setBackgroundColor(Color.parseColor("#F44336"));
+ break;
+ default:
+ holder.tvStatus.setBackgroundColor(Color.parseColor("#9E9E9E"));
+ break;
+ }
+
+ holder.itemView.setOnClickListener(v -> listener.onPurchaseOrderClick(position));
+ }
+
+ @Override
+ public int getItemCount() {
+ return list.size();
+ }
+}
\ No newline at end of file
diff --git a/android/app/src/main/java/com/example/petstoremobile/adapters/SaleAdapter.java b/android/app/src/main/java/com/example/petstoremobile/adapters/SaleAdapter.java
new file mode 100644
index 00000000..d5a73e42
--- /dev/null
+++ b/android/app/src/main/java/com/example/petstoremobile/adapters/SaleAdapter.java
@@ -0,0 +1,78 @@
+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.Sale;
+import java.util.List;
+
+public class SaleAdapter extends RecyclerView.Adapter {
+
+ private List saleList;
+ private OnSaleClickListener saleClickListener;
+
+ public interface OnSaleClickListener {
+ void onSaleClick(int position);
+ }
+
+ public SaleAdapter(List saleList, OnSaleClickListener saleClickListener) {
+ this.saleList = saleList;
+ this.saleClickListener = saleClickListener;
+ }
+
+ public static class SaleViewHolder extends RecyclerView.ViewHolder {
+ TextView tvSaleId, tvItemName, tvEmployeeName, tvSaleDate, tvTotal, tvPaymentMethod, tvRefundBadge;
+
+ public SaleViewHolder(@NonNull View v) {
+ super(v);
+ tvSaleId = v.findViewById(R.id.tvSaleId);
+ tvItemName = v.findViewById(R.id.tvSaleItemName);
+ tvEmployeeName = v.findViewById(R.id.tvSaleEmployee);
+ tvSaleDate = v.findViewById(R.id.tvSaleDate);
+ tvTotal = v.findViewById(R.id.tvSaleTotal);
+ tvPaymentMethod = v.findViewById(R.id.tvSalePayment);
+ tvRefundBadge = v.findViewById(R.id.tvRefundBadge);
+ }
+ }
+
+ @NonNull
+ @Override
+ public SaleViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
+ View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_sale, parent, false);
+ return new SaleViewHolder(v);
+ }
+
+ @Override
+ public void onBindViewHolder(@NonNull SaleViewHolder holder, int position) {
+ Sale sale = saleList.get(position);
+
+ holder.tvSaleId.setText("ID: " + sale.getSaleId());
+ holder.tvItemName.setText(sale.getItemName());
+ holder.tvEmployeeName.setText("By: " + sale.getEmployeeName());
+ holder.tvSaleDate.setText(sale.getSaleDate());
+ holder.tvTotal.setText("$" + String.format("%.2f", sale.getTotal()));
+ holder.tvPaymentMethod.setText(sale.getPaymentMethod());
+
+ // Show refund badge if it's a refund
+ if (sale.isRefund()) {
+ holder.tvRefundBadge.setVisibility(View.VISIBLE);
+ holder.tvRefundBadge.setBackgroundColor(Color.parseColor("#F44336"));
+ holder.tvTotal.setTextColor(Color.parseColor("#F44336"));
+ } else {
+ holder.tvRefundBadge.setVisibility(View.GONE);
+ holder.tvTotal.setTextColor(Color.parseColor("#4CAF50"));
+ }
+
+ holder.itemView.setOnClickListener(v -> saleClickListener.onSaleClick(position));
+ }
+
+ @Override
+ public int getItemCount() {
+ return saleList.size();
+ }
+}
diff --git a/android/app/src/main/java/com/example/petstoremobile/api/AdoptionApi.java b/android/app/src/main/java/com/example/petstoremobile/api/AdoptionApi.java
new file mode 100644
index 00000000..2f704a41
--- /dev/null
+++ b/android/app/src/main/java/com/example/petstoremobile/api/AdoptionApi.java
@@ -0,0 +1,34 @@
+package com.example.petstoremobile.api;
+
+import com.example.petstoremobile.dtos.AdoptionDTO;
+import com.example.petstoremobile.dtos.PageResponse;
+
+import retrofit2.Call;
+import retrofit2.http.Body;
+import retrofit2.http.DELETE;
+import retrofit2.http.GET;
+import retrofit2.http.POST;
+import retrofit2.http.PUT;
+import retrofit2.http.Path;
+import retrofit2.http.Query;
+
+public interface AdoptionApi {
+
+ @GET("api/v1/adoptions")
+ Call> getAllAdoptions(
+ @Query("page") int page,
+ @Query("size") int size);
+
+ @GET("api/v1/adoptions/{id}")
+ Call getAdoptionById(@Path("id") Long id);
+
+ @POST("api/v1/adoptions")
+ Call createAdoption(@Body AdoptionDTO adoption);
+
+ @PUT("api/v1/adoptions/{id}")
+ Call updateAdoption(@Path("id") Long id, @Body AdoptionDTO adoption);
+
+ @DELETE("api/v1/adoptions/{id}")
+ Call deleteAdoption(@Path("id") Long id);
+}
+
diff --git a/android/app/src/main/java/com/example/petstoremobile/api/AppointmentApi.java b/android/app/src/main/java/com/example/petstoremobile/api/AppointmentApi.java
new file mode 100644
index 00000000..5d7044cf
--- /dev/null
+++ b/android/app/src/main/java/com/example/petstoremobile/api/AppointmentApi.java
@@ -0,0 +1,33 @@
+package com.example.petstoremobile.api;
+
+import com.example.petstoremobile.dtos.AppointmentDTO;
+import com.example.petstoremobile.dtos.PageResponse;
+
+import retrofit2.Call;
+import retrofit2.http.Body;
+import retrofit2.http.DELETE;
+import retrofit2.http.GET;
+import retrofit2.http.POST;
+import retrofit2.http.PUT;
+import retrofit2.http.Path;
+import retrofit2.http.Query;
+
+public interface AppointmentApi {
+
+ @GET("api/v1/appointments")
+ Call> getAllAppointments(
+ @Query("page") int page,
+ @Query("size") int size);
+
+ @GET("api/v1/appointments/{id}")
+ Call getAppointmentById(@Path("id") Long id);
+
+ @POST("api/v1/appointments")
+ Call createAppointment(@Body AppointmentDTO appointment);
+
+ @PUT("api/v1/appointments/{id}")
+ Call updateAppointment(@Path("id") Long id, @Body AppointmentDTO appointment);
+
+ @DELETE("api/v1/appointments/{id}")
+ Call deleteAppointment(@Path("id") Long id);
+}
\ No newline at end of file
diff --git a/android/app/src/main/java/com/example/petstoremobile/api/CategoryApi.java b/android/app/src/main/java/com/example/petstoremobile/api/CategoryApi.java
new file mode 100644
index 00000000..d1084482
--- /dev/null
+++ b/android/app/src/main/java/com/example/petstoremobile/api/CategoryApi.java
@@ -0,0 +1,15 @@
+package com.example.petstoremobile.api;
+
+import com.example.petstoremobile.dtos.CategoryDTO;
+import com.example.petstoremobile.dtos.PageResponse;
+import retrofit2.Call;
+import retrofit2.http.GET;
+import retrofit2.http.Query;
+
+public interface CategoryApi {
+
+ @GET("api/v1/categories")
+ Call> getAllCategories(
+ @Query("page") int page,
+ @Query("size") int size);
+}
\ No newline at end of file
diff --git a/android/app/src/main/java/com/example/petstoremobile/api/InventoryApi.java b/android/app/src/main/java/com/example/petstoremobile/api/InventoryApi.java
new file mode 100644
index 00000000..f54616ee
--- /dev/null
+++ b/android/app/src/main/java/com/example/petstoremobile/api/InventoryApi.java
@@ -0,0 +1,46 @@
+package com.example.petstoremobile.api;
+
+import com.example.petstoremobile.dtos.BulkDeleteRequest;
+import com.example.petstoremobile.dtos.InventoryDTO;
+import com.example.petstoremobile.dtos.InventoryRequest;
+import com.example.petstoremobile.dtos.PageResponse;
+
+import retrofit2.Call;
+import retrofit2.http.Body;
+import retrofit2.http.DELETE;
+import retrofit2.http.GET;
+import retrofit2.http.POST;
+import retrofit2.http.PUT;
+import retrofit2.http.Path;
+import retrofit2.http.Query;
+
+public interface InventoryApi {
+
+ // GET /api/v1/inventory?q=...&page=...&size=...
+ @GET("api/v1/inventory")
+ Call> getAllInventory(
+ @Query("q") String query,
+ @Query("page") int page,
+ @Query("size") int size,
+ @Query("sort") String sort);
+
+ // GET /api/v1/inventory/{id}
+ @GET("api/v1/inventory/{id}")
+ Call getInventoryById(@Path("id") Long id);
+
+ // POST /api/v1/inventory
+ @POST("api/v1/inventory")
+ Call createInventory(@Body InventoryRequest request);
+
+ // PUT /api/v1/inventory/{id}
+ @PUT("api/v1/inventory/{id}")
+ Call updateInventory(@Path("id") Long id, @Body InventoryRequest request);
+
+ // DELETE /api/v1/inventory/{id}
+ @DELETE("api/v1/inventory/{id}")
+ Call deleteInventory(@Path("id") Long id);
+
+ // DELETE /api/v1/inventory (bulk delete)
+ @DELETE("api/v1/inventory")
+ Call bulkDeleteInventory(@Body BulkDeleteRequest request);
+}
diff --git a/android/app/src/main/java/com/example/petstoremobile/api/ProductApi.java b/android/app/src/main/java/com/example/petstoremobile/api/ProductApi.java
new file mode 100644
index 00000000..422a39e5
--- /dev/null
+++ b/android/app/src/main/java/com/example/petstoremobile/api/ProductApi.java
@@ -0,0 +1,27 @@
+package com.example.petstoremobile.api;
+
+import com.example.petstoremobile.dtos.PageResponse;
+import com.example.petstoremobile.dtos.ProductDTO;
+import retrofit2.Call;
+import retrofit2.http.*;
+
+public interface ProductApi {
+
+ @GET("api/v1/products")
+ Call> getAllProducts(
+ @Query("q") String query,
+ @Query("page") int page,
+ @Query("size") int size);
+
+ @GET("api/v1/products/{id}")
+ Call getProductById(@Path("id") Long id);
+
+ @POST("api/v1/products")
+ Call createProduct(@Body ProductDTO product);
+
+ @PUT("api/v1/products/{id}")
+ Call updateProduct(@Path("id") Long id, @Body ProductDTO product);
+
+ @DELETE("api/v1/products/{id}")
+ Call deleteProduct(@Path("id") Long id);
+}
\ No newline at end of file
diff --git a/android/app/src/main/java/com/example/petstoremobile/api/ProductSupplierApi.java b/android/app/src/main/java/com/example/petstoremobile/api/ProductSupplierApi.java
new file mode 100644
index 00000000..32810b12
--- /dev/null
+++ b/android/app/src/main/java/com/example/petstoremobile/api/ProductSupplierApi.java
@@ -0,0 +1,28 @@
+package com.example.petstoremobile.api;
+
+import com.example.petstoremobile.dtos.PageResponse;
+import com.example.petstoremobile.dtos.ProductSupplierDTO;
+import retrofit2.Call;
+import retrofit2.http.*;
+
+public interface ProductSupplierApi {
+
+ @GET("api/v1/product-suppliers")
+ Call> getAllProductSuppliers(
+ @Query("page") int page,
+ @Query("size") int size);
+
+ @POST("api/v1/product-suppliers")
+ Call createProductSupplier(@Body ProductSupplierDTO dto);
+
+ @PUT("api/v1/product-suppliers/{productId}/{supplierId}")
+ Call updateProductSupplier(
+ @Path("productId") Long productId,
+ @Path("supplierId") Long supplierId,
+ @Body ProductSupplierDTO dto);
+
+ @DELETE("api/v1/product-suppliers/{productId}/{supplierId}")
+ Call deleteProductSupplier(
+ @Path("productId") Long productId,
+ @Path("supplierId") Long supplierId);
+}
\ No newline at end of file
diff --git a/android/app/src/main/java/com/example/petstoremobile/api/PurchaseOrderApi.java b/android/app/src/main/java/com/example/petstoremobile/api/PurchaseOrderApi.java
new file mode 100644
index 00000000..1e4f4ffe
--- /dev/null
+++ b/android/app/src/main/java/com/example/petstoremobile/api/PurchaseOrderApi.java
@@ -0,0 +1,19 @@
+package com.example.petstoremobile.api;
+
+import com.example.petstoremobile.dtos.PageResponse;
+import com.example.petstoremobile.dtos.PurchaseOrderDTO;
+import retrofit2.Call;
+import retrofit2.http.GET;
+import retrofit2.http.Path;
+import retrofit2.http.Query;
+
+public interface PurchaseOrderApi {
+
+ @GET("api/v1/purchase-orders")
+ Call> getAllPurchaseOrders(
+ @Query("page") int page,
+ @Query("size") int size);
+
+ @GET("api/v1/purchase-orders/{id}")
+ Call getPurchaseOrderById(@Path("id") Long id);
+}
\ No newline at end of file
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
index 1500315f..b08af532 100644
--- a/android/app/src/main/java/com/example/petstoremobile/api/RetrofitClient.java
+++ b/android/app/src/main/java/com/example/petstoremobile/api/RetrofitClient.java
@@ -2,7 +2,9 @@ package com.example.petstoremobile.api;
import android.content.Context;
import android.os.Build;
+import android.util.Log;
+import com.example.petstoremobile.BuildConfig;
import com.example.petstoremobile.api.auth.AuthApi;
import com.example.petstoremobile.api.auth.AuthInterceptor;
@@ -11,24 +13,32 @@ import okhttp3.logging.HttpLoggingInterceptor;
import retrofit2.Retrofit;
import retrofit2.converter.gson.GsonConverterFactory;
+import java.util.concurrent.TimeUnit;
+
//Retrofit client Used for API calls
public class RetrofitClient {
+ private static final String TAG = "RetrofitClient";
public static final String BASE_URL = getBaseUrl();
// Helper function to determine BASE_URL based on whether we are testing on an emulator or a real device
private static String getBaseUrl() {
- if (Build.FINGERPRINT.contains("generic")
- || Build.FINGERPRINT.contains("unknown")
+ String url = isEmulator() ? BuildConfig.EMULATOR_BACKEND_URL : BuildConfig.DEVICE_BACKEND_URL;
+ Log.i(TAG, "Using backend URL: " + url + " (emulator=" + isEmulator() + ")");
+ return url;
+ }
+
+ private static boolean isEmulator() {
+ return Build.FINGERPRINT.startsWith("generic")
+ || Build.FINGERPRINT.startsWith("unknown")
|| Build.MODEL.contains("google_sdk")
|| Build.MODEL.contains("Emulator")
|| Build.MODEL.contains("Android SDK built for x86")
|| Build.MANUFACTURER.contains("Genymotion")
- || (Build.BRAND.startsWith("generic") && Build.DEVICE.startsWith("generic"))
- || "google_sdk".equals(Build.PRODUCT)) {
- return "http://10.0.2.2:8080/"; //emulator testing
- } else {
- return "http://10.0.0.200:8080/"; //Hardware testing
- }
+ || Build.HARDWARE.contains("goldfish")
+ || Build.HARDWARE.contains("ranchu")
+ || Build.PRODUCT.contains("sdk")
+ || Build.PRODUCT.contains("sdk_gphone")
+ || (Build.BRAND.startsWith("generic") && Build.DEVICE.startsWith("generic"));
}
private static Retrofit retrofit = null;
@@ -41,6 +51,9 @@ public class RetrofitClient {
OkHttpClient client = new OkHttpClient.Builder()
.addInterceptor(interceptor)
.addInterceptor(new AuthInterceptor(context))
+ .connectTimeout(30, TimeUnit.SECONDS)
+ .readTimeout(30, TimeUnit.SECONDS)
+ .writeTimeout(30, TimeUnit.SECONDS)
.build();
//build the retrofit object with all needed properties
@@ -66,6 +79,34 @@ public class RetrofitClient {
return getClient(context).create(SupplierApi.class);
}
+ public static AdoptionApi getAdoptionApi(Context context) {
+ return getClient(context).create(AdoptionApi.class);
+ }
+
+ public static AppointmentApi getAppointmentApi(Context context) {
+ return getClient(context).create(AppointmentApi.class);
+ }
+
+ public static ProductApi getProductApi(Context context) {
+ return getClient(context).create(ProductApi.class);
+ }
+
+ public static SaleApi getSaleApi(Context context) {
+ return getClient(context).create(SaleApi.class);
+ }
+
+ public static PurchaseOrderApi getPurchaseOrderApi(Context context) {
+ return getClient(context).create(PurchaseOrderApi.class);
+ }
+
+ public static ProductSupplierApi getProductSupplierApi(Context context) {
+ return getClient(context).create(ProductSupplierApi.class);
+ }
+
+ public static InventoryApi getInventoryApi(Context context) {
+ return getClient(context).create(InventoryApi.class);
+ }
+
public static AuthApi getAuthApi(Context context) {
return getClient(context).create(AuthApi.class);
}
@@ -82,4 +123,12 @@ public class RetrofitClient {
return getClient(context).create(MessageApi.class);
}
+ public static StoreApi getStoreApi(Context context) {
+ return getClient(context).create(StoreApi.class);
+ }
+
+ public static CategoryApi getCategoryApi(Context context) {
+ return getClient(context).create(CategoryApi.class);
+ }
+
}
diff --git a/android/app/src/main/java/com/example/petstoremobile/api/SaleApi.java b/android/app/src/main/java/com/example/petstoremobile/api/SaleApi.java
new file mode 100644
index 00000000..55d1eef8
--- /dev/null
+++ b/android/app/src/main/java/com/example/petstoremobile/api/SaleApi.java
@@ -0,0 +1,33 @@
+package com.example.petstoremobile.api;
+
+import com.example.petstoremobile.dtos.PageResponse;
+import com.example.petstoremobile.dtos.SaleDTO;
+
+import retrofit2.Call;
+import retrofit2.http.Body;
+import retrofit2.http.DELETE;
+import retrofit2.http.GET;
+import retrofit2.http.POST;
+import retrofit2.http.PUT;
+import retrofit2.http.Path;
+import retrofit2.http.Query;
+
+public interface SaleApi {
+
+ @GET("api/v1/sales")
+ Call> getAllSales(
+ @Query("page") int page,
+ @Query("size") int size);
+
+ @GET("api/v1/sales/{id}")
+ Call getSaleById(@Path("id") Long id);
+
+ @POST("api/v1/sales")
+ Call createSale(@Body SaleDTO sale);
+
+ @PUT("api/v1/sales/{id}")
+ Call updateSale(@Path("id") Long id, @Body SaleDTO sale);
+
+ @DELETE("api/v1/sales/{id}")
+ Call deleteSale(@Path("id") Long id);
+}
\ No newline at end of file
diff --git a/android/app/src/main/java/com/example/petstoremobile/api/StoreApi.java b/android/app/src/main/java/com/example/petstoremobile/api/StoreApi.java
new file mode 100644
index 00000000..8fe7f9c1
--- /dev/null
+++ b/android/app/src/main/java/com/example/petstoremobile/api/StoreApi.java
@@ -0,0 +1,16 @@
+package com.example.petstoremobile.api;
+
+import com.example.petstoremobile.dtos.PageResponse;
+import com.example.petstoremobile.dtos.StoreDTO;
+
+import retrofit2.Call;
+import retrofit2.http.GET;
+import retrofit2.http.Query;
+
+public interface StoreApi {
+
+ @GET("api/v1/stores")
+ Call> getAllStores(
+ @Query("page") int page,
+ @Query("size") int size);
+}
diff --git a/android/app/src/main/java/com/example/petstoremobile/dtos/AdoptionDTO.java b/android/app/src/main/java/com/example/petstoremobile/dtos/AdoptionDTO.java
new file mode 100644
index 00000000..03758473
--- /dev/null
+++ b/android/app/src/main/java/com/example/petstoremobile/dtos/AdoptionDTO.java
@@ -0,0 +1,68 @@
+package com.example.petstoremobile.dtos;
+
+import java.math.BigDecimal;
+
+public class AdoptionDTO {
+ private Long adoptionId;
+ private Long petId;
+ private String petName;
+ private Long customerId;
+ private String customerName;
+ private String adoptionDate;
+ private String adoptionStatus;
+ private BigDecimal adoptionFee;
+ private String createdAt;
+ private String updatedAt;
+
+ // Constructor for create/update requests
+ public AdoptionDTO(Long petId, Long customerId, String adoptionDate, String adoptionStatus) {
+ this.petId = petId;
+ this.customerId = customerId;
+ this.adoptionDate = adoptionDate;
+ this.adoptionStatus = adoptionStatus;
+ }
+
+ public Long getAdoptionId() {
+ return adoptionId;
+ }
+
+ public Long getPetId() {
+ return petId;
+ }
+
+ public String getPetName() {
+ return petName;
+ }
+
+ public Long getCustomerId() {
+ return customerId;
+ }
+
+ public String getCustomerName() {
+ return customerName;
+ }
+
+ public String getAdoptionDate() {
+ return adoptionDate;
+ }
+
+ public String getAdoptionStatus() {
+ return adoptionStatus;
+ }
+
+ public String getStatus() {
+ return adoptionStatus;
+ }
+
+ public BigDecimal getAdoptionFee() {
+ return adoptionFee;
+ }
+
+ public String getCreatedAt() {
+ return createdAt;
+ }
+
+ public String getUpdatedAt() {
+ return updatedAt;
+ }
+}
\ No newline at end of file
diff --git a/android/app/src/main/java/com/example/petstoremobile/dtos/AppointmentDTO.java b/android/app/src/main/java/com/example/petstoremobile/dtos/AppointmentDTO.java
new file mode 100644
index 00000000..4c7a91b7
--- /dev/null
+++ b/android/app/src/main/java/com/example/petstoremobile/dtos/AppointmentDTO.java
@@ -0,0 +1,120 @@
+package com.example.petstoremobile.dtos;
+
+import java.math.BigDecimal;
+import java.util.List;
+
+public class AppointmentDTO {
+ // Response fields (from server)
+ private Long appointmentId;
+ private Long customerId;
+ private String customerName;
+ private Long storeId;
+ private String storeName;
+ private Long serviceId;
+ private String serviceName;
+ private String appointmentDate;
+ private String appointmentTime;
+ private String appointmentStatus;
+ private List petNames;
+ private List petIds;
+ private String createdAt;
+ private String updatedAt;
+
+ // Constructor for CREATE/UPDATE request body
+ // Matches AppointmentRequest exactly
+ public AppointmentDTO(Long customerId, Long storeId, Long serviceId,
+ String appointmentDate, String appointmentTime,
+ String appointmentStatus, List petIds) {
+ this.customerId = customerId;
+ this.storeId = storeId;
+ this.serviceId = serviceId;
+ this.appointmentDate = appointmentDate;
+ this.appointmentTime = appointmentTime;
+ this.appointmentStatus = appointmentStatus;
+ this.petIds = petIds;
+ }
+
+ // Getters
+ public Long getAppointmentId() {
+ return appointmentId;
+ }
+
+ public Long getCustomerId() {
+ return customerId;
+ }
+
+ public String getCustomerName() {
+ return customerName;
+ }
+
+ public Long getStoreId() {
+ return storeId;
+ }
+
+ public String getStoreName() {
+ return storeName;
+ }
+
+ public Long getServiceId() {
+ return serviceId;
+ }
+
+ public String getServiceName() {
+ return serviceName;
+ }
+
+ public String getAppointmentDate() {
+ return appointmentDate;
+ }
+
+ public String getAppointmentTime() {
+ return appointmentTime;
+ }
+
+ public String getAppointmentStatus() {
+ return appointmentStatus;
+ }
+
+ public List getPetNames() {
+ return petNames;
+ }
+
+ public List getPetIds() {
+ return petIds;
+ }
+
+ public String getCreatedAt() {
+ return createdAt;
+ }
+
+ public String getUpdatedAt() {
+ return updatedAt;
+ }
+
+ // Convenience getters for adapter/list display
+ public String getPetName() {
+ return (petNames != null && !petNames.isEmpty()) ? petNames.get(0) : "";
+ }
+
+ public Long getPetID() {
+ return (petIds != null && !petIds.isEmpty()) ? petIds.get(0) : null;
+ }
+
+ public Long getPetId() {
+ return getPetID();
+ }
+
+ // Keep old name so adapter doesn't break
+ public String getServiceType() {
+ return serviceName;
+ }
+
+ public Long getServiceID() {
+ return serviceId;
+ }
+
+ // Status alias
+ public String getStatus() {
+ return appointmentStatus;
+ }
+}
diff --git a/android/app/src/main/java/com/example/petstoremobile/dtos/BulkDeleteRequest.java b/android/app/src/main/java/com/example/petstoremobile/dtos/BulkDeleteRequest.java
new file mode 100644
index 00000000..49f92f06
--- /dev/null
+++ b/android/app/src/main/java/com/example/petstoremobile/dtos/BulkDeleteRequest.java
@@ -0,0 +1,22 @@
+package com.example.petstoremobile.dtos;
+
+import java.util.List;
+
+public class BulkDeleteRequest {
+ private List ids;
+
+ public BulkDeleteRequest() {
+ }
+
+ public BulkDeleteRequest(List ids) {
+ this.ids = ids;
+ }
+
+ public List getIds() {
+ return ids;
+ }
+
+ public void setIds(List ids) {
+ this.ids = ids;
+ }
+}
diff --git a/android/app/src/main/java/com/example/petstoremobile/dtos/CategoryDTO.java b/android/app/src/main/java/com/example/petstoremobile/dtos/CategoryDTO.java
new file mode 100644
index 00000000..6596845b
--- /dev/null
+++ b/android/app/src/main/java/com/example/petstoremobile/dtos/CategoryDTO.java
@@ -0,0 +1,29 @@
+package com.example.petstoremobile.dtos;
+
+public class CategoryDTO {
+ private Long categoryId;
+ private String categoryName;
+ private String categoryType;
+ private String createdAt;
+ private String updatedAt;
+
+ public Long getCategoryId() {
+ return categoryId;
+ }
+
+ public String getCategoryName() {
+ return categoryName;
+ }
+
+ public String getCategoryType() {
+ return categoryType;
+ }
+
+ public String getCreatedAt() {
+ return createdAt;
+ }
+
+ public String getUpdatedAt() {
+ return updatedAt;
+ }
+}
\ 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
index 1a135a6d..178b0033 100644
--- a/android/app/src/main/java/com/example/petstoremobile/dtos/CustomerDTO.java
+++ b/android/app/src/main/java/com/example/petstoremobile/dtos/CustomerDTO.java
@@ -1,59 +1,38 @@
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() {}
+ private String createdAt;
+ private String updatedAt;
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 : "");
+ return firstName + " " + lastName;
+ }
+
+ public String getCreatedAt() {
+ return createdAt;
+ }
+
+ public String getUpdatedAt() {
+ return updatedAt;
}
}
\ No newline at end of file
diff --git a/android/app/src/main/java/com/example/petstoremobile/dtos/InventoryDTO.java b/android/app/src/main/java/com/example/petstoremobile/dtos/InventoryDTO.java
new file mode 100644
index 00000000..fe2ec542
--- /dev/null
+++ b/android/app/src/main/java/com/example/petstoremobile/dtos/InventoryDTO.java
@@ -0,0 +1,77 @@
+package com.example.petstoremobile.dtos;
+
+public class InventoryDTO {
+ // Response fields (from backend InventoryResponse)
+ private Long inventoryId;
+ private Long prodId;
+ private String productName;
+ private String categoryName;
+ private Integer quantity;
+ private String createdAt;
+ private String updatedAt;
+
+ public InventoryDTO() {
+ }
+
+ // Constructor for create/update requests (matches InventoryRequest)
+ public InventoryDTO(Long prodId, Integer quantity) {
+ this.prodId = prodId;
+ this.quantity = quantity;
+ }
+
+ public Long getInventoryId() {
+ return inventoryId;
+ }
+
+ public void setInventoryId(Long inventoryId) {
+ this.inventoryId = inventoryId;
+ }
+
+ public Long getProdId() {
+ return prodId;
+ }
+
+ public void setProdId(Long prodId) {
+ this.prodId = prodId;
+ }
+
+ public String getProductName() {
+ return productName;
+ }
+
+ public void setProductName(String productName) {
+ this.productName = productName;
+ }
+
+ public String getCategoryName() {
+ return categoryName;
+ }
+
+ public void setCategoryName(String categoryName) {
+ this.categoryName = categoryName;
+ }
+
+ public Integer getQuantity() {
+ return quantity;
+ }
+
+ public void setQuantity(Integer quantity) {
+ this.quantity = quantity;
+ }
+
+ public String getCreatedAt() {
+ return createdAt;
+ }
+
+ public void setCreatedAt(String createdAt) {
+ this.createdAt = createdAt;
+ }
+
+ public String getUpdatedAt() {
+ return updatedAt;
+ }
+
+ public void setUpdatedAt(String updatedAt) {
+ this.updatedAt = updatedAt;
+ }
+}
diff --git a/android/app/src/main/java/com/example/petstoremobile/dtos/InventoryRequest.java b/android/app/src/main/java/com/example/petstoremobile/dtos/InventoryRequest.java
new file mode 100644
index 00000000..f84dfb5f
--- /dev/null
+++ b/android/app/src/main/java/com/example/petstoremobile/dtos/InventoryRequest.java
@@ -0,0 +1,31 @@
+package com.example.petstoremobile.dtos;
+
+public class InventoryRequest {
+ private Long prodId;
+ private Integer quantity;
+
+ public InventoryRequest() {
+ }
+
+ public InventoryRequest(Long prodId, Integer quantity) {
+ this.prodId = prodId;
+ this.quantity = quantity;
+ }
+
+ public Long getProdId() {
+ return prodId;
+ }
+
+ public void setProdId(Long prodId) {
+ this.prodId = prodId;
+ }
+
+ public Integer getQuantity() {
+ return quantity;
+ }
+
+ public void setQuantity(Integer quantity) {
+ this.quantity = quantity;
+ }
+}
+
diff --git a/android/app/src/main/java/com/example/petstoremobile/dtos/ProductDTO.java b/android/app/src/main/java/com/example/petstoremobile/dtos/ProductDTO.java
new file mode 100644
index 00000000..2b016b28
--- /dev/null
+++ b/android/app/src/main/java/com/example/petstoremobile/dtos/ProductDTO.java
@@ -0,0 +1,81 @@
+package com.example.petstoremobile.dtos;
+
+import java.math.BigDecimal;
+
+public class ProductDTO {
+ private Long prodId;
+ private String prodName;
+ private Long categoryId;
+ private String categoryName;
+ private String prodDesc;
+ private BigDecimal prodPrice;
+ private String createdAt;
+ private String updatedAt;
+
+ public ProductDTO() {
+ }
+
+ // Constructor for create/update
+ public ProductDTO(String prodName, Long categoryId, String prodDesc, BigDecimal prodPrice) {
+ this.prodName = prodName;
+ this.categoryId = categoryId;
+ this.prodDesc = prodDesc;
+ this.prodPrice = prodPrice;
+ }
+
+ public Long getProdId() {
+ return prodId;
+ }
+
+ public void setProdId(Long prodId) {
+ this.prodId = prodId;
+ }
+
+ public String getProdName() {
+ return prodName;
+ }
+
+ public void setProdName(String prodName) {
+ this.prodName = prodName;
+ }
+
+ public Long getCategoryId() {
+ return categoryId;
+ }
+
+ public void setCategoryId(Long categoryId) {
+ this.categoryId = categoryId;
+ }
+
+ public String getCategoryName() {
+ return categoryName;
+ }
+
+ public void setCategoryName(String categoryName) {
+ this.categoryName = categoryName;
+ }
+
+ public String getProdDesc() {
+ return prodDesc;
+ }
+
+ public void setProdDesc(String prodDesc) {
+ this.prodDesc = prodDesc;
+ }
+
+ public BigDecimal getProdPrice() {
+ return prodPrice;
+ }
+
+ public void setProdPrice(BigDecimal prodPrice) {
+ this.prodPrice = prodPrice;
+ }
+
+ public String getCreatedAt() {
+ return createdAt;
+ }
+
+ public String getUpdatedAt() {
+ return updatedAt;
+ }
+}
\ No newline at end of file
diff --git a/android/app/src/main/java/com/example/petstoremobile/dtos/ProductSupplierDTO.java b/android/app/src/main/java/com/example/petstoremobile/dtos/ProductSupplierDTO.java
new file mode 100644
index 00000000..887d29e1
--- /dev/null
+++ b/android/app/src/main/java/com/example/petstoremobile/dtos/ProductSupplierDTO.java
@@ -0,0 +1,48 @@
+package com.example.petstoremobile.dtos;
+
+import java.math.BigDecimal;
+
+public class ProductSupplierDTO {
+ private Long productId;
+ private String productName;
+ private Long supplierId;
+ private String supplierName;
+ private BigDecimal cost;
+ private String createdAt;
+ private String updatedAt;
+
+ // Constructor for create/update
+ public ProductSupplierDTO(Long productId, Long supplierId, BigDecimal cost) {
+ this.productId = productId;
+ this.supplierId = supplierId;
+ this.cost = cost;
+ }
+
+ public Long getProductId() {
+ return productId;
+ }
+
+ public String getProductName() {
+ return productName;
+ }
+
+ public Long getSupplierId() {
+ return supplierId;
+ }
+
+ public String getSupplierName() {
+ return supplierName;
+ }
+
+ public BigDecimal getCost() {
+ return cost;
+ }
+
+ public String getCreatedAt() {
+ return createdAt;
+ }
+
+ public String getUpdatedAt() {
+ return updatedAt;
+ }
+}
\ No newline at end of file
diff --git a/android/app/src/main/java/com/example/petstoremobile/dtos/PurchaseOrderDTO.java b/android/app/src/main/java/com/example/petstoremobile/dtos/PurchaseOrderDTO.java
new file mode 100644
index 00000000..d7a392ea
--- /dev/null
+++ b/android/app/src/main/java/com/example/petstoremobile/dtos/PurchaseOrderDTO.java
@@ -0,0 +1,39 @@
+package com.example.petstoremobile.dtos;
+
+public class PurchaseOrderDTO {
+ private Long purchaseOrderId;
+ private Long supId;
+ private String supplierName;
+ private String orderDate;
+ private String status;
+ private String createdAt;
+ private String updatedAt;
+
+ public Long getPurchaseOrderId() {
+ return purchaseOrderId;
+ }
+
+ public Long getSupId() {
+ return supId;
+ }
+
+ public String getSupplierName() {
+ return supplierName;
+ }
+
+ public String getOrderDate() {
+ return orderDate;
+ }
+
+ public String getStatus() {
+ return status;
+ }
+
+ public String getCreatedAt() {
+ return createdAt;
+ }
+
+ public String getUpdatedAt() {
+ return updatedAt;
+ }
+}
\ No newline at end of file
diff --git a/android/app/src/main/java/com/example/petstoremobile/dtos/SaleDTO.java b/android/app/src/main/java/com/example/petstoremobile/dtos/SaleDTO.java
new file mode 100644
index 00000000..2cdb625f
--- /dev/null
+++ b/android/app/src/main/java/com/example/petstoremobile/dtos/SaleDTO.java
@@ -0,0 +1,82 @@
+package com.example.petstoremobile.dtos;
+
+public class SaleDTO {
+
+ private Long saleId;
+
+ private Long productId;
+ private String productName;
+
+ private Integer quantity;
+ private Double price;
+ private Double totalAmount;
+
+ private String saleDate;
+ private String customerName;
+
+ public SaleDTO() {}
+
+ public Long getSaleId() {
+ return saleId;
+ }
+
+ public void setSaleId(Long saleId) {
+ this.saleId = saleId;
+ }
+
+ public Long getProductId() {
+ return productId;
+ }
+
+ public void setProductId(Long productId) {
+ this.productId = productId;
+ }
+
+ public String getProductName() {
+ return productName;
+ }
+
+ public void setProductName(String productName) {
+ this.productName = productName;
+ }
+
+ public Integer getQuantity() {
+ return quantity;
+ }
+
+ public void setQuantity(Integer quantity) {
+ this.quantity = quantity;
+ }
+
+ public Double getPrice() {
+ return price;
+ }
+
+ public void setPrice(Double price) {
+ this.price = price;
+ }
+
+ public Double getTotalAmount() {
+ return totalAmount;
+ }
+
+ public void setTotalAmount(Double totalAmount) {
+ this.totalAmount = totalAmount;
+ }
+
+ public String getSaleDate() {
+ return saleDate;
+ }
+
+ public void setSaleDate(String saleDate) {
+ this.saleDate = saleDate;
+ }
+
+ public String getCustomerName() {
+ return customerName;
+ }
+
+ public void setCustomerName(String customerName) {
+ this.customerName = customerName;
+ }
+}
\ No newline at end of file
diff --git a/android/app/src/main/java/com/example/petstoremobile/dtos/StoreDTO.java b/android/app/src/main/java/com/example/petstoremobile/dtos/StoreDTO.java
new file mode 100644
index 00000000..da66f046
--- /dev/null
+++ b/android/app/src/main/java/com/example/petstoremobile/dtos/StoreDTO.java
@@ -0,0 +1,25 @@
+package com.example.petstoremobile.dtos;
+
+public class StoreDTO {
+ private Long storeId;
+ private String storeName;
+ private String address;
+ private String phone;
+ private String email;
+ private String createdAt;
+ private String updatedAt;
+
+ // Constructor for hardcoded fallback
+ public StoreDTO(Long storeId, String storeName) {
+ this.storeId = storeId;
+ this.storeName = storeName;
+ }
+
+ public Long getStoreId() { return storeId; }
+ public String getStoreName() { return storeName; }
+ public String getAddress() { return address; }
+ public String getPhone() { return phone; }
+ public String getEmail() { return email; }
+ public String getCreatedAt() { return createdAt; }
+ public String getUpdatedAt() { return updatedAt; }
+}
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
index 75d8aacd..b63b42b1 100644
--- a/android/app/src/main/java/com/example/petstoremobile/fragments/ListFragment.java
+++ b/android/app/src/main/java/com/example/petstoremobile/fragments/ListFragment.java
@@ -22,6 +22,9 @@ import com.example.petstoremobile.fragments.listfragments.AdoptionFragment;
import com.example.petstoremobile.fragments.listfragments.AppointmentFragment;
import com.example.petstoremobile.fragments.listfragments.InventoryFragment;
import com.example.petstoremobile.fragments.listfragments.ProductFragment;
+import com.example.petstoremobile.fragments.listfragments.ProductSupplierFragment;
+import com.example.petstoremobile.fragments.listfragments.PurchaseOrderFragment;
+import com.example.petstoremobile.fragments.listfragments.SaleFragment;
//The Fragment for the displaying the list of entities to be viewed
public class ListFragment extends Fragment {
@@ -32,7 +35,7 @@ public class ListFragment extends Fragment {
// Adoptions, Appointments, Inventory, Products
- private LinearLayout drawerAdoptions, drawerAppointments, drawerInventory, drawerProducts;
+ private LinearLayout drawerAdoptions, drawerAppointments, drawerInventory, drawerProducts, drawerProductSupplier, drawerPurchaseOrderView, drawerSale;
@Override
@@ -49,6 +52,10 @@ public class ListFragment extends Fragment {
drawerAppointments = view.findViewById(R.id.drawerAppointments);
drawerInventory = view.findViewById(R.id.drawerInventory);
drawerProducts = view.findViewById(R.id.drawerProducts);
+ drawerProductSupplier=view.findViewById(R.id.drawerProductSupplier);
+ drawerSale=view.findViewById(R.id.drawerSale);
+ drawerPurchaseOrderView=view.findViewById(R.id.drawerPurchaseOrderView);
+
// Check user role and restrict access for STAFF
String role = TokenManager.getInstance(requireContext()).getRole();
@@ -116,7 +123,7 @@ public class ListFragment extends Fragment {
drawerLayout.closeDrawers();
});
- //Appoinment
+ //Appointment
drawerAppointments.setOnClickListener(v -> {
loadFragment(new AppointmentFragment());
drawerLayout.closeDrawers();
@@ -134,6 +141,27 @@ public class ListFragment extends Fragment {
drawerLayout.closeDrawers();
});
+ //ProductSupplier
+
+ drawerProductSupplier.setOnClickListener(v -> {
+ loadFragment(new ProductSupplierFragment());
+ drawerLayout.closeDrawers();
+ });
+
+ //Purchase
+
+ drawerPurchaseOrderView.setOnClickListener(v -> {
+ loadFragment(new PurchaseOrderFragment());
+ drawerLayout.closeDrawers();
+ });
+
+ //Sale
+
+ drawerSale.setOnClickListener(v -> {
+ loadFragment(new SaleFragment());
+ drawerLayout.closeDrawers();
+ });
+
return view;
}
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
index f72d6836..34e7d602 100644
--- 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
@@ -1,36 +1,33 @@
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 android.text.*;
+import android.util.Log;
+import android.view.*;
+import android.widget.*;
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.api.AdoptionApi;
+import com.example.petstoremobile.api.RetrofitClient;
+import com.example.petstoremobile.dtos.AdoptionDTO;
+import com.example.petstoremobile.dtos.PageResponse;
import com.example.petstoremobile.fragments.ListFragment;
import com.example.petstoremobile.fragments.listfragments.detailfragments.AdoptionDetailFragment;
-import com.example.petstoremobile.models.Adoption;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
-import java.util.ArrayList;
-import java.util.List;
+import java.util.*;
+import retrofit2.*;
public class AdoptionFragment extends Fragment implements AdoptionAdapter.OnAdoptionClickListener {
- private List adoptionList = new ArrayList<>();
- private List filteredList = new ArrayList<>();
+ private List adoptionList = new ArrayList<>();
+ private List filteredList = new ArrayList<>();
private AdoptionAdapter adapter;
- private SwipeRefreshLayout swipeRefreshLayout;
+ private AdoptionApi api;
+ private SwipeRefreshLayout swipeRefresh;
private EditText etSearch;
private ImageButton hamburger;
@@ -39,51 +36,58 @@ public class AdoptionFragment extends Fragment implements AdoptionAdapter.OnAdop
Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_adoption, container, false);
- hamburger = view.findViewById(R.id.btnHamburger);
+ api = RetrofitClient.getAdoptionApi(requireContext());
+ hamburger = view.findViewById(R.id.btnHamburgerAdoption);
- loadAdoptionData();
- // Replace with actual API call when backend is ready
setupRecyclerView(view);
setupSearch(view);
setupSwipeRefresh(view);
+ loadAdoptions();
- FloatingActionButton fabAddAdoption = view.findViewById(R.id.fabAddAdoption);
- fabAddAdoption.setOnClickListener(v -> openAdoptionDetails(-1));
+ FloatingActionButton fab = view.findViewById(R.id.fabAddAdoption);
+ fab.setOnClickListener(v -> openDetail(-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();
- }
+ ListFragment lf = (ListFragment) getParentFragment();
+ if (lf != null) lf.openDrawer();
});
return view;
}
- // Filters adoption list by adopter name or pet name
+ private void setupRecyclerView(View view) {
+ RecyclerView rv = view.findViewById(R.id.recyclerViewAdoptions);
+ adapter = new AdoptionAdapter(filteredList, this);
+ rv.setLayoutManager(new LinearLayoutManager(getContext()));
+ rv.setAdapter(adapter);
+ }
+
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());
+ public void beforeTextChanged(CharSequence s, int a, int b, int c) {}
+ public void afterTextChanged(Editable s) {}
+ public void onTextChanged(CharSequence s, int a, int b, int c) {
+ filter(s.toString());
}
- @Override public void afterTextChanged(Editable s) {}
});
}
- private void filterAdoptions(String query) {
+ private void setupSwipeRefresh(View view) {
+ swipeRefresh = view.findViewById(R.id.swipeRefreshAdoption);
+ swipeRefresh.setOnRefreshListener(this::loadAdoptions);
+ }
+
+ private void filter(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)) {
+ for (AdoptionDTO a : adoptionList) {
+ if ((a.getCustomerName() != null && a.getCustomerName().toLowerCase().contains(lower))
+ || (a.getPetName() != null && a.getPetName().toLowerCase().contains(lower))
+ || (a.getAdoptionStatus() != null && a.getAdoptionStatus().toLowerCase().contains(lower))) {
filteredList.add(a);
}
}
@@ -91,73 +95,47 @@ public class AdoptionFragment extends Fragment implements AdoptionAdapter.OnAdop
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 loadAdoptions() {
+ if (swipeRefresh != null) swipeRefresh.setRefreshing(true);
+ api.getAllAdoptions(0, 100).enqueue(new Callback>() {
+ public void onResponse(Call> c,
+ Response> r) {
+ if (swipeRefresh != null) swipeRefresh.setRefreshing(false);
+ if (r.isSuccessful() && r.body() != null) {
+ adoptionList.clear();
+ adoptionList.addAll(r.body().getContent());
+ filter(etSearch != null ? etSearch.getText().toString() : "");
+ } else {
+ Toast.makeText(getContext(), "Failed to load adoptions", Toast.LENGTH_SHORT).show();
+ Log.e("AdoptionFragment", "Error: " + r.message());
+ }
+ }
+ public void onFailure(Call> c, Throwable t) {
+ if (swipeRefresh != null) swipeRefresh.setRefreshing(false);
+ Toast.makeText(getContext(), "Network error", Toast.LENGTH_SHORT).show();
+ Log.e("AdoptionFragment", t.getMessage());
+ }
});
}
- private void openAdoptionDetails(int position) {
- AdoptionDetailFragment detailFragment = new AdoptionDetailFragment();
+ private void openDetail(int position) {
+ AdoptionDetailFragment detail = 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());
+ AdoptionDTO a = filteredList.get(position);
+ args.putLong("adoptionId", a.getAdoptionId());
+ args.putLong("petId", a.getPetId() != null ? a.getPetId() : -1);
+ args.putLong("customerId", a.getCustomerId() != null ? a.getCustomerId() : -1);
+ args.putString("adoptionDate", a.getAdoptionDate());
+ args.putString("adoptionStatus", a.getAdoptionStatus());
}
- 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());
+ detail.setArguments(args);
+ ListFragment lf = (ListFragment) getParentFragment();
+ if (lf != null) lf.loadFragment(detail);
}
@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);
- }
-}
+ public void onAdoptionClick(int position) { openDetail(position); }
+}
\ No newline at end of file
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
index 09ad489b..f8aa734f 100644
--- 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
@@ -1,35 +1,52 @@
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.annotation.NonNull;
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.AppointmentAdapter;
+import com.example.petstoremobile.api.AppointmentApi;
+import com.example.petstoremobile.api.PetApi;
+import com.example.petstoremobile.api.ServiceApi;
+import com.example.petstoremobile.api.RetrofitClient;
+import com.example.petstoremobile.dtos.AppointmentDTO;
+import com.example.petstoremobile.dtos.ServiceDTO;
+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.AppointmentDetailFragment;
-import com.example.petstoremobile.models.Appointment;
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 AppointmentFragment extends Fragment implements AppointmentAdapter.OnAppointmentClickListener {
- private List appointmentList = new ArrayList<>(); // full data list
- private List filteredList = new ArrayList<>(); // filtered display list
+ private List appointmentList = new ArrayList<>();
+ private List filteredList = new ArrayList<>();
+ private List petList = new ArrayList<>();
+ private List serviceList = new ArrayList<>();
+
private AppointmentAdapter adapter;
+ private AppointmentApi api;
private SwipeRefreshLayout swipeRefreshLayout;
private EditText etSearch;
private ImageButton hamburger;
@@ -39,51 +56,57 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter.
Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_appointment, container, false);
+ api = RetrofitClient.getAppointmentApi(requireContext());
hamburger = view.findViewById(R.id.btnHamburger);
- loadAppointmentData(); // TODO: Replace with actual API call when backend is ready
setupRecyclerView(view);
setupSearch(view);
setupSwipeRefresh(view);
+ loadAppointmentData();
+ loadPets();
+ loadServices();
- FloatingActionButton fabAddAppointment = view.findViewById(R.id.fabAddAppointment);
- fabAddAppointment.setOnClickListener(v -> openAppointmentDetails(-1));
- //Make the hamburger button open the drawer from listFragment
+ FloatingActionButton fabAdd = view.findViewById(R.id.fabAddAppointment);
+ fabAdd.setOnClickListener(v -> openAppointmentDetails(-1));
+
hamburger.setOnClickListener(v -> {
ListFragment listFragment = (ListFragment) getParentFragment();
- //if list fragment is found then use its helper function to open the drawer
- if (listFragment != null) {
+ 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) {
+ @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) {}
+
+ @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)) {
+ for (AppointmentDTO a : appointmentList) {
+ if ((a.getCustomerName() != null && a.getCustomerName().toLowerCase().contains(lower))
+ || (a.getServiceType() != null && a.getServiceType().toLowerCase().contains(lower))
+ || (a.getPetName() != null && a.getPetName().toLowerCase().contains(lower))) {
filteredList.add(a);
}
}
@@ -91,43 +114,33 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter.
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);
- });
+ swipeRefreshLayout.setOnRefreshListener(this::loadAppointmentData);
}
private void openAppointmentDetails(int position) {
AppointmentDetailFragment detailFragment = new AppointmentDetailFragment();
Bundle args = new Bundle();
- args.putInt("position", position);
if (position != -1) {
- Appointment appointment = filteredList.get(position);
- // Find the real position in the full list for save/delete callbacks
- int realPosition = appointmentList.indexOf(appointment);
- args.putInt("position", realPosition);
- args.putInt("appointmentId", appointment.getAppointmentId());
- args.putString("customerName", appointment.getCustomerName());
- args.putString("petName", appointment.getPetName());
- args.putString("serviceType", appointment.getServiceType());
- args.putString("appointmentDate", appointment.getAppointmentDate());
- args.putString("appointmentTime", appointment.getAppointmentTime());
- args.putString("status", appointment.getStatus());
+ AppointmentDTO a = filteredList.get(position);
+ args.putLong("appointmentId", a.getAppointmentId());
+ args.putString("appointmentDate", a.getAppointmentDate());
+ args.putString("appointmentTime", a.getAppointmentTime());
+ args.putString("appointmentStatus", a.getAppointmentStatus());
+ // IDs for pre-selecting spinners
+ if (a.getPetID() != null) args.putLong("petId", a.getPetID());
+ if (a.getServiceId() != null) args.putLong("serviceId", a.getServiceId());
+ if (a.getCustomerId() != null) args.putLong("customerId", a.getCustomerId());
+ if (a.getStoreId() != null) args.putLong("storeId", a.getStoreId());
}
detailFragment.setArguments(args);
- detailFragment.setAppointmentFragment(this);
-
- ListFragment listFragment = (ListFragment) getParentFragment();
- if (listFragment != null) listFragment.loadFragment(detailFragment);
+ ListFragment lf = (ListFragment) getParentFragment();
+ if (lf != null) lf.loadFragment(detailFragment);
}
-
- public void onAppointmentSaved(int position, Appointment appointment) {
+ public void onAppointmentSaved(int position, AppointmentDTO appointment) {
if (position == -1) {
appointmentList.add(appointment);
} else {
@@ -146,21 +159,100 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter.
openAppointmentDetails(position);
}
- // Helper function to load hardcoded sample data
- // Replace with API call
private void loadAppointmentData() {
- appointmentList.clear();
- appointmentList.add(new Appointment(1, "John Smith", "Buddy", "Grooming", "2026-03-10", "10:00 AM", "Confirmed"));
- appointmentList.add(new Appointment(2, "Jane Doe", "Luna", "Vet Checkup", "2026-03-11", "02:00 PM", "Pending"));
- appointmentList.add(new Appointment(3, "Bob Lee", "Max", "Training", "2026-03-12", "11:00 AM", "Confirmed"));
- appointmentList.add(new Appointment(4, "Alice Brown", "Milo", "Grooming", "2026-03-13", "03:00 PM", "Cancelled"));
- filteredList.clear();
- filteredList.addAll(appointmentList);
+ if (swipeRefreshLayout != null)
+ swipeRefreshLayout.setRefreshing(true);
+ api.getAllAppointments(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) {
+ appointmentList.clear();
+ appointmentList.addAll(response.body().getContent());
+ filterAppointments(etSearch != null ? etSearch.getText().toString() : "");
+ } else {
+ Log.e("AppointmentFragment", "Error: " + response.message());
+ Toast.makeText(getContext(), "Failed to load appointments", Toast.LENGTH_SHORT).show();
+ }
+ }
+
+ @Override
+ public void onFailure(Call> call, Throwable t) {
+ if (swipeRefreshLayout != null)
+ swipeRefreshLayout.setRefreshing(false);
+ Toast.makeText(getContext(), "Network error: " + t.getMessage(), Toast.LENGTH_SHORT).show();
+ Log.e("AppointmentFragment", t.getMessage());
+ }
+ });
+ }
+
+
+
+ // Load Pets
+ private void loadPets() {
+ PetApi petApi = RetrofitClient.getPetApi(requireContext());
+ petApi.getAllPets(0,100).enqueue(new Callback>() {
+
+ @Override
+ public void onResponse(Call> call, Response> response) {
+ if (response.isSuccessful() && response.body() !=null) {
+ petList.clear();
+ petList.addAll(response.body().getContent());
+ }
+ }
+
+ @Override
+ public void onFailure(Call> call, Throwable t) {
+
+ Log.e("AppointmentFragment", "Pet load error:" + t.getMessage());
+
+ }
+ });
+ }
+
+ // Load Services
+
+ private void loadServices() {
+ ServiceApi serviceApi = RetrofitClient.getServiceApi(requireContext());
+
+ serviceApi.getAllServices(0,100).enqueue(new Callback>() {
+ @Override
+ public void onResponse(Call> call, Response> response) {
+ if (response.isSuccessful() && response.body() != null) {
+ serviceList.clear();
+ serviceList.addAll(response.body().getContent());
+
+ }
+ }
+
+ @Override
+ public void onFailure(Call> call, Throwable t) {
+ Log.e("AppointmentFragmnet", "Service load error: " + t.getMessage());
+
+ }
+ });
+ }
+
+ private String getPetName(Long id) {
+ for (PetDTO p : petList) {
+ if (p.getPetId().equals(id)) return p.getPetName();
+
+ }
+ return "";
+ }
+
+ private String getServiceName(Long id) {
+ for (ServiceDTO s : serviceList) {
+ if (s.getServiceId().equals(id))return s.getServiceName();
+ }
+ return "";
}
private void setupRecyclerView(View view) {
RecyclerView recyclerView = view.findViewById(R.id.recyclerViewAppointments);
- adapter = new AppointmentAdapter(filteredList, this); // adapter uses filteredList
+ adapter = new AppointmentAdapter(filteredList, this);
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
index 38546e83..37e533ee 100644
--- 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
@@ -1,162 +1,379 @@
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 android.os.Handler;
+import android.os.Looper;
+import android.text.Editable;
+import android.text.TextWatcher;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AdapterView;
+import android.widget.ArrayAdapter;
+import android.widget.Button;
+import android.widget.EditText;
+import android.widget.ImageButton;
+import android.widget.Spinner;
+import android.widget.TextView;
+import android.widget.Toast;
+
import androidx.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.BlackTextArrayAdapter;
import com.example.petstoremobile.adapters.InventoryAdapter;
+import com.example.petstoremobile.api.CategoryApi;
+import com.example.petstoremobile.api.InventoryApi;
+import com.example.petstoremobile.api.RetrofitClient;
+import com.example.petstoremobile.dtos.BulkDeleteRequest;
+import com.example.petstoremobile.dtos.CategoryDTO;
+import com.example.petstoremobile.dtos.InventoryDTO;
+import com.example.petstoremobile.dtos.PageResponse;
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;
+import retrofit2.Call;
+import retrofit2.Callback;
+import retrofit2.Response;
+
public class InventoryFragment extends Fragment implements InventoryAdapter.OnInventoryClickListener {
- private List inventoryList = new ArrayList<>();
- private List filteredList = new ArrayList<>();
+ private static final String TAG = "InventoryFragment";
+ private static final int PAGE_SIZE = 20;
+
+ private final List inventoryList = new ArrayList<>();
+ private final List categoryList = new ArrayList<>();
private InventoryAdapter adapter;
+ private InventoryApi inventoryApi;
+ private CategoryApi categoryApi;
+
private SwipeRefreshLayout swipeRefreshLayout;
private EditText etSearch;
+ private Spinner spinnerCategory;
private ImageButton hamburger;
+ private Button btnBulkDelete;
+ private TextView tvSelectionCount;
+
+ // Debounce search
+ private final Handler searchHandler = new Handler(Looper.getMainLooper());
+ private Runnable searchRunnable;
+ private String currentQuery = "";
+
+ // Selected category filter — null means "All"
+ private String selectedCategory = null;
+
+ // Pagination
+ private int currentPage = 0;
+ private boolean isLastPage = false;
+ private boolean isLoading = false;
+
+ // Prevent spinner from firing on initial load
+ private boolean spinnerReady = false;
@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);
+ inventoryApi = RetrofitClient.getInventoryApi(requireContext());
+ categoryApi = RetrofitClient.getCategoryApi(requireContext());
+
+ hamburger = view.findViewById(R.id.btnHamburger);
+ btnBulkDelete = view.findViewById(R.id.btnBulkDelete);
+ tvSelectionCount = view.findViewById(R.id.tvSelectionCount);
+ spinnerCategory = view.findViewById(R.id.spinnerCategory);
- loadInventoryData(); // TODO: Replace with actual API call when backend is ready
setupRecyclerView(view);
setupSearch(view);
setupSwipeRefresh(view);
+ loadCategories(); // loads categories then triggers loadInventory
+ loadInventory(true);
- FloatingActionButton fabAddInventory = view.findViewById(R.id.fabAddInventory);
- fabAddInventory.setOnClickListener(v -> openInventoryDetails(-1));
+ view.findViewById(R.id.fabAddInventory)
+ .setOnClickListener(v -> openDetail(null));
- //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();
- }
+ ListFragment lf = (ListFragment) getParentFragment();
+ if (lf != null)
+ lf.openDrawer();
});
+ btnBulkDelete.setOnClickListener(v -> confirmBulkDelete());
+
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());
+ // Categories
+ private void loadCategories() {
+ categoryApi.getAllCategories(0, 100).enqueue(new Callback>() {
+ @Override
+ public void onResponse(Call> call,
+ Response> response) {
+ if (response.isSuccessful() && response.body() != null) {
+ categoryList.clear();
+ categoryList.addAll(response.body().getContent());
+ setupCategorySpinner();
+ }
+ }
+
+ @Override
+ public void onFailure(Call> call, Throwable t) {
+ Log.e(TAG, "Failed to load categories", t);
+ // Still setup spinner with just "All"
+ setupCategorySpinner();
}
- @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);
+ private void setupCategorySpinner() {
+ // First item is always "All Categories"
+ List categoryNames = new ArrayList<>();
+ categoryNames.add("All Categories");
+ for (CategoryDTO c : categoryList) {
+ categoryNames.add(c.getCategoryName());
+ }
+
+ BlackTextArrayAdapter spinnerAdapter = new BlackTextArrayAdapter<>(
+ requireContext(),
+ android.R.layout.simple_spinner_item,
+ categoryNames);
+ spinnerAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
+ spinnerCategory.setAdapter(spinnerAdapter);
+
+ spinnerCategory.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
+ @Override
+ public void onItemSelected(AdapterView> parent, View view, int position, long id) {
+ if (!spinnerReady) {
+ // Skip the first automatic trigger on setup
+ spinnerReady = true;
+ return;
+ }
+ if (position == 0) {
+ selectedCategory = null; // "All Categories"
+ } else {
+ selectedCategory = categoryList.get(position - 1).getCategoryName();
+ }
+ loadInventory(true);
+ }
+
+ @Override
+ public void onNothingSelected(AdapterView> parent) {
+ }
+ });
+ }
+
+ // Search
+
+ private void setupSearch(View view) {
+ etSearch = view.findViewById(R.id.etSearchInventory);
+ etSearch.addTextChangedListener(new TextWatcher() {
+ @Override
+ public void beforeTextChanged(CharSequence s, int i, int i1, int i2) {
+ }
+
+ @Override
+ public void afterTextChanged(Editable s) {
+ }
+
+ @Override
+ public void onTextChanged(CharSequence s, int start, int before, int count) {
+ if (searchRunnable != null)
+ searchHandler.removeCallbacks(searchRunnable);
+ searchRunnable = () -> {
+ currentQuery = s.toString().trim();
+ loadInventory(true);
+ };
+ searchHandler.postDelayed(searchRunnable, 400);
+ }
+ });
+ }
+
+ // RecyclerView + infinite scroll
+ private void setupRecyclerView(View view) {
+ RecyclerView rv = view.findViewById(R.id.recyclerViewInventory);
+ adapter = new InventoryAdapter(inventoryList, this);
+ rv.setLayoutManager(new LinearLayoutManager(getContext()));
+ rv.setAdapter(adapter);
+
+ rv.addOnScrollListener(new RecyclerView.OnScrollListener() {
+ @Override
+ public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
+ if (dy <= 0)
+ return;
+ LinearLayoutManager lm = (LinearLayoutManager) rv.getLayoutManager();
+ if (lm == null)
+ return;
+ int visible = lm.getChildCount();
+ int total = lm.getItemCount();
+ int firstVis = lm.findFirstVisibleItemPosition();
+ if (!isLoading && !isLastPage && (visible + firstVis) >= total - 3) {
+ loadInventory(false);
}
}
- }
- adapter.notifyDataSetChanged();
+ });
}
private void setupSwipeRefresh(View view) {
swipeRefreshLayout = view.findViewById(R.id.swipeRefreshInventory);
- swipeRefreshLayout.setOnRefreshListener(() -> {
- loadInventoryData(); // TODO: Replace with actual API call
- filterInventory(etSearch.getText().toString());
- swipeRefreshLayout.setRefreshing(false);
- });
+ swipeRefreshLayout.setOnRefreshListener(() -> loadInventory(true));
}
- private void openInventoryDetails(int position) {
- InventoryDetailFragment detailFragment = new InventoryDetailFragment();
+ // Load inventory
+ private void loadInventory(boolean reset) {
+ if (isLoading)
+ return;
+ isLoading = true;
+
+ if (reset) {
+ currentPage = 0;
+ isLastPage = false;
+ }
+
+ // Build query: combine search text + selected category
+ String q = buildQuery();
+
+ inventoryApi.getAllInventory(q, currentPage, PAGE_SIZE, "inventoryId,asc")
+ .enqueue(new Callback>() {
+ @Override
+ public void onResponse(Call> call,
+ Response> response) {
+ isLoading = false;
+ if (swipeRefreshLayout != null)
+ swipeRefreshLayout.setRefreshing(false);
+
+ if (response.isSuccessful() && response.body() != null) {
+ PageResponse page = response.body();
+ if (reset)
+ inventoryList.clear();
+ inventoryList.addAll(page.getContent());
+ adapter.notifyDataSetChanged();
+ isLastPage = page.isLast();
+ if (!isLastPage)
+ currentPage++;
+ } else {
+ Log.e(TAG, "Error " + response.code());
+ Toast.makeText(getContext(), "Failed to load inventory", Toast.LENGTH_SHORT).show();
+ }
+ }
+
+ @Override
+ public void onFailure(Call> call, Throwable t) {
+ isLoading = false;
+ if (swipeRefreshLayout != null)
+ swipeRefreshLayout.setRefreshing(false);
+ Log.e(TAG, "Network error", t);
+ Toast.makeText(getContext(), "Network error: " + t.getMessage(), Toast.LENGTH_SHORT).show();
+ }
+ });
+ }
+
+ // Combines search text and category into one query string for ?q=
+ private String buildQuery() {
+ String q = null;
+ if (!currentQuery.isEmpty() && selectedCategory != null) {
+ // Both active — prioritize search text, category acts as context
+ q = currentQuery;
+ } else if (!currentQuery.isEmpty()) {
+ q = currentQuery;
+ } else if (selectedCategory != null) {
+ q = selectedCategory;
+ }
+ return q;
+ }
+
+ // Bulk delete
+ private void confirmBulkDelete() {
+ List ids = adapter.getSelectedIds();
+ if (ids.isEmpty())
+ return;
+
+ new androidx.appcompat.app.AlertDialog.Builder(requireContext())
+ .setTitle("Delete " + ids.size() + " item(s)?")
+ .setMessage("This cannot be undone.")
+ .setPositiveButton("Delete", (d, w) -> bulkDelete(ids))
+ .setNegativeButton("Cancel", null)
+ .show();
+ }
+
+ private void bulkDelete(List ids) {
+ inventoryApi.bulkDeleteInventory(new BulkDeleteRequest(ids))
+ .enqueue(new Callback() {
+ @Override
+ public void onResponse(Call call, Response response) {
+ if (response.isSuccessful()) {
+ adapter.clearSelection();
+ hideBulkDeleteBar();
+ loadInventory(true);
+ Toast.makeText(getContext(), ids.size() + " item(s) deleted", Toast.LENGTH_SHORT).show();
+ } else {
+ Toast.makeText(getContext(), "Delete failed", Toast.LENGTH_SHORT).show();
+ }
+ }
+
+ @Override
+ public void onFailure(Call call, Throwable t) {
+ Toast.makeText(getContext(), "Network error", Toast.LENGTH_SHORT).show();
+ }
+ });
+ }
+
+ private void hideBulkDeleteBar() {
+ if (btnBulkDelete != null)
+ btnBulkDelete.setVisibility(View.GONE);
+ if (tvSelectionCount != null)
+ tvSelectionCount.setVisibility(View.GONE);
+ }
+
+ // Navigation
+ private void openDetail(InventoryDTO inv) {
+ InventoryDetailFragment detail = 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());
+ if (inv != null) {
+ args.putLong("inventoryId", inv.getInventoryId());
+ args.putLong("prodId", inv.getProdId() != null ? inv.getProdId() : -1);
+ args.putString("productName", inv.getProductName());
+ args.putString("categoryName", inv.getCategoryName());
+ args.putInt("quantity", inv.getQuantity() != null ? inv.getQuantity() : 0);
}
- detailFragment.setArguments(args);
- detailFragment.setInventoryFragment(this);
+ detail.setArguments(args);
+ detail.setInventoryFragment(this);
- ListFragment listFragment = (ListFragment) getParentFragment();
- if (listFragment != null) listFragment.loadFragment(detailFragment);
+ ListFragment lf = (ListFragment) getParentFragment();
+ if (lf != null)
+ lf.loadFragment(detail);
}
- public void onInventorySaved(int position, Inventory inventory) {
- if (position == -1) {
- inventoryList.add(inventory);
- } else {
- inventoryList.set(position, inventory);
- }
- filterInventory(etSearch.getText().toString());
+ public void onInventoryChanged() {
+ loadInventory(true);
}
- public void onInventoryDeleted(int position) {
- inventoryList.remove(position);
- filterInventory(etSearch.getText().toString());
- }
+ // Adapter callbacks
@Override
public void onInventoryClick(int position) {
- openInventoryDetails(position);
+ if (position >= 0 && position < inventoryList.size()) {
+ openDetail(inventoryList.get(position));
+ }
}
- private void loadInventoryData() {
- inventoryList.clear();
- inventoryList.add(new Inventory(1, "Dog Food - Large", "Food", 50, 25.99, "PetSupplies Co."));
- inventoryList.add(new Inventory(2, "Cat Litter", "Hygiene", 30, 12.99, "CleanPaws Ltd."));
- inventoryList.add(new Inventory(3, "Dog Leash", "Accessories", 4, 15.99, "PetGear Inc."));
- inventoryList.add(new Inventory(4, "Bird Cage - Medium", "Housing", 8, 79.99, "BirdWorld"));
- inventoryList.add(new Inventory(5, "Flea Treatment", "Medicine", 2, 34.99, "VetCare Supply"));
- filteredList.clear();
- filteredList.addAll(inventoryList);
+ @Override
+ public void onSelectionChanged(int selectedCount) {
+ if (selectedCount > 0) {
+ btnBulkDelete.setVisibility(View.VISIBLE);
+ tvSelectionCount.setVisibility(View.VISIBLE);
+ tvSelectionCount.setText(selectedCount + " selected");
+ } else {
+ hideBulkDeleteBar();
+ }
}
-
- 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);
- }
-}
+}
\ No newline at end of file
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
index 91257673..4c8effcf 100644
--- 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
@@ -21,6 +21,7 @@ import android.widget.Spinner;
import android.widget.Toast;
import com.example.petstoremobile.R;
+import com.example.petstoremobile.adapters.BlackTextArrayAdapter;
import com.example.petstoremobile.adapters.PetAdapter;
import com.example.petstoremobile.api.PetApi;
import com.example.petstoremobile.api.RetrofitClient;
@@ -63,7 +64,6 @@ public class PetFragment extends Fragment implements PetAdapter.OnPetClickListen
setupSearch(view);
setupStatusFilter(view);
setupSwipeRefresh(view);
- loadPetData();
//Add button to opens the add dialog
@@ -82,6 +82,12 @@ public class PetFragment extends Fragment implements PetAdapter.OnPetClickListen
return view;
}
+ @Override
+ public void onResume() {
+ super.onResume();
+ loadPetData();
+ }
+
private void setupSearch(View view) {
etSearch = view.findViewById(R.id.etSearchPet);
etSearch.addTextChangedListener(new TextWatcher() {
@@ -97,7 +103,7 @@ public class PetFragment extends Fragment implements PetAdapter.OnPetClickListen
private void setupStatusFilter(View view) {
spinnerStatus = view.findViewById(R.id.spinnerStatus);
String[] statuses = {"All Statuses", "Available", "Adopted"};
- ArrayAdapter adapter = new ArrayAdapter<>(requireContext(), android.R.layout.simple_spinner_item, statuses);
+ BlackTextArrayAdapter adapter = new BlackTextArrayAdapter<>(requireContext(), android.R.layout.simple_spinner_item, statuses);
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/ProductFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ProductFragment.java
index 65e14d4b..4e72b6cd 100644
--- 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
@@ -1,88 +1,87 @@
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 android.text.*;
+import android.util.Log;
+import android.view.*;
+import android.widget.*;
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.api.RetrofitClient;
+import com.example.petstoremobile.dtos.PageResponse;
+import com.example.petstoremobile.dtos.ProductDTO;
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;
+import java.util.*;
+import retrofit2.*;
public class ProductFragment extends Fragment implements ProductAdapter.OnProductClickListener {
- private List productList = new ArrayList<>();
- private List filteredList = new ArrayList<>();
+ private List productList = new ArrayList<>();
+ private List filteredList = new ArrayList<>();
private ProductAdapter adapter;
- private SwipeRefreshLayout swipeRefreshLayout;
+ private SwipeRefreshLayout swipeRefresh;
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);
+ loadProducts();
- FloatingActionButton fabAddProduct = view.findViewById(R.id.fabAddProduct);
- fabAddProduct.setOnClickListener(v -> openProductDetails(-1));
+ FloatingActionButton fab = view.findViewById(R.id.fabAddProduct);
+ fab.setOnClickListener(v -> openDetail(-1));
- //Make the hamburger button open the drawer from listFragment
+ ImageButton hamburger = view.findViewById(R.id.btnHamburgerProduct);
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();
- }
+ ListFragment lf = (ListFragment) getParentFragment();
+ if (lf != null) lf.openDrawer();
});
return view;
}
- // Filters products by name, description, or category
+ private void setupRecyclerView(View view) {
+ RecyclerView rv = view.findViewById(R.id.recyclerViewProducts);
+ adapter = new ProductAdapter(filteredList, this);
+ rv.setLayoutManager(new LinearLayoutManager(getContext()));
+ rv.setAdapter(adapter);
+ }
+
private void setupSearch(View view) {
etSearch = view.findViewById(R.id.etSearchProduct);
etSearch.addTextChangedListener(new TextWatcher() {
- @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());
+ public void beforeTextChanged(CharSequence s, int a, int b, int c) {}
+ public void afterTextChanged(Editable s) {}
+ public void onTextChanged(CharSequence s, int a, int b, int c) {
+ filter(s.toString());
}
- @Override public void afterTextChanged(Editable s) {}
});
}
- private void filterProducts(String query) {
+ private void setupSwipeRefresh(View view) {
+ swipeRefresh = view.findViewById(R.id.swipeRefreshProduct);
+ swipeRefresh.setOnRefreshListener(this::loadProducts);
+ }
+
+ private void filter(String query) {
filteredList.clear();
if (query.isEmpty()) {
filteredList.addAll(productList);
} else {
String lower = query.toLowerCase();
- for (Product p : productList) {
- if (p.getProductName().toLowerCase().contains(lower)
- || p.getCategory().toLowerCase().contains(lower)
- || p.getProductDesc().toLowerCase().contains(lower)) {
+ for (ProductDTO p : productList) {
+ if ((p.getProdName() != null && p.getProdName().toLowerCase().contains(lower))
+ || (p.getCategoryName() != null && p.getCategoryName().toLowerCase().contains(lower))) {
filteredList.add(p);
}
}
@@ -90,73 +89,45 @@ public class ProductFragment extends Fragment implements ProductAdapter.OnProduc
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 loadProducts() {
+ if (swipeRefresh != null) swipeRefresh.setRefreshing(true);
+ RetrofitClient.getProductApi(requireContext()).getAllProducts(null, 0, 100)
+ .enqueue(new Callback>() {
+ public void onResponse(Call> c,
+ Response> r) {
+ if (swipeRefresh != null) swipeRefresh.setRefreshing(false);
+ if (r.isSuccessful() && r.body() != null) {
+ productList.clear();
+ productList.addAll(r.body().getContent());
+ filter(etSearch != null ? etSearch.getText().toString() : "");
+ } else {
+ Toast.makeText(getContext(), "Failed to load products",
+ Toast.LENGTH_SHORT).show();
+ }
+ }
+ public void onFailure(Call> c, Throwable t) {
+ if (swipeRefresh != null) swipeRefresh.setRefreshing(false);
+ Log.e("ProductFragment", t.getMessage());
+ }
+ });
}
- private void openProductDetails(int position) {
- ProductDetailFragment detailFragment = new ProductDetailFragment();
+ private void openDetail(int position) {
+ ProductDetailFragment detail = new ProductDetailFragment();
Bundle args = new Bundle();
- args.putInt("position", position);
-
if (position != -1) {
- Product product = filteredList.get(position);
- int realPosition = productList.indexOf(product);
- args.putInt("position", realPosition);
- args.putInt("productId", product.getProductId());
- args.putString("productName", product.getProductName());
- args.putString("productDesc", product.getProductDesc());
- args.putString("category", product.getCategory());
- args.putDouble("productPrice", product.getProductPrice());
- args.putInt("stockQuantity", product.getStockQuantity());
+ ProductDTO p = filteredList.get(position);
+ args.putLong("prodId", p.getProdId());
+ args.putString("prodName", p.getProdName());
+ args.putString("prodDesc", p.getProdDesc() != null ? p.getProdDesc() : "");
+ args.putString("prodPrice", p.getProdPrice() != null ? p.getProdPrice().toString() : "");
+ args.putLong("categoryId", p.getCategoryId() != null ? p.getCategoryId() : -1);
}
-
- 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());
+ detail.setArguments(args);
+ ListFragment lf = (ListFragment) getParentFragment();
+ if (lf != null) lf.loadFragment(detail);
}
@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);
- }
-}
+ public void onProductClick(int position) { openDetail(position); }
+}
\ No newline at end of file
diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ProductSupplierFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ProductSupplierFragment.java
new file mode 100644
index 00000000..4ff88f16
--- /dev/null
+++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ProductSupplierFragment.java
@@ -0,0 +1,134 @@
+package com.example.petstoremobile.fragments.listfragments;
+
+import android.os.Bundle;
+import android.text.*;
+import android.util.Log;
+import android.view.*;
+import android.widget.*;
+import androidx.fragment.app.Fragment;
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
+import com.example.petstoremobile.R;
+import com.example.petstoremobile.adapters.ProductSupplierAdapter;
+import com.example.petstoremobile.api.RetrofitClient;
+import com.example.petstoremobile.dtos.PageResponse;
+import com.example.petstoremobile.dtos.ProductSupplierDTO;
+import com.example.petstoremobile.fragments.ListFragment;
+import com.example.petstoremobile.fragments.listfragments.detailfragments.ProductSupplierDetailFragment;
+import com.google.android.material.floatingactionbutton.FloatingActionButton;
+import java.util.*;
+import retrofit2.*;
+
+public class ProductSupplierFragment extends Fragment
+ implements ProductSupplierAdapter.OnProductSupplierClickListener {
+
+ private List psList = new ArrayList<>();
+ private List filteredList = new ArrayList<>();
+ private ProductSupplierAdapter adapter;
+ private SwipeRefreshLayout swipeRefresh;
+ private EditText etSearch;
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ View view = inflater.inflate(R.layout.fragment_product_supplier, container, false);
+
+ setupRecyclerView(view);
+ setupSearch(view);
+ setupSwipeRefresh(view);
+ loadData();
+
+ FloatingActionButton fab = view.findViewById(R.id.fabAddPS);
+ fab.setOnClickListener(v -> openDetail(-1));
+
+ ImageButton hamburger = view.findViewById(R.id.btnHamburgerPS);
+ hamburger.setOnClickListener(v -> {
+ ListFragment lf = (ListFragment) getParentFragment();
+ if (lf != null) lf.openDrawer();
+ });
+
+ return view;
+ }
+
+ private void setupRecyclerView(View view) {
+ RecyclerView rv = view.findViewById(R.id.recyclerViewPS);
+ adapter = new ProductSupplierAdapter(filteredList, this);
+ rv.setLayoutManager(new LinearLayoutManager(getContext()));
+ rv.setAdapter(adapter);
+ }
+
+ private void setupSearch(View view) {
+ etSearch = view.findViewById(R.id.etSearchPS);
+ etSearch.addTextChangedListener(new TextWatcher() {
+ public void beforeTextChanged(CharSequence s, int a, int b, int c) {}
+ public void afterTextChanged(Editable s) {}
+ public void onTextChanged(CharSequence s, int a, int b, int c) {
+ filter(s.toString());
+ }
+ });
+ }
+
+ private void setupSwipeRefresh(View view) {
+ swipeRefresh = view.findViewById(R.id.swipeRefreshPS);
+ swipeRefresh.setOnRefreshListener(this::loadData);
+ }
+
+ private void filter(String query) {
+ filteredList.clear();
+ if (query.isEmpty()) {
+ filteredList.addAll(psList);
+ } else {
+ String lower = query.toLowerCase();
+ for (ProductSupplierDTO ps : psList) {
+ if ((ps.getProductName() != null && ps.getProductName().toLowerCase().contains(lower))
+ || (ps.getSupplierName() != null && ps.getSupplierName().toLowerCase().contains(lower))) {
+ filteredList.add(ps);
+ }
+ }
+ }
+ adapter.notifyDataSetChanged();
+ }
+
+ private void loadData() {
+ if (swipeRefresh != null) swipeRefresh.setRefreshing(true);
+ RetrofitClient.getProductSupplierApi(requireContext()).getAllProductSuppliers(0, 100)
+ .enqueue(new Callback>() {
+ public void onResponse(Call> c,
+ Response> r) {
+ if (swipeRefresh != null) swipeRefresh.setRefreshing(false);
+ if (r.isSuccessful() && r.body() != null) {
+ psList.clear();
+ psList.addAll(r.body().getContent());
+ filter(etSearch != null ? etSearch.getText().toString() : "");
+ } else {
+ Toast.makeText(getContext(), "Failed to load",
+ Toast.LENGTH_SHORT).show();
+ }
+ }
+ public void onFailure(Call> c, Throwable t) {
+ if (swipeRefresh != null) swipeRefresh.setRefreshing(false);
+ Log.e("PSFragment", t.getMessage());
+ }
+ });
+ }
+
+ private void openDetail(int position) {
+ ProductSupplierDetailFragment detail = new ProductSupplierDetailFragment();
+ Bundle args = new Bundle();
+ if (position != -1) {
+ ProductSupplierDTO ps = filteredList.get(position);
+ args.putLong("productId", ps.getProductId());
+ args.putLong("supplierId", ps.getSupplierId());
+ args.putString("productName", ps.getProductName());
+ args.putString("supplierName", ps.getSupplierName());
+ args.putString("cost", ps.getCost() != null ? ps.getCost().toString() : "");
+ }
+ detail.setArguments(args);
+ ListFragment lf = (ListFragment) getParentFragment();
+ if (lf != null) lf.loadFragment(detail);
+ }
+
+ @Override
+ public void onProductSupplierClick(int position) { openDetail(position); }
+}
\ No newline at end of file
diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/PurchaseOrderFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/PurchaseOrderFragment.java
new file mode 100644
index 00000000..76527f09
--- /dev/null
+++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/PurchaseOrderFragment.java
@@ -0,0 +1,139 @@
+package com.example.petstoremobile.fragments.listfragments;
+
+import android.os.Bundle;
+import android.text.*;
+import android.util.Log;
+import android.view.*;
+import android.widget.*;
+import androidx.fragment.app.Fragment;
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
+import com.example.petstoremobile.R;
+import com.example.petstoremobile.adapters.PurchaseOrderAdapter;
+import com.example.petstoremobile.api.RetrofitClient;
+import com.example.petstoremobile.dtos.PageResponse;
+import com.example.petstoremobile.dtos.PurchaseOrderDTO;
+import com.example.petstoremobile.fragments.ListFragment;
+import com.example.petstoremobile.fragments.listfragments.detailfragments.PurchaseOrderDetailFragment;
+import java.util.*;
+import retrofit2.*;
+
+public class PurchaseOrderFragment extends Fragment
+ implements PurchaseOrderAdapter.OnPurchaseOrderClickListener {
+
+ private List poList = new ArrayList<>();
+ private List filteredList = new ArrayList<>();
+ private PurchaseOrderAdapter adapter;
+ private SwipeRefreshLayout swipeRefresh;
+ private EditText etSearch;
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ View view = inflater.inflate(R.layout.fragment_purchase_order, container, false);
+
+ setupRecyclerView(view);
+ setupSearch(view);
+ setupSwipeRefresh(view);
+ loadData();
+
+ ImageButton hamburger = view.findViewById(R.id.btnHamburgerPO);
+ hamburger.setOnClickListener(v -> {
+ ListFragment lf = (ListFragment) getParentFragment();
+ if (lf != null)
+ lf.openDrawer();
+ });
+
+ return view;
+ }
+
+ private void setupRecyclerView(View view) {
+ RecyclerView rv = view.findViewById(R.id.recyclerViewPO);
+ adapter = new PurchaseOrderAdapter(filteredList, this);
+ rv.setLayoutManager(new LinearLayoutManager(getContext()));
+ rv.setAdapter(adapter);
+ }
+
+ private void setupSearch(View view) {
+ etSearch = view.findViewById(R.id.etSearchPO);
+ etSearch.addTextChangedListener(new TextWatcher() {
+ public void beforeTextChanged(CharSequence s, int a, int b, int c) {
+ }
+
+ public void afterTextChanged(Editable s) {
+ }
+
+ public void onTextChanged(CharSequence s, int a, int b, int c) {
+ filter(s.toString());
+ }
+ });
+ }
+
+ private void setupSwipeRefresh(View view) {
+ swipeRefresh = view.findViewById(R.id.swipeRefreshPO);
+ swipeRefresh.setOnRefreshListener(this::loadData);
+ }
+
+ private void filter(String query) {
+ filteredList.clear();
+ if (query.isEmpty()) {
+ filteredList.addAll(poList);
+ } else {
+ String lower = query.toLowerCase();
+ for (PurchaseOrderDTO po : poList) {
+ if ((po.getSupplierName() != null && po.getSupplierName().toLowerCase().contains(lower))
+ || (po.getStatus() != null && po.getStatus().toLowerCase().contains(lower))) {
+ filteredList.add(po);
+ }
+ }
+ }
+ adapter.notifyDataSetChanged();
+ }
+
+ private void loadData() {
+ if (swipeRefresh != null)
+ swipeRefresh.setRefreshing(true);
+ RetrofitClient.getPurchaseOrderApi(requireContext()).getAllPurchaseOrders(0, 100)
+ .enqueue(new Callback>() {
+ public void onResponse(Call> c,
+ Response> r) {
+ if (swipeRefresh != null)
+ swipeRefresh.setRefreshing(false);
+ if (r.isSuccessful() && r.body() != null) {
+ poList.clear();
+ poList.addAll(r.body().getContent());
+ filter(etSearch != null ? etSearch.getText().toString() : "");
+ } else {
+ Toast.makeText(getContext(), "Failed to load purchase orders",
+ Toast.LENGTH_SHORT).show();
+ }
+ }
+
+ public void onFailure(Call> c, Throwable t) {
+ if (swipeRefresh != null)
+ swipeRefresh.setRefreshing(false);
+ Log.e("POFragment", t.getMessage());
+ }
+ });
+ }
+
+ private void openDetail(int position) {
+ PurchaseOrderDetailFragment detail = new PurchaseOrderDetailFragment();
+ Bundle args = new Bundle();
+ PurchaseOrderDTO po = filteredList.get(position);
+ args.putLong("purchaseOrderId", po.getPurchaseOrderId());
+ args.putString("supplierName", po.getSupplierName());
+ args.putString("orderDate", po.getOrderDate());
+ args.putString("status", po.getStatus());
+ detail.setArguments(args);
+ ListFragment lf = (ListFragment) getParentFragment();
+ if (lf != null)
+ lf.loadFragment(detail);
+ }
+
+ @Override
+ public void onPurchaseOrderClick(int position) {
+ openDetail(position);
+ }
+}
\ No newline at end of file
diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/SaleFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/SaleFragment.java
new file mode 100644
index 00000000..c813aa5a
--- /dev/null
+++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/SaleFragment.java
@@ -0,0 +1,144 @@
+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.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.SaleAdapter;
+import com.example.petstoremobile.fragments.ListFragment;
+import com.example.petstoremobile.fragments.listfragments.detailfragments.RefundDetailFragment;
+import com.example.petstoremobile.models.Sale;
+import java.util.ArrayList;
+import java.util.List;
+
+public class SaleFragment extends Fragment implements SaleAdapter.OnSaleClickListener {
+
+ private List saleList = new ArrayList<>();
+ private List filteredList = new ArrayList<>();
+ private SaleAdapter adapter;
+ private SwipeRefreshLayout swipeRefreshLayout;
+ private EditText etSearch;
+ private ImageButton btnHamburger;
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ View view = inflater.inflate(R.layout.fragment_sale, container, false);
+
+ btnHamburger = view.findViewById(R.id.btnHamburger);
+
+ setupRecyclerView(view);
+ loadSaleData();
+ setupSearch(view);
+ setupSwipeRefresh(view);
+
+ // Make the hamburger button open the drawer from listFragment
+ if (btnHamburger != null) {
+ btnHamburger.setOnClickListener(v -> {
+ ListFragment listFragment = (ListFragment) getParentFragment();
+ if (listFragment != null) {
+ listFragment.openDrawer();
+ }
+ });
+ }
+
+ return view;
+ }
+
+ private void setupSearch(View view) {
+ etSearch = view.findViewById(R.id.etSearchSale);
+ 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) {
+ filterSales(s.toString());
+ }
+
+ @Override
+ public void afterTextChanged(Editable s) {
+ }
+ });
+ }
+
+ private void filterSales(String query) {
+ filteredList.clear();
+ if (query.isEmpty()) {
+ filteredList.addAll(saleList);
+ } else {
+ String lower = query.toLowerCase();
+ for (Sale s : saleList) {
+ if (s.getItemName().toLowerCase().contains(lower)
+ || s.getEmployeeName().toLowerCase().contains(lower)
+ || s.getSaleDate().toLowerCase().contains(lower)
+ || s.getPaymentMethod().toLowerCase().contains(lower)
+ || String.valueOf(s.getSaleId()).contains(lower)) {
+ filteredList.add(s);
+ }
+ }
+ }
+ adapter.notifyDataSetChanged();
+ }
+
+ private void setupSwipeRefresh(View view) {
+ swipeRefreshLayout = view.findViewById(R.id.swipeRefreshSale);
+ swipeRefreshLayout.setOnRefreshListener(() -> {
+ loadSaleData();
+ swipeRefreshLayout.setRefreshing(false);
+ });
+ }
+
+ // When a sale row is clicked, open the refund screen for that sale
+ @Override
+ public void onSaleClick(int position) {
+ Sale sale = filteredList.get(position);
+ RefundDetailFragment refundFragment = new RefundDetailFragment();
+ Bundle args = new Bundle();
+ args.putInt("saleId", sale.getSaleId());
+ args.putString("saleDate", sale.getSaleDate());
+ args.putString("employeeName", sale.getEmployeeName());
+ args.putDouble("total", sale.getTotal());
+ args.putString("paymentMethod", sale.getPaymentMethod());
+ refundFragment.setArguments(args);
+ refundFragment.setSaleFragment(this);
+
+ ListFragment listFragment = (ListFragment) getParentFragment();
+ if (listFragment != null)
+ listFragment.loadFragment(refundFragment);
+ }
+
+ public void reloadSales() {
+ loadSaleData();
+ }
+
+ // TODO: Replace with actual API call - GET v1/sales
+ private void loadSaleData() {
+ saleList.clear();
+ saleList.add(new Sale(1, "2026-03-01", "John Smith", "Premium Dog Food", 2, 45.99, 91.98, "Card", false));
+ saleList.add(new Sale(2, "2026-03-02", "Jane Doe", "Cat Toy Bundle", 1, 19.99, 19.99, "Cash", false));
+ saleList.add(new Sale(3, "2026-03-03", "John Smith", "Pet Shampoo", 3, 12.99, 38.97, "Card", false));
+ saleList.add(new Sale(4, "2026-03-04", "Jane Doe", "Dog Bed - Large", 1, 89.99, 89.99, "Cash", true));
+ filteredList.clear();
+ filteredList.addAll(saleList);
+ if (adapter != null)
+ adapter.notifyDataSetChanged();
+ }
+
+ private void setupRecyclerView(View view) {
+ RecyclerView recyclerView = view.findViewById(R.id.recyclerViewSales);
+ adapter = new SaleAdapter(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
index 32c06996..47733da2 100644
--- 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
@@ -1,172 +1,258 @@
package com.example.petstoremobile.fragments.listfragments.detailfragments;
-
-// Uses InputValidator for detailed field validation and ActivityLogger to log all changes.
-
+import android.app.DatePickerDialog;
import android.os.Bundle;
-
+import android.util.Log;
+import android.view.*;
+import android.widget.*;
import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.core.content.ContextCompat;
+import androidx.appcompat.app.AlertDialog;
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.adapters.BlackTextArrayAdapter;
+import com.example.petstoremobile.api.*;
+import com.example.petstoremobile.dtos.*;
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;
+import java.util.*;
+import retrofit2.*;
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;
+ private EditText etAdoptionDate;
+ private Spinner spinnerPet, spinnerCustomer, spinnerStatus;
+ private Button btnSave, btnDelete, btnBack;
- // Set the adoption fragment as parent so we refer back when save or delete is done
- public void setAdoptionFragment(AdoptionFragment fragment) {
- this.adoptionFragment = fragment;
- }
+ private long adoptionId = -1;
+ private boolean isEditing = false;
+ private long preselectedPetId = -1;
+ private long preselectedCustomerId = -1;
+
+ private List petList = new ArrayList<>();
+ private List customerList = new ArrayList<>();
+
+ private final String[] STATUSES = {"Pending", "Approved", "Rejected"};
@Override
- public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_adoption_detail, container, false);
-
initViews(view);
- setupSpinner();
+ setupSpinners();
+ setupDatePicker();
+ loadData();
handleArguments();
- btnBack.setOnClickListener(v -> {
- ListFragment listFragment = (ListFragment) getParentFragment();
- if (listFragment != null) listFragment.getChildFragmentManager().popBackStack();
- });
- btnSaveAdoption.setOnClickListener(v -> saveAdoption());
- btnDeleteAdoption.setOnClickListener(v -> deleteAdoption());
-
+ btnBack.setOnClickListener(v -> navigateBack());
+ btnSave.setOnClickListener(v -> saveAdoption());
+ btnDelete.setOnClickListener(v -> confirmDelete());
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;
+ private void initViews(View v) {
+ tvMode = v.findViewById(R.id.tvAdoptionMode);
+ tvAdoptionId = v.findViewById(R.id.tvAdoptionId);
+ etAdoptionDate = v.findViewById(R.id.etAdoptionDate);
+ spinnerPet = v.findViewById(R.id.spinnerAdoptionPet);
+ spinnerCustomer= v.findViewById(R.id.spinnerAdoptionCustomer);
+ spinnerStatus = v.findViewById(R.id.spinnerAdoptionStatus);
+ btnSave = v.findViewById(R.id.btnSaveAdoption);
+ btnDelete = v.findViewById(R.id.btnDeleteAdoption);
+ btnBack = v.findViewById(R.id.btnAdoptionBack);
+ }
- 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();
+ private void setupSpinners() {
+ spinnerStatus.setAdapter(new BlackTextArrayAdapter<>(requireContext(),
+ android.R.layout.simple_spinner_item, STATUSES));
+ }
- 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();
+ private void setupDatePicker() {
+ etAdoptionDate.setOnClickListener(v -> {
+ Calendar c = Calendar.getInstance();
+ new DatePickerDialog(requireContext(),
+ (dp, y, m, d) -> etAdoptionDate.setText(
+ String.format("%04d-%02d-%02d", y, m + 1, d)),
+ c.get(Calendar.YEAR),
+ c.get(Calendar.MONTH),
+ c.get(Calendar.DAY_OF_MONTH)).show();
+ });
+ }
+
+ private void loadData() {
+ loadPets();
+ loadCustomers();
+ }
+
+ private void loadPets() {
+ RetrofitClient.getPetApi(requireContext()).getAllPets(0, 200)
+ .enqueue(new Callback>() {
+ public void onResponse(Call> c,
+ Response> r) {
+ if (r.isSuccessful() && r.body() != null) {
+ petList = r.body().getContent();
+ populatePetSpinner();
+ }
+ }
+ public void onFailure(Call> c, Throwable t) {
+ Log.e("ADOPTION", "Pet load failed: " + t.getMessage());
+ }
+ });
+ }
+
+ private void populatePetSpinner() {
+ List names = new ArrayList<>();
+ names.add("-- Select Pet --");
+ for (PetDTO p : petList) names.add(p.getPetName());
+ spinnerPet.setAdapter(new BlackTextArrayAdapter<>(requireContext(),
+ android.R.layout.simple_spinner_item, names));
+ if (preselectedPetId != -1) {
+ for (int i = 0; i < petList.size(); i++) {
+ if (petList.get(i).getPetId().equals(preselectedPetId)) {
+ spinnerPet.setSelection(i + 1); break;
+ }
}
- 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 loadCustomers() {
+ RetrofitClient.getCustomerApi(requireContext()).getAllCustomers(0, 200)
+ .enqueue(new Callback>() {
+ public void onResponse(Call> c,
+ Response> r) {
+ if (r.isSuccessful() && r.body() != null) {
+ customerList = r.body().getContent();
+ populateCustomerSpinner();
+ }
+ }
+ public void onFailure(Call> c, Throwable t) {
+ Log.e("ADOPTION", "Customer load failed: " + t.getMessage());
+ }
+ });
+ }
+
+ private void populateCustomerSpinner() {
+ List names = new ArrayList<>();
+ names.add("-- Select Customer --");
+ for (CustomerDTO c : customerList)
+ names.add(c.getFirstName() + " " + c.getLastName());
+ spinnerCustomer.setAdapter(new BlackTextArrayAdapter<>(requireContext(),
+ android.R.layout.simple_spinner_item, names));
+ if (preselectedCustomerId != -1) {
+ for (int i = 0; i < customerList.size(); i++) {
+ if (customerList.get(i).getCustomerId().equals(preselectedCustomerId)) {
+ spinnerCustomer.setSelection(i + 1); break;
+ }
+ }
}
}
private void handleArguments() {
- if (getArguments() != null && getArguments().containsKey("adoptionId")) {
+ Bundle a = getArguments();
+ if (a != null && a.containsKey("adoptionId")) {
isEditing = true;
- adoptionId = getArguments().getInt("adoptionId");
- position = getArguments().getInt("position");
+ adoptionId = a.getLong("adoptionId");
+ preselectedPetId = a.getLong("petId", -1);
+ preselectedCustomerId = a.getLong("customerId", -1);
+
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);
+ tvAdoptionId.setVisibility(View.VISIBLE);
+ etAdoptionDate.setText(a.getString("adoptionDate"));
+ btnDelete.setVisibility(View.VISIBLE);
+
+ // Pre-fill status
+ String status = a.getString("adoptionStatus", "Pending");
+ for (int i = 0; i < STATUSES.length; i++) {
+ if (STATUSES[i].equals(status)) {
+ spinnerStatus.setSelection(i); break;
+ }
+ }
} else {
- isEditing = false;
tvMode.setText("Add Adoption");
+ btnDelete.setVisibility(View.GONE);
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 saveAdoption() {
+ if (spinnerCustomer.getSelectedItemPosition() == 0) {
+ Toast.makeText(getContext(), "Select a customer", Toast.LENGTH_SHORT).show(); return;
+ }
+ if (spinnerPet.getSelectedItemPosition() == 0) {
+ Toast.makeText(getContext(), "Select a pet", Toast.LENGTH_SHORT).show(); return;
+ }
+ String date = etAdoptionDate.getText().toString().trim();
+ if (date.isEmpty()) {
+ Toast.makeText(getContext(), "Select a date", Toast.LENGTH_SHORT).show(); return;
+ }
+
+ CustomerDTO customer = customerList.get(spinnerCustomer.getSelectedItemPosition() - 1);
+ PetDTO pet = petList.get(spinnerPet.getSelectedItemPosition() - 1);
+ String status = STATUSES[spinnerStatus.getSelectedItemPosition()];
+
+ AdoptionDTO dto = new AdoptionDTO(
+ pet.getPetId(),
+ customer.getCustomerId(),
+ date,
+ status
+ );
+
+ Log.d("ADOPTION_SAVE", "petId=" + pet.getPetId()
+ + " customerId=" + customer.getCustomerId()
+ + " date=" + date + " status=" + status);
+
+ AdoptionApi api = RetrofitClient.getAdoptionApi(requireContext());
+ if (isEditing) {
+ api.updateAdoption(adoptionId, dto).enqueue(simpleCallback("Updated"));
+ } else {
+ api.createAdoption(dto).enqueue(simpleCallback("Saved"));
+ }
}
- 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;
+ private Callback simpleCallback(String msg) {
+ return new Callback<>() {
+ public void onResponse(Call c, Response r) {
+ Log.d("ADOPTION_SAVE", "Response: " + r.code());
+ if (r.isSuccessful()) {
+ Toast.makeText(getContext(), msg, Toast.LENGTH_SHORT).show();
+ navigateBack();
+ } else {
+ try {
+ String err = r.errorBody().string();
+ Log.e("ADOPTION_SAVE", "Error: " + err);
+ Toast.makeText(getContext(), "Error " + r.code(), Toast.LENGTH_SHORT).show();
+ } catch (Exception e) {
+ Log.e("ADOPTION_SAVE", "Failed to read error");
+ }
+ }
+ }
+ public void onFailure(Call c, Throwable t) {
+ Log.e("ADOPTION_SAVE", "Failure: " + t.getMessage());
+ Toast.makeText(getContext(), "Network error", Toast.LENGTH_SHORT).show();
}
};
- adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
- spinnerAdoptionStatus.setAdapter(adapter);
}
-}
+
+ private void confirmDelete() {
+ new AlertDialog.Builder(requireContext())
+ .setTitle("Delete Adoption?")
+ .setPositiveButton("Yes", (d, w) ->
+ RetrofitClient.getAdoptionApi(requireContext())
+ .deleteAdoption(adoptionId)
+ .enqueue(new Callback() {
+ public void onResponse(Call c, Response r) {
+ navigateBack();
+ }
+ public void onFailure(Call c, Throwable t) {
+ Toast.makeText(getContext(), "Delete failed",
+ Toast.LENGTH_SHORT).show();
+ }
+ }))
+ .setNegativeButton("No", null).show();
+ }
+
+ private void navigateBack() {
+ ListFragment lf = (ListFragment) getParentFragment();
+ if (lf != null) lf.getChildFragmentManager().popBackStack();
+ }
+}
\ No newline at end of file
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
index bfd2d6b5..eaabc061 100644
--- 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
@@ -1,178 +1,444 @@
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.app.DatePickerDialog;
import android.os.Bundle;
-
+import android.util.Log;
+import android.view.*;
+import android.widget.*;
import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.core.content.ContextCompat;
+import androidx.appcompat.app.AlertDialog;
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.adapters.BlackTextArrayAdapter;
+import com.example.petstoremobile.api.*;
+import com.example.petstoremobile.dtos.*;
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;
+import java.util.*;
+import retrofit2.*;
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;
+ private EditText etAppointmentDate;
+ private Spinner spinnerPet, spinnerService, spinnerStatus, spinnerHour, spinnerMinute;
+ private Spinner spinnerCustomer, spinnerStore;
+ private Button btnSave, btnDelete, btnBack;
- // Set the appointment fragment as parent so we refer back when save or delete is done
- public void setAppointmentFragment(AppointmentFragment fragment) {
- this.appointmentFragment = fragment;
- }
+ private long appointmentId = -1;
+ private boolean isEditing = false;
+ private long preselectedPetId = -1;
+ private long preselectedServiceId = -1;
+ private long preselectedCustomerId = -1;
+ private long preselectedStoreId = -1;
+
+ private List petList = new ArrayList<>();
+ private List serviceList = new ArrayList<>();
+ private List customerList = new ArrayList<>();
+ private List storeList = new ArrayList<>();
+ private List allAppointments = new ArrayList<>();
+
+ private final Integer[] HOURS = {9,10,11,12,13,14,15,16,17};
+ private final Integer[] MINUTES = {0,15,30,45};
+ private final String[] STATUSES = {"Booked","Completed","Cancelled"};
@Override
- public View onCreateView(LayoutInflater inflater, ViewGroup container,
- Bundle savedInstanceState) {
+ public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_appointment_detail, container, false);
-
initViews(view);
- setupSpinner();
+ setupSpinners();
+ setupDatePicker();
+ loadData();
handleArguments();
- btnBack.setOnClickListener(v -> {
- ListFragment listFragment = (ListFragment) getParentFragment();
- if (listFragment != null) {
- listFragment.getChildFragmentManager().popBackStack();
- }
- });
- btnSaveAppointment.setOnClickListener(v -> saveAppointment());
- btnDeleteAppointment.setOnClickListener(v -> deleteAppointment());
-
+ btnBack.setOnClickListener(v -> navigateBack());
+ btnSave.setOnClickListener(v -> saveAppointment());
+ btnDelete.setOnClickListener(v -> confirmDelete());
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;
+ private void initViews(View v) {
+ tvMode = v.findViewById(R.id.tvApptMode);
+ tvAppointmentId = v.findViewById(R.id.tvAppointmentId);
+ etAppointmentDate= v.findViewById(R.id.etAppointmentDate);
+ spinnerPet = v.findViewById(R.id.spinnerPet);
+ spinnerService = v.findViewById(R.id.spinnerService);
+ spinnerStatus = v.findViewById(R.id.spinnerAppointmentStatus);
+ spinnerHour = v.findViewById(R.id.spinnerHour);
+ spinnerMinute = v.findViewById(R.id.spinnerMinute);
+ spinnerCustomer = v.findViewById(R.id.spinnerCustomer);
+ spinnerStore = v.findViewById(R.id.spinnerStore);
+ btnSave = v.findViewById(R.id.btnSaveAppointment);
+ btnDelete = v.findViewById(R.id.btnDeleteAppointment);
+ btnBack = v.findViewById(R.id.btnApptBack);
+ }
- 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();
+ private void setupSpinners() {
+ spinnerStatus.setAdapter(new BlackTextArrayAdapter<>(requireContext(),
+ android.R.layout.simple_spinner_item, STATUSES));
- 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();
+ String[] hours = new String[HOURS.length];
+ for (int i = 0; i < HOURS.length; i++)
+ hours[i] = String.format("%02d:00", HOURS[i]);
+ spinnerHour.setAdapter(new BlackTextArrayAdapter<>(requireContext(),
+ android.R.layout.simple_spinner_item, hours));
+ spinnerMinute.setAdapter(new BlackTextArrayAdapter<>(requireContext(),
+ android.R.layout.simple_spinner_item, new String[]{"00","15","30","45"}));
+ }
+
+ private void setupDatePicker() {
+ etAppointmentDate.setOnClickListener(v -> {
+ Calendar c = Calendar.getInstance();
+ DatePickerDialog d = new DatePickerDialog(requireContext(),
+ (dp,y,m,d1) -> etAppointmentDate.setText(
+ String.format("%04d-%02d-%02d", y, m+1, d1)),
+ c.get(Calendar.YEAR), c.get(Calendar.MONTH),
+ c.get(Calendar.DAY_OF_MONTH));
+ d.getDatePicker().setMinDate(System.currentTimeMillis() - 1000);
+ d.show();
+ });
+ }
+
+ private void loadData() {
+ loadPets();
+ loadServices();
+ loadCustomers();
+ loadStores();
+ loadAllAppointments();
+ }
+
+ private void loadPets() {
+ RetrofitClient.getPetApi(requireContext()).getAllPets(0, 200)
+ .enqueue(new Callback>() {
+ public void onResponse(Call> c, Response> r) {
+ if (r.isSuccessful() && r.body() != null) {
+ petList = r.body().getContent();
+ populatePetSpinner();
+ }
+ }
+ public void onFailure(Call> c, Throwable t) {
+ Log.e("APPT", "Pet load failed: " + t.getMessage());
+ }
+ });
+ }
+
+ private void populatePetSpinner() {
+ List names = new ArrayList<>();
+ names.add("-- Select Pet --");
+ for (PetDTO p : petList) names.add(p.getPetName());
+ spinnerPet.setAdapter(new BlackTextArrayAdapter<>(requireContext(),
+ android.R.layout.simple_spinner_item, names));
+ if (preselectedPetId != -1) {
+ for (int i = 0; i < petList.size(); i++) {
+ if (petList.get(i).getPetId().equals(preselectedPetId)) {
+ spinnerPet.setSelection(i + 1); break;
+ }
}
- // 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();
+ private void loadServices() {
+ RetrofitClient.getServiceApi(requireContext()).getAllServices(0, 200)
+ .enqueue(new Callback>() {
+ public void onResponse(Call> c, Response> r) {
+ if (r.isSuccessful() && r.body() != null) {
+ serviceList = r.body().getContent();
+ populateServiceSpinner();
+ }
+ }
+ public void onFailure(Call> c, Throwable t) {
+ Log.e("APPT", "Service load failed: " + t.getMessage());
+ }
+ });
+ }
+
+ private void populateServiceSpinner() {
+ List names = new ArrayList<>();
+ names.add("-- Select Service --");
+ for (ServiceDTO s : serviceList) names.add(s.getServiceName());
+ spinnerService.setAdapter(new BlackTextArrayAdapter<>(requireContext(),
+ android.R.layout.simple_spinner_item, names));
+ if (preselectedServiceId != -1) {
+ for (int i = 0; i < serviceList.size(); i++) {
+ if (serviceList.get(i).getServiceId().equals(preselectedServiceId)) {
+ spinnerService.setSelection(i + 1); break;
+ }
+ }
}
}
- // Determines if the fragment is in add or edit mode and populates fields accordingly
+ private void loadCustomers() {
+ RetrofitClient.getCustomerApi(requireContext()).getAllCustomers(0, 200)
+ .enqueue(new Callback>() {
+ public void onResponse(Call> c, Response> r) {
+ if (r.isSuccessful() && r.body() != null) {
+ customerList = r.body().getContent();
+ populateCustomerSpinner();
+ }
+ }
+ public void onFailure(Call> c, Throwable t) {
+ Log.e("APPT", "Customer load failed: " + t.getMessage());
+ }
+ });
+ }
+
+ private void populateCustomerSpinner() {
+ List names = new ArrayList<>();
+ names.add("-- Select Customer --");
+ for (CustomerDTO c : customerList)
+ names.add(c.getFirstName() + " " + c.getLastName());
+ spinnerCustomer.setAdapter(new BlackTextArrayAdapter<>(requireContext(),
+ android.R.layout.simple_spinner_item, names));
+ if (preselectedCustomerId != -1) {
+ for (int i = 0; i < customerList.size(); i++) {
+ if (customerList.get(i).getCustomerId().equals(preselectedCustomerId)) {
+ spinnerCustomer.setSelection(i + 1); break;
+ }
+ }
+ }
+ }
+
+ private void loadStores() {
+ RetrofitClient.getStoreApi(requireContext()).getAllStores(0, 50)
+ .enqueue(new Callback>() {
+ public void onResponse(Call> c, Response> r) {
+ if (r.isSuccessful() && r.body() != null) {
+ storeList = r.body().getContent();
+ populateStoreSpinner();
+ }
+ }
+ public void onFailure(Call> c, Throwable t) {
+ Log.e("APPT", "Store load failed: " + t.getMessage());
+ }
+ });
+ }
+
+ private void populateStoreSpinner() {
+ List names = new ArrayList<>();
+ names.add("-- Select Store --");
+ for (StoreDTO s : storeList) names.add(s.getStoreName());
+ spinnerStore.setAdapter(new BlackTextArrayAdapter<>(requireContext(),
+ android.R.layout.simple_spinner_item, names));
+ if (preselectedStoreId != -1) {
+ for (int i = 0; i < storeList.size(); i++) {
+ if (storeList.get(i).getStoreId().equals(preselectedStoreId)) {
+ spinnerStore.setSelection(i + 1); break;
+ }
+ }
+ }
+ }
+
+ private void loadAllAppointments() {
+ RetrofitClient.getAppointmentApi(requireContext()).getAllAppointments(0, 500)
+ .enqueue(new Callback>() {
+ public void onResponse(Call> c, Response> r) {
+ if (r.isSuccessful() && r.body() != null)
+ allAppointments = r.body().getContent();
+ }
+ public void onFailure(Call> c, Throwable t) {}
+ });
+ }
+
private void handleArguments() {
- if (getArguments() != null && getArguments().containsKey("appointmentId")) {
+ Bundle a = getArguments();
+ if (a != null && a.containsKey("appointmentId")) {
isEditing = true;
- appointmentId = getArguments().getInt("appointmentId");
- position = getArguments().getInt("position");
+ appointmentId = a.getLong("appointmentId");
+ preselectedPetId = a.getLong("petId", -1);
+ preselectedServiceId= a.getLong("serviceId", -1);
+ preselectedCustomerId = a.getLong("customerId", -1);
+ preselectedStoreId = a.getLong("storeId", -1);
+
tvMode.setText("Edit Appointment");
tvAppointmentId.setText("ID: " + appointmentId);
- 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);
+ tvAppointmentId.setVisibility(View.VISIBLE);
+ etAppointmentDate.setText(a.getString("appointmentDate"));
+ btnDelete.setVisibility(View.VISIBLE);
+
+ // Pre-fill time spinners
+ String time = a.getString("appointmentTime", "09:00");
+ if (time.length() > 5) time = time.substring(0, 5);
+ String[] parts = time.split(":");
+ if (parts.length == 2) {
+ int hour = Integer.parseInt(parts[0]);
+ int min = Integer.parseInt(parts[1]);
+ for (int i = 0; i < HOURS.length; i++)
+ if (HOURS[i] == hour) { spinnerHour.setSelection(i); break; }
+ for (int i = 0; i < MINUTES.length; i++)
+ if (MINUTES[i] == min) { spinnerMinute.setSelection(i); break; }
+ }
+
+ // Pre-fill status
+ String status = a.getString("appointmentStatus", "Booked");
+ for (int i = 0; i < STATUSES.length; i++)
+ if (STATUSES[i].equals(status)) { spinnerStatus.setSelection(i); break; }
+
} else {
- isEditing = false;
tvMode.setText("Add Appointment");
+ btnDelete.setVisibility(View.GONE);
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 saveAppointment() {
+ if (spinnerCustomer.getSelectedItemPosition() == 0) {
+ Toast.makeText(getContext(), "Select a customer", Toast.LENGTH_SHORT).show(); return;
+ }
+ if (spinnerStore.getSelectedItemPosition() == 0) {
+ Toast.makeText(getContext(), "Select a store", Toast.LENGTH_SHORT).show(); return;
+ }
+ if (spinnerPet.getSelectedItemPosition() == 0) {
+ Toast.makeText(getContext(), "Select a pet", Toast.LENGTH_SHORT).show(); return;
+ }
+ if (spinnerService.getSelectedItemPosition() == 0) {
+ Toast.makeText(getContext(), "Select a service", Toast.LENGTH_SHORT).show(); return;
+ }
+ String date = etAppointmentDate.getText().toString().trim();
+ if (date.isEmpty()) {
+ Toast.makeText(getContext(), "Select a date", Toast.LENGTH_SHORT).show(); return;
+ }
+
+ CustomerDTO customer = customerList.get(spinnerCustomer.getSelectedItemPosition() - 1);
+ StoreDTO store = storeList.get(spinnerStore.getSelectedItemPosition() - 1);
+ PetDTO pet = petList.get(spinnerPet.getSelectedItemPosition() - 1);
+ ServiceDTO service = serviceList.get(spinnerService.getSelectedItemPosition() - 1);
+
+ String time = String.format("%02d:%02d",
+ HOURS[spinnerHour.getSelectedItemPosition()],
+ MINUTES[spinnerMinute.getSelectedItemPosition()]);
+ String status = STATUSES[spinnerStatus.getSelectedItemPosition()];
+
+
+ // Validate future date+time if status is Booked
+ if ("Booked".equalsIgnoreCase(status)) {
+ try {
+ String[] dateParts = date.split("-");
+ String[] timeParts = time.split(":");
+ Calendar selected = Calendar.getInstance();
+ selected.set(
+ Integer.parseInt(dateParts[0]),
+ Integer.parseInt(dateParts[1]) - 1,
+ Integer.parseInt(dateParts[2]),
+ Integer.parseInt(timeParts[0]),
+ Integer.parseInt(timeParts[1]),
+ 0
+ );
+ if (selected.before(Calendar.getInstance())) {
+ showErrorDialog("Invalid Time",
+ "Booked appointments must be in the future. " +
+ "Please select a future date and time.");
+ return;
+ }
+ } catch (Exception e) {
+ Log.e("APPT_SAVE", "Date parse error: " + e.getMessage());
+ }
+ }
+
+ // Build DTO with all required IDs
+ AppointmentDTO dto = new AppointmentDTO(
+ customer.getCustomerId(),
+ store.getStoreId(),
+ service.getServiceId(),
+ date,
+ time,
+ status,
+ Collections.singletonList(pet.getPetId())
+ );
+
+ Log.d("APPT_SAVE", "customerId=" + customer.getCustomerId()
+ + " storeId=" + store.getStoreId()
+ + " serviceId=" + service.getServiceId()
+ + " petId=" + pet.getPetId()
+ + " date=" + date + " time=" + time);
+
+ AppointmentApi api = RetrofitClient.getAppointmentApi(requireContext());
+ if (isEditing) {
+ api.updateAppointment(appointmentId, dto).enqueue(simpleCallback("Updated"));
+ } else {
+ api.createAppointment(dto).enqueue(simpleCallback("Saved"));
+ }
}
- private void setupSpinner() {
- ArrayAdapter adapter = new ArrayAdapter(requireContext(),
- android.R.layout.simple_spinner_item,
- new String[]{"Confirmed", "Pending", "Cancelled"}) {
+ private Callback simpleCallback(String msg) {
+ return new Callback<>() {
+ public void onResponse(Call c, Response r) {
+ Log.d("APPT_SAVE", "Response: " + r.code());
+ if (r.isSuccessful()) {
+ Toast.makeText(getContext(), msg, Toast.LENGTH_SHORT).show();
+ navigateBack();
+ } else {
+ try {
+ String errorBody = r.errorBody().string();
+ Log.e("APPT_SAVE", "Error: " + errorBody);
- //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;
+ // Show proper dialog based on error type
+ if (errorBody.toLowerCase().contains("future")) {
+ showErrorDialog("Invalid Date/Time",
+ "Booked appointments must be scheduled in the future. " +
+ "Please select a future date and time.");
+ //------------------------------------------
+ } else if (errorBody.toLowerCase().contains("not available") ||
+ errorBody.toLowerCase().contains("time is not available")) {
+ showNoAvailabilityDialog();
+ } else if (r.code() == 404) {
+ showErrorDialog("Not Found",
+ "The selected pet, customer or service was not found.");
+ } else if (r.code() == 403) {
+ showErrorDialog("Access Denied",
+ "You don't have permission to perform this action.");
+ } else if (r.code() == 400) {
+ showErrorDialog("Invalid Request", errorBody);
+ } else {
+ showErrorDialog("Error", "Something went wrong. Please try again.");
+ }
+ //-----------------------------
+ } catch (Exception e) {
+ Log.e("APPT_SAVE", "Failed to read error body");
+ showErrorDialog("Error", "Something went wrong. Please try again.");
+ }
+ }
+ }
+
+ public void onFailure(Call c, Throwable t) {
+ Log.e("APPT_SAVE", "Failure: " + t.getMessage());
+ Toast.makeText(getContext(), "Network error", Toast.LENGTH_SHORT).show();
}
};
- adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
- spinnerStatus.setAdapter(adapter);
}
-}
+
+ private void showNoAvailabilityDialog() {
+ new AlertDialog.Builder(requireContext())
+ .setTitle("No Availability")
+ .setMessage("This time slot is already booked for the selected service and store. Please choose a different time or date.")
+ .setPositiveButton("Change Time", (d, w) -> d.dismiss())
+ .setNegativeButton("Cancel Booking", (d, w) -> navigateBack())
+ .setCancelable(false)
+ .show();
+ }
+
+ private void showErrorDialog(String title, String message) {
+ new AlertDialog.Builder(requireContext())
+ .setTitle(title)
+ .setMessage(message)
+ .setPositiveButton("OK", null)
+ .show();
+ }
+ private void confirmDelete() {
+ new AlertDialog.Builder(requireContext())
+ .setTitle("Delete Appointment?")
+ .setPositiveButton("Yes", (d, w) ->
+ RetrofitClient.getAppointmentApi(requireContext())
+ .deleteAppointment(appointmentId)
+ .enqueue(new Callback() {
+ public void onResponse(Call c, Response r) { navigateBack(); }
+ public void onFailure(Call c, Throwable t) {
+ Toast.makeText(getContext(), "Delete failed", Toast.LENGTH_SHORT).show();
+ }
+ }))
+ .setNegativeButton("No", null).show();
+ }
+
+ private void navigateBack() {
+ ListFragment lf = (ListFragment) getParentFragment();
+ if (lf != null) lf.getChildFragmentManager().popBackStack();
+ }
+}
\ No newline at end of file
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
index 175feb91..f98351e3 100644
--- 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
@@ -1,34 +1,66 @@
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.os.Handler;
+import android.os.Looper;
+import android.text.Editable;
+import android.text.TextWatcher;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+import android.widget.AutoCompleteTextView;
import android.widget.Button;
-import android.widget.EditText;
import android.widget.TextView;
import android.widget.Toast;
+
+import androidx.appcompat.app.AlertDialog;
+import androidx.fragment.app.Fragment;
+
import com.example.petstoremobile.R;
+import com.example.petstoremobile.adapters.BlackTextArrayAdapter;
+import com.example.petstoremobile.api.InventoryApi;
+import com.example.petstoremobile.api.ProductApi;
+import com.example.petstoremobile.api.RetrofitClient;
+import com.example.petstoremobile.dtos.InventoryDTO;
+import com.example.petstoremobile.dtos.InventoryRequest;
+import com.example.petstoremobile.dtos.PageResponse;
+import com.example.petstoremobile.dtos.ProductDTO;
import com.example.petstoremobile.fragments.ListFragment;
import com.example.petstoremobile.fragments.listfragments.InventoryFragment;
-import com.example.petstoremobile.models.Inventory;
-import com.example.petstoremobile.utils.ActivityLogger;
-import com.example.petstoremobile.utils.InputValidator;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import retrofit2.Call;
+import retrofit2.Callback;
+import retrofit2.Response;
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 TextView tvMode, tvInventoryId, tvProductInfo;
+ private AutoCompleteTextView etProductSearch;
+ private android.widget.EditText etQuantity;
+ private Button btnSave, btnDelete, btnBack;
+
+ private InventoryApi inventoryApi;
+ private ProductApi productApi;
private InventoryFragment inventoryFragment;
- // Set the inventory fragment as parent so we refer back when save or delete is done
+ private boolean isEditing = false;
+ private long inventoryId = -1;
+
+ // The product selected from the dropdown
+ private ProductDTO selectedProduct = null;
+
+ // For debouncing product search
+ private final Handler searchHandler = new Handler(Looper.getMainLooper());
+ private Runnable searchRunnable;
+
+ // Dropdown list
+ private final List productSuggestions = new ArrayList<>();
+ private ArrayAdapter dropdownAdapter;
+
public void setInventoryFragment(InventoryFragment fragment) {
this.inventoryFragment = fragment;
}
@@ -38,102 +70,271 @@ public class InventoryDetailFragment extends Fragment {
Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_inventory_detail, container, false);
+ inventoryApi = RetrofitClient.getInventoryApi(requireContext());
+ productApi = RetrofitClient.getProductApi(requireContext());
+
initViews(view);
+ setupProductSearch();
handleArguments();
- btnBack.setOnClickListener(v -> {
- ListFragment listFragment = (ListFragment) getParentFragment();
- if (listFragment != null) listFragment.getChildFragmentManager().popBackStack();
- });
- btnSaveInventory.setOnClickListener(v -> saveInventory());
- btnDeleteInventory.setOnClickListener(v -> deleteInventory());
+ btnBack.setOnClickListener(v -> navigateBack());
+ btnSave.setOnClickListener(v -> saveInventory());
+ btnDelete.setOnClickListener(v -> confirmDelete());
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);
+ tvProductInfo = view.findViewById(R.id.tvProductInfo);
+ etProductSearch = view.findViewById(R.id.etProductSearch);
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);
+ btnSave = view.findViewById(R.id.btnSaveInventory);
+ btnDelete = view.findViewById(R.id.btnDeleteInventory);
btnBack = view.findViewById(R.id.btnInventoryBack);
+
+ // Setup dropdown adapter
+ dropdownAdapter = new BlackTextArrayAdapter<>(requireContext(),
+ android.R.layout.simple_dropdown_item_1line, new ArrayList<>());
+ etProductSearch.setAdapter(dropdownAdapter);
+ etProductSearch.setThreshold(1); // start showing after 1 character
}
-}
+
+ // Product search dropdown
+ private void setupProductSearch() {
+ etProductSearch.addTextChangedListener(new TextWatcher() {
+ @Override
+ public void beforeTextChanged(CharSequence s, int i, int i1, int i2) {
+ }
+
+ @Override
+ public void afterTextChanged(Editable s) {
+ }
+
+ @Override
+ public void onTextChanged(CharSequence s, int start, int before, int count) {
+ // Clear selected product when user is typing again
+ selectedProduct = null;
+ tvProductInfo.setVisibility(View.GONE);
+
+ if (searchRunnable != null)
+ searchHandler.removeCallbacks(searchRunnable);
+ String query = s.toString().trim();
+ if (query.isEmpty())
+ return;
+
+ searchRunnable = () -> searchProducts(query);
+ searchHandler.postDelayed(searchRunnable, 400);
+ }
+ });
+
+ // When user picks an item from the dropdown
+ etProductSearch.setOnItemClickListener((parent, view, position, id) -> {
+ if (position < productSuggestions.size()) {
+ selectedProduct = productSuggestions.get(position);
+ // Show product details below the search box
+ tvProductInfo.setText(
+ "ID: " + selectedProduct.getProdId()
+ + " • " + selectedProduct.getCategoryName());
+ tvProductInfo.setVisibility(View.VISIBLE);
+ }
+ });
+ }
+
+ private void searchProducts(String query) {
+ productApi.getAllProducts(query, 0, 20).enqueue(new Callback>() {
+ @Override
+ public void onResponse(Call> call,
+ Response> response) {
+ if (response.isSuccessful() && response.body() != null) {
+ productSuggestions.clear();
+ productSuggestions.addAll(response.body().getContent());
+
+ // Build display strings: "Product Name (ID: X)"
+ List names = new ArrayList<>();
+ for (ProductDTO p : productSuggestions) {
+ names.add(p.getProdName() + " (ID: " + p.getProdId() + ")");
+ }
+
+ dropdownAdapter.clear();
+ dropdownAdapter.addAll(names);
+ dropdownAdapter.notifyDataSetChanged();
+ etProductSearch.showDropDown();
+ }
+ }
+
+ @Override
+ public void onFailure(Call> call, Throwable t) {
+ Toast.makeText(getContext(), "Failed to load products", Toast.LENGTH_SHORT).show();
+ }
+ });
+ }
+
+ // Arguments (edit mode)
+
+ private void handleArguments() {
+ Bundle args = getArguments();
+ if (args != null && args.containsKey("inventoryId")) {
+ isEditing = true;
+ inventoryId = args.getLong("inventoryId");
+
+ tvMode.setText("Edit Inventory");
+ tvInventoryId.setText("Inventory ID: " + inventoryId);
+ tvInventoryId.setVisibility(View.VISIBLE);
+
+ // Pre-fill search box with existing product name
+ String productName = args.getString("productName", "");
+ long prodId = args.getLong("prodId", -1);
+ etProductSearch.setText(productName);
+
+ // Show existing product info
+ if (prodId != -1) {
+ tvProductInfo.setText(
+ "ID: " + prodId
+ + " • " + args.getString("categoryName", ""));
+ tvProductInfo.setVisibility(View.VISIBLE);
+
+ // Build a minimal ProductDTO so selectedProduct is not null on save
+ selectedProduct = new ProductDTO(productName, null, null, null);
+ selectedProduct.setProdId(prodId);
+ }
+
+ etQuantity.setText(String.valueOf(args.getInt("quantity", 0)));
+ btnDelete.setVisibility(View.VISIBLE);
+ btnSave.setText("Save");
+ } else {
+ isEditing = false;
+ tvMode.setText("Add Inventory");
+ tvInventoryId.setVisibility(View.GONE);
+ tvProductInfo.setVisibility(View.GONE);
+ btnDelete.setVisibility(View.GONE);
+ btnSave.setText("Add");
+ }
+ }
+
+ // Save
+ private void saveInventory() {
+ if (selectedProduct == null) {
+ etProductSearch.setError("Please select a product from the list");
+ etProductSearch.requestFocus();
+ return;
+ }
+
+ String quantityStr = etQuantity.getText().toString().trim();
+ if (quantityStr.isEmpty()) {
+ etQuantity.setError("Quantity is required");
+ etQuantity.requestFocus();
+ return;
+ }
+
+ int quantity;
+ try {
+ quantity = Integer.parseInt(quantityStr);
+ } catch (NumberFormatException e) {
+ etQuantity.setError("Invalid quantity");
+ return;
+ }
+
+ if (quantity < 0) {
+ etQuantity.setError("Quantity must be 0 or more");
+ etQuantity.requestFocus();
+ return;
+ }
+
+ InventoryRequest request = new InventoryRequest(selectedProduct.getProdId(), quantity);
+ setButtonsEnabled(false);
+
+ if (isEditing) {
+ inventoryApi.updateInventory(inventoryId, request).enqueue(new Callback() {
+ @Override
+ public void onResponse(Call call, Response response) {
+ setButtonsEnabled(true);
+ if (response.isSuccessful()) {
+ Toast.makeText(getContext(), "Inventory updated", Toast.LENGTH_SHORT).show();
+ notifyParentAndGoBack();
+ } else {
+ Toast.makeText(getContext(), "Update failed: " + response.code(), Toast.LENGTH_SHORT).show();
+ }
+ }
+
+ @Override
+ public void onFailure(Call call, Throwable t) {
+ setButtonsEnabled(true);
+ Toast.makeText(getContext(), "Network error", Toast.LENGTH_SHORT).show();
+ }
+ });
+ } else {
+ inventoryApi.createInventory(request).enqueue(new Callback() {
+ @Override
+ public void onResponse(Call call, Response response) {
+ setButtonsEnabled(true);
+ if (response.isSuccessful()) {
+ Toast.makeText(getContext(), "Inventory created", Toast.LENGTH_SHORT).show();
+ notifyParentAndGoBack();
+ } else {
+ Toast.makeText(getContext(), "Create failed: " + response.code(), Toast.LENGTH_SHORT).show();
+ }
+ }
+
+ @Override
+ public void onFailure(Call call, Throwable t) {
+ setButtonsEnabled(true);
+ Toast.makeText(getContext(), "Network error", Toast.LENGTH_SHORT).show();
+ }
+ });
+ }
+ }
+
+ // Delete
+ private void confirmDelete() {
+ new AlertDialog.Builder(requireContext())
+ .setTitle("Delete inventory item?")
+ .setMessage("This cannot be undone.")
+ .setPositiveButton("Delete", (d, w) -> deleteInventory())
+ .setNegativeButton("Cancel", null)
+ .show();
+ }
+
+ private void deleteInventory() {
+ setButtonsEnabled(false);
+ inventoryApi.deleteInventory(inventoryId).enqueue(new Callback() {
+ @Override
+ public void onResponse(Call call, Response response) {
+ setButtonsEnabled(true);
+ if (response.isSuccessful()) {
+ Toast.makeText(getContext(), "Inventory deleted", Toast.LENGTH_SHORT).show();
+ notifyParentAndGoBack();
+ } else {
+ Toast.makeText(getContext(), "Delete failed: " + response.code(), Toast.LENGTH_SHORT).show();
+ }
+ }
+
+ @Override
+ public void onFailure(Call call, Throwable t) {
+ setButtonsEnabled(true);
+ Toast.makeText(getContext(), "Network error", Toast.LENGTH_SHORT).show();
+ }
+ });
+ }
+
+ // Helpers
+
+ private void notifyParentAndGoBack() {
+ if (inventoryFragment != null)
+ inventoryFragment.onInventoryChanged();
+ navigateBack();
+ }
+
+ private void navigateBack() {
+ ListFragment lf = (ListFragment) getParentFragment();
+ if (lf != null)
+ lf.getChildFragmentManager().popBackStack();
+ }
+
+ private void setButtonsEnabled(boolean enabled) {
+ btnSave.setEnabled(enabled);
+ btnDelete.setEnabled(enabled);
+ btnBack.setEnabled(enabled);
+ }
+}
\ No newline at end of file
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
index 70c19833..c37558a7 100644
--- 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
@@ -20,6 +20,7 @@ import android.widget.TextView;
import android.widget.Toast;
import com.example.petstoremobile.R;
+import com.example.petstoremobile.adapters.BlackTextArrayAdapter;
import com.example.petstoremobile.api.PetApi;
import com.example.petstoremobile.api.RetrofitClient;
import com.example.petstoremobile.dtos.PetDTO;
@@ -234,19 +235,9 @@ public class PetDetailFragment extends Fragment {
//helper function to set up the spinner menu for pet status
private void setupSpinner() {
- ArrayAdapter adapter = new ArrayAdapter(requireContext(),
+ BlackTextArrayAdapter adapter = new BlackTextArrayAdapter<>(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;
- }
- };
+ new String[]{"Available", "Adopted"});
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
index 4179c4f5..8427ab65 100644
--- 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
@@ -1,139 +1,191 @@
package com.example.petstoremobile.fragments.listfragments.detailfragments;
-// Uses InputValidator for detailed field validation and ActivityLogger to log all changes.
-
import android.os.Bundle;
+import android.util.Log;
+import android.view.*;
+import android.widget.*;
+import androidx.annotation.NonNull;
+import androidx.appcompat.app.AlertDialog;
import androidx.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.adapters.BlackTextArrayAdapter;
+import com.example.petstoremobile.api.*;
+import com.example.petstoremobile.dtos.*;
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;
+import java.math.BigDecimal;
+import java.util.*;
+import retrofit2.*;
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;
+ private EditText etProductName, etProductDesc, etProductPrice;
+ private Spinner spinnerCategory;
+ private Button btnSave, btnDelete, btnBack;
- // Set the product fragment as parent so we refer back when save or delete is done
- public void setProductFragment(ProductFragment fragment) {
- this.productFragment = fragment;
- }
+ private long prodId = -1;
+ private boolean isEditing = false;
+ private long preselectedCategoryId = -1;
+
+ private List categoryList = new ArrayList<>();
@Override
- public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_product_detail, container, false);
-
initViews(view);
+ loadCategories();
handleArguments();
- btnBack.setOnClickListener(v -> {
- ListFragment listFragment = (ListFragment) getParentFragment();
- if (listFragment != null) listFragment.getChildFragmentManager().popBackStack();
- });
- btnSaveProduct.setOnClickListener(v -> saveProduct());
- btnDeleteProduct.setOnClickListener(v -> deleteProduct());
-
+ btnBack.setOnClickListener(v -> navigateBack());
+ btnSave.setOnClickListener(v -> saveProduct());
+ btnDelete.setOnClickListener(v -> confirmDelete());
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();
- }
+ private void initViews(View v) {
+ tvMode = v.findViewById(R.id.tvProductMode);
+ tvProductId = v.findViewById(R.id.tvProductId);
+ etProductName = v.findViewById(R.id.etProductName);
+ etProductDesc = v.findViewById(R.id.etProductDesc);
+ etProductPrice = v.findViewById(R.id.etProductPrice);
+ spinnerCategory = v.findViewById(R.id.spinnerProductCategory);
+ btnSave = v.findViewById(R.id.btnSaveProduct);
+ btnDelete = v.findViewById(R.id.btnDeleteProduct);
+ btnBack = v.findViewById(R.id.btnProductBack);
}
- // 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 loadCategories() {
+ RetrofitClient.getCategoryApi(requireContext()).getAllCategories(0, 100)
+ .enqueue(new Callback>() {
+ public void onResponse(Call> c,
+ Response> r) {
+ if (r.isSuccessful() && r.body() != null) {
+ categoryList = r.body().getContent();
+ populateCategorySpinner();
+ }
+ }
+ public void onFailure(Call> c, Throwable t) {
+ Log.e("ProductDetail", "Category load failed: " + t.getMessage());
+ }
+ });
+ }
+
+ private void populateCategorySpinner() {
+ List names = new ArrayList<>();
+ names.add("-- Select Category --");
+ for (CategoryDTO c : categoryList) names.add(c.getCategoryName());
+ spinnerCategory.setAdapter(new BlackTextArrayAdapter<>(requireContext(),
+ android.R.layout.simple_spinner_item, names));
+ if (preselectedCategoryId != -1) {
+ for (int i = 0; i < categoryList.size(); i++) {
+ if (categoryList.get(i).getCategoryId().equals(preselectedCategoryId)) {
+ spinnerCategory.setSelection(i + 1); break;
+ }
+ }
}
}
private void handleArguments() {
- if (getArguments() != null && getArguments().containsKey("productId")) {
+ Bundle a = getArguments();
+ if (a != null && a.containsKey("prodId")) {
isEditing = true;
- productId = getArguments().getInt("productId");
- position = getArguments().getInt("position");
+ prodId = a.getLong("prodId");
+ preselectedCategoryId = a.getLong("categoryId", -1);
+
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);
+ tvProductId.setText("ID: " + prodId);
+ tvProductId.setVisibility(View.VISIBLE);
+ etProductName.setText(a.getString("prodName"));
+ etProductDesc.setText(a.getString("prodDesc"));
+ etProductPrice.setText(a.getString("prodPrice"));
+ btnDelete.setVisibility(View.VISIBLE);
} else {
- isEditing = false;
tvMode.setText("Add Product");
+ btnDelete.setVisibility(View.GONE);
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);
+ private void saveProduct() {
+ String name = etProductName.getText().toString().trim();
+ String desc = etProductDesc.getText().toString().trim();
+ String priceStr = etProductPrice.getText().toString().trim();
+
+ if (name.isEmpty()) {
+ etProductName.setError("Enter product name"); return;
+ }
+ if (spinnerCategory.getSelectedItemPosition() == 0) {
+ Toast.makeText(getContext(), "Select a category", Toast.LENGTH_SHORT).show(); return;
+ }
+ if (priceStr.isEmpty()) {
+ etProductPrice.setError("Enter price"); return;
+ }
+
+ CategoryDTO category = categoryList.get(spinnerCategory.getSelectedItemPosition() - 1);
+ BigDecimal price;
+ try {
+ price = new BigDecimal(priceStr);
+ } catch (Exception e) {
+ etProductPrice.setError("Invalid price"); return;
+ }
+
+ ProductDTO dto = new ProductDTO(name, category.getCategoryId(), desc, price);
+
+ Log.d("PRODUCT_SAVE", "name=" + name + " categoryId=" + category.getCategoryId()
+ + " price=" + price);
+
+ ProductApi api = RetrofitClient.getProductApi(requireContext());
+ if (isEditing) {
+ api.updateProduct(prodId, dto).enqueue(simpleCallback("Updated"));
+ } else {
+ api.createProduct(dto).enqueue(simpleCallback("Saved"));
+ }
}
-}
+
+ private Callback simpleCallback(String msg) {
+ return new Callback<>() {
+ public void onResponse(Call c, Response r) {
+ if (r.isSuccessful()) {
+ Toast.makeText(getContext(), msg, Toast.LENGTH_SHORT).show();
+ navigateBack();
+ } else {
+ try {
+ String err = r.errorBody().string();
+ Log.e("PRODUCT_SAVE", "Error: " + err);
+ Toast.makeText(getContext(), "Error " + r.code(), Toast.LENGTH_SHORT).show();
+ } catch (Exception e) {
+ Log.e("PRODUCT_SAVE", "Failed to read error");
+ }
+ }
+ }
+ public void onFailure(Call c, Throwable t) {
+ Log.e("PRODUCT_SAVE", "Failure: " + t.getMessage());
+ Toast.makeText(getContext(), "Network error", Toast.LENGTH_SHORT).show();
+ }
+ };
+ }
+
+ private void confirmDelete() {
+ new AlertDialog.Builder(requireContext())
+ .setTitle("Delete Product?")
+ .setPositiveButton("Yes", (d, w) ->
+ RetrofitClient.getProductApi(requireContext())
+ .deleteProduct(prodId)
+ .enqueue(new Callback() {
+ public void onResponse(Call c, Response r) {
+ navigateBack();
+ }
+ public void onFailure(Call c, Throwable t) {
+ Toast.makeText(getContext(), "Delete failed",
+ Toast.LENGTH_SHORT).show();
+ }
+ }))
+ .setNegativeButton("No", null).show();
+ }
+
+ private void navigateBack() {
+ ListFragment lf = (ListFragment) getParentFragment();
+ if (lf != null) lf.getChildFragmentManager().popBackStack();
+ }
+}
\ No newline at end of file
diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ProductSupplierDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ProductSupplierDetailFragment.java
new file mode 100644
index 00000000..2bd47432
--- /dev/null
+++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ProductSupplierDetailFragment.java
@@ -0,0 +1,221 @@
+package com.example.petstoremobile.fragments.listfragments.detailfragments;
+
+import android.os.Bundle;
+import android.util.Log;
+import android.view.*;
+import android.widget.*;
+import androidx.annotation.NonNull;
+import androidx.appcompat.app.AlertDialog;
+import androidx.fragment.app.Fragment;
+import com.example.petstoremobile.R;
+import com.example.petstoremobile.adapters.BlackTextArrayAdapter;
+import com.example.petstoremobile.api.*;
+import com.example.petstoremobile.dtos.*;
+import com.example.petstoremobile.fragments.ListFragment;
+import java.math.BigDecimal;
+import java.util.*;
+import retrofit2.*;
+
+public class ProductSupplierDetailFragment extends Fragment {
+
+ private TextView tvMode;
+ private Spinner spinnerProduct, spinnerSupplier;
+ private EditText etCost;
+ private Button btnSave, btnDelete, btnBack;
+
+ private boolean isEditing = false;
+ private long editProductId = -1;
+ private long editSupplierId = -1;
+ private long preselectedProductId = -1;
+ private long preselectedSupplierId = -1;
+
+ private List productList = new ArrayList<>();
+ private List supplierList = new ArrayList<>();
+
+ @Override
+ public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ View view = inflater.inflate(R.layout.fragment_product_supplier_detail, container, false);
+ initViews(view);
+ loadData();
+ handleArguments();
+
+ btnBack.setOnClickListener(v -> navigateBack());
+ btnSave.setOnClickListener(v -> save());
+ btnDelete.setOnClickListener(v -> confirmDelete());
+ return view;
+ }
+
+ private void initViews(View v) {
+ tvMode = v.findViewById(R.id.tvPSMode);
+ spinnerProduct = v.findViewById(R.id.spinnerPSProduct);
+ spinnerSupplier = v.findViewById(R.id.spinnerPSSupplier);
+ etCost = v.findViewById(R.id.etPSCost);
+ btnSave = v.findViewById(R.id.btnSavePS);
+ btnDelete = v.findViewById(R.id.btnDeletePS);
+ btnBack = v.findViewById(R.id.btnPSBack);
+ }
+
+ private void loadData() {
+ loadProducts();
+ loadSuppliers();
+ }
+
+ private void loadProducts() {
+ RetrofitClient.getProductApi(requireContext()).getAllProducts(null, 0, 200)
+ .enqueue(new Callback>() {
+ public void onResponse(Call> c,
+ Response