Web products #60

Merged
RecentRunner merged 3 commits from web-products into main 2026-03-30 09:51:34 -06:00
140 changed files with 7952 additions and 1650 deletions
Showing only changes of commit 84c70a1568 - Show all commits

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

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

View File

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

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

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectRootManager" version="2">
<output url="file://$PROJECT_DIR$/out" />
</component>
</project>

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

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

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

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

View File

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

View File

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

View File

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

View File

@@ -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<AdoptionAdapter.AdoptionViewHolder> {
private List<Adoption> adoptionList;
private OnAdoptionClickListener adoptionClickListener;
private List<AdoptionDTO> adoptionList;
private OnAdoptionClickListener listener;
// Interface for adoption click on recycler view
public interface OnAdoptionClickListener {
void onAdoptionClick(int position);
}
// Constructor
public AdoptionAdapter(List<Adoption> adoptionList, OnAdoptionClickListener adoptionClickListener) {
public AdoptionAdapter(List<AdoptionDTO> 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(); }
}

View File

@@ -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<AppointmentAdapter.AppointmentViewHolder> {
private List<Appointment> appointmentList;
private List<AppointmentDTO> appointmentList;
private OnAppointmentClickListener appointmentClickListener;
// Interface for appointment click on recycler view
public interface OnAppointmentClickListener {
void onAppointmentClick(int position);
}
// Constructor
public AppointmentAdapter(List<Appointment> appointmentList, OnAppointmentClickListener appointmentClickListener) {
public AppointmentAdapter(List<AppointmentDTO> 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<AppointmentAdapter.
}
}
// Create a new row view
@NonNull
@Override
public AppointmentViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
@@ -51,31 +46,34 @@ public class AppointmentAdapter extends RecyclerView.Adapter<AppointmentAdapter.
return new AppointmentViewHolder(v);
}
// Populate the row with appointment data
@Override
public void onBindViewHolder(@NonNull AppointmentViewHolder holder, int position) {
Appointment appointment = appointmentList.get(position);
AppointmentDTO a = appointmentList.get(position);
holder.tvCustomerName.setText(appointment.getCustomerName());
holder.tvPetName.setText("Pet: " + appointment.getPetName());
holder.tvServiceType.setText(appointment.getServiceType());
holder.tvDateTime.setText(appointment.getAppointmentDate() + " at " + appointment.getAppointmentTime());
holder.tvAppointmentStatus.setText(appointment.getStatus());
holder.tvCustomerName.setText(a.getCustomerName() != null ? a.getCustomerName() : "");
holder.tvPetName.setText("Pet: " + (a.getPetName() != null ? a.getPetName() : ""));
holder.tvServiceType.setText(a.getServiceType() != null ? a.getServiceType() : "");
holder.tvDateTime.setText((a.getAppointmentDate() != null ? a.getAppointmentDate() : "") +
" at " + (a.getAppointmentTime() != null ? a.getAppointmentTime() : ""));
// Set the status color depending on appointment status
switch (appointment.getStatus()) {
case "Confirmed":
holder.tvAppointmentStatus.setBackgroundColor(Color.parseColor("#4CAF50"));
String status = a.getStatus() != null ? a.getStatus() : "";
holder.tvAppointmentStatus.setText(status);
switch (status) {
case "Booked":
holder.tvAppointmentStatus.setBackgroundColor(Color.parseColor("#2196F3")); // blue
break;
case "Pending":
holder.tvAppointmentStatus.setBackgroundColor(Color.parseColor("#FF9800"));
case "Completed":
holder.tvAppointmentStatus.setBackgroundColor(Color.parseColor("#4CAF50")); // green
break;
case "Cancelled":
holder.tvAppointmentStatus.setBackgroundColor(Color.parseColor("#F44336")); // red
break;
default:
holder.tvAppointmentStatus.setBackgroundColor(Color.parseColor("#F44336"));
holder.tvAppointmentStatus.setBackgroundColor(Color.parseColor("#9E9E9E")); // gray
break;
}
// When a row is clicked, open the detail view
holder.itemView.setOnClickListener(v -> appointmentClickListener.onAppointmentClick(position));
}

View File

@@ -0,0 +1,44 @@
package com.example.petstoremobile.adapters;
import android.content.Context;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat;
import com.example.petstoremobile.R;
import java.util.List;
// A class that overrides the arrayAdapter so the text color changes based on theme
public class BlackTextArrayAdapter<T> extends ArrayAdapter<T> {
public BlackTextArrayAdapter(@NonNull Context context, int resource, @NonNull T[] objects) {
super(context, resource, objects);
}
public BlackTextArrayAdapter(@NonNull Context context, int resource, @NonNull List<T> objects) {
super(context, resource, objects);
}
@NonNull
@Override
public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) {
View view = super.getView(position, convertView, parent);
view.setBackgroundColor(ContextCompat.getColor(getContext(), R.color.white));
if (view instanceof TextView) {
((TextView) view).setTextColor(ContextCompat.getColor(getContext(), R.color.spinner_text));
}
return view;
}
@Override
public View getDropDownView(int position, @Nullable View convertView, @NonNull ViewGroup parent) {
View view = super.getDropDownView(position, convertView, parent);
view.setBackgroundColor(ContextCompat.getColor(getContext(), R.color.white));
if (view instanceof TextView) {
((TextView) view).setTextColor(ContextCompat.getColor(getContext(), R.color.spinner_text));
}
return view;
}
}

View File

@@ -1,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<InventoryAdapter.InventoryViewHolder> {
private List<Inventory> inventoryList;
private OnInventoryClickListener inventoryClickListener;
private final List<InventoryDTO> inventoryList;
private final OnInventoryClickListener clickListener;
private final List<Long> 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<Inventory> inventoryList, OnInventoryClickListener inventoryClickListener) {
public InventoryAdapter(List<InventoryDTO> 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<Long> getSelectedIds() {
return new ArrayList<>(selectedIds);
}
public void clearSelection() {
selectedIds.clear();
selectionMode = false;
notifyDataSetChanged();
}
public boolean isInSelectionMode() {
return selectionMode;
}
@Override
public int getItemCount() {
return inventoryList.size();
}
}
}

View File

@@ -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<PetAdapter.PetViewHolder> {
// 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<PetAdapter.PetViewHolder> {
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<PetAdapter.PetViewHolder> {
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));
}

View File

@@ -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<ProductAdapter.ProductViewHolder> {
private List<Product> productList;
private OnProductClickListener productClickListener;
private List<ProductDTO> productList;
private OnProductClickListener listener;
// Interface for product click on recycler view
public interface OnProductClickListener {
void onProductClick(int position);
}
// Constructor
public ProductAdapter(List<Product> productList, OnProductClickListener productClickListener) {
public ProductAdapter(List<ProductDTO> productList, OnProductClickListener listener) {
this.productList = productList;
this.productClickListener = productClickListener;
this.listener = listener;
}
// Get the controls of each row in recycler view
public static class ProductViewHolder extends RecyclerView.ViewHolder {
TextView tvProductName, tvProductDesc, tvCategory, tvProductPrice, tvStockQuantity;
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(); }
}

View File

@@ -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<ProductSupplierAdapter.PSViewHolder> {
private List<ProductSupplierDTO> list;
private OnProductSupplierClickListener listener;
public interface OnProductSupplierClickListener {
void onProductSupplierClick(int position);
}
public ProductSupplierAdapter(List<ProductSupplierDTO> 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(); }
}

View File

@@ -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<PurchaseOrderAdapter.POViewHolder> {
private List<PurchaseOrderDTO> list;
private OnPurchaseOrderClickListener listener;
public interface OnPurchaseOrderClickListener {
void onPurchaseOrderClick(int position);
}
public PurchaseOrderAdapter(List<PurchaseOrderDTO> list, OnPurchaseOrderClickListener listener) {
this.list = list;
this.listener = listener;
}
public static class POViewHolder extends RecyclerView.ViewHolder {
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();
}
}

View File

@@ -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<SaleAdapter.SaleViewHolder> {
private List<Sale> saleList;
private OnSaleClickListener saleClickListener;
public interface OnSaleClickListener {
void onSaleClick(int position);
}
public SaleAdapter(List<Sale> 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();
}
}

View File

@@ -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<PageResponse<AdoptionDTO>> getAllAdoptions(
@Query("page") int page,
@Query("size") int size);
@GET("api/v1/adoptions/{id}")
Call<AdoptionDTO> getAdoptionById(@Path("id") Long id);
@POST("api/v1/adoptions")
Call<AdoptionDTO> createAdoption(@Body AdoptionDTO adoption);
@PUT("api/v1/adoptions/{id}")
Call<AdoptionDTO> updateAdoption(@Path("id") Long id, @Body AdoptionDTO adoption);
@DELETE("api/v1/adoptions/{id}")
Call<Void> deleteAdoption(@Path("id") Long id);
}

View File

@@ -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<PageResponse<AppointmentDTO>> getAllAppointments(
@Query("page") int page,
@Query("size") int size);
@GET("api/v1/appointments/{id}")
Call<AppointmentDTO> getAppointmentById(@Path("id") Long id);
@POST("api/v1/appointments")
Call<AppointmentDTO> createAppointment(@Body AppointmentDTO appointment);
@PUT("api/v1/appointments/{id}")
Call<AppointmentDTO> updateAppointment(@Path("id") Long id, @Body AppointmentDTO appointment);
@DELETE("api/v1/appointments/{id}")
Call<Void> deleteAppointment(@Path("id") Long id);
}

View File

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

View File

@@ -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<PageResponse<InventoryDTO>> 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<InventoryDTO> getInventoryById(@Path("id") Long id);
// POST /api/v1/inventory
@POST("api/v1/inventory")
Call<InventoryDTO> createInventory(@Body InventoryRequest request);
// PUT /api/v1/inventory/{id}
@PUT("api/v1/inventory/{id}")
Call<InventoryDTO> updateInventory(@Path("id") Long id, @Body InventoryRequest request);
// DELETE /api/v1/inventory/{id}
@DELETE("api/v1/inventory/{id}")
Call<Void> deleteInventory(@Path("id") Long id);
// DELETE /api/v1/inventory (bulk delete)
@DELETE("api/v1/inventory")
Call<Void> bulkDeleteInventory(@Body BulkDeleteRequest request);
}

View File

@@ -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<PageResponse<ProductDTO>> getAllProducts(
@Query("q") String query,
@Query("page") int page,
@Query("size") int size);
@GET("api/v1/products/{id}")
Call<ProductDTO> getProductById(@Path("id") Long id);
@POST("api/v1/products")
Call<ProductDTO> createProduct(@Body ProductDTO product);
@PUT("api/v1/products/{id}")
Call<ProductDTO> updateProduct(@Path("id") Long id, @Body ProductDTO product);
@DELETE("api/v1/products/{id}")
Call<Void> deleteProduct(@Path("id") Long id);
}

View File

@@ -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<PageResponse<ProductSupplierDTO>> getAllProductSuppliers(
@Query("page") int page,
@Query("size") int size);
@POST("api/v1/product-suppliers")
Call<ProductSupplierDTO> createProductSupplier(@Body ProductSupplierDTO dto);
@PUT("api/v1/product-suppliers/{productId}/{supplierId}")
Call<ProductSupplierDTO> updateProductSupplier(
@Path("productId") Long productId,
@Path("supplierId") Long supplierId,
@Body ProductSupplierDTO dto);
@DELETE("api/v1/product-suppliers/{productId}/{supplierId}")
Call<Void> deleteProductSupplier(
@Path("productId") Long productId,
@Path("supplierId") Long supplierId);
}

View File

@@ -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<PageResponse<PurchaseOrderDTO>> getAllPurchaseOrders(
@Query("page") int page,
@Query("size") int size);
@GET("api/v1/purchase-orders/{id}")
Call<PurchaseOrderDTO> getPurchaseOrderById(@Path("id") Long id);
}

View File

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

View File

@@ -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<PageResponse<SaleDTO>> getAllSales(
@Query("page") int page,
@Query("size") int size);
@GET("api/v1/sales/{id}")
Call<SaleDTO> getSaleById(@Path("id") Long id);
@POST("api/v1/sales")
Call<SaleDTO> createSale(@Body SaleDTO sale);
@PUT("api/v1/sales/{id}")
Call<SaleDTO> updateSale(@Path("id") Long id, @Body SaleDTO sale);
@DELETE("api/v1/sales/{id}")
Call<Void> deleteSale(@Path("id") Long id);
}

View File

@@ -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<PageResponse<StoreDTO>> getAllStores(
@Query("page") int page,
@Query("size") int size);
}

View File

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

View File

@@ -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<String> petNames;
private List<Long> 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<Long> 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<String> getPetNames() {
return petNames;
}
public List<Long> getPetIds() {
return petIds;
}
public String getCreatedAt() {
return createdAt;
}
public String getUpdatedAt() {
return updatedAt;
}
// Convenience getters for adapter/list display
public String getPetName() {
return (petNames != null && !petNames.isEmpty()) ? petNames.get(0) : "";
}
public Long getPetID() {
return (petIds != null && !petIds.isEmpty()) ? petIds.get(0) : null;
}
public Long getPetId() {
return getPetID();
}
// Keep old name so adapter doesn't break
public String getServiceType() {
return serviceName;
}
public Long getServiceID() {
return serviceId;
}
// Status alias
public String getStatus() {
return appointmentStatus;
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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<Adoption> adoptionList = new ArrayList<>();
private List<Adoption> filteredList = new ArrayList<>();
private List<AdoptionDTO> adoptionList = new ArrayList<>();
private List<AdoptionDTO> 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<PageResponse<AdoptionDTO>>() {
public void onResponse(Call<PageResponse<AdoptionDTO>> c,
Response<PageResponse<AdoptionDTO>> 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<PageResponse<AdoptionDTO>> 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); }
}

