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