diff --git a/android/app/src/main/java/com/example/petstoremobile/adapters/EmployeeAdapter.java b/android/app/src/main/java/com/example/petstoremobile/adapters/EmployeeAdapter.java index e860e30e..3dca0d7c 100644 --- a/android/app/src/main/java/com/example/petstoremobile/adapters/EmployeeAdapter.java +++ b/android/app/src/main/java/com/example/petstoremobile/adapters/EmployeeAdapter.java @@ -2,10 +2,12 @@ package com.example.petstoremobile.adapters; import android.graphics.Color; import android.view.*; +import android.widget.ImageView; import android.widget.TextView; import androidx.annotation.NonNull; import androidx.recyclerview.widget.RecyclerView; import com.example.petstoremobile.R; +import com.example.petstoremobile.databinding.ItemEmployeeBinding; import com.example.petstoremobile.dtos.EmployeeDTO; import java.util.List; @@ -24,47 +26,48 @@ public class EmployeeAdapter extends RecyclerView.Adapter listener.onEmployeeClick(position)); } 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 index 503186be..deca5624 100644 --- a/android/app/src/main/java/com/example/petstoremobile/adapters/SaleAdapter.java +++ b/android/app/src/main/java/com/example/petstoremobile/adapters/SaleAdapter.java @@ -1,11 +1,12 @@ package com.example.petstoremobile.adapters; -import android.graphics.Color; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import androidx.annotation.NonNull; +import androidx.core.content.ContextCompat; import androidx.recyclerview.widget.RecyclerView; +import com.example.petstoremobile.R; import com.example.petstoremobile.databinding.ItemSaleBinding; import com.example.petstoremobile.dtos.SaleDTO; import java.util.List; @@ -53,11 +54,10 @@ public class SaleAdapter extends RecyclerView.Adapter listener.onSaleClick(position)); 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 index 72bfd8f4..fa11f100 100644 --- a/android/app/src/main/java/com/example/petstoremobile/api/SaleApi.java +++ b/android/app/src/main/java/com/example/petstoremobile/api/SaleApi.java @@ -15,11 +15,15 @@ public interface SaleApi { @GET("api/v1/sales") Call> getAllSales( @Query("page") int page, - @Query("size") int size); + @Query("size") int size, + @Query("q") String query, + @Query("paymentMethod") String paymentMethod, + @Query("storeId") Long storeId, + @Query("sort") String sort); @GET("api/v1/sales/{id}") Call getSaleById(@Path("id") Long id); @POST("api/v1/sales") Call createSale(@Body SaleDTO sale); -} \ 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 index 2de34a38..2c7189ee 100644 --- a/android/app/src/main/java/com/example/petstoremobile/dtos/SaleDTO.java +++ b/android/app/src/main/java/com/example/petstoremobile/dtos/SaleDTO.java @@ -12,9 +12,16 @@ public class SaleDTO { private Long storeId; private String storeName; private BigDecimal totalAmount; + private BigDecimal subtotalAmount; + private BigDecimal couponDiscountAmount; + private BigDecimal employeeDiscountAmount; private String paymentMethod; + private String channel; private Boolean isRefund; private Long originalSaleId; + private Long cartId; + private Long couponId; + private Integer pointsEarned; private List items; private String createdAt; @@ -60,10 +67,26 @@ public class SaleDTO { return totalAmount; } + public BigDecimal getSubtotalAmount() { + return subtotalAmount; + } + + public BigDecimal getCouponDiscountAmount() { + return couponDiscountAmount; + } + + public BigDecimal getEmployeeDiscountAmount() { + return employeeDiscountAmount; + } + public String getPaymentMethod() { return paymentMethod; } + public String getChannel() { + return channel; + } + public Boolean getIsRefund() { return isRefund; } @@ -72,6 +95,18 @@ public class SaleDTO { return originalSaleId; } + public Long getCartId() { + return cartId; + } + + public Long getCouponId() { + return couponId; + } + + public Integer getPointsEarned() { + return pointsEarned; + } + public List getItems() { return items; } @@ -118,4 +153,4 @@ public class SaleDTO { return unitPrice; } } -} \ No newline at end of file +} diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AdoptionFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AdoptionFragment.java index f4757314..577d58b1 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 @@ -2,8 +2,6 @@ package com.example.petstoremobile.fragments.listfragments; import android.graphics.Color; import android.os.Bundle; -import android.text.Editable; -import android.text.TextWatcher; import android.util.Log; import android.view.LayoutInflater; import android.view.View; @@ -22,10 +20,10 @@ import com.example.petstoremobile.adapters.AdoptionAdapter; import com.example.petstoremobile.databinding.FragmentAdoptionBinding; import com.example.petstoremobile.dtos.AdoptionDTO; import com.example.petstoremobile.dtos.StoreDTO; -import com.example.petstoremobile.fragments.ListFragment; import com.example.petstoremobile.utils.BulkDeleteHandler; import com.example.petstoremobile.utils.Resource; import com.example.petstoremobile.utils.SpinnerUtils; +import com.example.petstoremobile.utils.UIUtils; import com.example.petstoremobile.viewmodels.AdoptionViewModel; import com.example.petstoremobile.utils.EventDecorator; import com.example.petstoremobile.viewmodels.StoreViewModel; @@ -86,15 +84,7 @@ public class AdoptionFragment extends Fragment implements AdoptionAdapter.OnAdop binding.fabAddAdoption.setOnClickListener(v -> openDetail(-1)); - binding.btnHamburgerAdoption.setOnClickListener(v -> { - Fragment parent = getParentFragment(); - if (parent != null) { - Fragment grandParent = parent.getParentFragment(); - if (grandParent instanceof ListFragment) { - ((ListFragment) grandParent).openDrawer(); - } - } - }); + UIUtils.setupHamburgerMenu(binding.btnHamburgerAdoption, this); binding.btnToggleCalendarModeAdoption.setOnClickListener(v -> toggleCalendarMode()); @@ -141,20 +131,24 @@ public class AdoptionFragment extends Fragment implements AdoptionAdapter.OnAdop * Sets up the filter toggle button to show/hide the filter layout. */ private void setupFilterToggle() { - binding.btnToggleFilterAdoption.setOnClickListener(v -> { - if (binding.layoutFilterAdoption.getVisibility() == View.GONE) { - binding.layoutFilterAdoption.setVisibility(View.VISIBLE); - binding.btnToggleFilterAdoption.setImageResource(android.R.drawable.ic_menu_close_clear_cancel); - } else { - binding.layoutFilterAdoption.setVisibility(View.GONE); - binding.btnToggleFilterAdoption.setImageResource(android.R.drawable.ic_menu_search); + UIUtils.setupFilterToggle(binding.btnToggleFilterAdoption, binding.layoutFilterAdoption, + binding.etSearchAdoption, binding.spinnerStatusAdoption, binding.spinnerStoreAdoption); - // Reset filters when closing + binding.btnToggleFilterAdoption.setOnClickListener(v -> { + boolean isVisible = binding.layoutFilterAdoption.getVisibility() == View.VISIBLE; + binding.layoutFilterAdoption.setVisibility(isVisible ? View.GONE : View.VISIBLE); + + binding.btnToggleFilterAdoption.setImageResource(isVisible ? + android.R.drawable.ic_menu_search : + android.R.drawable.ic_menu_close_clear_cancel); + + if (isVisible) { binding.etSearchAdoption.setText(""); binding.spinnerStatusAdoption.setSelection(0); binding.spinnerStoreAdoption.setSelection(0); selectedCalendarDay = null; binding.calendarViewAdoption.clearSelection(); + loadAdoptions(); } }); } @@ -214,13 +208,7 @@ public class AdoptionFragment extends Fragment implements AdoptionAdapter.OnAdop * Sets up the search bar for filtering */ private void setupSearch() { - binding.etSearchAdoption.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) { - loadAdoptions(); - } - @Override public void afterTextChanged(Editable s) {} - }); + UIUtils.attachSearch(binding.etSearchAdoption, this::loadAdoptions); } /** diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AnalyticsFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AnalyticsFragment.java index c9eca2dd..dbd24c75 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AnalyticsFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AnalyticsFragment.java @@ -5,91 +5,77 @@ import android.os.Bundle; import android.util.Log; import android.view.*; import android.widget.*; +import androidx.annotation.NonNull; import androidx.fragment.app.Fragment; -import com.example.petstoremobile.R; -import com.example.petstoremobile.api.RetrofitClient; -import com.example.petstoremobile.dtos.PageResponse; +import androidx.lifecycle.ViewModelProvider; +import com.example.petstoremobile.databinding.FragmentAnalyticsBinding; import com.example.petstoremobile.dtos.SaleDTO; -import com.example.petstoremobile.fragments.ListFragment; +import com.example.petstoremobile.utils.UIUtils; +import com.example.petstoremobile.viewmodels.SaleViewModel; +import dagger.hilt.android.AndroidEntryPoint; import java.math.BigDecimal; import java.math.RoundingMode; import java.util.*; -import retrofit2.*; +@AndroidEntryPoint public class AnalyticsFragment extends Fragment { - private TextView tvTotalRevenue, tvTotalTransactions, tvAvgTransaction, tvTotalItems; - private LinearLayout llTopRevenue, llTopQuantity, llPaymentMethods, llEmployeePerformance, llDailyRevenue; - private Button btnRefresh; - private ImageButton hamburger; + private FragmentAnalyticsBinding binding; + private SaleViewModel saleViewModel; @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_analytics, container, false); + binding = FragmentAnalyticsBinding.inflate(inflater, container, false); + saleViewModel = new ViewModelProvider(this).get(SaleViewModel.class); - initViews(view); loadAnalytics(); - btnRefresh.setOnClickListener(v -> loadAnalytics()); - hamburger.setOnClickListener(v -> openDrawer()); + binding.btnRefreshAnalytics.setOnClickListener(v -> loadAnalytics()); + + UIUtils.setupHamburgerMenu(binding.btnHamburgerAnalytics, this); - return view; + return binding.getRoot(); } - private void openDrawer() { - Fragment parent = getParentFragment(); - if (parent != null) { - Fragment grandParent = parent.getParentFragment(); - if (grandParent instanceof ListFragment) { - ((ListFragment) grandParent).openDrawer(); - } - } - } - - private void initViews(View v) { - tvTotalRevenue = v.findViewById(R.id.tvTotalRevenue); - tvTotalTransactions = v.findViewById(R.id.tvTotalTransactions); - tvAvgTransaction = v.findViewById(R.id.tvAvgTransaction); - tvTotalItems = v.findViewById(R.id.tvTotalItems); - llTopRevenue = v.findViewById(R.id.llTopRevenue); - llTopQuantity = v.findViewById(R.id.llTopQuantity); - llPaymentMethods = v.findViewById(R.id.llPaymentMethods); - llEmployeePerformance = v.findViewById(R.id.llEmployeePerformance); - llDailyRevenue = v.findViewById(R.id.llDailyRevenue); - btnRefresh = v.findViewById(R.id.btnRefreshAnalytics); - hamburger = v.findViewById(R.id.btnHamburgerAnalytics); + @Override + public void onDestroyView() { + super.onDestroyView(); + binding = null; } private void loadAnalytics() { // Clear all sections - llTopRevenue.removeAllViews(); - llTopQuantity.removeAllViews(); - llPaymentMethods.removeAllViews(); - llEmployeePerformance.removeAllViews(); - llDailyRevenue.removeAllViews(); + binding.llTopRevenue.removeAllViews(); + binding.llTopQuantity.removeAllViews(); + binding.llPaymentMethods.removeAllViews(); + binding.llEmployeePerformance.removeAllViews(); + binding.llDailyRevenue.removeAllViews(); // Show loading - tvTotalRevenue.setText("Loading..."); - tvTotalTransactions.setText("..."); - tvAvgTransaction.setText("..."); - tvTotalItems.setText("..."); + binding.tvTotalRevenue.setText("Loading..."); + binding.tvTotalTransactions.setText("..."); + binding.tvAvgTransaction.setText("..."); + binding.tvTotalItems.setText("..."); - RetrofitClient.getSaleApi(requireContext()).getAllSales(0, 1000) - .enqueue(new Callback>() { - public void onResponse(Call> c, - Response> r) { - if (r.isSuccessful() && r.body() != null) { - computeAndDisplay(r.body().getContent()); - } else { - showError("Failed to load sales data"); + saleViewModel.getAllSales(0, 1000, null, null, null, "saleDate,desc") + .observe(getViewLifecycleOwner(), resource -> { + if (resource != null) { + switch (resource.status) { + case SUCCESS: + if (resource.data != null) { + computeAndDisplay(resource.data.getContent()); + } + break; + case ERROR: + Log.e("Analytics", resource.message != null ? resource.message : "Error loading sales"); + showError("Failed to load sales data"); + break; + case LOADING: + // Already showing loading state in UI + break; } } - - public void onFailure(Call> c, Throwable t) { - Log.e("Analytics", t.getMessage()); - showError("Network error"); - } }); } @@ -121,10 +107,10 @@ public class AnalyticsFragment extends Fragment { ? totalRevenue.divide(BigDecimal.valueOf(totalTx), 2, RoundingMode.HALF_UP) : BigDecimal.ZERO; - tvTotalRevenue.setText("$" + totalRevenue.setScale(2, RoundingMode.HALF_UP)); - tvTotalTransactions.setText(String.valueOf(totalTx)); - tvAvgTransaction.setText("$" + avgTx); - tvTotalItems.setText(String.valueOf(totalItems)); + binding.tvTotalRevenue.setText("$" + totalRevenue.setScale(2, RoundingMode.HALF_UP)); + binding.tvTotalTransactions.setText(String.valueOf(totalTx)); + binding.tvAvgTransaction.setText("$" + avgTx); + binding.tvTotalItems.setText(String.valueOf(totalItems)); // ── Top Products by Revenue ─────────────────────────── Map revenueByProduct = new LinkedHashMap<>(); @@ -150,28 +136,28 @@ public class AnalyticsFragment extends Fragment { topRevenue.sort((a, b) -> b.getValue().compareTo(a.getValue())); BigDecimal maxRevenue = topRevenue.isEmpty() ? BigDecimal.ONE : topRevenue.get(0).getValue(); - llTopRevenue.removeAllViews(); + binding.llTopRevenue.removeAllViews(); for (int i = 0; i < Math.min(5, topRevenue.size()); i++) { Map.Entry e = topRevenue.get(i); - addBarRow(llTopRevenue, e.getKey(), "$" + e.getValue().setScale(2, RoundingMode.HALF_UP), + addBarRow(binding.llTopRevenue, e.getKey(), "$" + e.getValue().setScale(2, RoundingMode.HALF_UP), e.getValue().floatValue() / maxRevenue.floatValue(), "#ff6b35"); } if (topRevenue.isEmpty()) - addEmptyRow(llTopRevenue, "No data"); + addEmptyRow(binding.llTopRevenue, "No data"); // Sort by quantity desc, take top 5 List> topQuantity = new ArrayList<>(quantityByProduct.entrySet()); topQuantity.sort((a, b) -> b.getValue() - a.getValue()); int maxQty = topQuantity.isEmpty() ? 1 : topQuantity.get(0).getValue(); - llTopQuantity.removeAllViews(); + binding.llTopQuantity.removeAllViews(); for (int i = 0; i < Math.min(5, topQuantity.size()); i++) { Map.Entry e = topQuantity.get(i); - addBarRow(llTopQuantity, e.getKey(), e.getValue() + " units", + addBarRow(binding.llTopQuantity, e.getKey(), e.getValue() + " units", (float) e.getValue() / maxQty, "#4ecdc4"); } if (topQuantity.isEmpty()) - addEmptyRow(llTopQuantity, "No data"); + addEmptyRow(binding.llTopQuantity, "No data"); // ── Payment Methods ─────────────────────────────────── Map paymentCount = new LinkedHashMap<>(); @@ -183,16 +169,16 @@ public class AnalyticsFragment extends Fragment { int maxPayment = paymentCount.values().stream().max(Integer::compare).orElse(1); String[] paymentColors = { "#1a759f", "#ff9f1c", "#577590", "#90be6d" }; int ci = 0; - llPaymentMethods.removeAllViews(); + binding.llPaymentMethods.removeAllViews(); for (Map.Entry e : paymentCount.entrySet()) { - addBarRow(llPaymentMethods, e.getKey(), + addBarRow(binding.llPaymentMethods, e.getKey(), e.getValue() + " transactions", (float) e.getValue() / maxPayment, paymentColors[ci % paymentColors.length]); ci++; } if (paymentCount.isEmpty()) - addEmptyRow(llPaymentMethods, "No data"); + addEmptyRow(binding.llPaymentMethods, "No data"); // ── Employee Performance ────────────────────────────── Map employeeRevenue = new LinkedHashMap<>(); @@ -206,15 +192,15 @@ public class AnalyticsFragment extends Fragment { empList.sort((a, b) -> b.getValue().compareTo(a.getValue())); BigDecimal maxEmp = empList.isEmpty() ? BigDecimal.ONE : empList.get(0).getValue(); - llEmployeePerformance.removeAllViews(); + binding.llEmployeePerformance.removeAllViews(); for (Map.Entry e : empList) { - addBarRow(llEmployeePerformance, e.getKey(), + addBarRow(binding.llEmployeePerformance, e.getKey(), "$" + e.getValue().setScale(2, RoundingMode.HALF_UP), e.getValue().floatValue() / maxEmp.floatValue(), "#1a759f"); } if (empList.isEmpty()) - addEmptyRow(llEmployeePerformance, "No data"); + addEmptyRow(binding.llEmployeePerformance, "No data"); // ── Daily Revenue (last 7 days) ─────────────────────── Map dailyRevenue = new TreeMap<>(); @@ -247,26 +233,25 @@ public class AnalyticsFragment extends Fragment { if (maxDaily.compareTo(BigDecimal.ZERO) == 0) maxDaily = BigDecimal.ONE; - llDailyRevenue.removeAllViews(); + binding.llDailyRevenue.removeAllViews(); for (Map.Entry e : dailyRevenue.entrySet()) { // Show just MM-DD String label = e.getKey().length() >= 10 ? e.getKey().substring(5) : e.getKey(); - addBarRow(llDailyRevenue, label, + addBarRow(binding.llDailyRevenue, label, "$" + e.getValue().setScale(2, RoundingMode.HALF_UP), e.getValue().floatValue() / maxDaily.floatValue(), "#ff6b35"); } } - // Adds a horizontal bar row with label, value and a proportional bar private void addBarRow(LinearLayout parent, String label, String value, float ratio, String color) { + if (getContext() == null) return; LinearLayout row = new LinearLayout(getContext()); row.setOrientation(LinearLayout.VERTICAL); row.setPadding(0, 6, 0, 6); - // Label + value row LinearLayout labelRow = new LinearLayout(getContext()); labelRow.setOrientation(LinearLayout.HORIZONTAL); @@ -286,7 +271,6 @@ public class AnalyticsFragment extends Fragment { labelRow.addView(tvLabel); labelRow.addView(tvValue); - // Bar background LinearLayout barBg = new LinearLayout(getContext()); LinearLayout.LayoutParams bgParams = new LinearLayout.LayoutParams( LinearLayout.LayoutParams.MATCH_PARENT, 12); @@ -294,16 +278,13 @@ public class AnalyticsFragment extends Fragment { barBg.setLayoutParams(bgParams); barBg.setBackgroundColor(Color.parseColor("#EEEEEE")); - // Bar fill View barFill = new View(getContext()); - int fillWidth = (int) (ratio * 100); LinearLayout.LayoutParams fillParams = new LinearLayout.LayoutParams( 0, LinearLayout.LayoutParams.MATCH_PARENT, ratio); barFill.setLayoutParams(fillParams); barFill.setBackgroundColor(Color.parseColor(color)); barBg.addView(barFill); - // Empty space View spacer = new View(getContext()); spacer.setLayoutParams(new LinearLayout.LayoutParams( 0, LinearLayout.LayoutParams.MATCH_PARENT, 1f - ratio)); @@ -315,6 +296,7 @@ public class AnalyticsFragment extends Fragment { } private void addEmptyRow(LinearLayout parent, String message) { + if (getContext() == null) return; TextView tv = new TextView(getContext()); tv.setText(message); tv.setTextColor(Color.parseColor("#888780")); @@ -323,12 +305,12 @@ public class AnalyticsFragment extends Fragment { } private void showError(String msg) { - if (getContext() == null) + if (getContext() == null || binding == null) return; - tvTotalRevenue.setText("Error"); - tvTotalTransactions.setText("—"); - tvAvgTransaction.setText("—"); - tvTotalItems.setText("—"); + binding.tvTotalRevenue.setText("Error"); + binding.tvTotalTransactions.setText("—"); + binding.tvAvgTransaction.setText("—"); + binding.tvTotalItems.setText("—"); Toast.makeText(getContext(), msg, Toast.LENGTH_SHORT).show(); } } 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 f7aed714..3d4d6cd6 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 @@ -10,8 +10,6 @@ import androidx.lifecycle.ViewModelProvider; import androidx.navigation.fragment.NavHostFragment; import androidx.recyclerview.widget.LinearLayoutManager; -import android.text.Editable; -import android.text.TextWatcher; import android.util.Log; import android.view.LayoutInflater; import android.view.View; @@ -23,10 +21,10 @@ import com.example.petstoremobile.adapters.AppointmentAdapter; import com.example.petstoremobile.databinding.FragmentAppointmentBinding; import com.example.petstoremobile.dtos.AppointmentDTO; import com.example.petstoremobile.dtos.StoreDTO; -import com.example.petstoremobile.fragments.ListFragment; import com.example.petstoremobile.utils.BulkDeleteHandler; import com.example.petstoremobile.utils.Resource; import com.example.petstoremobile.utils.SpinnerUtils; +import com.example.petstoremobile.utils.UIUtils; import com.example.petstoremobile.viewmodels.AppointmentViewModel; import com.example.petstoremobile.utils.EventDecorator; import com.example.petstoremobile.viewmodels.AuthViewModel; @@ -94,15 +92,7 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter. binding.fabAddAppointment.setOnClickListener(v -> openAppointmentDetails(-1)); - binding.btnHamburger.setOnClickListener(v -> { - Fragment parent = getParentFragment(); - if (parent != null) { - Fragment grandParent = parent.getParentFragment(); - if (grandParent instanceof ListFragment) { - ((ListFragment) grandParent).openDrawer(); - } - } - }); + UIUtils.setupHamburgerMenu(binding.btnHamburger, this); binding.btnToggleCalendarMode.setOnClickListener(v -> toggleCalendarMode()); @@ -171,21 +161,26 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter. * Sets up the filter toggle button to show/hide the filter layout. */ private void setupFilterToggle() { + UIUtils.setupFilterToggle(binding.btnToggleFilter, binding.layoutFilter, binding.etSearchAppointment, + binding.spinnerStatus, binding.spinnerStore); + + // Add additional reset logic for elements specific to this fragment binding.btnToggleFilter.setOnClickListener(v -> { - if (binding.layoutFilter.getVisibility() == View.GONE) { - binding.layoutFilter.setVisibility(View.VISIBLE); - binding.btnToggleFilter.setImageResource(android.R.drawable.ic_menu_close_clear_cancel); - } else { - binding.layoutFilter.setVisibility(View.GONE); - binding.btnToggleFilter.setImageResource(android.R.drawable.ic_menu_search); + boolean isVisible = binding.layoutFilter.getVisibility() == View.VISIBLE; + binding.layoutFilter.setVisibility(isVisible ? View.GONE : View.VISIBLE); + + binding.btnToggleFilter.setImageResource(isVisible ? + android.R.drawable.ic_menu_search : + android.R.drawable.ic_menu_close_clear_cancel); - // Reset filters when closing + if (isVisible) { binding.etSearchAppointment.setText(""); binding.spinnerStatus.setSelection(0); binding.spinnerStore.setSelection(0); binding.btnMyAppointments.setChecked(false); selectedCalendarDay = null; binding.calendarView.clearSelection(); + loadAppointmentData(); } }); } @@ -214,10 +209,11 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter. */ private void updateCalendarDecorators() { HashSet datesWithAppointments = new HashSet<>(); + SimpleDateFormat displayFormat = new SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()); for (AppointmentDTO appointment : appointmentList) { try { //Get the appointment date - Date date = dateFormat.parse(appointment.getAppointmentDate()); + Date date = displayFormat.parse(appointment.getAppointmentDate()); //if the date is not null, add it to the hashset if (date != null) { Calendar cal = Calendar.getInstance(); @@ -237,13 +233,7 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter. * Configures the search bar for filtering. */ private void setupSearch() { - binding.etSearchAppointment.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) { - loadAppointmentData(); - } - @Override public void afterTextChanged(Editable s) {} - }); + UIUtils.attachSearch(binding.etSearchAppointment, this::loadAppointmentData); } /** 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 bf78e2b8..259a0a9e 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,8 +1,6 @@ package com.example.petstoremobile.fragments.listfragments; import android.os.Bundle; -import android.text.Editable; -import android.text.TextWatcher; import android.util.Log; import android.view.LayoutInflater; import android.view.View; @@ -22,8 +20,8 @@ import com.example.petstoremobile.adapters.InventoryAdapter; import com.example.petstoremobile.databinding.FragmentInventoryBinding; import com.example.petstoremobile.dtos.InventoryDTO; import com.example.petstoremobile.dtos.StoreDTO; -import com.example.petstoremobile.fragments.ListFragment; import com.example.petstoremobile.utils.BulkDeleteHandler; +import com.example.petstoremobile.utils.UIUtils; import com.example.petstoremobile.viewmodels.InventoryViewModel; import com.example.petstoremobile.utils.Resource; import com.example.petstoremobile.utils.SpinnerUtils; @@ -79,15 +77,7 @@ public class InventoryFragment extends Fragment implements InventoryAdapter.OnIn binding.fabAddInventory.setOnClickListener(v -> openDetail(null)); - binding.btnHamburger.setOnClickListener(v -> { - Fragment parent = getParentFragment(); - if (parent != null) { - Fragment grandParent = parent.getParentFragment(); - if (grandParent instanceof ListFragment) { - ((ListFragment) grandParent).openDrawer(); - } - } - }); + UIUtils.setupHamburgerMenu(binding.btnHamburger, this); return binding.getRoot(); } @@ -115,32 +105,14 @@ public class InventoryFragment extends Fragment implements InventoryAdapter.OnIn * Sets up the filter toggle button to show/hide the filter layout. */ private void setupFilterToggle() { - binding.btnToggleFilter.setOnClickListener(v -> { - if (binding.layoutFilter.getVisibility() == View.GONE) { - binding.layoutFilter.setVisibility(View.VISIBLE); - binding.btnToggleFilter.setImageResource(android.R.drawable.ic_menu_close_clear_cancel); - } else { - binding.layoutFilter.setVisibility(View.GONE); - binding.btnToggleFilter.setImageResource(android.R.drawable.ic_menu_search); - - // Reset filters when closing - binding.etSearchInventory.setText(""); - binding.spinnerStore.setSelection(0); - } - }); + UIUtils.setupFilterToggle(binding.btnToggleFilter, binding.layoutFilter, binding.etSearchInventory, binding.spinnerStore); } /** * Sets up the search bar for filtering. */ private void setupSearch() { - binding.etSearchInventory.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) { - loadInventory(true); - } - @Override public void afterTextChanged(Editable s) {} - }); + UIUtils.attachSearch(binding.etSearchInventory, () -> loadInventory(true)); } /** 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 578022c3..e7e36eae 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 @@ -9,8 +9,6 @@ import androidx.lifecycle.ViewModelProvider; import androidx.navigation.fragment.NavHostFragment; import androidx.recyclerview.widget.LinearLayoutManager; -import android.text.Editable; -import android.text.TextWatcher; import android.util.Log; import android.view.LayoutInflater; import android.view.View; @@ -19,13 +17,14 @@ import android.widget.Toast; import com.example.petstoremobile.R; import com.example.petstoremobile.adapters.PetAdapter; +import com.example.petstoremobile.api.auth.TokenManager; import com.example.petstoremobile.databinding.FragmentPetBinding; import com.example.petstoremobile.dtos.PetDTO; import com.example.petstoremobile.dtos.StoreDTO; -import com.example.petstoremobile.fragments.ListFragment; import com.example.petstoremobile.utils.BulkDeleteHandler; import com.example.petstoremobile.utils.Resource; import com.example.petstoremobile.utils.SpinnerUtils; +import com.example.petstoremobile.utils.UIUtils; import com.example.petstoremobile.viewmodels.PetViewModel; import com.example.petstoremobile.viewmodels.StoreViewModel; @@ -48,6 +47,7 @@ public class PetFragment extends Fragment implements PetAdapter.OnPetClickListen private BulkDeleteHandler bulkDeleteHandler; @Inject @Named("baseUrl") String baseUrl; + @Inject TokenManager tokenManager; /** * Initializes the fragment and its associated ViewModels. @@ -78,15 +78,7 @@ public class PetFragment extends Fragment implements PetAdapter.OnPetClickListen binding.fabAddPet.setOnClickListener(v -> openPetDetails()); - binding.btnHamburger.setOnClickListener(v -> { - Fragment parent = getParentFragment(); - if (parent != null) { - Fragment grandParent = parent.getParentFragment(); - if (grandParent instanceof ListFragment) { - ((ListFragment) grandParent).openDrawer(); - } - } - }); + UIUtils.setupHamburgerMenu(binding.btnHamburger, this); return binding.getRoot(); } @@ -124,34 +116,15 @@ public class PetFragment extends Fragment implements PetAdapter.OnPetClickListen * Sets up the filter toggle button to show/hide the filter layout. */ private void setupFilterToggle() { - binding.btnToggleFilter.setOnClickListener(v -> { - if (binding.layoutFilter.getVisibility() == View.GONE) { - binding.layoutFilter.setVisibility(View.VISIBLE); - binding.btnToggleFilter.setImageResource(android.R.drawable.ic_menu_close_clear_cancel); - } else { - binding.layoutFilter.setVisibility(View.GONE); - binding.btnToggleFilter.setImageResource(android.R.drawable.ic_menu_search); - - // Reset filters when closing - binding.etSearchPet.setText(""); - binding.spinnerStatus.setSelection(0); - binding.spinnerSpecies.setSelection(0); - binding.spinnerStore.setSelection(0); - } - }); + UIUtils.setupFilterToggle(binding.btnToggleFilter, binding.layoutFilter, binding.etSearchPet, + binding.spinnerStatus, binding.spinnerSpecies, binding.spinnerStore); } /** * Configures the search bar. */ private void setupSearch() { - binding.etSearchPet.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) { - loadPetData(); - } - @Override public void afterTextChanged(Editable s) {} - }); + UIUtils.attachSearch(binding.etSearchPet, this::loadPetData); } /** @@ -272,6 +245,7 @@ public class PetFragment extends Fragment implements PetAdapter.OnPetClickListen private void setupRecyclerView() { adapter = new PetAdapter(petList, this); adapter.setBaseUrl(baseUrl); + adapter.setToken(tokenManager.getToken()); binding.recyclerViewPets.setLayoutManager(new LinearLayoutManager(getContext())); binding.recyclerViewPets.setAdapter(adapter); } 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 b2c28fee..6ae3d349 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 @@ -9,8 +9,6 @@ import androidx.lifecycle.ViewModelProvider; import androidx.navigation.fragment.NavHostFragment; import androidx.recyclerview.widget.LinearLayoutManager; -import android.text.Editable; -import android.text.TextWatcher; import android.util.Log; import android.view.LayoutInflater; import android.view.View; @@ -22,9 +20,9 @@ import com.example.petstoremobile.adapters.ProductAdapter; import com.example.petstoremobile.databinding.FragmentProductBinding; import com.example.petstoremobile.dtos.CategoryDTO; import com.example.petstoremobile.dtos.ProductDTO; -import com.example.petstoremobile.fragments.ListFragment; import com.example.petstoremobile.utils.Resource; import com.example.petstoremobile.utils.SpinnerUtils; +import com.example.petstoremobile.utils.UIUtils; import com.example.petstoremobile.viewmodels.ProductViewModel; import java.util.ArrayList; @@ -71,15 +69,7 @@ public class ProductFragment extends Fragment implements ProductAdapter.OnProduc binding.fabAddProduct.setOnClickListener(v -> openProductDetails(-1)); - binding.btnHamburgerProduct.setOnClickListener(v -> { - Fragment parent = getParentFragment(); - if (parent != null) { - Fragment grandParent = parent.getParentFragment(); - if (grandParent instanceof ListFragment) { - ((ListFragment) grandParent).openDrawer(); - } - } - }); + UIUtils.setupHamburgerMenu(binding.btnHamburgerProduct, this); return binding.getRoot(); } @@ -104,32 +94,15 @@ public class ProductFragment extends Fragment implements ProductAdapter.OnProduc * Sets up the filter toggle button to show/hide the filter layout. */ private void setupFilterToggle() { - binding.btnToggleFilter.setOnClickListener(v -> { - if (binding.layoutFilter.getVisibility() == View.GONE) { - binding.layoutFilter.setVisibility(View.VISIBLE); - binding.btnToggleFilter.setImageResource(android.R.drawable.ic_menu_close_clear_cancel); - } else { - binding.layoutFilter.setVisibility(View.GONE); - binding.btnToggleFilter.setImageResource(android.R.drawable.ic_menu_search); - - // Reset filters when closing - binding.etSearchProduct.setText(""); - binding.spinnerCategory.setSelection(0); - } - }); + UIUtils.setupFilterToggle(binding.btnToggleFilter, binding.layoutFilter, + binding.etSearchProduct, binding.spinnerCategory); } /** * Configures the search bar for triggering data load from backend. */ private void setupSearch() { - binding.etSearchProduct.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) { - loadProductData(); - } - @Override public void afterTextChanged(Editable s) {} - }); + UIUtils.attachSearch(binding.etSearchProduct, this::loadProductData); } /** 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 index 578aa7a9..e1db78b6 100644 --- 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 @@ -1,8 +1,6 @@ package com.example.petstoremobile.fragments.listfragments; import android.os.Bundle; -import android.text.Editable; -import android.text.TextWatcher; import android.util.Log; import android.view.LayoutInflater; import android.view.View; @@ -22,10 +20,10 @@ import com.example.petstoremobile.databinding.FragmentProductSupplierBinding; import com.example.petstoremobile.dtos.ProductDTO; import com.example.petstoremobile.dtos.ProductSupplierDTO; import com.example.petstoremobile.dtos.SupplierDTO; -import com.example.petstoremobile.fragments.ListFragment; import com.example.petstoremobile.utils.BulkDeleteHandler; import com.example.petstoremobile.utils.Resource; import com.example.petstoremobile.utils.SpinnerUtils; +import com.example.petstoremobile.utils.UIUtils; import com.example.petstoremobile.viewmodels.ProductSupplierViewModel; import com.example.petstoremobile.viewmodels.ProductViewModel; import com.example.petstoremobile.viewmodels.SupplierViewModel; @@ -79,15 +77,7 @@ public class ProductSupplierFragment extends Fragment binding.fabAddPS.setOnClickListener(v -> openDetail(-1)); - binding.btnHamburgerPS.setOnClickListener(v -> { - Fragment parent = getParentFragment(); - if (parent != null) { - Fragment grandParent = parent.getParentFragment(); - if (grandParent instanceof ListFragment) { - ((ListFragment) grandParent).openDrawer(); - } - } - }); + UIUtils.setupHamburgerMenu(binding.btnHamburgerPS, this); return binding.getRoot(); } @@ -125,20 +115,8 @@ public class ProductSupplierFragment extends Fragment * Sets up the filter toggle button to show/hide the filter layout. */ private void setupFilterToggle() { - binding.btnToggleFilter.setOnClickListener(v -> { - if (binding.layoutFilter.getVisibility() == View.GONE) { - binding.layoutFilter.setVisibility(View.VISIBLE); - binding.btnToggleFilter.setImageResource(android.R.drawable.ic_menu_close_clear_cancel); - } else { - binding.layoutFilter.setVisibility(View.GONE); - binding.btnToggleFilter.setImageResource(android.R.drawable.ic_menu_search); - - // Reset filters when closing - binding.etSearchPS.setText(""); - binding.spinnerProduct.setSelection(0); - binding.spinnerSupplier.setSelection(0); - } - }); + UIUtils.setupFilterToggle(binding.btnToggleFilter, binding.layoutFilter, binding.etSearchPS, + binding.spinnerProduct, binding.spinnerSupplier); } /** @@ -154,13 +132,7 @@ public class ProductSupplierFragment extends Fragment * Configures the search bar for filtering. */ private void setupSearch() { - binding.etSearchPS.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) { - loadData(); - } - @Override public void afterTextChanged(Editable s) {} - }); + UIUtils.attachSearch(binding.etSearchPS, this::loadData); } /** 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 index 9a758cb8..b27c9c1f 100644 --- 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 @@ -1,8 +1,6 @@ package com.example.petstoremobile.fragments.listfragments; import android.os.Bundle; -import android.text.Editable; -import android.text.TextWatcher; import android.util.Log; import android.view.LayoutInflater; import android.view.View; @@ -21,9 +19,9 @@ import com.example.petstoremobile.adapters.PurchaseOrderAdapter; import com.example.petstoremobile.databinding.FragmentPurchaseOrderBinding; import com.example.petstoremobile.dtos.PurchaseOrderDTO; import com.example.petstoremobile.dtos.StoreDTO; -import com.example.petstoremobile.fragments.ListFragment; import com.example.petstoremobile.utils.Resource; import com.example.petstoremobile.utils.SpinnerUtils; +import com.example.petstoremobile.utils.UIUtils; import com.example.petstoremobile.viewmodels.PurchaseOrderViewModel; import com.example.petstoremobile.viewmodels.StoreViewModel; @@ -67,15 +65,7 @@ public class PurchaseOrderFragment extends Fragment setupSwipeRefresh(); setupFilterToggle(); - binding.btnHamburgerPO.setOnClickListener(v -> { - Fragment parent = getParentFragment(); - if (parent != null) { - Fragment grandParent = parent.getParentFragment(); - if (grandParent instanceof ListFragment) { - ((ListFragment) grandParent).openDrawer(); - } - } - }); + UIUtils.setupHamburgerMenu(binding.btnHamburgerPO, this); return binding.getRoot(); } @@ -100,32 +90,14 @@ public class PurchaseOrderFragment extends Fragment * Sets up the filter toggle button to show/hide the filter layout. */ private void setupFilterToggle() { - binding.btnToggleFilter.setOnClickListener(v -> { - if (binding.layoutFilter.getVisibility() == View.GONE) { - binding.layoutFilter.setVisibility(View.VISIBLE); - binding.btnToggleFilter.setImageResource(android.R.drawable.ic_menu_close_clear_cancel); - } else { - binding.layoutFilter.setVisibility(View.GONE); - binding.btnToggleFilter.setImageResource(android.R.drawable.ic_menu_search); - - // Reset filters when closing - binding.etSearchPO.setText(""); - binding.spinnerStore.setSelection(0); - } - }); + UIUtils.setupFilterToggle(binding.btnToggleFilter, binding.layoutFilter, binding.etSearchPO, binding.spinnerStore); } /** * Configures the search bar for filtering. */ private void setupSearch() { - binding.etSearchPO.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) { - loadData(); - } - @Override public void afterTextChanged(Editable s) {} - }); + UIUtils.attachSearch(binding.etSearchPO, this::loadData); } /** 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 index dc882fff..850717e4 100644 --- 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 @@ -7,19 +7,23 @@ import androidx.fragment.app.Fragment; import androidx.lifecycle.ViewModelProvider; import androidx.navigation.fragment.NavHostFragment; import androidx.recyclerview.widget.LinearLayoutManager; -import android.text.Editable; -import android.text.TextWatcher; +import androidx.recyclerview.widget.RecyclerView; +import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import android.widget.Toast; import com.example.petstoremobile.R; import com.example.petstoremobile.adapters.SaleAdapter; import com.example.petstoremobile.databinding.FragmentSaleBinding; import com.example.petstoremobile.dtos.SaleDTO; -import com.example.petstoremobile.fragments.ListFragment; +import com.example.petstoremobile.dtos.StoreDTO; import com.example.petstoremobile.utils.Resource; +import com.example.petstoremobile.utils.SpinnerUtils; +import com.example.petstoremobile.utils.UIUtils; import com.example.petstoremobile.viewmodels.SaleViewModel; +import com.example.petstoremobile.viewmodels.StoreViewModel; import java.util.ArrayList; import java.util.List; @@ -29,11 +33,20 @@ import dagger.hilt.android.AndroidEntryPoint; @AndroidEntryPoint public class SaleFragment extends Fragment implements SaleAdapter.OnSaleClickListener { + private static final String TAG = "SaleFragment"; + private static final int PAGE_SIZE = 200; + private FragmentSaleBinding binding; - private List saleList = new ArrayList<>(); - private List filteredList = new ArrayList<>(); + private final List saleList = new ArrayList<>(); + private final List storeList = new ArrayList<>(); private SaleAdapter adapter; private SaleViewModel saleViewModel; + private StoreViewModel storeViewModel; + + // Pagination + private int currentPage = 0; + private boolean isLastPage = false; + private boolean isLoading = false; @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, @@ -46,21 +59,17 @@ public class SaleFragment extends Fragment implements SaleAdapter.OnSaleClickLis public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); saleViewModel = new ViewModelProvider(this).get(SaleViewModel.class); + storeViewModel = new ViewModelProvider(this).get(StoreViewModel.class); setupRecyclerView(); setupSearch(); + setupStoreFilter(); + setupPaymentMethodFilter(); setupSwipeRefresh(); - loadSales(); + setupFilterToggle(); + loadSales(true); - binding.btnHamburger.setOnClickListener(v -> { - Fragment parent = getParentFragment(); - if (parent != null) { - Fragment grandParent = parent.getParentFragment(); - if (grandParent instanceof ListFragment) { - ((ListFragment) grandParent).openDrawer(); - } - } - }); + UIUtils.setupHamburgerMenu(binding.btnHamburger, this); binding.fabAddSale.setOnClickListener(v -> NavHostFragment.findNavController(this).navigate(R.id.nav_sale_detail)); @@ -69,6 +78,37 @@ public class SaleFragment extends Fragment implements SaleAdapter.OnSaleClickLis NavHostFragment.findNavController(this).navigate(R.id.nav_refund)); } + @Override + public void onResume() { + super.onResume(); + loadStoreData(); + } + + private void setupFilterToggle() { + UIUtils.setupFilterToggle(binding.btnToggleFilter, binding.layoutFilter, binding.etSearchSale, + binding.spinnerPaymentMethod, binding.spinnerStore); + } + + private void setupStoreFilter() { + SpinnerUtils.setupFilterSpinner(binding.spinnerStore, () -> loadSales(true)); + } + + private void loadStoreData() { + storeViewModel.getAllStores(0, 100).observe(getViewLifecycleOwner(), resource -> { + if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) { + storeList.clear(); + storeList.addAll(resource.data.getContent()); + SpinnerUtils.populateWhiteSpinner(requireContext(), binding.spinnerStore, storeList, + StoreDTO::getStoreName, "Stores", null, StoreDTO::getStoreId); + } + }); + } + + private void setupPaymentMethodFilter() { + String[] paymentMethods = {"Payments", "Cash", "Card"}; + SpinnerUtils.setupStringFilterSpinner(requireContext(), binding.spinnerPaymentMethod, paymentMethods, () -> loadSales(true)); + } + @Override public void onDestroyView() { super.onDestroyView(); @@ -76,61 +116,91 @@ public class SaleFragment extends Fragment implements SaleAdapter.OnSaleClickLis } private void setupRecyclerView() { - adapter = new SaleAdapter(filteredList, this); + adapter = new SaleAdapter(saleList, this); binding.recyclerViewSales.setLayoutManager(new LinearLayoutManager(getContext())); binding.recyclerViewSales.setAdapter(adapter); + + binding.recyclerViewSales.addOnScrollListener(new RecyclerView.OnScrollListener() { + @Override + public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) { + if (dy <= 0) return; + LinearLayoutManager lm = (LinearLayoutManager) binding.recyclerViewSales.getLayoutManager(); + if (lm != null) { + int visible = lm.getChildCount(); + int total = lm.getItemCount(); + int firstVis = lm.findFirstVisibleItemPosition(); + if (!isLoading && !isLastPage && (visible + firstVis) >= total - 3) { + loadSales(false); + } + } + } + }); } private void setupSearch() { - binding.etSearchSale.addTextChangedListener(new TextWatcher() { - @Override public void beforeTextChanged(CharSequence s, int start, int count, int after) {} - @Override public void afterTextChanged(Editable s) {} - @Override - public void onTextChanged(CharSequence s, int start, int before, int count) { - filterSales(s.toString()); - } - }); - } - - private void filterSales(String query) { - filteredList.clear(); - if (query.isEmpty()) { - filteredList.addAll(saleList); - } else { - String lower = query.toLowerCase(); - for (SaleDTO s : saleList) { - if ((s.getEmployeeName() != null && s.getEmployeeName().toLowerCase().contains(lower)) - || (s.getSaleDate() != null && s.getSaleDate().toLowerCase().contains(lower)) - || (s.getPaymentMethod() != null && s.getPaymentMethod().toLowerCase().contains(lower)) - || (s.getSaleId() != null && String.valueOf(s.getSaleId()).contains(lower))) { - filteredList.add(s); - } - } - } - if (adapter != null) adapter.notifyDataSetChanged(); + UIUtils.attachSearch(binding.etSearchSale, () -> loadSales(true)); } private void setupSwipeRefresh() { - binding.swipeRefreshSale.setOnRefreshListener(() -> { - loadSales(); - }); + binding.swipeRefreshSale.setOnRefreshListener(() -> loadSales(true)); } - private void loadSales() { - saleViewModel.getAllSales(0, 200).observe(getViewLifecycleOwner(), resource -> { - binding.swipeRefreshSale.setRefreshing(false); - if (resource.status == Resource.Status.SUCCESS && resource.data != null) { - saleList.clear(); - saleList.addAll(resource.data.getContent()); - filterSales(binding.etSearchSale.getText() != null - ? binding.etSearchSale.getText().toString() : ""); + private void loadSales(boolean reset) { + if (isLoading) return; + + if (reset) { + currentPage = 0; + isLastPage = false; + } + + String query = binding.etSearchSale != null ? binding.etSearchSale.getText().toString().trim() : ""; + if (query.isEmpty()) query = null; + + String paymentMethod = null; + if (binding.spinnerPaymentMethod.getSelectedItemPosition() > 0) { + paymentMethod = (String) binding.spinnerPaymentMethod.getSelectedItem(); + } + + Long storeId = null; + if (binding.spinnerStore.getSelectedItemPosition() > 0 && !storeList.isEmpty()) { + storeId = storeList.get(binding.spinnerStore.getSelectedItemPosition() - 1).getStoreId(); + } + + saleViewModel.getAllSales(currentPage, PAGE_SIZE, query, paymentMethod, storeId, "saleDate,desc").observe(getViewLifecycleOwner(), resource -> { + if (resource == null) return; + + switch (resource.status) { + case LOADING: + isLoading = true; + binding.swipeRefreshSale.setRefreshing(true); + break; + case SUCCESS: + isLoading = false; + binding.swipeRefreshSale.setRefreshing(false); + if (resource.data != null) { + if (reset) saleList.clear(); + saleList.addAll(resource.data.getContent()); + adapter.notifyDataSetChanged(); + isLastPage = resource.data.isLast(); + if (!isLastPage) currentPage++; + } + break; + case ERROR: + isLoading = false; + binding.swipeRefreshSale.setRefreshing(false); + Log.e(TAG, "Error loading sales: " + resource.message); + if (getContext() != null) { + Toast.makeText(getContext(), "Failed to load sales: " + resource.message, Toast.LENGTH_SHORT).show(); + } + break; } }); } @Override public void onSaleClick(int position) { - SaleDTO sale = filteredList.get(position); + if (position < 0 || position >= saleList.size()) return; + SaleDTO sale = saleList.get(position); Bundle args = new Bundle(); if (sale.getSaleId() != null) { args.putLong("saleId", sale.getSaleId()); diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ServiceFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ServiceFragment.java index 0923e4b1..77136d4e 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ServiceFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ServiceFragment.java @@ -1,8 +1,6 @@ package com.example.petstoremobile.fragments.listfragments; import android.os.Bundle; -import android.text.Editable; -import android.text.TextWatcher; import android.util.Log; import android.view.LayoutInflater; import android.view.View; @@ -21,9 +19,8 @@ import com.example.petstoremobile.R; import com.example.petstoremobile.adapters.ServiceAdapter; import com.example.petstoremobile.databinding.FragmentServiceBinding; import com.example.petstoremobile.dtos.ServiceDTO; -import com.example.petstoremobile.fragments.ListFragment; import com.example.petstoremobile.utils.BulkDeleteHandler; -import com.example.petstoremobile.utils.Resource; +import com.example.petstoremobile.utils.UIUtils; import com.example.petstoremobile.viewmodels.ServiceViewModel; import java.util.ArrayList; @@ -77,15 +74,7 @@ public class ServiceFragment extends Fragment implements ServiceAdapter.OnServic binding.fabAddService.setOnClickListener(v -> openDetail(null)); - binding.btnHamburger.setOnClickListener(v -> { - Fragment parent = getParentFragment(); - if (parent != null) { - Fragment grandParent = parent.getParentFragment(); - if (grandParent instanceof ListFragment) { - ((ListFragment) grandParent).openDrawer(); - } - } - }); + UIUtils.setupHamburgerMenu(binding.btnHamburger, this); return binding.getRoot(); } @@ -113,31 +102,14 @@ public class ServiceFragment extends Fragment implements ServiceAdapter.OnServic * Sets up the filter toggle button to show/hide the filter layout. */ private void setupFilterToggle() { - binding.btnToggleFilter.setOnClickListener(v -> { - if (binding.layoutFilter.getVisibility() == View.GONE) { - binding.layoutFilter.setVisibility(View.VISIBLE); - binding.btnToggleFilter.setImageResource(android.R.drawable.ic_menu_close_clear_cancel); - } else { - binding.layoutFilter.setVisibility(View.GONE); - binding.btnToggleFilter.setImageResource(android.R.drawable.ic_menu_search); - - // Reset filters when closing - binding.etSearchService.setText(""); - } - }); + UIUtils.setupFilterToggle(binding.btnToggleFilter, binding.layoutFilter, binding.etSearchService); } /** * Sets up the search bar for filtering. */ private void setupSearch() { - binding.etSearchService.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) { - loadServices(true); - } - @Override public void afterTextChanged(Editable s) {} - }); + UIUtils.attachSearch(binding.etSearchService, () -> loadServices(true)); } /** diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/StaffFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/StaffFragment.java index 1c8fcf4e..383d702a 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/StaffFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/StaffFragment.java @@ -1,85 +1,63 @@ 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.annotation.NonNull; -import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; +import androidx.lifecycle.ViewModelProvider; import androidx.navigation.fragment.NavHostFragment; import androidx.recyclerview.widget.LinearLayoutManager; -import androidx.recyclerview.widget.RecyclerView; -import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; import com.example.petstoremobile.R; import com.example.petstoremobile.adapters.EmployeeAdapter; -import com.example.petstoremobile.api.RetrofitClient; +import com.example.petstoremobile.databinding.FragmentStaffBinding; import com.example.petstoremobile.dtos.EmployeeDTO; -import com.example.petstoremobile.dtos.PageResponse; -import com.example.petstoremobile.fragments.ListFragment; -import com.google.android.material.floatingactionbutton.FloatingActionButton; +import com.example.petstoremobile.utils.UIUtils; +import com.example.petstoremobile.viewmodels.EmployeeViewModel; +import dagger.hilt.android.AndroidEntryPoint; import java.util.*; -import retrofit2.*; +@AndroidEntryPoint public class StaffFragment extends Fragment implements EmployeeAdapter.OnEmployeeClickListener { + private FragmentStaffBinding binding; + private EmployeeViewModel employeeViewModel; private List employeeList = new ArrayList<>(); private List filteredList = new ArrayList<>(); private EmployeeAdapter adapter; - private SwipeRefreshLayout swipeRefresh; - private EditText etSearch; @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - View view = inflater.inflate(R.layout.fragment_staff, container, false); - - setupRecyclerView(view); - setupSearch(view); - setupSwipeRefresh(view); + binding = FragmentStaffBinding.inflate(inflater, container, false); + employeeViewModel = new ViewModelProvider(this).get(EmployeeViewModel.class); + + setupRecyclerView(); + setupSearch(); + setupSwipeRefresh(); loadStaff(); - FloatingActionButton fab = view.findViewById(R.id.fabAddStaff); - fab.setOnClickListener(v -> openDetail(-1)); + binding.fabAddStaff.setOnClickListener(v -> openDetail(-1)); - ImageButton hamburger = view.findViewById(R.id.btnHamburgerStaff); - hamburger.setOnClickListener(v -> openDrawer()); + UIUtils.setupHamburgerMenu(binding.btnHamburgerStaff, this); + UIUtils.setupFilterToggle(binding.btnToggleFilterStaff, binding.layoutFilterStaff, binding.etSearchStaff); - return view; + return binding.getRoot(); } - private void openDrawer() { - Fragment parent = getParentFragment(); - if (parent != null) { - Fragment grandParent = parent.getParentFragment(); - if (grandParent instanceof ListFragment) { - ((ListFragment) grandParent).openDrawer(); - } - } - } - - private void setupRecyclerView(View view) { - RecyclerView rv = view.findViewById(R.id.recyclerViewStaff); + private void setupRecyclerView() { adapter = new EmployeeAdapter(filteredList, this); - rv.setLayoutManager(new LinearLayoutManager(getContext())); - rv.setAdapter(adapter); + binding.recyclerViewStaff.setLayoutManager(new LinearLayoutManager(getContext())); + binding.recyclerViewStaff.setAdapter(adapter); } - private void setupSearch(View view) { - etSearch = view.findViewById(R.id.etSearchStaff); - 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 setupSearch() { + UIUtils.attachSearch(binding.etSearchStaff, () -> filter(binding.etSearchStaff.getText().toString())); } - private void setupSwipeRefresh(View view) { - swipeRefresh = view.findViewById(R.id.swipeRefreshStaff); - swipeRefresh.setOnRefreshListener(this::loadStaff); + private void setupSwipeRefresh() { + binding.swipeRefreshStaff.setOnRefreshListener(this::loadStaff); } private void filter(String query) { @@ -101,26 +79,31 @@ public class StaffFragment extends Fragment implements EmployeeAdapter.OnEmploye } private void loadStaff() { - if (swipeRefresh != null) swipeRefresh.setRefreshing(true); - RetrofitClient.getEmployeeApi(requireContext()).getAllEmployees(0, 100) - .enqueue(new Callback>() { - public void onResponse(Call> c, - Response> r) { - if (swipeRefresh != null) swipeRefresh.setRefreshing(false); - if (r.isSuccessful() && r.body() != null) { + binding.swipeRefreshStaff.setRefreshing(true); + employeeViewModel.getAllEmployees(0, 100).observe(getViewLifecycleOwner(), resource -> { + if (resource != null) { + switch (resource.status) { + case SUCCESS: + binding.swipeRefreshStaff.setRefreshing(false); + if (resource.data != null) { employeeList.clear(); - employeeList.addAll(r.body().getContent()); - filter(etSearch != null ? etSearch.getText().toString() : ""); - } else { - Toast.makeText(getContext(), "Failed to load staff", + employeeList.addAll(resource.data.getContent()); + filter(binding != null ? binding.etSearchStaff.getText().toString() : ""); + } + break; + case ERROR: + binding.swipeRefreshStaff.setRefreshing(false); + if (getContext() != null) { + Toast.makeText(getContext(), resource.message != null ? resource.message : "Failed to load staff", Toast.LENGTH_SHORT).show(); } - } - public void onFailure(Call> c, Throwable t) { - if (swipeRefresh != null) swipeRefresh.setRefreshing(false); - Log.e("StaffFragment", t.getMessage()); - } - }); + break; + case LOADING: + binding.swipeRefreshStaff.setRefreshing(true); + break; + } + } + }); } private void openDetail(int position) { @@ -144,4 +127,10 @@ public class StaffFragment extends Fragment implements EmployeeAdapter.OnEmploye public void onEmployeeClick(int position) { openDetail(position); } + + @Override + public void onDestroyView() { + super.onDestroyView(); + binding = null; + } } diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/SupplierFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/SupplierFragment.java index 6d6f28ac..eca755bb 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/SupplierFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/SupplierFragment.java @@ -9,8 +9,6 @@ import androidx.lifecycle.ViewModelProvider; import androidx.navigation.fragment.NavHostFragment; import androidx.recyclerview.widget.LinearLayoutManager; -import android.text.Editable; -import android.text.TextWatcher; import android.util.Log; import android.view.LayoutInflater; import android.view.View; @@ -21,9 +19,9 @@ import com.example.petstoremobile.R; import com.example.petstoremobile.adapters.SupplierAdapter; import com.example.petstoremobile.databinding.FragmentSupplierBinding; import com.example.petstoremobile.dtos.SupplierDTO; -import com.example.petstoremobile.fragments.ListFragment; import com.example.petstoremobile.utils.BulkDeleteHandler; import com.example.petstoremobile.utils.Resource; +import com.example.petstoremobile.utils.UIUtils; import com.example.petstoremobile.viewmodels.SupplierViewModel; import java.util.ArrayList; @@ -67,16 +65,7 @@ public class SupplierFragment extends Fragment implements SupplierAdapter.OnSupp //Add button to opens the add dialog binding.fabAddSupplier.setOnClickListener(v -> openSupplierDetails(-1)); - //Make the hamburger button open the drawer from listFragment - binding.btnHamburger.setOnClickListener(v -> { - Fragment parent = getParentFragment(); - if (parent != null) { - Fragment grandParent = parent.getParentFragment(); - if (grandParent instanceof ListFragment) { - ((ListFragment) grandParent).openDrawer(); - } - } - }); + UIUtils.setupHamburgerMenu(binding.btnHamburger, this); return binding.getRoot(); } @@ -104,31 +93,14 @@ public class SupplierFragment extends Fragment implements SupplierAdapter.OnSupp * Sets up the filter toggle button to show/hide the filter layout. */ private void setupFilterToggle() { - binding.btnToggleFilter.setOnClickListener(v -> { - if (binding.layoutFilter.getVisibility() == View.GONE) { - binding.layoutFilter.setVisibility(View.VISIBLE); - binding.btnToggleFilter.setImageResource(android.R.drawable.ic_menu_close_clear_cancel); - } else { - binding.layoutFilter.setVisibility(View.GONE); - binding.btnToggleFilter.setImageResource(android.R.drawable.ic_menu_search); - - // Reset search when closing - binding.etSearchSupplier.setText(""); - } - }); + UIUtils.setupFilterToggle(binding.btnToggleFilter, binding.layoutFilter, binding.etSearchSupplier); } /** * Configures the search bar for filtering. */ private void setupSearch() { - binding.etSearchSupplier.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) { - loadSupplierData(); - } - @Override public void afterTextChanged(Editable s) {} - }); + UIUtils.attachSearch(binding.etSearchSupplier, this::loadSupplierData); } /** 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 7757f156..1081462a 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 @@ -415,7 +415,7 @@ public class AppointmentDetailFragment extends Fragment { } else if (errorMessage.toLowerCase().contains("not available")) { showNoAvailabilityDialog(); } else { - Toast.makeText(getContext(), "Operation failed", Toast.LENGTH_SHORT).show(); + Toast.makeText(getContext(), errorMessage, Toast.LENGTH_SHORT).show(); } } else { Toast.makeText(getContext(), "Something went wrong", Toast.LENGTH_SHORT).show(); 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 62d8bf42..ea08d16d 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 @@ -220,6 +220,13 @@ public class PetDetailFragment extends Fragment { binding.tvPetId.setText("ID: " + petId); binding.tvPetId.setVisibility(View.VISIBLE); binding.btnDeletePet.setVisibility(View.VISIBLE); + + // Disable species and breed fields in edit mode + binding.etPetSpecies.setEnabled(false); + binding.etPetBreed.setEnabled(false); + binding.etPetSpecies.setAlpha(0.5f); + binding.etPetBreed.setAlpha(0.5f); + loadPetData(); } else { // Pet is being added @@ -229,6 +236,12 @@ public class PetDetailFragment extends Fragment { binding.tvPetId.setVisibility(View.GONE); binding.btnDeletePet.setVisibility(View.GONE); binding.btnSavePet.setText("Add"); + + // Enable species and breed fields in edit mode + binding.etPetSpecies.setEnabled(true); + binding.etPetBreed.setEnabled(true); + binding.etPetSpecies.setAlpha(1.0f); + binding.etPetBreed.setAlpha(1.0f); } } @@ -334,16 +347,20 @@ public class PetDetailFragment extends Fragment { if ("Available".equalsIgnoreCase(status)) { binding.spinnerCustomer.setSelection(0); binding.spinnerCustomer.setEnabled(false); + binding.spinnerCustomer.setAlpha(0.5f); } else { binding.spinnerCustomer.setEnabled(true); + binding.spinnerCustomer.setAlpha(1.0f); } //Disable the store spinner if the status is "Owned" if ("Owned".equalsIgnoreCase(status)) { binding.spinnerStore.setSelection(0); binding.spinnerStore.setEnabled(false); + binding.spinnerStore.setAlpha(0.5f); } else { binding.spinnerStore.setEnabled(true); + binding.spinnerStore.setAlpha(1.0f); } } 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 48a988d2..d2527d71 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 @@ -11,6 +11,7 @@ import androidx.lifecycle.ViewModelProvider; import androidx.navigation.fragment.NavHostFragment; import com.bumptech.glide.Glide; +import com.bumptech.glide.load.engine.DiskCacheStrategy; import com.example.petstoremobile.R; import com.example.petstoremobile.api.*; import com.example.petstoremobile.api.auth.TokenManager; @@ -73,7 +74,11 @@ public class ProductDetailFragment extends Fragment { @Override public void onImagePicked(Uri uri) { photoUri = uri; - Glide.with(ProductDetailFragment.this).load(uri).into(binding.ivProductImage); + Glide.with(ProductDetailFragment.this) + .load(uri) + .diskCacheStrategy(DiskCacheStrategy.NONE) + .skipMemoryCache(true) + .into(binding.ivProductImage); hasImage = true; isImageChanged = true; isImageRemoved = false; diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/RefundFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/RefundFragment.java index 7f2eec10..8d05252c 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/RefundFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/RefundFragment.java @@ -7,25 +7,22 @@ import android.view.*; import android.widget.*; import androidx.annotation.NonNull; import androidx.fragment.app.Fragment; +import androidx.lifecycle.ViewModelProvider; import androidx.navigation.fragment.NavHostFragment; import com.example.petstoremobile.R; -import com.example.petstoremobile.api.RetrofitClient; +import com.example.petstoremobile.databinding.FragmentRefundBinding; import com.example.petstoremobile.dtos.SaleDTO; -import com.example.petstoremobile.dtos.PageResponse; +import com.example.petstoremobile.viewmodels.SaleViewModel; +import dagger.hilt.android.AndroidEntryPoint; import java.math.BigDecimal; import java.math.RoundingMode; import java.util.*; -import retrofit2.*; +@AndroidEntryPoint public class RefundFragment extends Fragment { - private EditText etSaleId; - private Button btnLoadSale, btnProcessRefund, btnBack; - private TextView tvSaleInfo, tvRefundTotal; - private LinearLayout llOriginalItems, llRefundItems; - private LinearLayout cardOriginalItems, cardRefundItems, cardPayment; - private Spinner spinnerPayment; - + private FragmentRefundBinding binding; + private SaleViewModel saleViewModel; private SaleDTO currentSale; private List allSales = new ArrayList<>(); @@ -60,8 +57,9 @@ public class RefundFragment extends Fragment { @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - View view = inflater.inflate(R.layout.fragment_refund, container, false); - initViews(view); + binding = FragmentRefundBinding.inflate(inflater, container, false); + saleViewModel = new ViewModelProvider(this).get(SaleViewModel.class); + setupSpinner(); loadAllSales(); @@ -69,59 +67,47 @@ public class RefundFragment extends Fragment { Bundle args = getArguments(); if (args != null && args.containsKey("saleId")) { long saleId = args.getLong("saleId"); - etSaleId.setText(String.valueOf(saleId)); + binding.etRefundSaleId.setText(String.valueOf(saleId)); // Auto-load after sales are fetched } - btnLoadSale.setOnClickListener(v -> loadSale()); - btnProcessRefund.setOnClickListener(v -> processRefund()); - btnBack.setOnClickListener(v -> navigateBack()); + binding.btnLoadSale.setOnClickListener(v -> loadSale()); + binding.btnProcessRefund.setOnClickListener(v -> processRefund()); + binding.btnRefundBack.setOnClickListener(v -> navigateBack()); - return view; - } - - private void initViews(View v) { - etSaleId = v.findViewById(R.id.etRefundSaleId); - btnLoadSale = v.findViewById(R.id.btnLoadSale); - btnProcessRefund= v.findViewById(R.id.btnProcessRefund); - btnBack = v.findViewById(R.id.btnRefundBack); - tvSaleInfo = v.findViewById(R.id.tvSaleInfo); - tvRefundTotal = v.findViewById(R.id.tvRefundTotal); - llOriginalItems = v.findViewById(R.id.llOriginalItems); - llRefundItems = v.findViewById(R.id.llRefundItems); - cardOriginalItems = v.findViewById(R.id.cardOriginalItems); - cardRefundItems = v.findViewById(R.id.cardRefundItems); - cardPayment = v.findViewById(R.id.cardPayment); - spinnerPayment = v.findViewById(R.id.spinnerRefundPayment); + return binding.getRoot(); } private void setupSpinner() { - spinnerPayment.setAdapter(new ArrayAdapter<>(requireContext(), + binding.spinnerRefundPayment.setAdapter(new ArrayAdapter<>(requireContext(), android.R.layout.simple_spinner_item, PAYMENT_METHODS)); } private void loadAllSales() { - RetrofitClient.getSaleApi(requireContext()).getAllSales(0, 1000) - .enqueue(new Callback>() { - public void onResponse(Call> c, - Response> r) { - if (r.isSuccessful() && r.body() != null) { - allSales = r.body().getContent(); - // Auto-load if saleId was pre-filled - Bundle args = getArguments(); - if (args != null && args.containsKey("saleId")) { - loadSale(); - } + saleViewModel.getAllSales(0, 1000, null, null, null, "saleDate,desc") + .observe(getViewLifecycleOwner(), resource -> { + if (resource != null) { + switch (resource.status) { + case SUCCESS: + if (resource.data != null) { + allSales = resource.data.getContent(); + // Auto-load if saleId was pre-filled + Bundle args = getArguments(); + if (args != null && args.containsKey("saleId")) { + loadSale(); + } + } + break; + case ERROR: + Log.e("Refund", "Failed to load sales: " + resource.message); + break; } } - public void onFailure(Call> c, Throwable t) { - Log.e("Refund", "Failed to load sales: " + t.getMessage()); - } }); } private void loadSale() { - String idStr = etSaleId.getText().toString().trim(); + String idStr = binding.etRefundSaleId.getText().toString().trim(); if (idStr.isEmpty()) { Toast.makeText(getContext(), "Enter a Sale ID", Toast.LENGTH_SHORT).show(); return; @@ -156,8 +142,8 @@ public class RefundFragment extends Fragment { currentSale = found; // Show sale info - tvSaleInfo.setVisibility(View.VISIBLE); - tvSaleInfo.setText("Sale #" + currentSale.getSaleId() + binding.tvSaleInfo.setVisibility(View.VISIBLE); + binding.tvSaleInfo.setText("Sale #" + currentSale.getSaleId() + " | " + (currentSale.getSaleDate() != null ? currentSale.getSaleDate().substring(0, 10) : "") + " | Employee: " + (currentSale.getEmployeeName() != null @@ -169,7 +155,7 @@ public class RefundFragment extends Fragment { if (currentSale.getPaymentMethod() != null) { for (int i = 0; i < PAYMENT_METHODS.length; i++) { if (PAYMENT_METHODS[i].equalsIgnoreCase(currentSale.getPaymentMethod())) { - spinnerPayment.setSelection(i); break; + binding.spinnerRefundPayment.setSelection(i); break; } } } @@ -187,10 +173,10 @@ public class RefundFragment extends Fragment { refundCart.clear(); // Show cards - cardOriginalItems.setVisibility(View.VISIBLE); - cardRefundItems.setVisibility(View.VISIBLE); - cardPayment.setVisibility(View.VISIBLE); - btnProcessRefund.setVisibility(View.VISIBLE); + binding.cardOriginalItems.setVisibility(View.VISIBLE); + binding.cardRefundItems.setVisibility(View.VISIBLE); + binding.cardPayment.setVisibility(View.VISIBLE); + binding.btnProcessRefund.setVisibility(View.VISIBLE); renderOriginalItems(); renderRefundCart(); @@ -233,10 +219,10 @@ public class RefundFragment extends Fragment { } private void renderOriginalItems() { - llOriginalItems.removeAllViews(); + binding.llOriginalItems.removeAllViews(); // Header - addTableHeader(llOriginalItems); + addTableHeader(binding.llOriginalItems); for (RefundItem item : availableItems) { // Calculate pending in cart @@ -254,23 +240,23 @@ public class RefundFragment extends Fragment { true, // show add button () -> showQuantityDialog(item) ); - llOriginalItems.addView(row); + binding.llOriginalItems.addView(row); } } private void renderRefundCart() { - llRefundItems.removeAllViews(); + binding.llRefundItems.removeAllViews(); if (refundCart.isEmpty()) { TextView empty = new TextView(getContext()); empty.setText("No items added to refund yet"); empty.setTextColor(0xFF888780); empty.setTextSize(13f); - llRefundItems.addView(empty); + binding.llRefundItems.addView(empty); return; } - addTableHeader(llRefundItems); + addTableHeader(binding.llRefundItems); for (RefundItem item : refundCart) { LinearLayout row = buildItemRow( @@ -285,11 +271,12 @@ public class RefundFragment extends Fragment { updateRefundTotal(); } ); - llRefundItems.addView(row); + binding.llRefundItems.addView(row); } } private void addTableHeader(LinearLayout parent) { + if (getContext() == null) return; LinearLayout header = new LinearLayout(getContext()); header.setOrientation(LinearLayout.HORIZONTAL); header.setPadding(0, 0, 0, 8); @@ -310,6 +297,7 @@ public class RefundFragment extends Fragment { private LinearLayout buildItemRow(String name, int qty, BigDecimal unitPrice, boolean isAdd, Runnable action) { + if (getContext() == null) return new LinearLayout(getContext()); LinearLayout row = new LinearLayout(getContext()); row.setOrientation(LinearLayout.HORIZONTAL); row.setGravity(android.view.Gravity.CENTER_VERTICAL); @@ -428,7 +416,7 @@ public class RefundFragment extends Fragment { private void updateRefundTotal() { BigDecimal total = BigDecimal.ZERO; for (RefundItem item : refundCart) total = total.add(item.getTotal()); - tvRefundTotal.setText("Refund Total: $" + total.setScale(2, RoundingMode.HALF_UP)); + binding.tvRefundTotal.setText("Refund Total: $" + total.setScale(2, RoundingMode.HALF_UP)); } private void processRefund() { @@ -442,7 +430,7 @@ public class RefundFragment extends Fragment { return; } - String payment = PAYMENT_METHODS[spinnerPayment.getSelectedItemPosition()]; + String payment = PAYMENT_METHODS[binding.spinnerRefundPayment.getSelectedItemPosition()]; // Confirm dialog BigDecimal total = BigDecimal.ZERO; @@ -478,33 +466,34 @@ public class RefundFragment extends Fragment { Log.d("REFUND", "Submitting refund for saleId=" + currentSale.getSaleId() + " items=" + items.size()); - RetrofitClient.getSaleApi(requireContext()).createSale(dto) - .enqueue(new Callback() { - public void onResponse(Call c, Response r) { - if (r.isSuccessful() && r.body() != null) { + saleViewModel.createSale(dto).observe(getViewLifecycleOwner(), resource -> { + if (resource != null) { + switch (resource.status) { + case SUCCESS: + if (resource.data != null) { Toast.makeText(getContext(), - "Refund #" + r.body().getSaleId() + " processed successfully!", + "Refund #" + resource.data.getSaleId() + " processed successfully!", Toast.LENGTH_LONG).show(); navigateBack(); - } else { - try { - String err = r.errorBody().string(); - Log.e("REFUND", "Error: " + err); - Toast.makeText(getContext(), "Error: " + err, - Toast.LENGTH_LONG).show(); - } catch (Exception e) { - Log.e("REFUND", "Failed to read error"); - } } - } - public void onFailure(Call c, Throwable t) { - Log.e("REFUND", "Failure: " + t.getMessage()); - Toast.makeText(getContext(), "Network error", Toast.LENGTH_SHORT).show(); - } - }); + break; + case ERROR: + Log.e("REFUND", "Error: " + resource.message); + Toast.makeText(getContext(), "Error: " + resource.message, + Toast.LENGTH_LONG).show(); + break; + } + } + }); } private void navigateBack() { NavHostFragment.findNavController(this).popBackStack(); } + + @Override + public void onDestroyView() { + super.onDestroyView(); + binding = null; + } } diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/SaleDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/SaleDetailFragment.java index d5e36be2..40c7896b 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/SaleDetailFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/SaleDetailFragment.java @@ -5,23 +5,28 @@ 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 androidx.lifecycle.ViewModelProvider; import androidx.navigation.fragment.NavHostFragment; import com.example.petstoremobile.R; -import com.example.petstoremobile.api.*; +import com.example.petstoremobile.databinding.FragmentSaleDetailBinding; import com.example.petstoremobile.dtos.*; +import com.example.petstoremobile.viewmodels.*; +import com.example.petstoremobile.utils.SpinnerUtils; +import com.example.petstoremobile.utils.DialogUtils; +import com.example.petstoremobile.utils.Resource; +import dagger.hilt.android.AndroidEntryPoint; import java.math.BigDecimal; import java.util.*; -import retrofit2.*; +@AndroidEntryPoint public class SaleDetailFragment extends Fragment { - private TextView tvMode, tvSaleDetailId, tvTotal; - private Spinner spinnerStore, spinnerCustomer, spinnerPayment, spinnerProduct; - private EditText etQuantity; - private Button btnAddItem, btnSave, btnBack, btnRefund; - private LinearLayout llItems; + private FragmentSaleDetailBinding binding; + private SaleViewModel saleViewModel; + private StoreViewModel storeViewModel; + private CustomerViewModel customerViewModel; + private ProductViewModel productViewModel; private boolean viewOnly = false; private long saleId = -1; @@ -31,13 +36,20 @@ public class SaleDetailFragment extends Fragment { private List productList = new ArrayList<>(); private List cartItems = new ArrayList<>(); - private final String[] PAYMENT_METHODS = { "Cash", "Card" }; + private final String[] PAYMENT_METHODS = { "Cash", "Card"}; @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - View view = inflater.inflate(R.layout.fragment_sale_detail, container, false); - initViews(view); + binding = FragmentSaleDetailBinding.inflate(inflater, container, false); + + saleViewModel = new ViewModelProvider(this).get(SaleViewModel.class); + storeViewModel = new ViewModelProvider(this).get(StoreViewModel.class); + customerViewModel = new ViewModelProvider(this).get(CustomerViewModel.class); + productViewModel = new ViewModelProvider(this).get(ProductViewModel.class); + + SpinnerUtils.setupStringSpinner(requireContext(), binding.spinnerPaymentMethod, PAYMENT_METHODS); + handleArguments(); if (!viewOnly) { @@ -45,30 +57,11 @@ public class SaleDetailFragment extends Fragment { setupAddItem(); } - btnBack.setOnClickListener(v -> navigateBack()); - btnSave.setOnClickListener(v -> saveSale()); - btnRefund.setOnClickListener(v -> showRefundDialog()); + binding.btnSaleBack.setOnClickListener(v -> navigateBack()); + binding.btnSaveSale.setOnClickListener(v -> saveSale()); + binding.btnRefundSale.setOnClickListener(v -> showRefundDialog()); - return view; - } - - private void initViews(View v) { - tvMode = v.findViewById(R.id.tvSaleMode); - tvSaleDetailId = v.findViewById(R.id.tvSaleDetailId); - tvTotal = v.findViewById(R.id.tvSaleDetailTotal); - spinnerStore = v.findViewById(R.id.spinnerSaleStore); - spinnerCustomer = v.findViewById(R.id.spinnerSaleCustomer); - spinnerPayment = v.findViewById(R.id.spinnerPaymentMethod); - spinnerProduct = v.findViewById(R.id.spinnerSaleProduct); - etQuantity = v.findViewById(R.id.etSaleQuantity); - btnAddItem = v.findViewById(R.id.btnAddItem); - btnSave = v.findViewById(R.id.btnSaveSale); - btnBack = v.findViewById(R.id.btnSaleBack); - btnRefund = v.findViewById(R.id.btnRefundSale); - llItems = v.findViewById(R.id.llSaleItems); - - spinnerPayment.setAdapter(new ArrayAdapter<>(requireContext(), - android.R.layout.simple_spinner_item, PAYMENT_METHODS)); + return binding.getRoot(); } private void handleArguments() { @@ -76,31 +69,31 @@ public class SaleDetailFragment extends Fragment { if (a != null && a.containsKey("saleId")) { saleId = a.getLong("saleId"); viewOnly = a.getBoolean("viewOnly", false); - tvMode.setText("Sale #" + saleId); - tvSaleDetailId.setText("ID: " + saleId); + binding.tvSaleMode.setText("Sale #" + saleId); + binding.tvSaleDetailId.setText("ID: " + saleId); // Show refund button for existing non-refund sales if (!a.getBoolean("isRefund", false)) { - btnRefund.setVisibility(View.VISIBLE); + binding.btnRefundSale.setVisibility(View.VISIBLE); } // Hide save and input controls for view only if (viewOnly) { - btnSave.setVisibility(View.GONE); - spinnerStore.setEnabled(false); - spinnerCustomer.setEnabled(false); - spinnerPayment.setEnabled(false); - spinnerProduct.setEnabled(false); - etQuantity.setEnabled(false); - btnAddItem.setEnabled(false); + binding.btnSaveSale.setVisibility(View.GONE); + binding.spinnerSaleStore.setEnabled(false); + binding.spinnerSaleCustomer.setEnabled(false); + binding.spinnerPaymentMethod.setEnabled(false); + binding.llAddItemRow.setVisibility(View.GONE); + binding.llExtraInfo.setVisibility(View.VISIBLE); } // Load sale details loadSaleDetails(); } else { - tvMode.setText("New Sale"); - tvSaleDetailId.setVisibility(View.GONE); - btnRefund.setVisibility(View.GONE); + binding.tvSaleMode.setText("New Sale"); + binding.tvSaleDetailId.setVisibility(View.GONE); + binding.btnRefundSale.setVisibility(View.GONE); + binding.llExtraInfo.setVisibility(View.GONE); } } @@ -111,105 +104,108 @@ public class SaleDetailFragment extends Fragment { } private void loadStores() { - // Hardcoded since store endpoint is admin only - storeList = new ArrayList<>(); - storeList.add(new StoreDTO(1L, "Downtown Branch")); - List names = new ArrayList<>(); - names.add("-- Select Store --"); - names.add("Downtown Branch"); - spinnerStore.setAdapter(new ArrayAdapter<>(requireContext(), - android.R.layout.simple_spinner_item, names)); + storeViewModel.getAllStores(0, 50).observe(getViewLifecycleOwner(), resource -> { + if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) { + storeList = resource.data.getContent(); + if (binding != null) { + SpinnerUtils.populateSpinner(requireContext(), binding.spinnerSaleStore, storeList, + StoreDTO::getStoreName, "-- Select Store --", -1L, StoreDTO::getStoreId); + } + } else if (storeList.isEmpty()) { + storeList = Collections.singletonList(new StoreDTO(1L, "Downtown Branch")); + if (binding != null) { + SpinnerUtils.populateSpinner(requireContext(), binding.spinnerSaleStore, storeList, + StoreDTO::getStoreName, "-- Select Store --", -1L, StoreDTO::getStoreId); + } + } + }); } 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(); - List names = new ArrayList<>(); - names.add("-- No Customer --"); - for (CustomerDTO cu : customerList) - names.add(cu.getFirstName() + " " + cu.getLastName()); - spinnerCustomer.setAdapter(new ArrayAdapter<>(requireContext(), - android.R.layout.simple_spinner_item, names)); - } - } - - public void onFailure(Call> c, Throwable t) { - Log.e("SaleDetail", "Customer load failed: " + t.getMessage()); - } - }); + customerViewModel.getAllCustomers(0, 200).observe(getViewLifecycleOwner(), resource -> { + if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) { + customerList = resource.data.getContent(); + if (binding != null) { + SpinnerUtils.populateSpinner(requireContext(), binding.spinnerSaleCustomer, customerList, + CustomerDTO::getFullName, "-- No Customer --", -1L, CustomerDTO::getCustomerId); + } + } + }); } private void loadProducts() { - RetrofitClient.getProductApi(requireContext()).getAllProducts(null, null, 0, 200, null) - .enqueue(new Callback>() { - public void onResponse(Call> c, - Response> r) { - if (r.isSuccessful() && r.body() != null) { - productList = r.body().getContent(); - List names = new ArrayList<>(); - names.add("-- Select Product --"); - for (ProductDTO p : productList) - names.add(p.getProdName()); - spinnerProduct.setAdapter(new ArrayAdapter<>(requireContext(), - android.R.layout.simple_spinner_item, names)); - } - } - - public void onFailure(Call> c, Throwable t) { - Log.e("SaleDetail", "Product load failed: " + t.getMessage()); - } - }); + productViewModel.getAllProducts(null, null, 0, 200, null).observe(getViewLifecycleOwner(), resource -> { + if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) { + productList = resource.data.getContent(); + if (binding != null) { + SpinnerUtils.populateSpinner(requireContext(), binding.spinnerSaleProduct, productList, + ProductDTO::getProdName, "Select Product", -1L, ProductDTO::getProdId); + } + } + }); } private void loadSaleDetails() { - RetrofitClient.getSaleApi(requireContext()).getSaleById(saleId) - .enqueue(new Callback() { - public void onResponse(Call c, Response r) { - if (r.isSuccessful() && r.body() != null) { - SaleDTO sale = r.body(); - tvTotal.setText("Total: $" + sale.getTotalAmount()); - // Display items - if (sale.getItems() != null) { - llItems.removeAllViews(); - for (SaleDTO.SaleItemDTO item : sale.getItems()) { - addItemRow(item.getProductName(), - Math.abs(item.getQuantity()), - item.getUnitPrice()); - } - } - } + saleViewModel.getSaleById(saleId).observe(getViewLifecycleOwner(), resource -> { + if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) { + SaleDTO sale = resource.data; + if (binding != null) { + binding.tvSaleDetailTotal.setText("Total: $" + sale.getTotalAmount()); + binding.tvSaleSubtotal.setText("$" + (sale.getSubtotalAmount() != null ? sale.getSubtotalAmount() : sale.getTotalAmount())); + + if (sale.getCouponDiscountAmount() != null && sale.getCouponDiscountAmount().compareTo(BigDecimal.ZERO) > 0) { + binding.llCouponDiscount.setVisibility(View.VISIBLE); + binding.tvSaleCouponDiscount.setText("-$" + sale.getCouponDiscountAmount()); + } else { + binding.llCouponDiscount.setVisibility(View.GONE); } - public void onFailure(Call c, Throwable t) { - Log.e("SaleDetail", "Load failed: " + t.getMessage()); + if (sale.getEmployeeDiscountAmount() != null && sale.getEmployeeDiscountAmount().compareTo(BigDecimal.ZERO) > 0) { + binding.llEmployeeDiscount.setVisibility(View.VISIBLE); + binding.tvSaleEmployeeDiscount.setText("-$" + sale.getEmployeeDiscountAmount()); + } else { + binding.llEmployeeDiscount.setVisibility(View.GONE); } - }); + + binding.tvSaleChannel.setText(sale.getChannel() != null ? sale.getChannel() : "—"); + binding.tvSalePoints.setText(String.valueOf(sale.getPointsEarned() != null ? sale.getPointsEarned() : 0)); + + SpinnerUtils.setSelectionByValue(binding.spinnerPaymentMethod, sale.getPaymentMethod()); + + // Display items + if (sale.getItems() != null) { + binding.llSaleItems.removeAllViews(); + for (SaleDTO.SaleItemDTO item : sale.getItems()) { + addItemRow(item.getProductName(), + Math.abs(item.getQuantity()), + item.getUnitPrice()); + } + } + } + } + }); } private void setupAddItem() { - btnAddItem.setOnClickListener(v -> { - if (spinnerProduct.getSelectedItemPosition() == 0) { + binding.btnAddItem.setOnClickListener(v -> { + if (binding.spinnerSaleProduct.getSelectedItemPosition() == 0) { Toast.makeText(getContext(), "Select a product", Toast.LENGTH_SHORT).show(); return; } - String qtyStr = etQuantity.getText().toString().trim(); + String qtyStr = binding.etSaleQuantity.getText().toString().trim(); if (qtyStr.isEmpty()) { - etQuantity.setError("Enter quantity"); + binding.etSaleQuantity.setError("Enter quantity"); return; } int qty; try { qty = Integer.parseInt(qtyStr); } catch (Exception e) { - etQuantity.setError("Invalid quantity"); + binding.etSaleQuantity.setError("Invalid quantity"); return; } - ProductDTO product = productList.get(spinnerProduct.getSelectedItemPosition() - 1); + ProductDTO product = productList.get(binding.spinnerSaleProduct.getSelectedItemPosition() - 1); // Check if product already in cart for (SaleDTO.SaleItemDTO existing : cartItems) { @@ -223,11 +219,12 @@ public class SaleDetailFragment extends Fragment { cartItems.add(item); addItemRow(product.getProdName(), qty, product.getProdPrice()); updateTotal(); - etQuantity.setText(""); + binding.etSaleQuantity.setText(""); }); } private void addItemRow(String name, int qty, BigDecimal price) { + if (getContext() == null) return; LinearLayout row = new LinearLayout(getContext()); row.setOrientation(LinearLayout.HORIZONTAL); row.setPadding(0, 8, 0, 8); @@ -250,28 +247,26 @@ public class SaleDetailFragment extends Fragment { row.addView(tvName); row.addView(tvQty); row.addView(tvPrice); - llItems.addView(row); + binding.llSaleItems.addView(row); } private void updateTotal() { BigDecimal total = BigDecimal.ZERO; - int productIdx = 0; for (SaleDTO.SaleItemDTO item : cartItems) { - if (productIdx < productList.size()) { - for (ProductDTO p : productList) { - if (p.getProdId().equals(item.getProdId()) && p.getProdPrice() != null) { - total = total.add(p.getProdPrice() - .multiply(BigDecimal.valueOf(item.getQuantity()))); - break; - } + for (ProductDTO p : productList) { + if (p.getProdId().equals(item.getProdId()) && p.getProdPrice() != null) { + total = total.add(p.getProdPrice() + .multiply(BigDecimal.valueOf(item.getQuantity()))); + break; } } } - tvTotal.setText("Total: $" + total); + binding.tvSaleSubtotal.setText("$" + total); + binding.tvSaleDetailTotal.setText("Total: $" + total); } private void saveSale() { - if (spinnerStore.getSelectedItemPosition() == 0) { + if (binding.spinnerSaleStore.getSelectedItemPosition() == 0) { Toast.makeText(getContext(), "Select a store", Toast.LENGTH_SHORT).show(); return; } @@ -280,13 +275,13 @@ public class SaleDetailFragment extends Fragment { return; } - StoreDTO store = storeList.get(0); // only one store - String payment = PAYMENT_METHODS[spinnerPayment.getSelectedItemPosition()]; + StoreDTO store = storeList.get(binding.spinnerSaleStore.getSelectedItemPosition() - 1); + String payment = PAYMENT_METHODS[binding.spinnerPaymentMethod.getSelectedItemPosition()]; // Optional customer Long customerId = null; - if (spinnerCustomer.getSelectedItemPosition() > 0) { - customerId = customerList.get(spinnerCustomer.getSelectedItemPosition() - 1) + if (binding.spinnerSaleCustomer.getSelectedItemPosition() > 0) { + customerId = customerList.get(binding.spinnerSaleCustomer.getSelectedItemPosition() - 1) .getCustomerId(); } @@ -298,71 +293,38 @@ public class SaleDetailFragment extends Fragment { null, customerId); - Log.d("SALE_SAVE", "storeId=" + store.getStoreId() - + " payment=" + payment - + " items=" + cartItems.size() - + " customerId=" + customerId); - - RetrofitClient.getSaleApi(requireContext()).createSale(dto) - .enqueue(new Callback() { - public void onResponse(Call c, Response r) { - if (r.isSuccessful()) { - Toast.makeText(getContext(), "Sale saved!", Toast.LENGTH_SHORT).show(); - navigateBack(); - } else { - try { - String err = r.errorBody().string(); - Log.e("SALE_SAVE", "Error: " + err); - Toast.makeText(getContext(), "Error " + r.code() + ": " + err, - Toast.LENGTH_LONG).show(); - } catch (Exception e) { - Log.e("SALE_SAVE", "Failed to read error"); - } - } - } - - public void onFailure(Call c, Throwable t) { - Log.e("SALE_SAVE", "Failure: " + t.getMessage()); - Toast.makeText(getContext(), "Network error", Toast.LENGTH_SHORT).show(); - } - }); + saleViewModel.createSale(dto).observe(getViewLifecycleOwner(), resource -> { + if (resource != null) { + switch (resource.status) { + case SUCCESS: + Toast.makeText(getContext(), "Sale saved!", Toast.LENGTH_SHORT).show(); + navigateBack(); + break; + case ERROR: + Log.e("SALE_SAVE", "Error: " + resource.message); + DialogUtils.showInfoDialog(requireContext(), "Save Error", resource.message); + break; + } + } + }); } private void showRefundDialog() { - Bundle args = new Bundle(); - args.putLong("saleId", saleId); - NavHostFragment.findNavController(this).navigate(R.id.nav_refund, args); - } - - private void submitRefund() { - RefundDTO refundDTO = new RefundDTO(saleId, "Refund requested from mobile app"); - RetrofitClient.getRefundApi(requireContext()).createRefund(refundDTO) - .enqueue(new Callback() { - public void onResponse(Call c, Response r) { - if (r.isSuccessful()) { - Toast.makeText(getContext(), "Refund request submitted!", - Toast.LENGTH_SHORT).show(); - btnRefund.setVisibility(View.GONE); - } else { - try { - String err = r.errorBody().string(); - Log.e("REFUND", "Error: " + err); - Toast.makeText(getContext(), "Error: " + err, - Toast.LENGTH_LONG).show(); - } catch (Exception e) { - Log.e("REFUND", "Failed to read error"); - } - } - } - - public void onFailure(Call c, Throwable t) { - Log.e("REFUND", "Failure: " + t.getMessage()); - Toast.makeText(getContext(), "Network error", Toast.LENGTH_SHORT).show(); - } - }); + DialogUtils.showConfirmDialog(requireContext(), "Process Refund", + "Are you sure you want to process a refund for this sale?", () -> { + Bundle args = new Bundle(); + args.putLong("saleId", saleId); + NavHostFragment.findNavController(this).navigate(R.id.nav_refund, args); + }); } private void navigateBack() { NavHostFragment.findNavController(this).popBackStack(); } + + @Override + public void onDestroyView() { + super.onDestroyView(); + binding = null; + } } diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/StaffDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/StaffDetailFragment.java index 2750db72..508282bc 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/StaffDetailFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/StaffDetailFragment.java @@ -7,20 +7,19 @@ import android.widget.*; import androidx.annotation.NonNull; import androidx.appcompat.app.AlertDialog; import androidx.fragment.app.Fragment; +import androidx.lifecycle.ViewModelProvider; import androidx.navigation.fragment.NavHostFragment; import com.example.petstoremobile.R; -import com.example.petstoremobile.api.EmployeeApi; -import com.example.petstoremobile.api.RetrofitClient; +import com.example.petstoremobile.databinding.FragmentStaffDetailBinding; import com.example.petstoremobile.dtos.EmployeeDTO; -import retrofit2.*; +import com.example.petstoremobile.viewmodels.EmployeeViewModel; +import dagger.hilt.android.AndroidEntryPoint; +@AndroidEntryPoint public class StaffDetailFragment extends Fragment { - private TextView tvMode, tvStaffId; - private EditText etUsername, etPassword, etFirstName, etLastName, etEmail, etPhone; - private Spinner spinnerRole, spinnerStatus; - private Button btnSave, btnDelete, btnBack; - + private FragmentStaffDetailBinding binding; + private EmployeeViewModel employeeViewModel; private long employeeId = -1; private boolean isEditing = false; @@ -30,37 +29,22 @@ public class StaffDetailFragment extends Fragment { @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - View view = inflater.inflate(R.layout.fragment_staff_detail, container, false); - initViews(view); + binding = FragmentStaffDetailBinding.inflate(inflater, container, false); + employeeViewModel = new ViewModelProvider(this).get(EmployeeViewModel.class); + setupSpinners(); 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.tvStaffMode); - tvStaffId = v.findViewById(R.id.tvStaffId); - etUsername = v.findViewById(R.id.etStaffUsername); - etPassword = v.findViewById(R.id.etStaffPassword); - etFirstName = v.findViewById(R.id.etStaffFirstName); - etLastName = v.findViewById(R.id.etStaffLastName); - etEmail = v.findViewById(R.id.etStaffEmail); - etPhone = v.findViewById(R.id.etStaffPhone); - spinnerRole = v.findViewById(R.id.spinnerStaffRole); - spinnerStatus = v.findViewById(R.id.spinnerStaffStatus); - btnSave = v.findViewById(R.id.btnSaveStaff); - btnDelete = v.findViewById(R.id.btnDeleteStaff); - btnBack = v.findViewById(R.id.btnStaffBack); + binding.btnStaffBack.setOnClickListener(v -> navigateBack()); + binding.btnSaveStaff.setOnClickListener(v -> save()); + binding.btnDeleteStaff.setOnClickListener(v -> confirmDelete()); + return binding.getRoot(); } private void setupSpinners() { - spinnerRole.setAdapter(new ArrayAdapter<>(requireContext(), + binding.spinnerStaffRole.setAdapter(new ArrayAdapter<>(requireContext(), android.R.layout.simple_spinner_item, ROLES)); - spinnerStatus.setAdapter(new ArrayAdapter<>(requireContext(), + binding.spinnerStaffStatus.setAdapter(new ArrayAdapter<>(requireContext(), android.R.layout.simple_spinner_item, STATUSES)); } @@ -70,61 +54,61 @@ public class StaffDetailFragment extends Fragment { isEditing = true; employeeId = a.getLong("employeeId", -1); - tvMode.setText("Edit Staff Account"); - tvStaffId.setText("ID: " + employeeId); - tvStaffId.setVisibility(View.VISIBLE); - etUsername.setText(a.getString("username", "")); - etFirstName.setText(a.getString("firstName", "")); - etLastName.setText(a.getString("lastName", "")); - etEmail.setText(a.getString("email", "")); // ← was showing fullName - etPhone.setText(a.getString("phone", "")); - btnDelete.setVisibility(View.VISIBLE); + binding.tvStaffMode.setText("Edit Staff Account"); + binding.tvStaffId.setText("ID: " + employeeId); + binding.tvStaffId.setVisibility(View.VISIBLE); + binding.etStaffUsername.setText(a.getString("username", "")); + binding.etStaffFirstName.setText(a.getString("firstName", "")); + binding.etStaffLastName.setText(a.getString("lastName", "")); + binding.etStaffEmail.setText(a.getString("email", "")); + binding.etStaffPhone.setText(a.getString("phone", "")); + binding.btnDeleteStaff.setVisibility(View.VISIBLE); // Pre-fill role String role = a.getString("role", "STAFF"); for (int i = 0; i < ROLES.length; i++) { if (ROLES[i].equals(role)) { - spinnerRole.setSelection(i); + binding.spinnerStaffRole.setSelection(i); break; } } // Pre-fill status boolean active = a.getBoolean("active", true); - spinnerStatus.setSelection(active ? 0 : 1); + binding.spinnerStaffStatus.setSelection(active ? 0 : 1); } else { isEditing = false; employeeId = -1; - tvMode.setText("Add Staff Account"); - btnDelete.setVisibility(View.GONE); - tvStaffId.setVisibility(View.GONE); + binding.tvStaffMode.setText("Add Staff Account"); + binding.btnDeleteStaff.setVisibility(View.GONE); + binding.tvStaffId.setVisibility(View.GONE); } } private void save() { - String username = etUsername.getText() != null ? etUsername.getText().toString().trim() : ""; - String password = etPassword.getText() != null ? etPassword.getText().toString().trim() : ""; - String firstName = etFirstName.getText() != null ? etFirstName.getText().toString().trim() : ""; - String lastName = etLastName.getText() != null ? etLastName.getText().toString().trim() : ""; - String email = etEmail.getText() != null ? etEmail.getText().toString().trim() : ""; - String phone = etPhone.getText() != null ? etPhone.getText().toString().trim() : ""; - String role = ROLES[spinnerRole.getSelectedItemPosition()]; - boolean active = spinnerStatus.getSelectedItemPosition() == 0; + String username = binding.etStaffUsername.getText() != null ? binding.etStaffUsername.getText().toString().trim() : ""; + String password = binding.etStaffPassword.getText() != null ? binding.etStaffPassword.getText().toString().trim() : ""; + String firstName = binding.etStaffFirstName.getText() != null ? binding.etStaffFirstName.getText().toString().trim() : ""; + String lastName = binding.etStaffLastName.getText() != null ? binding.etStaffLastName.getText().toString().trim() : ""; + String email = binding.etStaffEmail.getText() != null ? binding.etStaffEmail.getText().toString().trim() : ""; + String phone = binding.etStaffPhone.getText() != null ? binding.etStaffPhone.getText().toString().trim() : ""; + String role = ROLES[binding.spinnerStaffRole.getSelectedItemPosition()]; + boolean active = binding.spinnerStaffStatus.getSelectedItemPosition() == 0; // Validation - if (username.isEmpty()) { etUsername.setError("Required"); return; } + if (username.isEmpty()) { binding.etStaffUsername.setError("Required"); return; } if (!isEditing && password.isEmpty()) { - etPassword.setError("Required for new account"); return; + binding.etStaffPassword.setError("Required for new account"); return; } if (!isEditing && password.length() < 6) { - etPassword.setError("At least 6 characters"); return; + binding.etStaffPassword.setError("At least 6 characters"); return; } - if (firstName.isEmpty()) { etFirstName.setError("Required"); return; } - if (lastName.isEmpty()) { etLastName.setError("Required"); return; } - if (email.isEmpty()) { etEmail.setError("Required"); return; } - if (phone.isEmpty()) { etPhone.setError("Required"); return; } + if (firstName.isEmpty()) { binding.etStaffFirstName.setError("Required"); return; } + if (lastName.isEmpty()) { binding.etStaffLastName.setError("Required"); return; } + if (email.isEmpty()) { binding.etStaffEmail.setError("Required"); return; } + if (phone.isEmpty()) { binding.etStaffPhone.setError("Required"); return; } EmployeeDTO dto = new EmployeeDTO( username, @@ -137,41 +121,35 @@ public class StaffDetailFragment extends Fragment { active ); - Log.d("STAFF_SAVE", "isEditing=" + isEditing - + " employeeId=" + employeeId - + " username=" + username); - - EmployeeApi api = RetrofitClient.getEmployeeApi(requireContext()); if (isEditing && employeeId > 0) { - api.updateEmployee(employeeId, dto).enqueue(simpleCallback("Updated successfully")); - } else { - api.createEmployee(dto).enqueue(simpleCallback("Staff account created")); - } - } - - private Callback simpleCallback(String msg) { - return new Callback<>() { - public void onResponse(Call c, Response r) { - Log.d("STAFF_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("STAFF_SAVE", "Error: " + err); - Toast.makeText(getContext(), "Error " + r.code() + ": " + err, - Toast.LENGTH_LONG).show(); - } catch (Exception e) { - Log.e("STAFF_SAVE", "Failed to read error"); + employeeViewModel.updateEmployee(employeeId, dto).observe(getViewLifecycleOwner(), resource -> { + if (resource != null) { + switch (resource.status) { + case SUCCESS: + Toast.makeText(getContext(), "Updated successfully", Toast.LENGTH_SHORT).show(); + navigateBack(); + break; + case ERROR: + Toast.makeText(getContext(), "Error: " + resource.message, Toast.LENGTH_LONG).show(); + break; } } - } - public void onFailure(Call c, Throwable t) { - Log.e("STAFF_SAVE", "Failure: " + t.getMessage()); - Toast.makeText(getContext(), "Network error", Toast.LENGTH_SHORT).show(); - } - }; + }); + } else { + employeeViewModel.createEmployee(dto).observe(getViewLifecycleOwner(), resource -> { + if (resource != null) { + switch (resource.status) { + case SUCCESS: + Toast.makeText(getContext(), "Staff account created", Toast.LENGTH_SHORT).show(); + navigateBack(); + break; + case ERROR: + Toast.makeText(getContext(), "Error: " + resource.message, Toast.LENGTH_LONG).show(); + break; + } + } + }); + } } private void confirmDelete() { @@ -179,21 +157,29 @@ public class StaffDetailFragment extends Fragment { .setTitle("Delete Staff Account?") .setMessage("This will permanently delete this staff account.") .setPositiveButton("Yes", (d, w) -> - RetrofitClient.getEmployeeApi(requireContext()) - .deleteEmployee(employeeId) - .enqueue(new Callback() { - public void onResponse(Call c, Response r) { + employeeViewModel.deleteEmployee(employeeId).observe(getViewLifecycleOwner(), resource -> { + if (resource != null) { + switch (resource.status) { + case SUCCESS: navigateBack(); - } - public void onFailure(Call c, Throwable t) { - Toast.makeText(getContext(), "Delete failed", + break; + case ERROR: + Toast.makeText(getContext(), "Delete failed: " + resource.message, Toast.LENGTH_SHORT).show(); - } - })) + break; + } + } + })) .setNegativeButton("No", null).show(); } private void navigateBack() { NavHostFragment.findNavController(this).popBackStack(); } + + @Override + public void onDestroyView() { + super.onDestroyView(); + binding = null; + } } diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/listprofilefragments/PetProfileFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/listprofilefragments/PetProfileFragment.java index 9e876c69..371e5c20 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/listprofilefragments/PetProfileFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/listprofilefragments/PetProfileFragment.java @@ -131,11 +131,30 @@ public class PetProfileFragment extends Fragment { binding.tvPetPrice.setText("$0.00"); } - // Display owner name if available, otherwise show No Owner - if (pet.getCustomerName() != null && !pet.getCustomerName().isEmpty()) { - binding.tvPetOwner.setText(pet.getCustomerName()); + String status = pet.getPetStatus(); + + // Display owner name only if the pet is Adopted or Owned + if ("Adopted".equalsIgnoreCase(status) || "Owned".equalsIgnoreCase(status)) { + binding.layoutPetOwner.setVisibility(View.VISIBLE); + if (pet.getCustomerName() != null && !pet.getCustomerName().isEmpty()) { + binding.tvPetOwner.setText(pet.getCustomerName()); + } else { + binding.tvPetOwner.setText("No Owner"); + } } else { - binding.tvPetOwner.setText("No Owner"); + binding.layoutPetOwner.setVisibility(View.GONE); + } + + // Display store name only if the pet is Adopted or Available + if ("Available".equalsIgnoreCase(status) || "Adopted".equalsIgnoreCase(status)) { + binding.layoutPetStore.setVisibility(View.VISIBLE); + if (pet.getStoreName() != null && !pet.getStoreName().isEmpty()) { + binding.tvPetStore.setText(pet.getStoreName()); + } else { + binding.tvPetStore.setText("No Store"); + } + } else { + binding.layoutPetStore.setVisibility(View.GONE); } } else if (resource.status == Resource.Status.ERROR) { Toast.makeText(getContext(), "Failed to load pet data: " + resource.message, Toast.LENGTH_SHORT).show(); diff --git a/android/app/src/main/java/com/example/petstoremobile/repositories/SaleRepository.java b/android/app/src/main/java/com/example/petstoremobile/repositories/SaleRepository.java index 4068c472..4f436163 100644 --- a/android/app/src/main/java/com/example/petstoremobile/repositories/SaleRepository.java +++ b/android/app/src/main/java/com/example/petstoremobile/repositories/SaleRepository.java @@ -20,8 +20,8 @@ public class SaleRepository extends BaseRepository { this.saleApi = saleApi; } - public LiveData>> getAllSales(int page, int size) { - return executeCall(saleApi.getAllSales(page, size)); + public LiveData>> getAllSales(int page, int size, String query, String paymentMethod, Long storeId, String sortBy) { + return executeCall(saleApi.getAllSales(page, size, query, paymentMethod, storeId, sortBy)); } public LiveData> getSaleById(Long id) { diff --git a/android/app/src/main/java/com/example/petstoremobile/utils/UIUtils.java b/android/app/src/main/java/com/example/petstoremobile/utils/UIUtils.java index 22b8d4e0..399cabc4 100644 --- a/android/app/src/main/java/com/example/petstoremobile/utils/UIUtils.java +++ b/android/app/src/main/java/com/example/petstoremobile/utils/UIUtils.java @@ -1,18 +1,76 @@ package com.example.petstoremobile.utils; import android.telephony.PhoneNumberFormattingTextWatcher; +import android.text.Editable; import android.text.InputFilter; +import android.text.TextWatcher; +import android.view.View; import android.widget.EditText; +import android.widget.ImageButton; +import android.widget.Spinner; + +import androidx.fragment.app.Fragment; +import com.example.petstoremobile.fragments.ListFragment; /** * Utility class for shared UI component logic and formatting. */ public class UIUtils { /** - * Formats an EditText for to phone format + * Formats an EditText to phone format. */ public static void formatPhoneInput(EditText editText) { editText.addTextChangedListener(new PhoneNumberFormattingTextWatcher("CA")); editText.setFilters(new InputFilter[]{new InputFilter.LengthFilter(14)}); } + + /** + * Sets up a toggle for a filter layout, including icon changes and field resets. + */ + public static void setupFilterToggle(ImageButton btnToggle, View layoutFilter, EditText etSearch, Spinner... spinners) { + btnToggle.setOnClickListener(v -> { + boolean isVisible = layoutFilter.getVisibility() == View.VISIBLE; + layoutFilter.setVisibility(isVisible ? View.GONE : View.VISIBLE); + + // Use Android default icons or app-specific ones if available + btnToggle.setImageResource(isVisible ? + android.R.drawable.ic_menu_search : + android.R.drawable.ic_menu_close_clear_cancel); + + if (isVisible) { + if (etSearch != null) etSearch.setText(""); + for (Spinner spinner : spinners) { + if (spinner != null) spinner.setSelection(0); + } + } + }); + } + + /** + * Simplifies setting up the hamburger menu to open the navigation drawer. + */ + public static void setupHamburgerMenu(ImageButton btnHamburger, Fragment fragment) { + btnHamburger.setOnClickListener(v -> { + Fragment parent = fragment.getParentFragment(); + if (parent != null) { + Fragment grandParent = parent.getParentFragment(); + if (grandParent instanceof ListFragment) { + ((ListFragment) grandParent).openDrawer(); + } + } + }); + } + + /** + * Attaches a simplified TextWatcher to an EditText for search functionality. + */ + public static void attachSearch(EditText etSearch, Runnable onQueryChanged) { + 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) { + onQueryChanged.run(); + } + @Override public void afterTextChanged(Editable s) {} + }); + } } diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/SaleViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/SaleViewModel.java index aca186f6..a02d3382 100644 --- a/android/app/src/main/java/com/example/petstoremobile/viewmodels/SaleViewModel.java +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/SaleViewModel.java @@ -21,8 +21,8 @@ public class SaleViewModel extends ViewModel { this.saleRepository = saleRepository; } - public LiveData>> getAllSales(int page, int size) { - return saleRepository.getAllSales(page, size); + public LiveData>> getAllSales(int page, int size, String query, String paymentMethod, Long storeId, String sortBy) { + return saleRepository.getAllSales(page, size, query, paymentMethod, storeId, sortBy); } public LiveData> getSaleById(Long id) { diff --git a/android/app/src/main/res/layout/fragment_pet_profile.xml b/android/app/src/main/res/layout/fragment_pet_profile.xml index f5af4445..25f2214c 100644 --- a/android/app/src/main/res/layout/fragment_pet_profile.xml +++ b/android/app/src/main/res/layout/fragment_pet_profile.xml @@ -222,13 +222,15 @@ + android:padding="16dp" + android:visibility="gone"> + + + + + + + + diff --git a/android/app/src/main/res/layout/fragment_sale.xml b/android/app/src/main/res/layout/fragment_sale.xml index cea233df..ff3a693c 100644 --- a/android/app/src/main/res/layout/fragment_sale.xml +++ b/android/app/src/main/res/layout/fragment_sale.xml @@ -29,35 +29,112 @@ android:contentDescription="Open menu"/> + android:textStyle="bold" + android:layout_marginStart="8dp"/> + + - + android:orientation="vertical" + android:paddingStart="12dp" + android:paddingEnd="12dp" + android:paddingTop="10dp" + android:paddingBottom="10dp" + android:visibility="gone" + android:background="@color/primary_dark" + android:elevation="4dp"> -