View File

@@ -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<Appointment> appointmentList = new ArrayList<>(); // full data list
private List<Appointment> filteredList = new ArrayList<>(); // filtered display list
private List<AppointmentDTO> appointmentList = new ArrayList<>();
private List<AppointmentDTO> filteredList = new ArrayList<>();
private List<PetDTO> petList = new ArrayList<>();
private List<ServiceDTO> serviceList = new ArrayList<>();
private 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<PageResponse<AppointmentDTO>>() {
@Override
public void onResponse(Call<PageResponse<AppointmentDTO>> call,
Response<PageResponse<AppointmentDTO>> response) {
if (swipeRefreshLayout != null)
swipeRefreshLayout.setRefreshing(false);
if (response.isSuccessful() && response.body() != null) {
appointmentList.clear();
appointmentList.addAll(response.body().getContent());
filterAppointments(etSearch != null ? etSearch.getText().toString() : "");
} else {
Log.e("AppointmentFragment", "Error: " + response.message());
Toast.makeText(getContext(), "Failed to load appointments", Toast.LENGTH_SHORT).show();
}
}
@Override
public void onFailure(Call<PageResponse<AppointmentDTO>> call, Throwable t) {
if (swipeRefreshLayout != null)
swipeRefreshLayout.setRefreshing(false);
Toast.makeText(getContext(), "Network error: " + t.getMessage(), Toast.LENGTH_SHORT).show();
Log.e("AppointmentFragment", t.getMessage());
}
});
}
// Load Pets
private void loadPets() {
PetApi petApi = RetrofitClient.getPetApi(requireContext());
petApi.getAllPets(0,100).enqueue(new Callback<PageResponse<PetDTO>>() {
@Override
public void onResponse(Call<PageResponse<PetDTO>> call, Response<PageResponse<PetDTO>> response) {
if (response.isSuccessful() && response.body() !=null) {
petList.clear();
petList.addAll(response.body().getContent());
}
}
@Override
public void onFailure(Call<PageResponse<PetDTO>> call, Throwable t) {
Log.e("AppointmentFragment", "Pet load error:" + t.getMessage());
}
});
}
// Load Services
private void loadServices() {
ServiceApi serviceApi = RetrofitClient.getServiceApi(requireContext());
serviceApi.getAllServices(0,100).enqueue(new Callback<PageResponse<ServiceDTO>>() {
@Override
public void onResponse(Call<PageResponse<ServiceDTO>> call, Response<PageResponse<ServiceDTO>> response) {
if (response.isSuccessful() && response.body() != null) {
serviceList.clear();
serviceList.addAll(response.body().getContent());
}
}
@Override
public void onFailure(Call<PageResponse<ServiceDTO>> call, Throwable t) {
Log.e("AppointmentFragmnet", "Service load error: " + t.getMessage());
}
});
}
private String getPetName(Long id) {
for (PetDTO p : petList) {
if (p.getPetId().equals(id)) return p.getPetName();
}
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);
}

View File

@@ -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<Inventory> inventoryList = new ArrayList<>();
private List<Inventory> filteredList = new ArrayList<>();
private static final String TAG = "InventoryFragment";
private static final int PAGE_SIZE = 20;
private final List<InventoryDTO> inventoryList = new ArrayList<>();
private final List<CategoryDTO> 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<PageResponse<CategoryDTO>>() {
@Override
public void onResponse(Call<PageResponse<CategoryDTO>> call,
Response<PageResponse<CategoryDTO>> response) {
if (response.isSuccessful() && response.body() != null) {
categoryList.clear();
categoryList.addAll(response.body().getContent());
setupCategorySpinner();
}
}
@Override
public void onFailure(Call<PageResponse<CategoryDTO>> call, Throwable t) {
Log.e(TAG, "Failed to load categories", t);
// Still setup spinner with just "All"
setupCategorySpinner();
}
@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<String> categoryNames = new ArrayList<>();
categoryNames.add("All Categories");
for (CategoryDTO c : categoryList) {
categoryNames.add(c.getCategoryName());
}
BlackTextArrayAdapter<String> spinnerAdapter = new BlackTextArrayAdapter<>(
requireContext(),
android.R.layout.simple_spinner_item,
categoryNames);
spinnerAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
spinnerCategory.setAdapter(spinnerAdapter);
spinnerCategory.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
@Override
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
if (!spinnerReady) {
// Skip the first automatic trigger on setup
spinnerReady = true;
return;
}
if (position == 0) {
selectedCategory = null; // "All Categories"
} else {
selectedCategory = categoryList.get(position - 1).getCategoryName();
}
loadInventory(true);
}
@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<PageResponse<InventoryDTO>>() {
@Override
public void onResponse(Call<PageResponse<InventoryDTO>> call,
Response<PageResponse<InventoryDTO>> response) {
isLoading = false;
if (swipeRefreshLayout != null)
swipeRefreshLayout.setRefreshing(false);
if (response.isSuccessful() && response.body() != null) {
PageResponse<InventoryDTO> page = response.body();
if (reset)
inventoryList.clear();
inventoryList.addAll(page.getContent());
adapter.notifyDataSetChanged();
isLastPage = page.isLast();
if (!isLastPage)
currentPage++;
} else {
Log.e(TAG, "Error " + response.code());
Toast.makeText(getContext(), "Failed to load inventory", Toast.LENGTH_SHORT).show();
}
}
@Override
public void onFailure(Call<PageResponse<InventoryDTO>> call, Throwable t) {
isLoading = false;
if (swipeRefreshLayout != null)
swipeRefreshLayout.setRefreshing(false);
Log.e(TAG, "Network error", t);
Toast.makeText(getContext(), "Network error: " + t.getMessage(), Toast.LENGTH_SHORT).show();
}
});
}
// Combines search text and category into one query string for ?q=
private String buildQuery() {
String q = null;
if (!currentQuery.isEmpty() && selectedCategory != null) {
// Both active — prioritize search text, category acts as context
q = currentQuery;
} else if (!currentQuery.isEmpty()) {
q = currentQuery;
} else if (selectedCategory != null) {
q = selectedCategory;
}
return q;
}
// Bulk delete
private void confirmBulkDelete() {
List<Long> 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<Long> ids) {
inventoryApi.bulkDeleteInventory(new BulkDeleteRequest(ids))
.enqueue(new Callback<Void>() {
@Override
public void onResponse(Call<Void> call, Response<Void> response) {
if (response.isSuccessful()) {
adapter.clearSelection();
hideBulkDeleteBar();
loadInventory(true);
Toast.makeText(getContext(), ids.size() + " item(s) deleted", Toast.LENGTH_SHORT).show();
} else {
Toast.makeText(getContext(), "Delete failed", Toast.LENGTH_SHORT).show();
}
}
@Override
public void onFailure(Call<Void> 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);
}
}
}

View File

@@ -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<String> adapter = new ArrayAdapter<>(requireContext(), android.R.layout.simple_spinner_item, statuses);
BlackTextArrayAdapter<String> adapter = new BlackTextArrayAdapter<>(requireContext(), android.R.layout.simple_spinner_item, statuses);
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
spinnerStatus.setAdapter(adapter);

View File

@@ -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<Product> productList = new ArrayList<>();
private List<Product> filteredList = new ArrayList<>();
private List<ProductDTO> productList = new ArrayList<>();
private List<ProductDTO> 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<PageResponse<ProductDTO>>() {
public void onResponse(Call<PageResponse<ProductDTO>> c,
Response<PageResponse<ProductDTO>> r) {
if (swipeRefresh != null) swipeRefresh.setRefreshing(false);
if (r.isSuccessful() && r.body() != null) {
productList.clear();
productList.addAll(r.body().getContent());
filter(etSearch != null ? etSearch.getText().toString() : "");
} else {
Toast.makeText(getContext(), "Failed to load products",
Toast.LENGTH_SHORT).show();
}
}
public void onFailure(Call<PageResponse<ProductDTO>> c, Throwable t) {
if (swipeRefresh != null) swipeRefresh.setRefreshing(false);
Log.e("ProductFragment", t.getMessage());
}
});
}
private void 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); }
}

View File

@@ -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<ProductSupplierDTO> psList = new ArrayList<>();
private List<ProductSupplierDTO> 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<PageResponse<ProductSupplierDTO>>() {
public void onResponse(Call<PageResponse<ProductSupplierDTO>> c,
Response<PageResponse<ProductSupplierDTO>> r) {
if (swipeRefresh != null) swipeRefresh.setRefreshing(false);
if (r.isSuccessful() && r.body() != null) {
psList.clear();
psList.addAll(r.body().getContent());
filter(etSearch != null ? etSearch.getText().toString() : "");
} else {
Toast.makeText(getContext(), "Failed to load",
Toast.LENGTH_SHORT).show();
}
}
public void onFailure(Call<PageResponse<ProductSupplierDTO>> c, Throwable t) {
if (swipeRefresh != null) swipeRefresh.setRefreshing(false);
Log.e("PSFragment", t.getMessage());
}
});
}
private void openDetail(int position) {
ProductSupplierDetailFragment detail = new ProductSupplierDetailFragment();
Bundle args = new Bundle();
if (position != -1) {
ProductSupplierDTO ps = filteredList.get(position);
args.putLong("productId", ps.getProductId());
args.putLong("supplierId", ps.getSupplierId());
args.putString("productName", ps.getProductName());
args.putString("supplierName", ps.getSupplierName());
args.putString("cost", ps.getCost() != null ? ps.getCost().toString() : "");
}
detail.setArguments(args);
ListFragment lf = (ListFragment) getParentFragment();
if (lf != null) lf.loadFragment(detail);
}
@Override
public void onProductSupplierClick(int position) { openDetail(position); }
}

View File

@@ -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<PurchaseOrderDTO> poList = new ArrayList<>();
private List<PurchaseOrderDTO> 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<PageResponse<PurchaseOrderDTO>>() {
public void onResponse(Call<PageResponse<PurchaseOrderDTO>> c,
Response<PageResponse<PurchaseOrderDTO>> r) {
if (swipeRefresh != null)
swipeRefresh.setRefreshing(false);
if (r.isSuccessful() && r.body() != null) {
poList.clear();
poList.addAll(r.body().getContent());
filter(etSearch != null ? etSearch.getText().toString() : "");
} else {
Toast.makeText(getContext(), "Failed to load purchase orders",
Toast.LENGTH_SHORT).show();
}
}
public void onFailure(Call<PageResponse<PurchaseOrderDTO>> c, Throwable t) {
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);
}
}

View File

@@ -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<Sale> saleList = new ArrayList<>();
private List<Sale> 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);
}
}

View File

@@ -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<PetDTO> petList = new ArrayList<>();
private List<CustomerDTO> 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<PageResponse<PetDTO>>() {
public void onResponse(Call<PageResponse<PetDTO>> c,
Response<PageResponse<PetDTO>> r) {
if (r.isSuccessful() && r.body() != null) {
petList = r.body().getContent();
populatePetSpinner();
}
}
public void onFailure(Call<PageResponse<PetDTO>> c, Throwable t) {
Log.e("ADOPTION", "Pet load failed: " + t.getMessage());
}
});
}
private void populatePetSpinner() {
List<String> names = new ArrayList<>();
names.add("-- Select Pet --");
for (PetDTO p : petList) names.add(p.getPetName());
spinnerPet.setAdapter(new 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<PageResponse<CustomerDTO>>() {
public void onResponse(Call<PageResponse<CustomerDTO>> c,
Response<PageResponse<CustomerDTO>> r) {
if (r.isSuccessful() && r.body() != null) {
customerList = r.body().getContent();
populateCustomerSpinner();
}
}
public void onFailure(Call<PageResponse<CustomerDTO>> c, Throwable t) {
Log.e("ADOPTION", "Customer load failed: " + t.getMessage());
}
});
}
private void populateCustomerSpinner() {
List<String> names = new ArrayList<>();
names.add("-- Select Customer --");
for (CustomerDTO c : customerList)
names.add(c.getFirstName() + " " + c.getLastName());
spinnerCustomer.setAdapter(new 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<String> adapter = new ArrayAdapter<String>(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<AdoptionDTO> simpleCallback(String msg) {
return new Callback<>() {
public void onResponse(Call<AdoptionDTO> c, Response<AdoptionDTO> r) {
Log.d("ADOPTION_SAVE", "Response: " + r.code());
if (r.isSuccessful()) {
Toast.makeText(getContext(), msg, Toast.LENGTH_SHORT).show();
navigateBack();
} else {
try {
String err = r.errorBody().string();
Log.e("ADOPTION_SAVE", "Error: " + err);
Toast.makeText(getContext(), "Error " + r.code(), Toast.LENGTH_SHORT).show();
} catch (Exception e) {
Log.e("ADOPTION_SAVE", "Failed to read error");
}
}
}
public void onFailure(Call<AdoptionDTO> c, Throwable t) {
Log.e("ADOPTION_SAVE", "Failure: " + t.getMessage());
Toast.makeText(getContext(), "Network error", Toast.LENGTH_SHORT).show();
}
};
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<Void>() {
public void onResponse(Call<Void> c, Response<Void> r) {
navigateBack();
}
public void onFailure(Call<Void> c, Throwable t) {
Toast.makeText(getContext(), "Delete failed",
Toast.LENGTH_SHORT).show();
}
}))
.setNegativeButton("No", null).show();
}
private void navigateBack() {
ListFragment lf = (ListFragment) getParentFragment();
if (lf != null) lf.getChildFragmentManager().popBackStack();
}
}

View File

@@ -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<PetDTO> petList = new ArrayList<>();
private List<ServiceDTO> serviceList = new ArrayList<>();
private List<CustomerDTO> customerList = new ArrayList<>();
private List<StoreDTO> storeList = new ArrayList<>();
private List<AppointmentDTO> allAppointments = new ArrayList<>();
private 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<PageResponse<PetDTO>>() {
public void onResponse(Call<PageResponse<PetDTO>> c, Response<PageResponse<PetDTO>> r) {
if (r.isSuccessful() && r.body() != null) {
petList = r.body().getContent();
populatePetSpinner();
}
}
public void onFailure(Call<PageResponse<PetDTO>> c, Throwable t) {
Log.e("APPT", "Pet load failed: " + t.getMessage());
}
});
}
private void populatePetSpinner() {
List<String> names = new ArrayList<>();
names.add("-- Select Pet --");
for (PetDTO p : petList) names.add(p.getPetName());
spinnerPet.setAdapter(new 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<PageResponse<ServiceDTO>>() {
public void onResponse(Call<PageResponse<ServiceDTO>> c, Response<PageResponse<ServiceDTO>> r) {
if (r.isSuccessful() && r.body() != null) {
serviceList = r.body().getContent();
populateServiceSpinner();
}
}
public void onFailure(Call<PageResponse<ServiceDTO>> c, Throwable t) {
Log.e("APPT", "Service load failed: " + t.getMessage());
}
});
}
private void populateServiceSpinner() {
List<String> names = new ArrayList<>();
names.add("-- Select Service --");
for (ServiceDTO s : serviceList) names.add(s.getServiceName());
spinnerService.setAdapter(new 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<PageResponse<CustomerDTO>>() {
public void onResponse(Call<PageResponse<CustomerDTO>> c, Response<PageResponse<CustomerDTO>> r) {
if (r.isSuccessful() && r.body() != null) {
customerList = r.body().getContent();
populateCustomerSpinner();
}
}
public void onFailure(Call<PageResponse<CustomerDTO>> c, Throwable t) {
Log.e("APPT", "Customer load failed: " + t.getMessage());
}
});
}
private void populateCustomerSpinner() {
List<String> names = new ArrayList<>();
names.add("-- Select Customer --");
for (CustomerDTO c : customerList)
names.add(c.getFirstName() + " " + c.getLastName());
spinnerCustomer.setAdapter(new 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<PageResponse<StoreDTO>>() {
public void onResponse(Call<PageResponse<StoreDTO>> c, Response<PageResponse<StoreDTO>> r) {
if (r.isSuccessful() && r.body() != null) {
storeList = r.body().getContent();
populateStoreSpinner();
}
}
public void onFailure(Call<PageResponse<StoreDTO>> c, Throwable t) {
Log.e("APPT", "Store load failed: " + t.getMessage());
}
});
}
private void populateStoreSpinner() {
List<String> names = new ArrayList<>();
names.add("-- Select Store --");
for (StoreDTO s : storeList) names.add(s.getStoreName());
spinnerStore.setAdapter(new 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<PageResponse<AppointmentDTO>>() {
public void onResponse(Call<PageResponse<AppointmentDTO>> c, Response<PageResponse<AppointmentDTO>> r) {
if (r.isSuccessful() && r.body() != null)
allAppointments = r.body().getContent();
}
public void onFailure(Call<PageResponse<AppointmentDTO>> c, Throwable t) {}
});
}
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<String> adapter = new ArrayAdapter<String>(requireContext(),
android.R.layout.simple_spinner_item,
new String[]{"Confirmed", "Pending", "Cancelled"}) {
private Callback<AppointmentDTO> simpleCallback(String msg) {
return new Callback<>() {
public void onResponse(Call<AppointmentDTO> c, Response<AppointmentDTO> r) {
Log.d("APPT_SAVE", "Response: " + r.code());
if (r.isSuccessful()) {
Toast.makeText(getContext(), msg, Toast.LENGTH_SHORT).show();
navigateBack();
} else {
try {
String errorBody = r.errorBody().string();
Log.e("APPT_SAVE", "Error: " + errorBody);
//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<AppointmentDTO> 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<Void>() {
public void onResponse(Call<Void> c, Response<Void> r) { navigateBack(); }
public void onFailure(Call<Void> c, Throwable t) {
Toast.makeText(getContext(), "Delete failed", Toast.LENGTH_SHORT).show();
}
}))
.setNegativeButton("No", null).show();
}
private void navigateBack() {
ListFragment lf = (ListFragment) getParentFragment();
if (lf != null) lf.getChildFragmentManager().popBackStack();
}
}

View File

@@ -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<ProductDTO> productSuggestions = new ArrayList<>();
private ArrayAdapter<String> 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<PageResponse<ProductDTO>>() {
@Override
public void onResponse(Call<PageResponse<ProductDTO>> call,
Response<PageResponse<ProductDTO>> response) {
if (response.isSuccessful() && response.body() != null) {
productSuggestions.clear();
productSuggestions.addAll(response.body().getContent());
// Build display strings: "Product Name (ID: X)"
List<String> names = new ArrayList<>();
for (ProductDTO p : productSuggestions) {
names.add(p.getProdName() + " (ID: " + p.getProdId() + ")");
}
dropdownAdapter.clear();
dropdownAdapter.addAll(names);
dropdownAdapter.notifyDataSetChanged();
etProductSearch.showDropDown();
}
}
@Override
public void onFailure(Call<PageResponse<ProductDTO>> call, Throwable t) {
Toast.makeText(getContext(), "Failed to load products", Toast.LENGTH_SHORT).show();
}
});
}
// 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<InventoryDTO>() {
@Override
public void onResponse(Call<InventoryDTO> call, Response<InventoryDTO> response) {
setButtonsEnabled(true);
if (response.isSuccessful()) {
Toast.makeText(getContext(), "Inventory updated", Toast.LENGTH_SHORT).show();
notifyParentAndGoBack();
} else {
Toast.makeText(getContext(), "Update failed: " + response.code(), Toast.LENGTH_SHORT).show();
}
}
@Override
public void onFailure(Call<InventoryDTO> call, Throwable t) {
setButtonsEnabled(true);
Toast.makeText(getContext(), "Network error", Toast.LENGTH_SHORT).show();
}
});
} else {
inventoryApi.createInventory(request).enqueue(new Callback<InventoryDTO>() {
@Override
public void onResponse(Call<InventoryDTO> call, Response<InventoryDTO> response) {
setButtonsEnabled(true);
if (response.isSuccessful()) {
Toast.makeText(getContext(), "Inventory created", Toast.LENGTH_SHORT).show();
notifyParentAndGoBack();
} else {
Toast.makeText(getContext(), "Create failed: " + response.code(), Toast.LENGTH_SHORT).show();
}
}
@Override
public void onFailure(Call<InventoryDTO> call, Throwable t) {
setButtonsEnabled(true);
Toast.makeText(getContext(), "Network error", Toast.LENGTH_SHORT).show();
}
});
}
}
// 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<Void>() {
@Override
public void onResponse(Call<Void> call, Response<Void> response) {
setButtonsEnabled(true);
if (response.isSuccessful()) {
Toast.makeText(getContext(), "Inventory deleted", Toast.LENGTH_SHORT).show();
notifyParentAndGoBack();
} else {
Toast.makeText(getContext(), "Delete failed: " + response.code(), Toast.LENGTH_SHORT).show();
}
}
@Override
public void onFailure(Call<Void> call, Throwable t) {
setButtonsEnabled(true);
Toast.makeText(getContext(), "Network error", Toast.LENGTH_SHORT).show();
}
});
}
// 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);
}
}

View File

@@ -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<String> adapter = new ArrayAdapter<String>(requireContext(),
BlackTextArrayAdapter<String> 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);
}

View File

@@ -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<CategoryDTO> 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<PageResponse<CategoryDTO>>() {
public void onResponse(Call<PageResponse<CategoryDTO>> c,
Response<PageResponse<CategoryDTO>> r) {
if (r.isSuccessful() && r.body() != null) {
categoryList = r.body().getContent();
populateCategorySpinner();
}
}
public void onFailure(Call<PageResponse<CategoryDTO>> c, Throwable t) {
Log.e("ProductDetail", "Category load failed: " + t.getMessage());
}
});
}
private void populateCategorySpinner() {
List<String> names = new ArrayList<>();
names.add("-- Select Category --");
for (CategoryDTO c : categoryList) names.add(c.getCategoryName());
spinnerCategory.setAdapter(new 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<ProductDTO> simpleCallback(String msg) {
return new Callback<>() {
public void onResponse(Call<ProductDTO> c, Response<ProductDTO> r) {
if (r.isSuccessful()) {
Toast.makeText(getContext(), msg, Toast.LENGTH_SHORT).show();
navigateBack();
} else {
try {
String err = r.errorBody().string();
Log.e("PRODUCT_SAVE", "Error: " + err);
Toast.makeText(getContext(), "Error " + r.code(), Toast.LENGTH_SHORT).show();
} catch (Exception e) {
Log.e("PRODUCT_SAVE", "Failed to read error");
}
}
}
public void onFailure(Call<ProductDTO> c, Throwable t) {
Log.e("PRODUCT_SAVE", "Failure: " + t.getMessage());
Toast.makeText(getContext(), "Network error", Toast.LENGTH_SHORT).show();
}
};
}
private void confirmDelete() {
new AlertDialog.Builder(requireContext())
.setTitle("Delete Product?")
.setPositiveButton("Yes", (d, w) ->
RetrofitClient.getProductApi(requireContext())
.deleteProduct(prodId)
.enqueue(new Callback<Void>() {
public void onResponse(Call<Void> c, Response<Void> r) {
navigateBack();
}
public void onFailure(Call<Void> c, Throwable t) {
Toast.makeText(getContext(), "Delete failed",
Toast.LENGTH_SHORT).show();
}
}))
.setNegativeButton("No", null).show();
}
private void navigateBack() {
ListFragment lf = (ListFragment) getParentFragment();
if (lf != null) lf.getChildFragmentManager().popBackStack();
}
}

View File

@@ -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<ProductDTO> productList = new ArrayList<>();
private List<SupplierDTO> 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<PageResponse<ProductDTO>>() {
public void onResponse(Call<PageResponse<ProductDTO>> c,
Response<PageResponse<ProductDTO>> r) {
if (r.isSuccessful() && r.body() != null) {
productList = r.body().getContent();
populateProductSpinner();
}
}
public void onFailure(Call<PageResponse<ProductDTO>> c, Throwable t) {
Log.e("PSDetail", "Product load failed: " + t.getMessage());
}
});
}
private void populateProductSpinner() {
List<String> names = new ArrayList<>();
names.add("-- Select Product --");
for (ProductDTO p : productList) names.add(p.getProdName());
spinnerProduct.setAdapter(new BlackTextArrayAdapter<>(requireContext(),
android.R.layout.simple_spinner_item, names));
if (preselectedProductId != -1) {
for (int i = 0; i < productList.size(); i++) {
if (productList.get(i).getProdId().equals(preselectedProductId)) {
spinnerProduct.setSelection(i + 1); break;
}
}
}
}
private void loadSuppliers() {
RetrofitClient.getSupplierApi(requireContext()).getAllSuppliers(0, 200)
.enqueue(new Callback<PageResponse<SupplierDTO>>() {
public void onResponse(Call<PageResponse<SupplierDTO>> c,
Response<PageResponse<SupplierDTO>> r) {
if (r.isSuccessful() && r.body() != null) {
supplierList = r.body().getContent();
populateSupplierSpinner();
}
}
public void onFailure(Call<PageResponse<SupplierDTO>> c, Throwable t) {
Log.e("PSDetail", "Supplier load failed: " + t.getMessage());
}
});
}
private void populateSupplierSpinner() {
List<String> names = new ArrayList<>();
names.add("-- Select Supplier --");
for (SupplierDTO s : supplierList) names.add(s.getSupCompany());
spinnerSupplier.setAdapter(new BlackTextArrayAdapter<>(requireContext(),
android.R.layout.simple_spinner_item, names));
if (preselectedSupplierId != -1) {
for (int i = 0; i < supplierList.size(); i++) {
if (supplierList.get(i).getSupId().equals(preselectedSupplierId)) {
spinnerSupplier.setSelection(i + 1); break;
}
}
}
}
private void handleArguments() {
Bundle a = getArguments();
if (a != null && a.containsKey("productId")) {
isEditing = true;
editProductId = a.getLong("productId");
editSupplierId = a.getLong("supplierId");
preselectedProductId = editProductId;
preselectedSupplierId = editSupplierId;
etCost.setText(a.getString("cost"));
tvMode.setText("Edit Product Supplier");
btnDelete.setVisibility(View.VISIBLE);
} else {
tvMode.setText("Add Product Supplier");
btnDelete.setVisibility(View.GONE);
}
}
private void save() {
if (spinnerProduct.getSelectedItemPosition() == 0) {
Toast.makeText(getContext(), "Select a product", Toast.LENGTH_SHORT).show(); return;
}
if (spinnerSupplier.getSelectedItemPosition() == 0) {
Toast.makeText(getContext(), "Select a supplier", Toast.LENGTH_SHORT).show(); return;
}
String costStr = etCost.getText().toString().trim();
if (costStr.isEmpty()) {
etCost.setError("Enter cost"); return;
}
ProductDTO product = productList.get(spinnerProduct.getSelectedItemPosition() - 1);
SupplierDTO supplier = supplierList.get(spinnerSupplier.getSelectedItemPosition() - 1);
BigDecimal cost;
try {
cost = new BigDecimal(costStr);
} catch (Exception e) {
etCost.setError("Invalid cost"); return;
}
ProductSupplierDTO dto = new ProductSupplierDTO(
product.getProdId(), supplier.getSupId(), cost);
ProductSupplierApi api = RetrofitClient.getProductSupplierApi(requireContext());
if (isEditing) {
api.updateProductSupplier(editProductId, editSupplierId, dto)
.enqueue(simpleCallback("Updated"));
} else {
api.createProductSupplier(dto).enqueue(simpleCallback("Saved"));
}
}
private Callback<ProductSupplierDTO> simpleCallback(String msg) {
return new Callback<>() {
public void onResponse(Call<ProductSupplierDTO> c, Response<ProductSupplierDTO> r) {
if (r.isSuccessful()) {
Toast.makeText(getContext(), msg, Toast.LENGTH_SHORT).show();
navigateBack();
} else {
try {
String err = r.errorBody().string();
Log.e("PS_SAVE", "Error: " + err);
Toast.makeText(getContext(), "Error " + r.code(), Toast.LENGTH_SHORT).show();
} catch (Exception e) {
Log.e("PS_SAVE", "Failed to read error");
}
}
}
public void onFailure(Call<ProductSupplierDTO> c, Throwable t) {
Log.e("PS_SAVE", "Failure: " + t.getMessage());
Toast.makeText(getContext(), "Network error", Toast.LENGTH_SHORT).show();
}
};
}
private void confirmDelete() {
new AlertDialog.Builder(requireContext())
.setTitle("Delete?")
.setPositiveButton("Yes", (d, w) ->
RetrofitClient.getProductSupplierApi(requireContext())
.deleteProductSupplier(editProductId, editSupplierId)
.enqueue(new Callback<Void>() {
public void onResponse(Call<Void> c, Response<Void> r) {
navigateBack();
}
public void onFailure(Call<Void> c, Throwable t) {
Toast.makeText(getContext(), "Delete failed",
Toast.LENGTH_SHORT).show();
}
}))
.setNegativeButton("No", null).show();
}
private void navigateBack() {
ListFragment lf = (ListFragment) getParentFragment();
if (lf != null) lf.getChildFragmentManager().popBackStack();
}
}

View File

@@ -0,0 +1,55 @@
package com.example.petstoremobile.fragments.listfragments.detailfragments;
import android.graphics.Color;
import android.os.Bundle;
import android.view.*;
import android.widget.*;
import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment;
import com.example.petstoremobile.R;
import com.example.petstoremobile.fragments.ListFragment;
public class PurchaseOrderDetailFragment extends Fragment {
private TextView tvId, tvSupplier, tvDate, tvStatus;
private Button btnBack;
@Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_purchase_order_detail, container, false);
tvId = view.findViewById(R.id.tvPODetailId);
tvSupplier = view.findViewById(R.id.tvPODetailSupplier);
tvDate = view.findViewById(R.id.tvPODetailDate);
tvStatus = view.findViewById(R.id.tvPODetailStatus);
btnBack = view.findViewById(R.id.btnPOBack);
Bundle a = getArguments();
if (a != null) {
tvId.setText("PO #" + a.getLong("purchaseOrderId"));
tvSupplier.setText(a.getString("supplierName"));
tvDate.setText(a.getString("orderDate"));
String status = a.getString("status", "");
tvStatus.setText(status);
switch (status) {
case "Completed":
tvStatus.setTextColor(Color.parseColor("#4CAF50")); break;
case "Pending":
tvStatus.setTextColor(Color.parseColor("#FF9800")); break;
case "Cancelled":
tvStatus.setTextColor(Color.parseColor("#F44336")); break;
default:
tvStatus.setTextColor(Color.parseColor("#9E9E9E")); break;
}
}
btnBack.setOnClickListener(v -> {
ListFragment lf = (ListFragment) getParentFragment();
if (lf != null) lf.getChildFragmentManager().popBackStack();
});
return view;
}
}

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,62 @@
package com.example.petstoremobile.models;
public class Sale {
private int saleId;
private String saleDate;
private String employeeName;
private String itemName;
private int quantity;
private double unitPrice;
private double total;
private String paymentMethod;
private boolean isRefund;
public Sale(int saleId, String saleDate, String employeeName, String itemName,
int quantity, double unitPrice, double total, String paymentMethod, boolean isRefund) {
this.saleId = saleId;
this.saleDate = saleDate;
this.employeeName = employeeName;
this.itemName = itemName;
this.quantity = quantity;
this.unitPrice = unitPrice;
this.total = total;
this.paymentMethod = paymentMethod;
this.isRefund = isRefund;
}
public int getSaleId() {
return saleId;
}
public String getSaleDate() {
return saleDate;
}
public String getEmployeeName() {
return employeeName;
}
public String getItemName() {
return itemName;
}
public int getQuantity() {
return quantity;
}
public double getUnitPrice() {
return unitPrice;
}
public double getTotal() {
return total;
}
public String getPaymentMethod() {
return paymentMethod;
}
public boolean isRefund() {
return isRefund;
}
}

View File

@@ -5,13 +5,14 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:background="@color/background_grey">
android:background="@color/primary_dark">
<FrameLayout
android:id="@+id/fragment_container"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"/>
android:layout_weight="1"
android:background="@color/background_grey"/>
<com.google.android.material.bottomnavigation.BottomNavigationView
android:id="@+id/bottom_navigation"

View File

@@ -12,7 +12,6 @@
android:orientation="vertical">
<LinearLayout
android:id="@+id/header"
android:layout_width="match_parent"
android:layout_height="56dp"
android:background="@color/primary_dark"
@@ -21,7 +20,7 @@
android:paddingEnd="16dp">
<ImageButton
android:id="@+id/btnHamburger"
android:id="@+id/btnHamburgerAdoption"
android:layout_width="48dp"
android:layout_height="48dp"
android:src="@drawable/baseline_menu_36"
@@ -43,7 +42,7 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:hint="Search by adopter or pet name..."
android:hint="Search by customer or pet..."
android:inputType="text"
android:drawableStart="@android:drawable/ic_menu_search"
android:drawablePadding="8dp"

View File

@@ -31,7 +31,7 @@
android:layout_marginStart="8dp"
android:backgroundTint="@color/accent_coral"
android:text="Delete"
android:textColor="@color/white" />
android:textColor="@color/white"/>
</LinearLayout>
@@ -65,74 +65,37 @@
android:layout_gravity="end"
android:layout_marginBottom="8dp"/>
<!-- Customer -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Adopter Name"
android:text="Customer"
android:textColor="@color/text_dark"
android:textSize="12sp"
android:layout_marginBottom="4dp"/>
<EditText
android:id="@+id/etAdopterName"
<Spinner
android:id="@+id/spinnerAdoptionCustomer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Enter adopter name"
android:inputType="text"
android:layout_marginBottom="16dp"
android:textColor="@color/text_dark"/>
android:layout_marginBottom="16dp"/>
<!-- Pet -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Adopter Email"
android:text="Pet"
android:textColor="@color/text_dark"
android:textSize="12sp"
android:layout_marginBottom="4dp"/>
<EditText
android:id="@+id/etAdopterEmail"
<Spinner
android:id="@+id/spinnerAdoptionPet"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Enter email address"
android:inputType="textEmailAddress"
android:layout_marginBottom="16dp"
android:textColor="@color/text_dark"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Adopter Phone"
android:textColor="@color/text_dark"
android:textSize="12sp"
android:layout_marginBottom="4dp"/>
<EditText
android:id="@+id/etAdopterPhone"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Enter phone number"
android:inputType="phone"
android:layout_marginBottom="16dp"
android:textColor="@color/text_dark"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Pet Name"
android:textColor="@color/text_dark"
android:textSize="12sp"
android:layout_marginBottom="4dp"/>
<EditText
android:id="@+id/etAdoptionPetName"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Enter pet name"
android:inputType="text"
android:layout_marginBottom="16dp"
android:textColor="@color/text_dark"/>
android:layout_marginBottom="16dp"/>
<!-- Adoption Date -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
@@ -145,11 +108,14 @@
android:id="@+id/etAdoptionDate"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="YYYY-MM-DD"
android:inputType="text"
android:layout_marginBottom="16dp"
android:textColor="@color/text_dark"/>
android:hint="Tap to select date"
android:inputType="none"
android:focusable="false"
android:clickable="true"
android:drawableEnd="@android:drawable/ic_menu_my_calendar"
android:layout_marginBottom="16dp"/>
<!-- Status -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
@@ -161,7 +127,8 @@
<Spinner
android:id="@+id/spinnerAdoptionStatus"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"/>
</LinearLayout>

View File

@@ -65,54 +65,76 @@
android:layout_gravity="end"
android:layout_marginBottom="8dp"/>
<!-- Customer -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Customer Name"
android:text="Customer"
android:textColor="@color/text_dark"
android:textSize="12sp"
android:layout_marginBottom="4dp"/>
<EditText
android:id="@+id/etCustomerName"
<Spinner
android:id="@+id/spinnerCustomer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Enter customer name"
android:inputType="text"
android:layout_marginBottom="16dp"/>
<!-- Store -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Pet Name"
android:text="Store"
android:textColor="@color/text_dark"
android:textSize="12sp"
android:layout_marginBottom="4dp"/>
<EditText
android:id="@+id/etApptPetName"
<Spinner
android:id="@+id/spinnerStore"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Enter pet name"
android:inputType="text"
android:layout_marginBottom="16dp"/>
<!-- Pet -->
<TextView
android:layout_width="wrap_content"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Service Type"
android:text="Pet"
android:textColor="@color/text_dark"
android:textSize="12sp"
android:layout_marginBottom="4dp"/>
<EditText
android:id="@+id/etServiceType"
<Spinner
android:id="@+id/spinnerPet"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="e.g. Grooming, Vet Checkup"
android:inputType="text"
android:layout_marginBottom="16dp"/>
<!-- Service -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Service"
android:textColor="@color/text_dark"
android:textSize="12sp"
android:layout_marginBottom="4dp"/>
<Spinner
android:id="@+id/spinnerService"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"/>
<!-- Appointment Date -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
@@ -125,10 +147,14 @@
android:id="@+id/etAppointmentDate"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="YYYY-MM-DD"
android:inputType="text"
android:hint="Tap to select date"
android:inputType="none"
android:focusable="false"
android:clickable="true"
android:drawableEnd="@android:drawable/ic_menu_my_calendar"
android:layout_marginBottom="16dp"/>
<!-- Appointment Time-->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
@@ -137,14 +163,45 @@
android:textSize="12sp"
android:layout_marginBottom="4dp"/>
<EditText
android:id="@+id/etAppointmentTime"
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="e.g. 10:00 AM"
android:inputType="text"
android:layout_marginBottom="16dp"/>
android:orientation="horizontal"
android:layout_marginBottom="16dp"
android:gravity="center_vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hour:"
android:textColor="@color/text_dark"
android:textSize="13sp"
android:layout_marginEnd="6dp"/>
<Spinner
android:id="@+id/spinnerHour"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginEnd="16dp"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Min:"
android:textColor="@color/text_dark"
android:textSize="13sp"
android:layout_marginEnd="6dp"/>
<Spinner
android:id="@+id/spinnerMinute"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"/>
</LinearLayout>
<!-- Status -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"

View File

@@ -11,6 +11,7 @@
android:layout_height="match_parent"
android:orientation="vertical">
<!-- Header -->
<LinearLayout
android:id="@+id/header"
android:layout_width="match_parent"
@@ -38,19 +39,68 @@
</LinearLayout>
<EditText
android:id="@+id/etSearchInventory"
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:hint="Search by item name or category..."
android:inputType="text"
android:drawableStart="@android:drawable/ic_menu_search"
android:drawablePadding="8dp"
android:background="@android:color/white"
android:padding="12dp"
android:textColor="@color/text_dark"/>
android:orientation="horizontal"
android:padding="8dp"
android:gravity="center_vertical">
<EditText
android:id="@+id/etSearchInventory"
android:layout_width="0dp"
android:layout_height="48dp"
android:layout_weight="1"
android:hint="Search by product or category…"
android:inputType="text"
android:drawableStart="@android:drawable/ic_menu_search"
android:drawablePadding="8dp"
android:background="@android:color/white"
android:padding="12dp"
android:textColor="@color/text_dark"/>
<Spinner
android:id="@+id/spinnerCategory"
android:layout_width="140dp"
android:layout_height="48dp"
android:layout_marginStart="8dp"
android:background="@android:color/white"
android:padding="10dp"/>
</LinearLayout>
<!-- Bulk-delete action bar (hidden until long-press) -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:background="@color/primary_medium"
android:paddingStart="16dp"
android:paddingEnd="8dp"
android:paddingTop="4dp"
android:paddingBottom="4dp">
<TextView
android:id="@+id/tvSelectionCount"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="0 selected"
android:textColor="@color/white"
android:visibility="gone"/>
<Button
android:id="@+id/btnBulkDelete"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Delete Selected"
android:backgroundTint="@color/accent_coral"
android:textColor="@color/white"
android:visibility="gone"/>
</LinearLayout>
<!-- Inventory list -->
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/swipeRefreshInventory"
android:layout_width="match_parent"
@@ -60,7 +110,8 @@
android:id="@+id/recyclerViewInventory"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="8dp"/>
android:padding="8dp"
android:clipToPadding="false"/>
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
@@ -73,7 +124,7 @@
android:layout_gravity="bottom|end"
android:layout_margin="16dp"
android:backgroundTint="@color/accent_coral"
android:contentDescription="Add Inventory Item"
android:contentDescription="Add Inventory"
app:srcCompat="@android:drawable/ic_input_add"
app:tint="@color/white"/>

View File

@@ -5,6 +5,7 @@
android:orientation="vertical"
android:background="@color/background_grey">
<!-- Header -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="56dp"
@@ -19,7 +20,7 @@
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Add Inventory Item"
android:text="Add Inventory"
android:textColor="@color/white"
android:textSize="20sp"
android:textStyle="bold"/>
@@ -28,10 +29,10 @@
android:id="@+id/btnDeleteInventory"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:backgroundTint="@color/accent_coral"
android:text="Delete"
android:textColor="@color/white" />
android:textColor="@color/white"
android:visibility="gone"/>
</LinearLayout>
@@ -51,52 +52,51 @@
android:layout_height="wrap_content"
android:orientation="vertical"
android:background="@drawable/rounded_card"
android:padding="16dp"
android:layout_marginBottom="16dp">
android:padding="16dp">
<!-- Inventory ID — edit mode only -->
<TextView
android:id="@+id/tvInventoryId"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="ID: #0"
android:text="Inventory ID: "
android:textColor="@color/text_light"
android:textSize="11sp"
android:textStyle="italic"
android:layout_gravity="end"
android:layout_marginBottom="8dp"/>
android:layout_marginBottom="12dp"
android:visibility="gone"/>
<!-- Product search label -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Item Name"
android:text="Product"
android:textColor="@color/text_dark"
android:textSize="12sp"
android:layout_marginBottom="4dp"/>
<EditText
android:id="@+id/etItemName"
<!-- AutoComplete search box -->
<AutoCompleteTextView
android:id="@+id/etProductSearch"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Enter item name"
android:hint="Search product name"
android:inputType="text"
android:layout_marginBottom="16dp"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Category"
android:textColor="@color/text_dark"
android:textSize="12sp"
android:completionThreshold="1"
android:layout_marginBottom="4dp"/>
<EditText
android:id="@+id/etInventoryCategory"
<!-- Selected product info (ID + category) shown after picking -->
<TextView
android:id="@+id/tvProductInfo"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="e.g. Food, Toys, Medicine"
android:inputType="text"
android:layout_marginBottom="16dp"/>
android:textColor="#888888"
android:textSize="12sp"
android:layout_marginBottom="16dp"
android:visibility="gone"/>
<!-- Quantity label -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
@@ -105,44 +105,13 @@
android:textSize="12sp"
android:layout_marginBottom="4dp"/>
<!-- Quantity input -->
<EditText
android:id="@+id/etQuantity"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Enter quantity"
android:inputType="number"
android:layout_marginBottom="16dp"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Unit Price"
android:textColor="@color/text_dark"
android:textSize="12sp"
android:layout_marginBottom="4dp"/>
<EditText
android:id="@+id/etUnitPrice"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Enter unit price"
android:inputType="numberDecimal"
android:layout_marginBottom="16dp"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Supplier"
android:textColor="@color/text_dark"
android:textSize="12sp"
android:layout_marginBottom="4dp"/>
<EditText
android:id="@+id/etInventorySupplier"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Enter supplier name"
android:inputType="text"/>
android:inputType="number"/>
</LinearLayout>
@@ -150,6 +119,7 @@
</ScrollView>
<!-- Bottom buttons -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"

View File

@@ -33,6 +33,8 @@
android:orientation="vertical"
android:background="@color/primary_dark">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
@@ -133,6 +135,30 @@
</LinearLayout>
<!-- Appointments -->
<LinearLayout
android:id="@+id/drawerAppointments"
android:layout_width="match_parent"
android:layout_height="48dp"
android:orientation="horizontal"
android:gravity="center_vertical"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:background="?attr/selectableItemBackground">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Appointments"
android:textColor="@color/white"
android:textSize="15sp"/>
</LinearLayout>
<!-- Adoptions -->
<LinearLayout
android:id="@+id/drawerAdoptions"
android:layout_width="match_parent"
@@ -152,25 +178,9 @@
</LinearLayout>
<LinearLayout
android:id="@+id/drawerAppointments"
android:layout_width="match_parent"
android:layout_height="48dp"
android:orientation="horizontal"
android:gravity="center_vertical"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:background="?attr/selectableItemBackground">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Appointments"
android:textColor="@color/white"
android:textSize="15sp"/>
</LinearLayout>
<!-- Inventory -->
<LinearLayout
android:id="@+id/drawerInventory"
android:layout_width="match_parent"
@@ -190,6 +200,9 @@
</LinearLayout>
<!-- Product -->
<LinearLayout
android:id="@+id/drawerProducts"
android:layout_width="match_parent"
@@ -209,6 +222,78 @@
</LinearLayout>
<!-- Sale -->
<LinearLayout
android:id="@+id/drawerSale"
android:layout_width="match_parent"
android:layout_height="48dp"
android:orientation="horizontal"
android:gravity="center_vertical"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:background="?attr/selectableItemBackground">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Sale"
android:textColor="@color/white"
android:textSize="15sp"/>
</LinearLayout>
<!-- PurchaseOrder -->
<LinearLayout
android:id="@+id/drawerPurchaseOrderView"
android:layout_width="match_parent"
android:layout_height="48dp"
android:orientation="horizontal"
android:gravity="center_vertical"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:background="?attr/selectableItemBackground">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="PurchaseOrder"
android:textColor="@color/white"
android:textSize="15sp"/>
</LinearLayout>
<!-- ProductSupplier -->
<LinearLayout
android:id="@+id/drawerProductSupplier"
android:layout_width="match_parent"
android:layout_height="48dp"
android:orientation="horizontal"
android:gravity="center_vertical"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:background="?attr/selectableItemBackground">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="ProductSupplier"
android:textColor="@color/white"
android:textSize="15sp"/>
</LinearLayout>
</LinearLayout>
</androidx.drawerlayout.widget.DrawerLayout>

View File

@@ -12,7 +12,6 @@
android:orientation="vertical">
<LinearLayout
android:id="@+id/header"
android:layout_width="match_parent"
android:layout_height="56dp"
android:background="@color/primary_dark"
@@ -21,7 +20,7 @@
android:paddingEnd="16dp">
<ImageButton
android:id="@+id/btnHamburger"
android:id="@+id/btnHamburgerProduct"
android:layout_width="48dp"
android:layout_height="48dp"
android:src="@drawable/baseline_menu_36"
@@ -43,7 +42,7 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:hint="Search by product name or category..."
android:hint="Search by name or category..."
android:inputType="text"
android:drawableStart="@android:drawable/ic_menu_search"
android:drawablePadding="8dp"

View File

@@ -28,10 +28,9 @@
android:id="@+id/btnDeleteProduct"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:backgroundTint="@color/accent_coral"
android:text="Delete"
android:textColor="@color/white" />
android:textColor="@color/white"/>
</LinearLayout>
@@ -65,6 +64,7 @@
android:layout_gravity="end"
android:layout_marginBottom="8dp"/>
<!-- Product Name -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
@@ -81,6 +81,22 @@
android:inputType="text"
android:layout_marginBottom="16dp"/>
<!-- Category -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Category"
android:textColor="@color/text_dark"
android:textSize="12sp"
android:layout_marginBottom="4dp"/>
<Spinner
android:id="@+id/spinnerProductCategory"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"/>
<!-- Description -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
@@ -93,27 +109,12 @@
android:id="@+id/etProductDesc"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:hint="Enter product description"
android:hint="Enter description"
android:inputType="textMultiLine"
android:minLines="1" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Category"
android:textColor="@color/text_dark"
android:textSize="12sp"
android:layout_marginBottom="4dp"/>
<EditText
android:id="@+id/etProductCategory"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="e.g. Food, Toys, Grooming"
android:inputType="text"
android:minLines="2"
android:layout_marginBottom="16dp"/>
<!-- Price -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
@@ -126,24 +127,9 @@
android:id="@+id/etProductPrice"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Enter price"
android:hint="0.00"
android:inputType="numberDecimal"
android:layout_marginBottom="16dp"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Stock Quantity"
android:textColor="@color/text_dark"
android:textSize="12sp"
android:layout_marginBottom="4dp"/>
<EditText
android:id="@+id/etStockQuantity"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Enter stock quantity"
android:inputType="number"/>
android:layout_marginBottom="8dp"/>
</LinearLayout>

View File

@@ -0,0 +1,79 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/background_grey">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="56dp"
android:background="@color/primary_dark"
android:gravity="center_vertical"
android:paddingStart="16dp"
android:paddingEnd="16dp">
<ImageButton
android:id="@+id/btnHamburgerPS"
android:layout_width="48dp"
android:layout_height="48dp"
android:src="@drawable/baseline_menu_36"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="Open menu"/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Product Suppliers"
android:textColor="@color/white"
android:textSize="20sp"
android:textStyle="bold"/>
</LinearLayout>
<EditText
android:id="@+id/etSearchPS"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:hint="Search by product or supplier..."
android:inputType="text"
android:drawableStart="@android:drawable/ic_menu_search"
android:drawablePadding="8dp"
android:background="@android:color/white"
android:padding="12dp"
android:textColor="@color/text_dark"/>
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/swipeRefreshPS"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerViewPS"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="8dp"/>
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
</LinearLayout>
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fabAddPS"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_margin="16dp"
android:backgroundTint="@color/accent_coral"
android:contentDescription="Add Product Supplier"
app:srcCompat="@android:drawable/ic_input_add"
app:tint="@color/white"/>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@@ -0,0 +1,138 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:background="@color/background_grey">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="56dp"
android:background="@color/primary_dark"
android:gravity="center_vertical"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:orientation="horizontal">
<TextView
android:id="@+id/tvPSMode"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Add Product Supplier"
android:textColor="@color/white"
android:textSize="20sp"
android:textStyle="bold"/>
<Button
android:id="@+id/btnDeletePS"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:backgroundTint="@color/accent_coral"
android:text="Delete"
android:textColor="@color/white"/>
</LinearLayout>
<ScrollView
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="24dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:background="@drawable/rounded_card"
android:padding="16dp"
android:layout_marginBottom="16dp">
<!-- Product -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Product"
android:textColor="@color/text_dark"
android:textSize="12sp"
android:layout_marginBottom="4dp"/>
<Spinner
android:id="@+id/spinnerPSProduct"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"/>
<!-- Supplier -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Supplier"
android:textColor="@color/text_dark"
android:textSize="12sp"
android:layout_marginBottom="4dp"/>
<Spinner
android:id="@+id/spinnerPSSupplier"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"/>
<!-- Cost -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Cost"
android:textColor="@color/text_dark"
android:textSize="12sp"
android:layout_marginBottom="4dp"/>
<EditText
android:id="@+id/etPSCost"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="0.00"
android:inputType="numberDecimal"
android:layout_marginBottom="8dp"/>
</LinearLayout>
</LinearLayout>
</ScrollView>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:background="@color/white"
android:padding="16dp">
<Button
android:id="@+id/btnPSBack"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginEnd="8dp"
android:text="Back"
android:backgroundTint="@color/primary_medium"
android:textColor="@color/white"/>
<Button
android:id="@+id/btnSavePS"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginStart="8dp"
android:text="Save"
android:backgroundTint="@color/accent_coral"
android:textColor="@color/white"/>
</LinearLayout>
</LinearLayout>

View File

@@ -0,0 +1,67 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/background_grey">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="56dp"
android:background="@color/primary_dark"
android:gravity="center_vertical"
android:paddingStart="16dp"
android:paddingEnd="16dp">
<ImageButton
android:id="@+id/btnHamburgerPO"
android:layout_width="48dp"
android:layout_height="48dp"
android:src="@drawable/baseline_menu_36"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="Open menu"/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Purchase Orders"
android:textColor="@color/white"
android:textSize="20sp"
android:textStyle="bold"/>
</LinearLayout>
<EditText
android:id="@+id/etSearchPO"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:hint="Search by supplier or status..."
android:inputType="text"
android:drawableStart="@android:drawable/ic_menu_search"
android:drawablePadding="8dp"
android:background="@android:color/white"
android:padding="12dp"
android:textColor="@color/text_dark"/>
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/swipeRefreshPO"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerViewPO"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="8dp"/>
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
</LinearLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@@ -0,0 +1,124 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:background="@color/background_grey">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="56dp"
android:background="@color/primary_dark"
android:gravity="center_vertical"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:orientation="horizontal">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Purchase Order Details"
android:textColor="@color/white"
android:textSize="20sp"
android:textStyle="bold"/>
</LinearLayout>
<ScrollView
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="24dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:background="@drawable/rounded_card"
android:padding="16dp"
android:layout_marginBottom="16dp">
<TextView
android:id="@+id/tvPODetailId"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/text_light"
android:textSize="11sp"
android:textStyle="italic"
android:layout_gravity="end"
android:layout_marginBottom="16dp"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Supplier"
android:textColor="@color/text_light"
android:textSize="12sp"/>
<TextView
android:id="@+id/tvPODetailSupplier"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/text_dark"
android:textSize="16sp"
android:textStyle="bold"
android:layout_marginBottom="16dp"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Order Date"
android:textColor="@color/text_light"
android:textSize="12sp"/>
<TextView
android:id="@+id/tvPODetailDate"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/text_dark"
android:textSize="15sp"
android:layout_marginBottom="16dp"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Status"
android:textColor="@color/text_light"
android:textSize="12sp"/>
<TextView
android:id="@+id/tvPODetailStatus"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="15sp"
android:layout_marginBottom="16dp"/>
</LinearLayout>
</LinearLayout>
</ScrollView>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/white"
android:padding="16dp">
<Button
android:id="@+id/btnPOBack"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Back"
android:backgroundTint="@color/primary_medium"
android:textColor="@color/white"/>
</LinearLayout>
</LinearLayout>

View File

@@ -0,0 +1,154 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:background="@color/background_grey">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="56dp"
android:background="@color/primary_dark"
android:gravity="center_vertical"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:orientation="horizontal">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Process Refund"
android:textColor="@color/white"
android:textSize="20sp"
android:textStyle="bold"/>
</LinearLayout>
<ScrollView
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="24dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:background="@drawable/rounded_card"
android:padding="16dp"
android:layout_marginBottom="16dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Sale ID"
android:textColor="@color/text_dark"
android:textSize="12sp"
android:layout_marginBottom="4dp"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<EditText
android:id="@+id/etRefundSaleId"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:hint="Enter Sale ID"
android:inputType="number"
android:layout_marginEnd="8dp"
android:layout_marginBottom="16dp"/>
<Button
android:id="@+id/btnLoadSale"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Load"
android:backgroundTint="@color/primary_medium"
android:textColor="@color/white"/>
</LinearLayout>
<TextView
android:id="@+id/tvSaleInfo"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Load a sale to see details"
android:textColor="#888888"
android:textSize="13sp"
android:layout_marginBottom="16dp"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Refund Reason"
android:textColor="@color/text_dark"
android:textSize="12sp"
android:layout_marginBottom="4dp"/>
<EditText
android:id="@+id/etRefundReason"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Enter reason for refund"
android:inputType="textMultiLine"
android:lines="3"
android:layout_marginBottom="16dp"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Payment Method"
android:textColor="@color/text_dark"
android:textSize="12sp"
android:layout_marginBottom="4dp"/>
<Spinner
android:id="@+id/spinnerRefundPayment"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</LinearLayout>
</LinearLayout>
</ScrollView>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:background="@color/white"
android:padding="16dp">
<Button
android:id="@+id/btnRefundBack"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginEnd="8dp"
android:text="Back"
android:backgroundTint="@color/primary_medium"
android:textColor="@color/white"/>
<Button
android:id="@+id/btnProcessRefund"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginStart="8dp"
android:text="Process Refund"
android:backgroundTint="@color/accent_coral"
android:textColor="@color/white"/>
</LinearLayout>
</LinearLayout>

View File

@@ -0,0 +1,69 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/background_grey">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<LinearLayout
android:id="@+id/header"
android:layout_width="match_parent"
android:layout_height="56dp"
android:background="@color/primary_dark"
android:gravity="center_vertical"
android:paddingStart="16dp"
android:paddingEnd="16dp">
<ImageButton
android:id="@+id/btnHamburger"
android:layout_width="48dp"
android:layout_height="48dp"
android:src="@drawable/baseline_menu_36"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="Open menu"/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Sales"
android:textColor="@color/white"
android:textSize="20sp"
android:textStyle="bold"/>
</LinearLayout>
<EditText
android:id="@+id/etSearchSale"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:hint="Search by item, employee or date..."
android:inputType="text"
android:drawableStart="@android:drawable/ic_menu_search"
android:drawablePadding="8dp"
android:background="@android:color/white"
android:padding="12dp"
android:textColor="@color/text_dark"/>
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/swipeRefreshSale"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerViewSales"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="8dp"/>
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
</LinearLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@@ -1,24 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp"
android:background="@android:color/white">
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:paddingTop="16dp"
android:paddingBottom="16dp"
android:background="@color/white">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
android:orientation="horizontal"
android:gravity="center_vertical">
<TextView
android:id="@+id/tvAdopterName"
android:id="@+id/tvAdoptionCustomerName"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Adopter Name"
android:textColor="#000000"
android:ellipsize="end"
android:maxLines="1"
android:text="Customer Name"
android:textColor="@color/text_dark"
android:textSize="18sp"
android:textStyle="bold" />
@@ -26,14 +31,14 @@
android:id="@+id/tvAdoptionStatus"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="#4CAF50"
android:paddingStart="8dp"
android:paddingTop="4dp"
android:paddingTop="3dp"
android:paddingEnd="8dp"
android:paddingBottom="4dp"
android:paddingBottom="3dp"
android:text="Status"
android:textColor="#FFFFFF"
android:textSize="12sp" />
android:textAllCaps="true"
android:textColor="@color/white"
android:textSize="11sp" />
</LinearLayout>
@@ -42,23 +47,43 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:text="Pet: name"
android:textColor="#666666"
android:ellipsize="end"
android:maxLines="1"
android:text="Pet Name"
android:textColor="#888888"
android:textSize="14sp" />
<TextView
android:id="@+id/tvAdoptionDate"
android:layout_width="wrap_content"
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:text="Date: "
android:textColor="#666666"
android:textSize="14sp" />
android:orientation="horizontal"
android:gravity="center_vertical"
android:layout_marginTop="8dp">
<TextView
android:id="@+id/tvAdoptionFee"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="$0.00"
android:textColor="@color/accent_coral"
android:textSize="16sp"
android:textStyle="bold" />
<TextView
android:id="@+id/tvAdoptionDate"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Date"
android:textColor="#888888"
android:textSize="13sp" />
</LinearLayout>
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="#EEEEEE"
android:background="#F0F0F0"
android:layout_marginTop="12dp"/>
</LinearLayout>
</LinearLayout>

View File

@@ -2,80 +2,91 @@
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:orientation="horizontal"
android:background="@color/white"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:paddingTop="16dp"
android:paddingBottom="16dp"
android:background="@color/white">
android:gravity="center_vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical">
<TextView
android:id="@+id/tvItemName"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:ellipsize="end"
android:maxLines="1"
android:text="Item Name"
android:textColor="@color/text_dark"
android:textSize="18sp"
android:textStyle="bold" />
<TextView
android:id="@+id/tvUnitPrice"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="$0.00"
android:textColor="@color/accent_coral"
android:textSize="16sp"
android:textStyle="bold" />
</LinearLayout>
<TextView
android:id="@+id/tvCategory"
<!-- Checkbox (visible only in bulk-delete selection mode) -->
<CheckBox
android:id="@+id/cbSelectInventory"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:text="Category"
android:textColor="#888888"
android:textSize="13sp" />
android:layout_marginEnd="12dp"
android:visibility="gone"
android:clickable="false"
android:focusable="false"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:layout_marginTop="8dp">
android:orientation="vertical">
<TextView
android:id="@+id/tvQuantity"
android:layout_width="0dp"
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Qty: 0"
android:textSize="13sp" />
android:orientation="horizontal"
android:gravity="center_vertical">
<TextView
android:id="@+id/tvProductName"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:ellipsize="end"
android:maxLines="1"
android:text="Product Name"
android:textColor="@color/text_dark"
android:textSize="18sp"
android:textStyle="bold" />
<TextView
android:id="@+id/tvQuantity"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="0"
android:textColor="@color/text_dark"
android:textSize="16sp"
android:textStyle="bold" />
</LinearLayout>
<TextView
android:id="@+id/tvInvSupplier"
android:id="@+id/tvInventoryId"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Supplier: "
android:layout_marginTop="4dp"
android:text="Inv ID: —"
android:textColor="#888888"
android:textSize="13sp" />
android:textSize="14sp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:layout_marginTop="8dp">
<TextView
android:id="@+id/tvProdId"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Prod ID: —"
android:textColor="#888888"
android:textSize="13sp" />
</LinearLayout>
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="#F0F0F0"
android:layout_marginTop="12dp"/>
</LinearLayout>
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="#F0F0F0"
android:layout_marginTop="12dp"/>
</LinearLayout>

View File

@@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
@@ -15,68 +16,96 @@
android:orientation="horizontal"
android:gravity="center_vertical">
<TextView
android:id="@+id/tvPetName"
<com.google.android.material.imageview.ShapeableImageView
android:id="@+id/ivPetProfile"
android:layout_width="80dp"
android:layout_height="80dp"
android:layout_marginEnd="16dp"
android:scaleType="centerCrop"
android:src="@drawable/placeholder"
app:shapeAppearanceOverlay="@style/CircleImageView"
app:strokeWidth="2dp"
app:strokeColor="#BDBDBD"
android:padding="2dp"
android:contentDescription="@string/pet_profile_image_desc" />
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:ellipsize="end"
android:maxLines="1"
android:text="Pet Name"
android:textColor="@color/text_dark"
android:textSize="18sp"
android:textStyle="bold" />
android:orientation="vertical">
<TextView
android:id="@+id/tvPetStatus"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingStart="8dp"
android:paddingTop="3dp"
android:paddingEnd="8dp"
android:paddingBottom="3dp"
android:text="Status"
android:textAllCaps="true"
android:textColor="@color/white"
android:textSize="11sp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical">
</LinearLayout>
<TextView
android:id="@+id/tvPetName"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:ellipsize="end"
android:maxLines="1"
android:text="Pet Name"
android:textColor="@color/text_dark"
android:textSize="18sp"
android:textStyle="bold" />
<TextView
android:id="@+id/tvPetSpeciesBreed"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:ellipsize="end"
android:maxLines="1"
android:text="Breed"
android:textColor="#888888"
android:textSize="14sp" />
<TextView
android:id="@+id/tvPetStatus"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingStart="8dp"
android:paddingTop="3dp"
android:paddingEnd="8dp"
android:paddingBottom="3dp"
android:text="Status"
android:textAllCaps="true"
android:textColor="@color/white"
android:textSize="11sp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:layout_marginTop="8dp">
</LinearLayout>
<TextView
android:id="@+id/tvPetPrice"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="$126.00"
android:textColor="@color/accent_coral"
android:textSize="16sp"
android:textStyle="bold" />
<TextView
android:id="@+id/tvPetSpeciesBreed"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:ellipsize="end"
android:maxLines="1"
android:text="Breed"
android:textColor="#888888"
android:textSize="14sp" />
<TextView
android:id="@+id/tvPetAge"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="1"
android:textColor="#888888"
android:textSize="13sp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:layout_marginTop="8dp">
<TextView
android:id="@+id/tvPetPrice"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="$126.00"
android:textColor="@color/accent_coral"
android:textSize="16sp"
android:textStyle="bold" />
<TextView
android:id="@+id/tvPetAge"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="1"
android:textColor="#888888"
android:textSize="13sp" />
</LinearLayout>
</LinearLayout>
</LinearLayout>

View File

@@ -9,34 +9,27 @@
android:paddingBottom="16dp"
android:background="@color/white">
<LinearLayout
<TextView
android:id="@+id/tvProductName"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical">
android:ellipsize="end"
android:maxLines="1"
android:text="Product Name"
android:textColor="@color/text_dark"
android:textSize="18sp"
android:textStyle="bold" />
<TextView
android:id="@+id/tvProductName"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:ellipsize="end"
android:maxLines="1"
android:text="Product Name"
android:textColor="@color/text_dark"
android:textSize="18sp"
android:textStyle="bold" />
<TextView
android:id="@+id/tvProductPrice"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="$0.00"
android:textColor="@color/accent_coral"
android:textSize="16sp"
android:textStyle="bold" />
</LinearLayout>
<TextView
android:id="@+id/tvProductCategory"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:ellipsize="end"
android:maxLines="1"
android:text="Category"
android:textColor="#888888"
android:textSize="14sp" />
<TextView
android:id="@+id/tvProductDesc"
@@ -44,8 +37,8 @@
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:ellipsize="end"
android:maxLines="1"
android:text="Description"
android:maxLines="2"
android:text="Product Description"
android:textColor="#888888"
android:textSize="13sp" />
@@ -57,21 +50,14 @@
android:layout_marginTop="8dp">
<TextView
android:id="@+id/tvProductCategory"
android:id="@+id/tvProductPrice"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Category"
android:textColor="#888888"
android:textSize="13sp" />
<TextView
android:id="@+id/tvStockQuantity"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Stock: 0"
android:textColor="#888888"
android:textSize="13sp" />
android:text="$0.00"
android:textColor="@color/accent_coral"
android:textSize="16sp"
android:textStyle="bold" />
</LinearLayout>

View File

@@ -0,0 +1,59 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:paddingTop="16dp"
android:paddingBottom="16dp"
android:background="@color/white">
<TextView
android:id="@+id/tvPSProductName"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ellipsize="end"
android:maxLines="1"
android:text="Product Name"
android:textColor="@color/text_dark"
android:textSize="18sp"
android:textStyle="bold" />
<TextView
android:id="@+id/tvPSSupplierName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:ellipsize="end"
android:maxLines="1"
android:text="Supplier Name"
android:textColor="#888888"
android:textSize="14sp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:layout_marginTop="8dp">
<TextView
android:id="@+id/tvPSCost"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="$0.00"
android:textColor="@color/accent_coral"
android:textSize="16sp"
android:textStyle="bold" />
</LinearLayout>
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="#F0F0F0"
android:layout_marginTop="12dp"/>
</LinearLayout>

View File

@@ -0,0 +1,80 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:paddingTop="16dp"
android:paddingBottom="16dp"
android:background="@color/white">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical">
<TextView
android:id="@+id/tvPOId"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:ellipsize="end"
android:maxLines="1"
android:text="PO #000"
android:textColor="@color/text_dark"
android:textSize="18sp"
android:textStyle="bold" />
<TextView
android:id="@+id/tvPOStatus"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingStart="8dp"
android:paddingTop="3dp"
android:paddingEnd="8dp"
android:paddingBottom="3dp"
android:text="Status"
android:textAllCaps="true"
android:textColor="@color/white"
android:textSize="11sp" />
</LinearLayout>
<TextView
android:id="@+id/tvPOSupplier"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:ellipsize="end"
android:maxLines="1"
android:text="Supplier Name"
android:textColor="#888888"
android:textSize="14sp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:layout_marginTop="8dp">
<TextView
android:id="@+id/tvPODate"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Date"
android:textColor="#888888"
android:textSize="13sp" />
</LinearLayout>
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="#F0F0F0"
android:layout_marginTop="12dp"/>
</LinearLayout>

View File

@@ -0,0 +1,104 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:paddingTop="16dp"
android:paddingBottom="16dp"
android:background="@color/white">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical">
<TextView
android:id="@+id/tvSaleItemName"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:textColor="@color/text_dark"
android:textSize="18sp"
android:textStyle="bold"
android:ellipsize="end"
android:maxLines="1"
android:text="Item Name"/>
<TextView
android:id="@+id/tvRefundBadge"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingStart="8dp"
android:paddingEnd="8dp"
android:paddingTop="3dp"
android:paddingBottom="3dp"
android:text="REFUND"
android:textColor="@color/white"
android:textSize="11sp"
android:visibility="gone"/>
</LinearLayout>
<TextView
android:id="@+id/tvSaleEmployee"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:textColor="#888888"
android:textSize="13sp"
android:text="By: Employee"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginTop="8dp"
android:gravity="center_vertical">
<TextView
android:id="@+id/tvSaleId"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:textColor="#888888"
android:textSize="12sp"
android:text="ID: 0"/>
<TextView
android:id="@+id/tvSaleDate"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:textColor="#888888"
android:textSize="12sp"
android:text="Date"/>
<TextView
android:id="@+id/tvSalePayment"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="#888888"
android:textSize="12sp"
android:layout_marginEnd="8dp"
android:text="Cash"/>
<TextView
android:id="@+id/tvSaleTotal"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="16sp"
android:textStyle="bold"
android:text="$0.00"/>
</LinearLayout>
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="#F0F0F0"
android:layout_marginTop="12dp"/>
</LinearLayout>

View File

@@ -11,4 +11,5 @@
<color name="status_available">#2ECC71</color>
<color name="status_adopted">#E74C3C</color>
<color name="accent_blue">#3498DB</color>
<color name="spinner_text">#000000</color>
</resources>

View File

@@ -2,4 +2,5 @@
<string name="app_name">Leons Pet Store</string>
<!-- TODO: Remove or change this placeholder text -->
<string name="hello_blank_fragment">Hello blank fragment</string>
<string name="pet_profile_image_desc">Pet Profile Image</string>
</resources>

View File

@@ -10,6 +10,11 @@
<item name="android:statusBarColor">@color/primary_dark</item>
<item name="android:windowLightStatusBar">false</item>
</style>
<style name="CircleImageView" parent="">
<item name="cornerSize">50%</item>
</style>
<style name="Widget.App.EditText" parent="Widget.AppCompat.EditText">
<item name="android:textColor">@color/text_dark</item>
</style>

View File

@@ -9,3 +9,10 @@
# Copy this file to local.properties and update the path below:
sdk.dir=/path/to/your/android/sdk
# Optional backend overrides for Android development:
# Emulator default should usually stay 10.0.2.2 for the local machine.
petstore.backend.emulatorUrl=http\://10.0.2.2\:8080/
# Physical device example. Replace with the machine IP running the backend.
petstore.backend.deviceUrl=http\://10.0.0.200\:8080/

View File

@@ -1,6 +1,6 @@
{
"info": {
"name": "PetShop API Complete Collection",
"name": "PetShop Complete Collection",
"_postman_id": "petshop-api-complete-v1",
"description": "Complete API collection with all 95+ verified endpoints",
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"

View File

@@ -33,6 +33,7 @@ public class DevStackApplication {
docker.ensureDockerAvailable();
docker.startDatabase();
context = new SpringApplicationBuilder(BackendApplication.class)
.profiles("local")
.initializers(new FlywayContextInitializer())
.run(args);
context.addApplicationListener(event -> {

View File

@@ -0,0 +1,37 @@
package com.petshop.backend.config;
import com.petshop.backend.repository.PetRepository;
import com.petshop.backend.repository.ProductRepository;
import org.springframework.boot.CommandLineRunner;
import org.springframework.context.annotation.Profile;
import org.springframework.core.io.ClassPathResource;
import org.springframework.jdbc.datasource.init.ResourceDatabasePopulator;
import org.springframework.stereotype.Component;
import javax.sql.DataSource;
@Component
@Profile("local")
public class LocalCatalogSeedInitializer implements CommandLineRunner {
private final DataSource dataSource;
private final PetRepository petRepository;
private final ProductRepository productRepository;
public LocalCatalogSeedInitializer(DataSource dataSource, PetRepository petRepository, ProductRepository productRepository) {
this.dataSource = dataSource;
this.petRepository = petRepository;
this.productRepository = productRepository;
}
@Override
public void run(String... args) {
if (petRepository.count() > 6 || productRepository.count() > 6) {
return;
}
ResourceDatabasePopulator populator = new ResourceDatabasePopulator(false, false, "UTF-8",
new ClassPathResource("dev/expand_pet_product_seed.sql"));
populator.execute(dataSource);
}
}

View File

@@ -1,26 +1,40 @@
package com.petshop.backend.controller;
import com.petshop.backend.dto.analytics.DashboardResponse;
import com.petshop.backend.entity.User;
import com.petshop.backend.repository.UserRepository;
import com.petshop.backend.service.AnalyticsService;
import com.petshop.backend.util.AuthenticationHelper;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.server.ResponseStatusException;
@RestController
@RequestMapping("/api/v1/analytics")
@PreAuthorize("hasRole('ADMIN')")
@PreAuthorize("hasAnyRole('ADMIN', 'STAFF')")
public class AnalyticsController {
private final AnalyticsService analyticsService;
private final UserRepository userRepository;
public AnalyticsController(AnalyticsService analyticsService) {
public AnalyticsController(AnalyticsService analyticsService, UserRepository userRepository) {
this.analyticsService = analyticsService;
this.userRepository = userRepository;
}
@GetMapping("/dashboard")
public ResponseEntity<DashboardResponse> getDashboard(
@RequestParam(defaultValue = "30") int days,
@RequestParam(defaultValue = "10") int top) {
return ResponseEntity.ok(analyticsService.getDashboardData(days, top));
if (days < 1 || days > 365) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "days must be between 1 and 365");
}
if (top < 1 || top > 50) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "top must be between 1 and 50");
}
User user = AuthenticationHelper.getAuthenticatedUser(userRepository);
return ResponseEntity.ok(analyticsService.getDashboardData(days, top, user));
}
}

View File

@@ -25,8 +25,9 @@ public class CategoryController {
@GetMapping
public ResponseEntity<Page<CategoryResponse>> getAllCategories(
@RequestParam(required = false) String q,
@RequestParam(required = false) String type,
Pageable pageable) {
return ResponseEntity.ok(categoryService.getAllCategories(q, pageable));
return ResponseEntity.ok(categoryService.getAllCategories(q, type, pageable));
}
@GetMapping("/{id}")

View File

@@ -4,6 +4,7 @@ import com.petshop.backend.dto.chat.ConversationRequest;
import com.petshop.backend.dto.chat.ConversationResponse;
import com.petshop.backend.dto.chat.MessageRequest;
import com.petshop.backend.dto.chat.MessageResponse;
import com.petshop.backend.dto.chat.UpdateConversationRequest;
import com.petshop.backend.entity.User;
import com.petshop.backend.repository.CustomerRepository;
import com.petshop.backend.repository.UserRepository;
@@ -96,4 +97,13 @@ public class ChatController {
chatRealtimeService.publishConversationUpdate(id);
return ResponseEntity.ok(conversation);
}
@PutMapping("/conversations/{id}")
@PreAuthorize("hasAnyRole('CUSTOMER', 'STAFF', 'ADMIN')")
public ResponseEntity<ConversationResponse> updateConversation(@PathVariable Long id, @Valid @RequestBody UpdateConversationRequest request) {
User user = getCurrentUser();
ConversationResponse conversation = chatService.updateConversation(id, user.getId(), user.getRole(), request);
chatRealtimeService.publishConversationUpdate(id);
return ResponseEntity.ok(conversation);
}
}

View File

@@ -82,6 +82,29 @@ public class DropdownController {
);
}
@GetMapping("/product-categories")
public ResponseEntity<List<DropdownOption>> getProductCategories() {
return ResponseEntity.ok(
categoryRepository.findAll().stream()
.filter(c -> "product".equalsIgnoreCase(c.getCategoryType()))
.map(c -> new DropdownOption(c.getCategoryId(), c.getCategoryName()))
.collect(Collectors.toList())
);
}
@GetMapping("/pet-species")
public ResponseEntity<List<DropdownOption>> getPetSpecies() {
return ResponseEntity.ok(
petRepository.findAll().stream()
.map(p -> p.getPetSpecies())
.filter(species -> species != null && !species.isBlank())
.distinct()
.sorted(String.CASE_INSENSITIVE_ORDER)
.map(species -> new DropdownOption(null, species))
.collect(Collectors.toList())
);
}
@GetMapping("/stores")
public ResponseEntity<List<DropdownOption>> getStores() {
return ResponseEntity.ok(

View File

@@ -25,8 +25,10 @@ public class PetController {
@GetMapping
public ResponseEntity<Page<PetResponse>> getAllPets(
@RequestParam(required = false) String q,
@RequestParam(required = false) String species,
@RequestParam(required = false) String status,
Pageable pageable) {
return ResponseEntity.ok(petService.getAllPets(q, pageable));
return ResponseEntity.ok(petService.getAllPets(q, species, status, pageable));
}
@GetMapping("/{id}")

View File

@@ -25,8 +25,9 @@ public class ProductController {
@GetMapping
public ResponseEntity<Page<ProductResponse>> getAllProducts(
@RequestParam(required = false) String q,
@RequestParam(required = false) Long categoryId,
Pageable pageable) {
return ResponseEntity.ok(productService.getAllProducts(q, pageable));
return ResponseEntity.ok(productService.getAllProducts(q, categoryId, pageable));
}
@GetMapping("/{id}")

View File

@@ -9,15 +9,19 @@ public class DashboardResponse {
private InventorySummary inventorySummary;
private List<TopProduct> topProducts;
private List<DailySales> dailySales;
private List<PaymentMethodData> paymentMethods;
private List<EmployeePerformanceData> employeePerformance;
public DashboardResponse() {
}
public DashboardResponse(SalesSummary salesSummary, InventorySummary inventorySummary, List<TopProduct> topProducts, List<DailySales> dailySales) {
public DashboardResponse(SalesSummary salesSummary, InventorySummary inventorySummary, List<TopProduct> topProducts, List<DailySales> dailySales, List<PaymentMethodData> paymentMethods, List<EmployeePerformanceData> employeePerformance) {
this.salesSummary = salesSummary;
this.inventorySummary = inventorySummary;
this.topProducts = topProducts;
this.dailySales = dailySales;
this.paymentMethods = paymentMethods;
this.employeePerformance = employeePerformance;
}
public SalesSummary getSalesSummary() {
@@ -52,17 +56,33 @@ public class DashboardResponse {
this.dailySales = dailySales;
}
public List<PaymentMethodData> getPaymentMethods() {
return paymentMethods;
}
public void setPaymentMethods(List<PaymentMethodData> paymentMethods) {
this.paymentMethods = paymentMethods;
}
public List<EmployeePerformanceData> getEmployeePerformance() {
return employeePerformance;
}
public void setEmployeePerformance(List<EmployeePerformanceData> employeePerformance) {
this.employeePerformance = employeePerformance;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
DashboardResponse that = (DashboardResponse) o;
return Objects.equals(salesSummary, that.salesSummary) && Objects.equals(inventorySummary, that.inventorySummary) && Objects.equals(topProducts, that.topProducts) && Objects.equals(dailySales, that.dailySales);
return Objects.equals(salesSummary, that.salesSummary) && Objects.equals(inventorySummary, that.inventorySummary) && Objects.equals(topProducts, that.topProducts) && Objects.equals(dailySales, that.dailySales) && Objects.equals(paymentMethods, that.paymentMethods) && Objects.equals(employeePerformance, that.employeePerformance);
}
@Override
public int hashCode() {
return Objects.hash(salesSummary, inventorySummary, topProducts, dailySales);
return Objects.hash(salesSummary, inventorySummary, topProducts, dailySales, paymentMethods, employeePerformance);
}
@Override
@@ -72,6 +92,8 @@ public class DashboardResponse {
", inventorySummary=" + inventorySummary +
", topProducts=" + topProducts +
", dailySales=" + dailySales +
", paymentMethods=" + paymentMethods +
", employeePerformance=" + employeePerformance +
'}';
}
@@ -80,15 +102,17 @@ public class DashboardResponse {
private Long totalSales;
private BigDecimal totalRefunds;
private Long totalRefundCount;
private Long totalItemsSold;
public SalesSummary() {
}
public SalesSummary(BigDecimal totalRevenue, Long totalSales, BigDecimal totalRefunds, Long totalRefundCount) {
public SalesSummary(BigDecimal totalRevenue, Long totalSales, BigDecimal totalRefunds, Long totalRefundCount, Long totalItemsSold) {
this.totalRevenue = totalRevenue;
this.totalSales = totalSales;
this.totalRefunds = totalRefunds;
this.totalRefundCount = totalRefundCount;
this.totalItemsSold = totalItemsSold;
}
public BigDecimal getTotalRevenue() {
@@ -123,17 +147,25 @@ public class DashboardResponse {
this.totalRefundCount = totalRefundCount;
}
public Long getTotalItemsSold() {
return totalItemsSold;
}
public void setTotalItemsSold(Long totalItemsSold) {
this.totalItemsSold = totalItemsSold;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
SalesSummary that = (SalesSummary) o;
return Objects.equals(totalRevenue, that.totalRevenue) && Objects.equals(totalSales, that.totalSales) && Objects.equals(totalRefunds, that.totalRefunds) && Objects.equals(totalRefundCount, that.totalRefundCount);
return Objects.equals(totalRevenue, that.totalRevenue) && Objects.equals(totalSales, that.totalSales) && Objects.equals(totalRefunds, that.totalRefunds) && Objects.equals(totalRefundCount, that.totalRefundCount) && Objects.equals(totalItemsSold, that.totalItemsSold);
}
@Override
public int hashCode() {
return Objects.hash(totalRevenue, totalSales, totalRefunds, totalRefundCount);
return Objects.hash(totalRevenue, totalSales, totalRefunds, totalRefundCount, totalItemsSold);
}
@Override
@@ -143,10 +175,69 @@ public class DashboardResponse {
", totalSales=" + totalSales +
", totalRefunds=" + totalRefunds +
", totalRefundCount=" + totalRefundCount +
", totalItemsSold=" + totalItemsSold +
'}';
}
}
public static class PaymentMethodData {
private String paymentMethod;
private Long count;
public PaymentMethodData() {
}
public PaymentMethodData(String paymentMethod, Long count) {
this.paymentMethod = paymentMethod;
this.count = count;
}
public String getPaymentMethod() {
return paymentMethod;
}
public void setPaymentMethod(String paymentMethod) {
this.paymentMethod = paymentMethod;
}
public Long getCount() {
return count;
}
public void setCount(Long count) {
this.count = count;
}
}
public static class EmployeePerformanceData {
private String employeeName;
private BigDecimal revenue;
public EmployeePerformanceData() {
}
public EmployeePerformanceData(String employeeName, BigDecimal revenue) {
this.employeeName = employeeName;
this.revenue = revenue;
}
public String getEmployeeName() {
return employeeName;
}
public void setEmployeeName(String employeeName) {
this.employeeName = employeeName;
}
public BigDecimal getRevenue() {
return revenue;
}
public void setRevenue(BigDecimal revenue) {
this.revenue = revenue;
}
}
public static class InventorySummary {
private Long totalProducts;
private Long lowStockProducts;

View File

@@ -0,0 +1,25 @@
package com.petshop.backend.dto.chat;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Pattern;
public class UpdateConversationRequest {
@NotBlank(message = "Status is required")
@Pattern(regexp = "^(OPEN|CLOSED)$", message = "Status must be OPEN or CLOSED")
private String status;
public UpdateConversationRequest() {
}
public UpdateConversationRequest(String status) {
this.status = status;
}
public String getStatus() {
return status;
}
public void setStatus(String status) {
this.status = status;
}
}

View File

@@ -16,7 +16,7 @@ public interface CategoryRepository extends JpaRepository<Category, Long> {
Optional<Category> findByCategoryName(String categoryName);
@Query("SELECT c FROM Category c WHERE " +
"LOWER(c.categoryName) LIKE LOWER(CONCAT('%', :q, '%')) OR " +
"LOWER(c.categoryType) LIKE LOWER(CONCAT('%', :q, '%'))")
Page<Category> searchCategories(@Param("q") String query, Pageable pageable);
"(:q IS NULL OR LOWER(c.categoryName) LIKE LOWER(CONCAT('%', :q, '%')) OR LOWER(c.categoryType) LIKE LOWER(CONCAT('%', :q, '%'))) AND " +
"(:type IS NULL OR LOWER(c.categoryType) = LOWER(:type))")
Page<Category> searchCategories(@Param("q") String query, @Param("type") String type, Pageable pageable);
}

View File

@@ -12,8 +12,8 @@ import org.springframework.stereotype.Repository;
public interface PetRepository extends JpaRepository<Pet, Long> {
@Query("SELECT p FROM Pet p WHERE " +
"LOWER(p.petName) LIKE LOWER(CONCAT('%', :q, '%')) OR " +
"LOWER(p.petSpecies) LIKE LOWER(CONCAT('%', :q, '%')) OR " +
"LOWER(p.petBreed) LIKE LOWER(CONCAT('%', :q, '%'))")
Page<Pet> searchPets(@Param("q") String query, Pageable pageable);
"(:q IS NULL OR LOWER(p.petName) LIKE LOWER(CONCAT('%', :q, '%')) OR LOWER(p.petSpecies) LIKE LOWER(CONCAT('%', :q, '%')) OR LOWER(p.petBreed) LIKE LOWER(CONCAT('%', :q, '%'))) AND " +
"(:species IS NULL OR LOWER(p.petSpecies) = LOWER(:species)) AND " +
"(:status IS NULL OR LOWER(p.petStatus) = LOWER(:status))")
Page<Pet> searchPets(@Param("q") String query, @Param("species") String species, @Param("status") String status, Pageable pageable);
}

View File

@@ -12,7 +12,7 @@ import org.springframework.stereotype.Repository;
public interface ProductRepository extends JpaRepository<Product, Long> {
@Query("SELECT p FROM Product p WHERE " +
"LOWER(p.prodName) LIKE LOWER(CONCAT('%', :q, '%')) OR " +
"LOWER(p.prodDesc) LIKE LOWER(CONCAT('%', :q, '%'))")
Page<Product> searchProducts(@Param("q") String query, Pageable pageable);
"(:q IS NULL OR LOWER(p.prodName) LIKE LOWER(CONCAT('%', :q, '%')) OR LOWER(COALESCE(p.prodDesc, '')) LIKE LOWER(CONCAT('%', :q, '%'))) AND " +
"(:categoryId IS NULL OR p.category.categoryId = :categoryId)")
Page<Product> searchProducts(@Param("q") String query, @Param("categoryId") Long categoryId, Pageable pageable);
}

View File

@@ -1,9 +1,12 @@
package com.petshop.backend.service;
import com.petshop.backend.dto.analytics.DashboardResponse;
import com.petshop.backend.entity.Employee;
import com.petshop.backend.entity.Inventory;
import com.petshop.backend.entity.Product;
import com.petshop.backend.entity.Sale;
import com.petshop.backend.entity.User;
import com.petshop.backend.repository.EmployeeRepository;
import com.petshop.backend.repository.InventoryRepository;
import com.petshop.backend.repository.ProductRepository;
import com.petshop.backend.repository.SaleRepository;
@@ -23,28 +26,33 @@ public class AnalyticsService {
private final SaleRepository saleRepository;
private final InventoryRepository inventoryRepository;
private final ProductRepository productRepository;
private final EmployeeRepository employeeRepository;
public AnalyticsService(SaleRepository saleRepository,
InventoryRepository inventoryRepository, ProductRepository productRepository) {
InventoryRepository inventoryRepository, ProductRepository productRepository, EmployeeRepository employeeRepository) {
this.saleRepository = saleRepository;
this.inventoryRepository = inventoryRepository;
this.productRepository = productRepository;
this.employeeRepository = employeeRepository;
}
@Transactional(readOnly = true)
public DashboardResponse getDashboardData(int days, int top) {
public DashboardResponse getDashboardData(int days, int top, User user) {
LocalDateTime startDate = LocalDateTime.now().minusDays(days);
List<Sale> sales = saleRepository.findAll().stream()
.filter(sale -> sale.getSaleDate().isAfter(startDate))
.filter(sale -> includeSaleForUser(sale, user))
.collect(Collectors.toList());
DashboardResponse.SalesSummary salesSummary = calculateSalesSummary(sales);
DashboardResponse.InventorySummary inventorySummary = calculateInventorySummary();
DashboardResponse.InventorySummary inventorySummary = user.getRole() == User.Role.ADMIN ? calculateInventorySummary() : null;
List<DashboardResponse.TopProduct> topProducts = calculateTopProducts(sales, top);
List<DashboardResponse.DailySales> dailySales = calculateDailySales(sales, days);
List<DashboardResponse.PaymentMethodData> paymentMethods = calculatePaymentMethods(sales);
List<DashboardResponse.EmployeePerformanceData> employeePerformance = calculateEmployeePerformance(sales, user);
return new DashboardResponse(salesSummary, inventorySummary, topProducts, dailySales);
return new DashboardResponse(salesSummary, inventorySummary, topProducts, dailySales, paymentMethods, employeePerformance);
}
private DashboardResponse.SalesSummary calculateSalesSummary(List<Sale> sales) {
@@ -66,7 +74,13 @@ public class AnalyticsService {
.filter(Sale::getIsRefund)
.count();
return new DashboardResponse.SalesSummary(totalRevenue, totalSales, totalRefunds, totalRefundCount);
Long totalItemsSold = sales.stream()
.filter(sale -> !sale.getIsRefund())
.flatMap(sale -> sale.getItems().stream())
.mapToLong(item -> item.getQuantity())
.sum();
return new DashboardResponse.SalesSummary(totalRevenue, totalSales, totalRefunds, totalRefundCount, totalItemsSold);
}
private DashboardResponse.InventorySummary calculateInventorySummary() {
@@ -93,6 +107,9 @@ public class AnalyticsService {
Map<Long, DashboardResponse.TopProduct> productSalesMap = new HashMap<>();
for (Sale sale : sales) {
if (sale.getIsRefund()) {
continue;
}
for (var item : sale.getItems()) {
Long productId = item.getProduct().getProdId();
String productName = item.getProduct().getProdName();
@@ -128,6 +145,9 @@ public class AnalyticsService {
}
for (Sale sale : sales) {
if (sale.getIsRefund()) {
continue;
}
LocalDate saleDate = sale.getSaleDate().toLocalDate();
if (dailySalesMap.containsKey(saleDate)) {
DashboardResponse.DailySales dailySale = dailySalesMap.get(saleDate);
@@ -138,4 +158,50 @@ public class AnalyticsService {
return new ArrayList<>(dailySalesMap.values());
}
private List<DashboardResponse.PaymentMethodData> calculatePaymentMethods(List<Sale> sales) {
return sales.stream()
.filter(sale -> !sale.getIsRefund())
.collect(Collectors.groupingBy(
sale -> sale.getPaymentMethod() == null ? "Unknown" : sale.getPaymentMethod(),
TreeMap::new,
Collectors.counting()))
.entrySet().stream()
.map(entry -> new DashboardResponse.PaymentMethodData(entry.getKey(), entry.getValue()))
.collect(Collectors.toList());
}
private List<DashboardResponse.EmployeePerformanceData> calculateEmployeePerformance(List<Sale> sales, User user) {
Map<String, BigDecimal> employeeRevenue = new TreeMap<>();
for (Sale sale : sales) {
if (sale.getIsRefund()) {
continue;
}
String employeeName = sale.getEmployee().getFirstName() + " " + sale.getEmployee().getLastName();
employeeRevenue.merge(employeeName, sale.getTotalAmount(), BigDecimal::add);
}
if (user.getRole() == User.Role.STAFF && employeeRevenue.isEmpty()) {
Employee employee = employeeRepository.findByUserId(user.getId()).orElse(null);
if (employee != null) {
String employeeName = employee.getFirstName() + " " + employee.getLastName();
employeeRevenue.put(employeeName, BigDecimal.ZERO);
}
}
return employeeRevenue.entrySet().stream()
.map(entry -> new DashboardResponse.EmployeePerformanceData(entry.getKey(), entry.getValue()))
.collect(Collectors.toList());
}
private boolean includeSaleForUser(Sale sale, User user) {
if (user.getRole() == User.Role.ADMIN) {
return true;
}
if (user.getRole() == User.Role.STAFF) {
return sale.getEmployee() != null && sale.getEmployee().getUserId() != null && sale.getEmployee().getUserId().equals(user.getId());
}
return false;
}
}

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