Update early fixes

This commit is contained in:
2026-04-08 16:43:50 -06:00
36 changed files with 1410 additions and 1091 deletions

View File

@@ -2,10 +2,12 @@ package com.example.petstoremobile.adapters;
import android.graphics.Color; import android.graphics.Color;
import android.view.*; import android.view.*;
import android.widget.ImageView;
import android.widget.TextView; import android.widget.TextView;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView;
import com.example.petstoremobile.R; import com.example.petstoremobile.R;
import com.example.petstoremobile.databinding.ItemEmployeeBinding;
import com.example.petstoremobile.dtos.EmployeeDTO; import com.example.petstoremobile.dtos.EmployeeDTO;
import java.util.List; import java.util.List;
@@ -24,47 +26,48 @@ public class EmployeeAdapter extends RecyclerView.Adapter<EmployeeAdapter.Employ
} }
public static class EmployeeViewHolder extends RecyclerView.ViewHolder { public static class EmployeeViewHolder extends RecyclerView.ViewHolder {
TextView tvFullName, tvUsername, tvEmail, tvPhone, tvRole, tvStatus; private final ItemEmployeeBinding binding;
public EmployeeViewHolder(@NonNull View v) { public EmployeeViewHolder(@NonNull ItemEmployeeBinding binding) {
super(v); super(binding.getRoot());
tvFullName = v.findViewById(R.id.tvEmployeeFullName); this.binding = binding;
tvUsername = v.findViewById(R.id.tvEmployeeUsername);
tvEmail = v.findViewById(R.id.tvEmployeeEmail);
tvPhone = v.findViewById(R.id.tvEmployeePhone);
tvRole = v.findViewById(R.id.tvEmployeeRole);
tvStatus = v.findViewById(R.id.tvEmployeeStatus);
} }
} }
@NonNull @NonNull
@Override @Override
public EmployeeViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { public EmployeeViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View v = LayoutInflater.from(parent.getContext()) ItemEmployeeBinding binding = ItemEmployeeBinding.inflate(
.inflate(R.layout.item_employee, parent, false); LayoutInflater.from(parent.getContext()), parent, false);
return new EmployeeViewHolder(v); return new EmployeeViewHolder(binding);
} }
@Override @Override
public void onBindViewHolder(@NonNull EmployeeViewHolder holder, int position) { public void onBindViewHolder(@NonNull EmployeeViewHolder holder, int position) {
EmployeeDTO e = list.get(position); EmployeeDTO e = list.get(position);
ItemEmployeeBinding binding = holder.binding;
holder.tvFullName.setText(e.getFullName() != null ? e.getFullName() : ""); binding.tvEmployeeFullName.setText(e.getFullName() != null ? e.getFullName() : "");
holder.tvUsername.setText("@" + (e.getUsername() != null ? e.getUsername() : "")); binding.tvEmployeeUsername.setText("@" + (e.getUsername() != null ? e.getUsername() : ""));
holder.tvEmail.setText(e.getEmail() != null ? e.getEmail() : ""); binding.tvEmployeeEmail.setText(e.getEmail() != null ? e.getEmail() : "");
holder.tvPhone.setText(e.getPhone() != null ? e.getPhone() : "");
// Role badge // Role badge
String role = e.getRole() != null ? e.getRole() : ""; String role = e.getRole() != null ? e.getRole() : "STAFF";
holder.tvRole.setText(role); binding.tvEmployeeRole.setText(role);
holder.tvRole.setBackgroundColor(
"ADMIN".equals(role) ? Color.parseColor("#1a759f") : Color.parseColor("#577590")); if ("ADMIN".equalsIgnoreCase(role)) {
binding.tvEmployeeRole.setBackgroundColor(Color.parseColor("#1a759f"));
// Status badge } else {
binding.tvEmployeeRole.setBackgroundColor(Color.parseColor("#577590"));
}
// Status text and color
boolean active = Boolean.TRUE.equals(e.getActive()); boolean active = Boolean.TRUE.equals(e.getActive());
holder.tvStatus.setText(active ? "Active" : "Inactive"); binding.tvEmployeeStatus.setText(active ? "Active" : "Inactive");
holder.tvStatus.setBackgroundColor( binding.tvEmployeeStatus.setTextColor(active ? Color.parseColor("#4CAF50") : Color.parseColor("#F44336"));
active ? Color.parseColor("#4CAF50") : Color.parseColor("#F44336"));
// Placeholder for profile image - matching Pet style
binding.ivEmployeeProfile.setImageResource(R.drawable.placeholder);
holder.itemView.setOnClickListener(v -> listener.onEmployeeClick(position)); holder.itemView.setOnClickListener(v -> listener.onEmployeeClick(position));
} }

View File

@@ -1,11 +1,12 @@
package com.example.petstoremobile.adapters; package com.example.petstoremobile.adapters;
import android.graphics.Color;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.core.content.ContextCompat;
import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView;
import com.example.petstoremobile.R;
import com.example.petstoremobile.databinding.ItemSaleBinding; import com.example.petstoremobile.databinding.ItemSaleBinding;
import com.example.petstoremobile.dtos.SaleDTO; import com.example.petstoremobile.dtos.SaleDTO;
import java.util.List; import java.util.List;
@@ -53,11 +54,10 @@ public class SaleAdapter extends RecyclerView.Adapter<SaleAdapter.SaleViewHolder
if (Boolean.TRUE.equals(s.getIsRefund())) { if (Boolean.TRUE.equals(s.getIsRefund())) {
binding.tvSaleRefundBadge.setVisibility(View.VISIBLE); binding.tvSaleRefundBadge.setVisibility(View.VISIBLE);
binding.tvSaleRefundBadge.setBackgroundColor(Color.parseColor("#F44336")); binding.tvSaleTotal.setTextColor(ContextCompat.getColor(holder.itemView.getContext(), R.color.status_adopted));
binding.tvSaleTotal.setTextColor(Color.parseColor("#F44336"));
} else { } else {
binding.tvSaleRefundBadge.setVisibility(View.GONE); binding.tvSaleRefundBadge.setVisibility(View.GONE);
binding.tvSaleTotal.setTextColor(Color.parseColor("#4CAF50")); binding.tvSaleTotal.setTextColor(ContextCompat.getColor(holder.itemView.getContext(), R.color.status_available));
} }
holder.itemView.setOnClickListener(v -> listener.onSaleClick(position)); holder.itemView.setOnClickListener(v -> listener.onSaleClick(position));

View File

@@ -15,11 +15,15 @@ public interface SaleApi {
@GET("api/v1/sales") @GET("api/v1/sales")
Call<PageResponse<SaleDTO>> getAllSales( Call<PageResponse<SaleDTO>> getAllSales(
@Query("page") int page, @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}") @GET("api/v1/sales/{id}")
Call<SaleDTO> getSaleById(@Path("id") Long id); Call<SaleDTO> getSaleById(@Path("id") Long id);
@POST("api/v1/sales") @POST("api/v1/sales")
Call<SaleDTO> createSale(@Body SaleDTO sale); Call<SaleDTO> createSale(@Body SaleDTO sale);
} }

View File

@@ -12,9 +12,16 @@ public class SaleDTO {
private Long storeId; private Long storeId;
private String storeName; private String storeName;
private BigDecimal totalAmount; private BigDecimal totalAmount;
private BigDecimal subtotalAmount;
private BigDecimal couponDiscountAmount;
private BigDecimal employeeDiscountAmount;
private String paymentMethod; private String paymentMethod;
private String channel;
private Boolean isRefund; private Boolean isRefund;
private Long originalSaleId; private Long originalSaleId;
private Long cartId;
private Long couponId;
private Integer pointsEarned;
private List<SaleItemDTO> items; private List<SaleItemDTO> items;
private String createdAt; private String createdAt;
@@ -60,10 +67,26 @@ public class SaleDTO {
return totalAmount; return totalAmount;
} }
public BigDecimal getSubtotalAmount() {
return subtotalAmount;
}
public BigDecimal getCouponDiscountAmount() {
return couponDiscountAmount;
}
public BigDecimal getEmployeeDiscountAmount() {
return employeeDiscountAmount;
}
public String getPaymentMethod() { public String getPaymentMethod() {
return paymentMethod; return paymentMethod;
} }
public String getChannel() {
return channel;
}
public Boolean getIsRefund() { public Boolean getIsRefund() {
return isRefund; return isRefund;
} }
@@ -72,6 +95,18 @@ public class SaleDTO {
return originalSaleId; return originalSaleId;
} }
public Long getCartId() {
return cartId;
}
public Long getCouponId() {
return couponId;
}
public Integer getPointsEarned() {
return pointsEarned;
}
public List<SaleItemDTO> getItems() { public List<SaleItemDTO> getItems() {
return items; return items;
} }
@@ -118,4 +153,4 @@ public class SaleDTO {
return unitPrice; return unitPrice;
} }
} }
} }

View File

@@ -2,8 +2,6 @@ package com.example.petstoremobile.fragments.listfragments;
import android.graphics.Color; import android.graphics.Color;
import android.os.Bundle; import android.os.Bundle;
import android.text.Editable;
import android.text.TextWatcher;
import android.util.Log; import android.util.Log;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
@@ -22,10 +20,10 @@ import com.example.petstoremobile.adapters.AdoptionAdapter;
import com.example.petstoremobile.databinding.FragmentAdoptionBinding; import com.example.petstoremobile.databinding.FragmentAdoptionBinding;
import com.example.petstoremobile.dtos.AdoptionDTO; import com.example.petstoremobile.dtos.AdoptionDTO;
import com.example.petstoremobile.dtos.StoreDTO; import com.example.petstoremobile.dtos.StoreDTO;
import com.example.petstoremobile.fragments.ListFragment;
import com.example.petstoremobile.utils.BulkDeleteHandler; import com.example.petstoremobile.utils.BulkDeleteHandler;
import com.example.petstoremobile.utils.Resource; import com.example.petstoremobile.utils.Resource;
import com.example.petstoremobile.utils.SpinnerUtils; import com.example.petstoremobile.utils.SpinnerUtils;
import com.example.petstoremobile.utils.UIUtils;
import com.example.petstoremobile.viewmodels.AdoptionViewModel; import com.example.petstoremobile.viewmodels.AdoptionViewModel;
import com.example.petstoremobile.utils.EventDecorator; import com.example.petstoremobile.utils.EventDecorator;
import com.example.petstoremobile.viewmodels.StoreViewModel; 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.fabAddAdoption.setOnClickListener(v -> openDetail(-1));
binding.btnHamburgerAdoption.setOnClickListener(v -> { UIUtils.setupHamburgerMenu(binding.btnHamburgerAdoption, this);
Fragment parent = getParentFragment();
if (parent != null) {
Fragment grandParent = parent.getParentFragment();
if (grandParent instanceof ListFragment) {
((ListFragment) grandParent).openDrawer();
}
}
});
binding.btnToggleCalendarModeAdoption.setOnClickListener(v -> toggleCalendarMode()); 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. * Sets up the filter toggle button to show/hide the filter layout.
*/ */
private void setupFilterToggle() { private void setupFilterToggle() {
binding.btnToggleFilterAdoption.setOnClickListener(v -> { UIUtils.setupFilterToggle(binding.btnToggleFilterAdoption, binding.layoutFilterAdoption,
if (binding.layoutFilterAdoption.getVisibility() == View.GONE) { binding.etSearchAdoption, binding.spinnerStatusAdoption, binding.spinnerStoreAdoption);
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);
// 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.etSearchAdoption.setText("");
binding.spinnerStatusAdoption.setSelection(0); binding.spinnerStatusAdoption.setSelection(0);
binding.spinnerStoreAdoption.setSelection(0); binding.spinnerStoreAdoption.setSelection(0);
selectedCalendarDay = null; selectedCalendarDay = null;
binding.calendarViewAdoption.clearSelection(); binding.calendarViewAdoption.clearSelection();
loadAdoptions();
} }
}); });
} }
@@ -214,13 +208,7 @@ public class AdoptionFragment extends Fragment implements AdoptionAdapter.OnAdop
* Sets up the search bar for filtering * Sets up the search bar for filtering
*/ */
private void setupSearch() { private void setupSearch() {
binding.etSearchAdoption.addTextChangedListener(new TextWatcher() { UIUtils.attachSearch(binding.etSearchAdoption, this::loadAdoptions);
@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) {}
});
} }
/** /**

View File

@@ -5,91 +5,77 @@ import android.os.Bundle;
import android.util.Log; import android.util.Log;
import android.view.*; import android.view.*;
import android.widget.*; import android.widget.*;
import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment; import androidx.fragment.app.Fragment;
import com.example.petstoremobile.R; import androidx.lifecycle.ViewModelProvider;
import com.example.petstoremobile.api.RetrofitClient; import com.example.petstoremobile.databinding.FragmentAnalyticsBinding;
import com.example.petstoremobile.dtos.PageResponse;
import com.example.petstoremobile.dtos.SaleDTO; 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.BigDecimal;
import java.math.RoundingMode; import java.math.RoundingMode;
import java.util.*; import java.util.*;
import retrofit2.*;
@AndroidEntryPoint
public class AnalyticsFragment extends Fragment { public class AnalyticsFragment extends Fragment {
private TextView tvTotalRevenue, tvTotalTransactions, tvAvgTransaction, tvTotalItems; private FragmentAnalyticsBinding binding;
private LinearLayout llTopRevenue, llTopQuantity, llPaymentMethods, llEmployeePerformance, llDailyRevenue; private SaleViewModel saleViewModel;
private Button btnRefresh;
private ImageButton hamburger;
@Override @Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) { 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(); loadAnalytics();
btnRefresh.setOnClickListener(v -> loadAnalytics()); binding.btnRefreshAnalytics.setOnClickListener(v -> loadAnalytics());
hamburger.setOnClickListener(v -> openDrawer());
UIUtils.setupHamburgerMenu(binding.btnHamburgerAnalytics, this);
return view; return binding.getRoot();
} }
private void openDrawer() { @Override
Fragment parent = getParentFragment(); public void onDestroyView() {
if (parent != null) { super.onDestroyView();
Fragment grandParent = parent.getParentFragment(); binding = null;
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);
} }
private void loadAnalytics() { private void loadAnalytics() {
// Clear all sections // Clear all sections
llTopRevenue.removeAllViews(); binding.llTopRevenue.removeAllViews();
llTopQuantity.removeAllViews(); binding.llTopQuantity.removeAllViews();
llPaymentMethods.removeAllViews(); binding.llPaymentMethods.removeAllViews();
llEmployeePerformance.removeAllViews(); binding.llEmployeePerformance.removeAllViews();
llDailyRevenue.removeAllViews(); binding.llDailyRevenue.removeAllViews();
// Show loading // Show loading
tvTotalRevenue.setText("Loading..."); binding.tvTotalRevenue.setText("Loading...");
tvTotalTransactions.setText("..."); binding.tvTotalTransactions.setText("...");
tvAvgTransaction.setText("..."); binding.tvAvgTransaction.setText("...");
tvTotalItems.setText("..."); binding.tvTotalItems.setText("...");
RetrofitClient.getSaleApi(requireContext()).getAllSales(0, 1000) saleViewModel.getAllSales(0, 1000, null, null, null, "saleDate,desc")
.enqueue(new Callback<PageResponse<SaleDTO>>() { .observe(getViewLifecycleOwner(), resource -> {
public void onResponse(Call<PageResponse<SaleDTO>> c, if (resource != null) {
Response<PageResponse<SaleDTO>> r) { switch (resource.status) {
if (r.isSuccessful() && r.body() != null) { case SUCCESS:
computeAndDisplay(r.body().getContent()); if (resource.data != null) {
} else { computeAndDisplay(resource.data.getContent());
showError("Failed to load sales data"); }
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<PageResponse<SaleDTO>> 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) ? totalRevenue.divide(BigDecimal.valueOf(totalTx), 2, RoundingMode.HALF_UP)
: BigDecimal.ZERO; : BigDecimal.ZERO;
tvTotalRevenue.setText("$" + totalRevenue.setScale(2, RoundingMode.HALF_UP)); binding.tvTotalRevenue.setText("$" + totalRevenue.setScale(2, RoundingMode.HALF_UP));
tvTotalTransactions.setText(String.valueOf(totalTx)); binding.tvTotalTransactions.setText(String.valueOf(totalTx));
tvAvgTransaction.setText("$" + avgTx); binding.tvAvgTransaction.setText("$" + avgTx);
tvTotalItems.setText(String.valueOf(totalItems)); binding.tvTotalItems.setText(String.valueOf(totalItems));
// ── Top Products by Revenue ─────────────────────────── // ── Top Products by Revenue ───────────────────────────
Map<String, BigDecimal> revenueByProduct = new LinkedHashMap<>(); Map<String, BigDecimal> revenueByProduct = new LinkedHashMap<>();
@@ -150,28 +136,28 @@ public class AnalyticsFragment extends Fragment {
topRevenue.sort((a, b) -> b.getValue().compareTo(a.getValue())); topRevenue.sort((a, b) -> b.getValue().compareTo(a.getValue()));
BigDecimal maxRevenue = topRevenue.isEmpty() ? BigDecimal.ONE : topRevenue.get(0).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++) { for (int i = 0; i < Math.min(5, topRevenue.size()); i++) {
Map.Entry<String, BigDecimal> e = topRevenue.get(i); Map.Entry<String, BigDecimal> 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"); e.getValue().floatValue() / maxRevenue.floatValue(), "#ff6b35");
} }
if (topRevenue.isEmpty()) if (topRevenue.isEmpty())
addEmptyRow(llTopRevenue, "No data"); addEmptyRow(binding.llTopRevenue, "No data");
// Sort by quantity desc, take top 5 // Sort by quantity desc, take top 5
List<Map.Entry<String, Integer>> topQuantity = new ArrayList<>(quantityByProduct.entrySet()); List<Map.Entry<String, Integer>> topQuantity = new ArrayList<>(quantityByProduct.entrySet());
topQuantity.sort((a, b) -> b.getValue() - a.getValue()); topQuantity.sort((a, b) -> b.getValue() - a.getValue());
int maxQty = topQuantity.isEmpty() ? 1 : topQuantity.get(0).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++) { for (int i = 0; i < Math.min(5, topQuantity.size()); i++) {
Map.Entry<String, Integer> e = topQuantity.get(i); Map.Entry<String, Integer> e = topQuantity.get(i);
addBarRow(llTopQuantity, e.getKey(), e.getValue() + " units", addBarRow(binding.llTopQuantity, e.getKey(), e.getValue() + " units",
(float) e.getValue() / maxQty, "#4ecdc4"); (float) e.getValue() / maxQty, "#4ecdc4");
} }
if (topQuantity.isEmpty()) if (topQuantity.isEmpty())
addEmptyRow(llTopQuantity, "No data"); addEmptyRow(binding.llTopQuantity, "No data");
// ── Payment Methods ─────────────────────────────────── // ── Payment Methods ───────────────────────────────────
Map<String, Integer> paymentCount = new LinkedHashMap<>(); Map<String, Integer> paymentCount = new LinkedHashMap<>();
@@ -183,16 +169,16 @@ public class AnalyticsFragment extends Fragment {
int maxPayment = paymentCount.values().stream().max(Integer::compare).orElse(1); int maxPayment = paymentCount.values().stream().max(Integer::compare).orElse(1);
String[] paymentColors = { "#1a759f", "#ff9f1c", "#577590", "#90be6d" }; String[] paymentColors = { "#1a759f", "#ff9f1c", "#577590", "#90be6d" };
int ci = 0; int ci = 0;
llPaymentMethods.removeAllViews(); binding.llPaymentMethods.removeAllViews();
for (Map.Entry<String, Integer> e : paymentCount.entrySet()) { for (Map.Entry<String, Integer> e : paymentCount.entrySet()) {
addBarRow(llPaymentMethods, e.getKey(), addBarRow(binding.llPaymentMethods, e.getKey(),
e.getValue() + " transactions", e.getValue() + " transactions",
(float) e.getValue() / maxPayment, (float) e.getValue() / maxPayment,
paymentColors[ci % paymentColors.length]); paymentColors[ci % paymentColors.length]);
ci++; ci++;
} }
if (paymentCount.isEmpty()) if (paymentCount.isEmpty())
addEmptyRow(llPaymentMethods, "No data"); addEmptyRow(binding.llPaymentMethods, "No data");
// ── Employee Performance ────────────────────────────── // ── Employee Performance ──────────────────────────────
Map<String, BigDecimal> employeeRevenue = new LinkedHashMap<>(); Map<String, BigDecimal> employeeRevenue = new LinkedHashMap<>();
@@ -206,15 +192,15 @@ public class AnalyticsFragment extends Fragment {
empList.sort((a, b) -> b.getValue().compareTo(a.getValue())); empList.sort((a, b) -> b.getValue().compareTo(a.getValue()));
BigDecimal maxEmp = empList.isEmpty() ? BigDecimal.ONE : empList.get(0).getValue(); BigDecimal maxEmp = empList.isEmpty() ? BigDecimal.ONE : empList.get(0).getValue();
llEmployeePerformance.removeAllViews(); binding.llEmployeePerformance.removeAllViews();
for (Map.Entry<String, BigDecimal> e : empList) { for (Map.Entry<String, BigDecimal> e : empList) {
addBarRow(llEmployeePerformance, e.getKey(), addBarRow(binding.llEmployeePerformance, e.getKey(),
"$" + e.getValue().setScale(2, RoundingMode.HALF_UP), "$" + e.getValue().setScale(2, RoundingMode.HALF_UP),
e.getValue().floatValue() / maxEmp.floatValue(), e.getValue().floatValue() / maxEmp.floatValue(),
"#1a759f"); "#1a759f");
} }
if (empList.isEmpty()) if (empList.isEmpty())
addEmptyRow(llEmployeePerformance, "No data"); addEmptyRow(binding.llEmployeePerformance, "No data");
// ── Daily Revenue (last 7 days) ─────────────────────── // ── Daily Revenue (last 7 days) ───────────────────────
Map<String, BigDecimal> dailyRevenue = new TreeMap<>(); Map<String, BigDecimal> dailyRevenue = new TreeMap<>();
@@ -247,26 +233,25 @@ public class AnalyticsFragment extends Fragment {
if (maxDaily.compareTo(BigDecimal.ZERO) == 0) if (maxDaily.compareTo(BigDecimal.ZERO) == 0)
maxDaily = BigDecimal.ONE; maxDaily = BigDecimal.ONE;
llDailyRevenue.removeAllViews(); binding.llDailyRevenue.removeAllViews();
for (Map.Entry<String, BigDecimal> e : dailyRevenue.entrySet()) { for (Map.Entry<String, BigDecimal> e : dailyRevenue.entrySet()) {
// Show just MM-DD // Show just MM-DD
String label = e.getKey().length() >= 10 String label = e.getKey().length() >= 10
? e.getKey().substring(5) ? e.getKey().substring(5)
: e.getKey(); : e.getKey();
addBarRow(llDailyRevenue, label, addBarRow(binding.llDailyRevenue, label,
"$" + e.getValue().setScale(2, RoundingMode.HALF_UP), "$" + e.getValue().setScale(2, RoundingMode.HALF_UP),
e.getValue().floatValue() / maxDaily.floatValue(), e.getValue().floatValue() / maxDaily.floatValue(),
"#ff6b35"); "#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) { private void addBarRow(LinearLayout parent, String label, String value, float ratio, String color) {
if (getContext() == null) return;
LinearLayout row = new LinearLayout(getContext()); LinearLayout row = new LinearLayout(getContext());
row.setOrientation(LinearLayout.VERTICAL); row.setOrientation(LinearLayout.VERTICAL);
row.setPadding(0, 6, 0, 6); row.setPadding(0, 6, 0, 6);
// Label + value row
LinearLayout labelRow = new LinearLayout(getContext()); LinearLayout labelRow = new LinearLayout(getContext());
labelRow.setOrientation(LinearLayout.HORIZONTAL); labelRow.setOrientation(LinearLayout.HORIZONTAL);
@@ -286,7 +271,6 @@ public class AnalyticsFragment extends Fragment {
labelRow.addView(tvLabel); labelRow.addView(tvLabel);
labelRow.addView(tvValue); labelRow.addView(tvValue);
// Bar background
LinearLayout barBg = new LinearLayout(getContext()); LinearLayout barBg = new LinearLayout(getContext());
LinearLayout.LayoutParams bgParams = new LinearLayout.LayoutParams( LinearLayout.LayoutParams bgParams = new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT, 12); LinearLayout.LayoutParams.MATCH_PARENT, 12);
@@ -294,16 +278,13 @@ public class AnalyticsFragment extends Fragment {
barBg.setLayoutParams(bgParams); barBg.setLayoutParams(bgParams);
barBg.setBackgroundColor(Color.parseColor("#EEEEEE")); barBg.setBackgroundColor(Color.parseColor("#EEEEEE"));
// Bar fill
View barFill = new View(getContext()); View barFill = new View(getContext());
int fillWidth = (int) (ratio * 100);
LinearLayout.LayoutParams fillParams = new LinearLayout.LayoutParams( LinearLayout.LayoutParams fillParams = new LinearLayout.LayoutParams(
0, LinearLayout.LayoutParams.MATCH_PARENT, ratio); 0, LinearLayout.LayoutParams.MATCH_PARENT, ratio);
barFill.setLayoutParams(fillParams); barFill.setLayoutParams(fillParams);
barFill.setBackgroundColor(Color.parseColor(color)); barFill.setBackgroundColor(Color.parseColor(color));
barBg.addView(barFill); barBg.addView(barFill);
// Empty space
View spacer = new View(getContext()); View spacer = new View(getContext());
spacer.setLayoutParams(new LinearLayout.LayoutParams( spacer.setLayoutParams(new LinearLayout.LayoutParams(
0, LinearLayout.LayoutParams.MATCH_PARENT, 1f - ratio)); 0, LinearLayout.LayoutParams.MATCH_PARENT, 1f - ratio));
@@ -315,6 +296,7 @@ public class AnalyticsFragment extends Fragment {
} }
private void addEmptyRow(LinearLayout parent, String message) { private void addEmptyRow(LinearLayout parent, String message) {
if (getContext() == null) return;
TextView tv = new TextView(getContext()); TextView tv = new TextView(getContext());
tv.setText(message); tv.setText(message);
tv.setTextColor(Color.parseColor("#888780")); tv.setTextColor(Color.parseColor("#888780"));
@@ -323,12 +305,12 @@ public class AnalyticsFragment extends Fragment {
} }
private void showError(String msg) { private void showError(String msg) {
if (getContext() == null) if (getContext() == null || binding == null)
return; return;
tvTotalRevenue.setText("Error"); binding.tvTotalRevenue.setText("Error");
tvTotalTransactions.setText(""); binding.tvTotalTransactions.setText("");
tvAvgTransaction.setText(""); binding.tvAvgTransaction.setText("");
tvTotalItems.setText(""); binding.tvTotalItems.setText("");
Toast.makeText(getContext(), msg, Toast.LENGTH_SHORT).show(); Toast.makeText(getContext(), msg, Toast.LENGTH_SHORT).show();
} }
} }

View File

@@ -10,8 +10,6 @@ import androidx.lifecycle.ViewModelProvider;
import androidx.navigation.fragment.NavHostFragment; import androidx.navigation.fragment.NavHostFragment;
import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.LinearLayoutManager;
import android.text.Editable;
import android.text.TextWatcher;
import android.util.Log; import android.util.Log;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
@@ -23,10 +21,10 @@ import com.example.petstoremobile.adapters.AppointmentAdapter;
import com.example.petstoremobile.databinding.FragmentAppointmentBinding; import com.example.petstoremobile.databinding.FragmentAppointmentBinding;
import com.example.petstoremobile.dtos.AppointmentDTO; import com.example.petstoremobile.dtos.AppointmentDTO;
import com.example.petstoremobile.dtos.StoreDTO; import com.example.petstoremobile.dtos.StoreDTO;
import com.example.petstoremobile.fragments.ListFragment;
import com.example.petstoremobile.utils.BulkDeleteHandler; import com.example.petstoremobile.utils.BulkDeleteHandler;
import com.example.petstoremobile.utils.Resource; import com.example.petstoremobile.utils.Resource;
import com.example.petstoremobile.utils.SpinnerUtils; import com.example.petstoremobile.utils.SpinnerUtils;
import com.example.petstoremobile.utils.UIUtils;
import com.example.petstoremobile.viewmodels.AppointmentViewModel; import com.example.petstoremobile.viewmodels.AppointmentViewModel;
import com.example.petstoremobile.utils.EventDecorator; import com.example.petstoremobile.utils.EventDecorator;
import com.example.petstoremobile.viewmodels.AuthViewModel; import com.example.petstoremobile.viewmodels.AuthViewModel;
@@ -94,15 +92,7 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter.
binding.fabAddAppointment.setOnClickListener(v -> openAppointmentDetails(-1)); binding.fabAddAppointment.setOnClickListener(v -> openAppointmentDetails(-1));
binding.btnHamburger.setOnClickListener(v -> { UIUtils.setupHamburgerMenu(binding.btnHamburger, this);
Fragment parent = getParentFragment();
if (parent != null) {
Fragment grandParent = parent.getParentFragment();
if (grandParent instanceof ListFragment) {
((ListFragment) grandParent).openDrawer();
}
}
});
binding.btnToggleCalendarMode.setOnClickListener(v -> toggleCalendarMode()); 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. * Sets up the filter toggle button to show/hide the filter layout.
*/ */
private void setupFilterToggle() { 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 -> { binding.btnToggleFilter.setOnClickListener(v -> {
if (binding.layoutFilter.getVisibility() == View.GONE) { boolean isVisible = binding.layoutFilter.getVisibility() == View.VISIBLE;
binding.layoutFilter.setVisibility(View.VISIBLE); binding.layoutFilter.setVisibility(isVisible ? View.GONE : View.VISIBLE);
binding.btnToggleFilter.setImageResource(android.R.drawable.ic_menu_close_clear_cancel);
} else { binding.btnToggleFilter.setImageResource(isVisible ?
binding.layoutFilter.setVisibility(View.GONE); android.R.drawable.ic_menu_search :
binding.btnToggleFilter.setImageResource(android.R.drawable.ic_menu_search); android.R.drawable.ic_menu_close_clear_cancel);
// Reset filters when closing if (isVisible) {
binding.etSearchAppointment.setText(""); binding.etSearchAppointment.setText("");
binding.spinnerStatus.setSelection(0); binding.spinnerStatus.setSelection(0);
binding.spinnerStore.setSelection(0); binding.spinnerStore.setSelection(0);
binding.btnMyAppointments.setChecked(false); binding.btnMyAppointments.setChecked(false);
selectedCalendarDay = null; selectedCalendarDay = null;
binding.calendarView.clearSelection(); binding.calendarView.clearSelection();
loadAppointmentData();
} }
}); });
} }
@@ -214,10 +209,11 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter.
*/ */
private void updateCalendarDecorators() { private void updateCalendarDecorators() {
HashSet<CalendarDay> datesWithAppointments = new HashSet<>(); HashSet<CalendarDay> datesWithAppointments = new HashSet<>();
SimpleDateFormat displayFormat = new SimpleDateFormat("yyyy-MM-dd", Locale.getDefault());
for (AppointmentDTO appointment : appointmentList) { for (AppointmentDTO appointment : appointmentList) {
try { try {
//Get the appointment date //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 the date is not null, add it to the hashset
if (date != null) { if (date != null) {
Calendar cal = Calendar.getInstance(); Calendar cal = Calendar.getInstance();
@@ -237,13 +233,7 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter.
* Configures the search bar for filtering. * Configures the search bar for filtering.
*/ */
private void setupSearch() { private void setupSearch() {
binding.etSearchAppointment.addTextChangedListener(new TextWatcher() { UIUtils.attachSearch(binding.etSearchAppointment, this::loadAppointmentData);
@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) {}
});
} }
/** /**

View File

@@ -1,8 +1,6 @@
package com.example.petstoremobile.fragments.listfragments; package com.example.petstoremobile.fragments.listfragments;
import android.os.Bundle; import android.os.Bundle;
import android.text.Editable;
import android.text.TextWatcher;
import android.util.Log; import android.util.Log;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
@@ -22,8 +20,8 @@ import com.example.petstoremobile.adapters.InventoryAdapter;
import com.example.petstoremobile.databinding.FragmentInventoryBinding; import com.example.petstoremobile.databinding.FragmentInventoryBinding;
import com.example.petstoremobile.dtos.InventoryDTO; import com.example.petstoremobile.dtos.InventoryDTO;
import com.example.petstoremobile.dtos.StoreDTO; import com.example.petstoremobile.dtos.StoreDTO;
import com.example.petstoremobile.fragments.ListFragment;
import com.example.petstoremobile.utils.BulkDeleteHandler; import com.example.petstoremobile.utils.BulkDeleteHandler;
import com.example.petstoremobile.utils.UIUtils;
import com.example.petstoremobile.viewmodels.InventoryViewModel; import com.example.petstoremobile.viewmodels.InventoryViewModel;
import com.example.petstoremobile.utils.Resource; import com.example.petstoremobile.utils.Resource;
import com.example.petstoremobile.utils.SpinnerUtils; 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.fabAddInventory.setOnClickListener(v -> openDetail(null));
binding.btnHamburger.setOnClickListener(v -> { UIUtils.setupHamburgerMenu(binding.btnHamburger, this);
Fragment parent = getParentFragment();
if (parent != null) {
Fragment grandParent = parent.getParentFragment();
if (grandParent instanceof ListFragment) {
((ListFragment) grandParent).openDrawer();
}
}
});
return binding.getRoot(); 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. * Sets up the filter toggle button to show/hide the filter layout.
*/ */
private void setupFilterToggle() { private void setupFilterToggle() {
binding.btnToggleFilter.setOnClickListener(v -> { UIUtils.setupFilterToggle(binding.btnToggleFilter, binding.layoutFilter, binding.etSearchInventory, binding.spinnerStore);
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);
}
});
} }
/** /**
* Sets up the search bar for filtering. * Sets up the search bar for filtering.
*/ */
private void setupSearch() { private void setupSearch() {
binding.etSearchInventory.addTextChangedListener(new TextWatcher() { UIUtils.attachSearch(binding.etSearchInventory, () -> loadInventory(true));
@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) {}
});
} }
/** /**

View File

@@ -9,8 +9,6 @@ import androidx.lifecycle.ViewModelProvider;
import androidx.navigation.fragment.NavHostFragment; import androidx.navigation.fragment.NavHostFragment;
import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.LinearLayoutManager;
import android.text.Editable;
import android.text.TextWatcher;
import android.util.Log; import android.util.Log;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
@@ -19,13 +17,14 @@ import android.widget.Toast;
import com.example.petstoremobile.R; import com.example.petstoremobile.R;
import com.example.petstoremobile.adapters.PetAdapter; import com.example.petstoremobile.adapters.PetAdapter;
import com.example.petstoremobile.api.auth.TokenManager;
import com.example.petstoremobile.databinding.FragmentPetBinding; import com.example.petstoremobile.databinding.FragmentPetBinding;
import com.example.petstoremobile.dtos.PetDTO; import com.example.petstoremobile.dtos.PetDTO;
import com.example.petstoremobile.dtos.StoreDTO; import com.example.petstoremobile.dtos.StoreDTO;
import com.example.petstoremobile.fragments.ListFragment;
import com.example.petstoremobile.utils.BulkDeleteHandler; import com.example.petstoremobile.utils.BulkDeleteHandler;
import com.example.petstoremobile.utils.Resource; import com.example.petstoremobile.utils.Resource;
import com.example.petstoremobile.utils.SpinnerUtils; import com.example.petstoremobile.utils.SpinnerUtils;
import com.example.petstoremobile.utils.UIUtils;
import com.example.petstoremobile.viewmodels.PetViewModel; import com.example.petstoremobile.viewmodels.PetViewModel;
import com.example.petstoremobile.viewmodels.StoreViewModel; import com.example.petstoremobile.viewmodels.StoreViewModel;
@@ -48,6 +47,7 @@ public class PetFragment extends Fragment implements PetAdapter.OnPetClickListen
private BulkDeleteHandler bulkDeleteHandler; private BulkDeleteHandler bulkDeleteHandler;
@Inject @Named("baseUrl") String baseUrl; @Inject @Named("baseUrl") String baseUrl;
@Inject TokenManager tokenManager;
/** /**
* Initializes the fragment and its associated ViewModels. * 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.fabAddPet.setOnClickListener(v -> openPetDetails());
binding.btnHamburger.setOnClickListener(v -> { UIUtils.setupHamburgerMenu(binding.btnHamburger, this);
Fragment parent = getParentFragment();
if (parent != null) {
Fragment grandParent = parent.getParentFragment();
if (grandParent instanceof ListFragment) {
((ListFragment) grandParent).openDrawer();
}
}
});
return binding.getRoot(); 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. * Sets up the filter toggle button to show/hide the filter layout.
*/ */
private void setupFilterToggle() { private void setupFilterToggle() {
binding.btnToggleFilter.setOnClickListener(v -> { UIUtils.setupFilterToggle(binding.btnToggleFilter, binding.layoutFilter, binding.etSearchPet,
if (binding.layoutFilter.getVisibility() == View.GONE) { binding.spinnerStatus, binding.spinnerSpecies, binding.spinnerStore);
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);
}
});
} }
/** /**
* Configures the search bar. * Configures the search bar.
*/ */
private void setupSearch() { private void setupSearch() {
binding.etSearchPet.addTextChangedListener(new TextWatcher() { UIUtils.attachSearch(binding.etSearchPet, this::loadPetData);
@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) {}
});
} }
/** /**
@@ -272,6 +245,7 @@ public class PetFragment extends Fragment implements PetAdapter.OnPetClickListen
private void setupRecyclerView() { private void setupRecyclerView() {
adapter = new PetAdapter(petList, this); adapter = new PetAdapter(petList, this);
adapter.setBaseUrl(baseUrl); adapter.setBaseUrl(baseUrl);
adapter.setToken(tokenManager.getToken());
binding.recyclerViewPets.setLayoutManager(new LinearLayoutManager(getContext())); binding.recyclerViewPets.setLayoutManager(new LinearLayoutManager(getContext()));
binding.recyclerViewPets.setAdapter(adapter); binding.recyclerViewPets.setAdapter(adapter);
} }

View File

@@ -9,8 +9,6 @@ import androidx.lifecycle.ViewModelProvider;
import androidx.navigation.fragment.NavHostFragment; import androidx.navigation.fragment.NavHostFragment;
import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.LinearLayoutManager;
import android.text.Editable;
import android.text.TextWatcher;
import android.util.Log; import android.util.Log;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
@@ -22,9 +20,9 @@ import com.example.petstoremobile.adapters.ProductAdapter;
import com.example.petstoremobile.databinding.FragmentProductBinding; import com.example.petstoremobile.databinding.FragmentProductBinding;
import com.example.petstoremobile.dtos.CategoryDTO; import com.example.petstoremobile.dtos.CategoryDTO;
import com.example.petstoremobile.dtos.ProductDTO; import com.example.petstoremobile.dtos.ProductDTO;
import com.example.petstoremobile.fragments.ListFragment;
import com.example.petstoremobile.utils.Resource; import com.example.petstoremobile.utils.Resource;
import com.example.petstoremobile.utils.SpinnerUtils; import com.example.petstoremobile.utils.SpinnerUtils;
import com.example.petstoremobile.utils.UIUtils;
import com.example.petstoremobile.viewmodels.ProductViewModel; import com.example.petstoremobile.viewmodels.ProductViewModel;
import java.util.ArrayList; import java.util.ArrayList;
@@ -71,15 +69,7 @@ public class ProductFragment extends Fragment implements ProductAdapter.OnProduc
binding.fabAddProduct.setOnClickListener(v -> openProductDetails(-1)); binding.fabAddProduct.setOnClickListener(v -> openProductDetails(-1));
binding.btnHamburgerProduct.setOnClickListener(v -> { UIUtils.setupHamburgerMenu(binding.btnHamburgerProduct, this);
Fragment parent = getParentFragment();
if (parent != null) {
Fragment grandParent = parent.getParentFragment();
if (grandParent instanceof ListFragment) {
((ListFragment) grandParent).openDrawer();
}
}
});
return binding.getRoot(); 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. * Sets up the filter toggle button to show/hide the filter layout.
*/ */
private void setupFilterToggle() { private void setupFilterToggle() {
binding.btnToggleFilter.setOnClickListener(v -> { UIUtils.setupFilterToggle(binding.btnToggleFilter, binding.layoutFilter,
if (binding.layoutFilter.getVisibility() == View.GONE) { binding.etSearchProduct, binding.spinnerCategory);
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);
}
});
} }
/** /**
* Configures the search bar for triggering data load from backend. * Configures the search bar for triggering data load from backend.
*/ */
private void setupSearch() { private void setupSearch() {
binding.etSearchProduct.addTextChangedListener(new TextWatcher() { UIUtils.attachSearch(binding.etSearchProduct, this::loadProductData);
@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) {}
});
} }
/** /**

View File

@@ -1,8 +1,6 @@
package com.example.petstoremobile.fragments.listfragments; package com.example.petstoremobile.fragments.listfragments;
import android.os.Bundle; import android.os.Bundle;
import android.text.Editable;
import android.text.TextWatcher;
import android.util.Log; import android.util.Log;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; 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.ProductDTO;
import com.example.petstoremobile.dtos.ProductSupplierDTO; import com.example.petstoremobile.dtos.ProductSupplierDTO;
import com.example.petstoremobile.dtos.SupplierDTO; import com.example.petstoremobile.dtos.SupplierDTO;
import com.example.petstoremobile.fragments.ListFragment;
import com.example.petstoremobile.utils.BulkDeleteHandler; import com.example.petstoremobile.utils.BulkDeleteHandler;
import com.example.petstoremobile.utils.Resource; import com.example.petstoremobile.utils.Resource;
import com.example.petstoremobile.utils.SpinnerUtils; import com.example.petstoremobile.utils.SpinnerUtils;
import com.example.petstoremobile.utils.UIUtils;
import com.example.petstoremobile.viewmodels.ProductSupplierViewModel; import com.example.petstoremobile.viewmodels.ProductSupplierViewModel;
import com.example.petstoremobile.viewmodels.ProductViewModel; import com.example.petstoremobile.viewmodels.ProductViewModel;
import com.example.petstoremobile.viewmodels.SupplierViewModel; import com.example.petstoremobile.viewmodels.SupplierViewModel;
@@ -79,15 +77,7 @@ public class ProductSupplierFragment extends Fragment
binding.fabAddPS.setOnClickListener(v -> openDetail(-1)); binding.fabAddPS.setOnClickListener(v -> openDetail(-1));
binding.btnHamburgerPS.setOnClickListener(v -> { UIUtils.setupHamburgerMenu(binding.btnHamburgerPS, this);
Fragment parent = getParentFragment();
if (parent != null) {
Fragment grandParent = parent.getParentFragment();
if (grandParent instanceof ListFragment) {
((ListFragment) grandParent).openDrawer();
}
}
});
return binding.getRoot(); return binding.getRoot();
} }
@@ -125,20 +115,8 @@ public class ProductSupplierFragment extends Fragment
* Sets up the filter toggle button to show/hide the filter layout. * Sets up the filter toggle button to show/hide the filter layout.
*/ */
private void setupFilterToggle() { private void setupFilterToggle() {
binding.btnToggleFilter.setOnClickListener(v -> { UIUtils.setupFilterToggle(binding.btnToggleFilter, binding.layoutFilter, binding.etSearchPS,
if (binding.layoutFilter.getVisibility() == View.GONE) { binding.spinnerProduct, binding.spinnerSupplier);
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);
}
});
} }
/** /**
@@ -154,13 +132,7 @@ public class ProductSupplierFragment extends Fragment
* Configures the search bar for filtering. * Configures the search bar for filtering.
*/ */
private void setupSearch() { private void setupSearch() {
binding.etSearchPS.addTextChangedListener(new TextWatcher() { UIUtils.attachSearch(binding.etSearchPS, this::loadData);
@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) {}
});
} }
/** /**

View File

@@ -1,8 +1,6 @@
package com.example.petstoremobile.fragments.listfragments; package com.example.petstoremobile.fragments.listfragments;
import android.os.Bundle; import android.os.Bundle;
import android.text.Editable;
import android.text.TextWatcher;
import android.util.Log; import android.util.Log;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
@@ -21,9 +19,9 @@ import com.example.petstoremobile.adapters.PurchaseOrderAdapter;
import com.example.petstoremobile.databinding.FragmentPurchaseOrderBinding; import com.example.petstoremobile.databinding.FragmentPurchaseOrderBinding;
import com.example.petstoremobile.dtos.PurchaseOrderDTO; import com.example.petstoremobile.dtos.PurchaseOrderDTO;
import com.example.petstoremobile.dtos.StoreDTO; import com.example.petstoremobile.dtos.StoreDTO;
import com.example.petstoremobile.fragments.ListFragment;
import com.example.petstoremobile.utils.Resource; import com.example.petstoremobile.utils.Resource;
import com.example.petstoremobile.utils.SpinnerUtils; import com.example.petstoremobile.utils.SpinnerUtils;
import com.example.petstoremobile.utils.UIUtils;
import com.example.petstoremobile.viewmodels.PurchaseOrderViewModel; import com.example.petstoremobile.viewmodels.PurchaseOrderViewModel;
import com.example.petstoremobile.viewmodels.StoreViewModel; import com.example.petstoremobile.viewmodels.StoreViewModel;
@@ -67,15 +65,7 @@ public class PurchaseOrderFragment extends Fragment
setupSwipeRefresh(); setupSwipeRefresh();
setupFilterToggle(); setupFilterToggle();
binding.btnHamburgerPO.setOnClickListener(v -> { UIUtils.setupHamburgerMenu(binding.btnHamburgerPO, this);
Fragment parent = getParentFragment();
if (parent != null) {
Fragment grandParent = parent.getParentFragment();
if (grandParent instanceof ListFragment) {
((ListFragment) grandParent).openDrawer();
}
}
});
return binding.getRoot(); return binding.getRoot();
} }
@@ -100,32 +90,14 @@ public class PurchaseOrderFragment extends Fragment
* Sets up the filter toggle button to show/hide the filter layout. * Sets up the filter toggle button to show/hide the filter layout.
*/ */
private void setupFilterToggle() { private void setupFilterToggle() {
binding.btnToggleFilter.setOnClickListener(v -> { UIUtils.setupFilterToggle(binding.btnToggleFilter, binding.layoutFilter, binding.etSearchPO, binding.spinnerStore);
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);
}
});
} }
/** /**
* Configures the search bar for filtering. * Configures the search bar for filtering.
*/ */
private void setupSearch() { private void setupSearch() {
binding.etSearchPO.addTextChangedListener(new TextWatcher() { UIUtils.attachSearch(binding.etSearchPO, this::loadData);
@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) {}
});
} }
/** /**

View File

@@ -7,19 +7,23 @@ import androidx.fragment.app.Fragment;
import androidx.lifecycle.ViewModelProvider; import androidx.lifecycle.ViewModelProvider;
import androidx.navigation.fragment.NavHostFragment; import androidx.navigation.fragment.NavHostFragment;
import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.LinearLayoutManager;
import android.text.Editable; import androidx.recyclerview.widget.RecyclerView;
import android.text.TextWatcher; import android.util.Log;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.Toast;
import com.example.petstoremobile.R; import com.example.petstoremobile.R;
import com.example.petstoremobile.adapters.SaleAdapter; import com.example.petstoremobile.adapters.SaleAdapter;
import com.example.petstoremobile.databinding.FragmentSaleBinding; import com.example.petstoremobile.databinding.FragmentSaleBinding;
import com.example.petstoremobile.dtos.SaleDTO; 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.Resource;
import com.example.petstoremobile.utils.SpinnerUtils;
import com.example.petstoremobile.utils.UIUtils;
import com.example.petstoremobile.viewmodels.SaleViewModel; import com.example.petstoremobile.viewmodels.SaleViewModel;
import com.example.petstoremobile.viewmodels.StoreViewModel;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@@ -29,11 +33,20 @@ import dagger.hilt.android.AndroidEntryPoint;
@AndroidEntryPoint @AndroidEntryPoint
public class SaleFragment extends Fragment implements SaleAdapter.OnSaleClickListener { 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 FragmentSaleBinding binding;
private List<SaleDTO> saleList = new ArrayList<>(); private final List<SaleDTO> saleList = new ArrayList<>();
private List<SaleDTO> filteredList = new ArrayList<>(); private final List<StoreDTO> storeList = new ArrayList<>();
private SaleAdapter adapter; private SaleAdapter adapter;
private SaleViewModel saleViewModel; private SaleViewModel saleViewModel;
private StoreViewModel storeViewModel;
// Pagination
private int currentPage = 0;
private boolean isLastPage = false;
private boolean isLoading = false;
@Override @Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, 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) { public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState); super.onViewCreated(view, savedInstanceState);
saleViewModel = new ViewModelProvider(this).get(SaleViewModel.class); saleViewModel = new ViewModelProvider(this).get(SaleViewModel.class);
storeViewModel = new ViewModelProvider(this).get(StoreViewModel.class);
setupRecyclerView(); setupRecyclerView();
setupSearch(); setupSearch();
setupStoreFilter();
setupPaymentMethodFilter();
setupSwipeRefresh(); setupSwipeRefresh();
loadSales(); setupFilterToggle();
loadSales(true);
binding.btnHamburger.setOnClickListener(v -> { UIUtils.setupHamburgerMenu(binding.btnHamburger, this);
Fragment parent = getParentFragment();
if (parent != null) {
Fragment grandParent = parent.getParentFragment();
if (grandParent instanceof ListFragment) {
((ListFragment) grandParent).openDrawer();
}
}
});
binding.fabAddSale.setOnClickListener(v -> binding.fabAddSale.setOnClickListener(v ->
NavHostFragment.findNavController(this).navigate(R.id.nav_sale_detail)); 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)); 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 @Override
public void onDestroyView() { public void onDestroyView() {
super.onDestroyView(); super.onDestroyView();
@@ -76,61 +116,91 @@ public class SaleFragment extends Fragment implements SaleAdapter.OnSaleClickLis
} }
private void setupRecyclerView() { private void setupRecyclerView() {
adapter = new SaleAdapter(filteredList, this); adapter = new SaleAdapter(saleList, this);
binding.recyclerViewSales.setLayoutManager(new LinearLayoutManager(getContext())); binding.recyclerViewSales.setLayoutManager(new LinearLayoutManager(getContext()));
binding.recyclerViewSales.setAdapter(adapter); 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() { private void setupSearch() {
binding.etSearchSale.addTextChangedListener(new TextWatcher() { UIUtils.attachSearch(binding.etSearchSale, () -> loadSales(true));
@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();
} }
private void setupSwipeRefresh() { private void setupSwipeRefresh() {
binding.swipeRefreshSale.setOnRefreshListener(() -> { binding.swipeRefreshSale.setOnRefreshListener(() -> loadSales(true));
loadSales();
});
} }
private void loadSales() { private void loadSales(boolean reset) {
saleViewModel.getAllSales(0, 200).observe(getViewLifecycleOwner(), resource -> { if (isLoading) return;
binding.swipeRefreshSale.setRefreshing(false);
if (resource.status == Resource.Status.SUCCESS && resource.data != null) { if (reset) {
saleList.clear(); currentPage = 0;
saleList.addAll(resource.data.getContent()); isLastPage = false;
filterSales(binding.etSearchSale.getText() != null }
? binding.etSearchSale.getText().toString() : "");
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 @Override
public void onSaleClick(int position) { 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(); Bundle args = new Bundle();
if (sale.getSaleId() != null) { if (sale.getSaleId() != null) {
args.putLong("saleId", sale.getSaleId()); args.putLong("saleId", sale.getSaleId());

View File

@@ -1,8 +1,6 @@
package com.example.petstoremobile.fragments.listfragments; package com.example.petstoremobile.fragments.listfragments;
import android.os.Bundle; import android.os.Bundle;
import android.text.Editable;
import android.text.TextWatcher;
import android.util.Log; import android.util.Log;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
@@ -21,9 +19,8 @@ import com.example.petstoremobile.R;
import com.example.petstoremobile.adapters.ServiceAdapter; import com.example.petstoremobile.adapters.ServiceAdapter;
import com.example.petstoremobile.databinding.FragmentServiceBinding; import com.example.petstoremobile.databinding.FragmentServiceBinding;
import com.example.petstoremobile.dtos.ServiceDTO; import com.example.petstoremobile.dtos.ServiceDTO;
import com.example.petstoremobile.fragments.ListFragment;
import com.example.petstoremobile.utils.BulkDeleteHandler; 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 com.example.petstoremobile.viewmodels.ServiceViewModel;
import java.util.ArrayList; import java.util.ArrayList;
@@ -77,15 +74,7 @@ public class ServiceFragment extends Fragment implements ServiceAdapter.OnServic
binding.fabAddService.setOnClickListener(v -> openDetail(null)); binding.fabAddService.setOnClickListener(v -> openDetail(null));
binding.btnHamburger.setOnClickListener(v -> { UIUtils.setupHamburgerMenu(binding.btnHamburger, this);
Fragment parent = getParentFragment();
if (parent != null) {
Fragment grandParent = parent.getParentFragment();
if (grandParent instanceof ListFragment) {
((ListFragment) grandParent).openDrawer();
}
}
});
return binding.getRoot(); 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. * Sets up the filter toggle button to show/hide the filter layout.
*/ */
private void setupFilterToggle() { private void setupFilterToggle() {
binding.btnToggleFilter.setOnClickListener(v -> { UIUtils.setupFilterToggle(binding.btnToggleFilter, binding.layoutFilter, binding.etSearchService);
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("");
}
});
} }
/** /**
* Sets up the search bar for filtering. * Sets up the search bar for filtering.
*/ */
private void setupSearch() { private void setupSearch() {
binding.etSearchService.addTextChangedListener(new TextWatcher() { UIUtils.attachSearch(binding.etSearchService, () -> loadServices(true));
@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) {}
});
} }
/** /**

View File

@@ -1,85 +1,63 @@
package com.example.petstoremobile.fragments.listfragments; package com.example.petstoremobile.fragments.listfragments;
import android.os.Bundle; import android.os.Bundle;
import android.text.*;
import android.util.Log; import android.util.Log;
import android.view.*; import android.view.*;
import android.widget.*; import android.widget.*;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment; import androidx.fragment.app.Fragment;
import androidx.lifecycle.ViewModelProvider;
import androidx.navigation.fragment.NavHostFragment; import androidx.navigation.fragment.NavHostFragment;
import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
import com.example.petstoremobile.R; import com.example.petstoremobile.R;
import com.example.petstoremobile.adapters.EmployeeAdapter; 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.EmployeeDTO;
import com.example.petstoremobile.dtos.PageResponse; import com.example.petstoremobile.utils.UIUtils;
import com.example.petstoremobile.fragments.ListFragment; import com.example.petstoremobile.viewmodels.EmployeeViewModel;
import com.google.android.material.floatingactionbutton.FloatingActionButton; import dagger.hilt.android.AndroidEntryPoint;
import java.util.*; import java.util.*;
import retrofit2.*;
@AndroidEntryPoint
public class StaffFragment extends Fragment implements EmployeeAdapter.OnEmployeeClickListener { public class StaffFragment extends Fragment implements EmployeeAdapter.OnEmployeeClickListener {
private FragmentStaffBinding binding;
private EmployeeViewModel employeeViewModel;
private List<EmployeeDTO> employeeList = new ArrayList<>(); private List<EmployeeDTO> employeeList = new ArrayList<>();
private List<EmployeeDTO> filteredList = new ArrayList<>(); private List<EmployeeDTO> filteredList = new ArrayList<>();
private EmployeeAdapter adapter; private EmployeeAdapter adapter;
private SwipeRefreshLayout swipeRefresh;
private EditText etSearch;
@Override @Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) { Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_staff, container, false); binding = FragmentStaffBinding.inflate(inflater, container, false);
employeeViewModel = new ViewModelProvider(this).get(EmployeeViewModel.class);
setupRecyclerView(view);
setupSearch(view); setupRecyclerView();
setupSwipeRefresh(view); setupSearch();
setupSwipeRefresh();
loadStaff(); loadStaff();
FloatingActionButton fab = view.findViewById(R.id.fabAddStaff); binding.fabAddStaff.setOnClickListener(v -> openDetail(-1));
fab.setOnClickListener(v -> openDetail(-1));
ImageButton hamburger = view.findViewById(R.id.btnHamburgerStaff); UIUtils.setupHamburgerMenu(binding.btnHamburgerStaff, this);
hamburger.setOnClickListener(v -> openDrawer()); UIUtils.setupFilterToggle(binding.btnToggleFilterStaff, binding.layoutFilterStaff, binding.etSearchStaff);
return view; return binding.getRoot();
} }
private void openDrawer() { private void setupRecyclerView() {
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);
adapter = new EmployeeAdapter(filteredList, this); adapter = new EmployeeAdapter(filteredList, this);
rv.setLayoutManager(new LinearLayoutManager(getContext())); binding.recyclerViewStaff.setLayoutManager(new LinearLayoutManager(getContext()));
rv.setAdapter(adapter); binding.recyclerViewStaff.setAdapter(adapter);
} }
private void setupSearch(View view) { private void setupSearch() {
etSearch = view.findViewById(R.id.etSearchStaff); UIUtils.attachSearch(binding.etSearchStaff, () -> filter(binding.etSearchStaff.getText().toString()));
etSearch.addTextChangedListener(new TextWatcher() {
public void beforeTextChanged(CharSequence s, int a, int b, int c) {}
public void afterTextChanged(Editable s) {}
public void onTextChanged(CharSequence s, int a, int b, int c) {
filter(s.toString());
}
});
} }
private void setupSwipeRefresh(View view) { private void setupSwipeRefresh() {
swipeRefresh = view.findViewById(R.id.swipeRefreshStaff); binding.swipeRefreshStaff.setOnRefreshListener(this::loadStaff);
swipeRefresh.setOnRefreshListener(this::loadStaff);
} }
private void filter(String query) { private void filter(String query) {
@@ -101,26 +79,31 @@ public class StaffFragment extends Fragment implements EmployeeAdapter.OnEmploye
} }
private void loadStaff() { private void loadStaff() {
if (swipeRefresh != null) swipeRefresh.setRefreshing(true); binding.swipeRefreshStaff.setRefreshing(true);
RetrofitClient.getEmployeeApi(requireContext()).getAllEmployees(0, 100) employeeViewModel.getAllEmployees(0, 100).observe(getViewLifecycleOwner(), resource -> {
.enqueue(new Callback<PageResponse<EmployeeDTO>>() { if (resource != null) {
public void onResponse(Call<PageResponse<EmployeeDTO>> c, switch (resource.status) {
Response<PageResponse<EmployeeDTO>> r) { case SUCCESS:
if (swipeRefresh != null) swipeRefresh.setRefreshing(false); binding.swipeRefreshStaff.setRefreshing(false);
if (r.isSuccessful() && r.body() != null) { if (resource.data != null) {
employeeList.clear(); employeeList.clear();
employeeList.addAll(r.body().getContent()); employeeList.addAll(resource.data.getContent());
filter(etSearch != null ? etSearch.getText().toString() : ""); filter(binding != null ? binding.etSearchStaff.getText().toString() : "");
} else { }
Toast.makeText(getContext(), "Failed to load staff", 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(); Toast.LENGTH_SHORT).show();
} }
} break;
public void onFailure(Call<PageResponse<EmployeeDTO>> c, Throwable t) { case LOADING:
if (swipeRefresh != null) swipeRefresh.setRefreshing(false); binding.swipeRefreshStaff.setRefreshing(true);
Log.e("StaffFragment", t.getMessage()); break;
} }
}); }
});
} }
private void openDetail(int position) { private void openDetail(int position) {
@@ -144,4 +127,10 @@ public class StaffFragment extends Fragment implements EmployeeAdapter.OnEmploye
public void onEmployeeClick(int position) { public void onEmployeeClick(int position) {
openDetail(position); openDetail(position);
} }
@Override
public void onDestroyView() {
super.onDestroyView();
binding = null;
}
} }

View File

@@ -9,8 +9,6 @@ import androidx.lifecycle.ViewModelProvider;
import androidx.navigation.fragment.NavHostFragment; import androidx.navigation.fragment.NavHostFragment;
import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.LinearLayoutManager;
import android.text.Editable;
import android.text.TextWatcher;
import android.util.Log; import android.util.Log;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
@@ -21,9 +19,9 @@ import com.example.petstoremobile.R;
import com.example.petstoremobile.adapters.SupplierAdapter; import com.example.petstoremobile.adapters.SupplierAdapter;
import com.example.petstoremobile.databinding.FragmentSupplierBinding; import com.example.petstoremobile.databinding.FragmentSupplierBinding;
import com.example.petstoremobile.dtos.SupplierDTO; import com.example.petstoremobile.dtos.SupplierDTO;
import com.example.petstoremobile.fragments.ListFragment;
import com.example.petstoremobile.utils.BulkDeleteHandler; import com.example.petstoremobile.utils.BulkDeleteHandler;
import com.example.petstoremobile.utils.Resource; import com.example.petstoremobile.utils.Resource;
import com.example.petstoremobile.utils.UIUtils;
import com.example.petstoremobile.viewmodels.SupplierViewModel; import com.example.petstoremobile.viewmodels.SupplierViewModel;
import java.util.ArrayList; import java.util.ArrayList;
@@ -67,16 +65,7 @@ public class SupplierFragment extends Fragment implements SupplierAdapter.OnSupp
//Add button to opens the add dialog //Add button to opens the add dialog
binding.fabAddSupplier.setOnClickListener(v -> openSupplierDetails(-1)); binding.fabAddSupplier.setOnClickListener(v -> openSupplierDetails(-1));
//Make the hamburger button open the drawer from listFragment UIUtils.setupHamburgerMenu(binding.btnHamburger, this);
binding.btnHamburger.setOnClickListener(v -> {
Fragment parent = getParentFragment();
if (parent != null) {
Fragment grandParent = parent.getParentFragment();
if (grandParent instanceof ListFragment) {
((ListFragment) grandParent).openDrawer();
}
}
});
return binding.getRoot(); 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. * Sets up the filter toggle button to show/hide the filter layout.
*/ */
private void setupFilterToggle() { private void setupFilterToggle() {
binding.btnToggleFilter.setOnClickListener(v -> { UIUtils.setupFilterToggle(binding.btnToggleFilter, binding.layoutFilter, binding.etSearchSupplier);
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("");
}
});
} }
/** /**
* Configures the search bar for filtering. * Configures the search bar for filtering.
*/ */
private void setupSearch() { private void setupSearch() {
binding.etSearchSupplier.addTextChangedListener(new TextWatcher() { UIUtils.attachSearch(binding.etSearchSupplier, this::loadSupplierData);
@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) {}
});
} }
/** /**

View File

@@ -415,7 +415,7 @@ public class AppointmentDetailFragment extends Fragment {
} else if (errorMessage.toLowerCase().contains("not available")) { } else if (errorMessage.toLowerCase().contains("not available")) {
showNoAvailabilityDialog(); showNoAvailabilityDialog();
} else { } else {
Toast.makeText(getContext(), "Operation failed", Toast.LENGTH_SHORT).show(); Toast.makeText(getContext(), errorMessage, Toast.LENGTH_SHORT).show();
} }
} else { } else {
Toast.makeText(getContext(), "Something went wrong", Toast.LENGTH_SHORT).show(); Toast.makeText(getContext(), "Something went wrong", Toast.LENGTH_SHORT).show();

View File

@@ -220,6 +220,13 @@ public class PetDetailFragment extends Fragment {
binding.tvPetId.setText("ID: " + petId); binding.tvPetId.setText("ID: " + petId);
binding.tvPetId.setVisibility(View.VISIBLE); binding.tvPetId.setVisibility(View.VISIBLE);
binding.btnDeletePet.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(); loadPetData();
} else { } else {
// Pet is being added // Pet is being added
@@ -229,6 +236,12 @@ public class PetDetailFragment extends Fragment {
binding.tvPetId.setVisibility(View.GONE); binding.tvPetId.setVisibility(View.GONE);
binding.btnDeletePet.setVisibility(View.GONE); binding.btnDeletePet.setVisibility(View.GONE);
binding.btnSavePet.setText("Add"); 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)) { if ("Available".equalsIgnoreCase(status)) {
binding.spinnerCustomer.setSelection(0); binding.spinnerCustomer.setSelection(0);
binding.spinnerCustomer.setEnabled(false); binding.spinnerCustomer.setEnabled(false);
binding.spinnerCustomer.setAlpha(0.5f);
} else { } else {
binding.spinnerCustomer.setEnabled(true); binding.spinnerCustomer.setEnabled(true);
binding.spinnerCustomer.setAlpha(1.0f);
} }
//Disable the store spinner if the status is "Owned" //Disable the store spinner if the status is "Owned"
if ("Owned".equalsIgnoreCase(status)) { if ("Owned".equalsIgnoreCase(status)) {
binding.spinnerStore.setSelection(0); binding.spinnerStore.setSelection(0);
binding.spinnerStore.setEnabled(false); binding.spinnerStore.setEnabled(false);
binding.spinnerStore.setAlpha(0.5f);
} else { } else {
binding.spinnerStore.setEnabled(true); binding.spinnerStore.setEnabled(true);
binding.spinnerStore.setAlpha(1.0f);
} }
} }

View File

@@ -11,6 +11,7 @@ import androidx.lifecycle.ViewModelProvider;
import androidx.navigation.fragment.NavHostFragment; import androidx.navigation.fragment.NavHostFragment;
import com.bumptech.glide.Glide; import com.bumptech.glide.Glide;
import com.bumptech.glide.load.engine.DiskCacheStrategy;
import com.example.petstoremobile.R; import com.example.petstoremobile.R;
import com.example.petstoremobile.api.*; import com.example.petstoremobile.api.*;
import com.example.petstoremobile.api.auth.TokenManager; import com.example.petstoremobile.api.auth.TokenManager;
@@ -73,7 +74,11 @@ public class ProductDetailFragment extends Fragment {
@Override @Override
public void onImagePicked(Uri uri) { public void onImagePicked(Uri uri) {
photoUri = 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; hasImage = true;
isImageChanged = true; isImageChanged = true;
isImageRemoved = false; isImageRemoved = false;

View File

@@ -7,25 +7,22 @@ import android.view.*;
import android.widget.*; import android.widget.*;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment; import androidx.fragment.app.Fragment;
import androidx.lifecycle.ViewModelProvider;
import androidx.navigation.fragment.NavHostFragment; import androidx.navigation.fragment.NavHostFragment;
import com.example.petstoremobile.R; 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.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.BigDecimal;
import java.math.RoundingMode; import java.math.RoundingMode;
import java.util.*; import java.util.*;
import retrofit2.*;
@AndroidEntryPoint
public class RefundFragment extends Fragment { public class RefundFragment extends Fragment {
private EditText etSaleId; private FragmentRefundBinding binding;
private Button btnLoadSale, btnProcessRefund, btnBack; private SaleViewModel saleViewModel;
private TextView tvSaleInfo, tvRefundTotal;
private LinearLayout llOriginalItems, llRefundItems;
private LinearLayout cardOriginalItems, cardRefundItems, cardPayment;
private Spinner spinnerPayment;
private SaleDTO currentSale; private SaleDTO currentSale;
private List<SaleDTO> allSales = new ArrayList<>(); private List<SaleDTO> allSales = new ArrayList<>();
@@ -60,8 +57,9 @@ public class RefundFragment extends Fragment {
@Override @Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) { Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_refund, container, false); binding = FragmentRefundBinding.inflate(inflater, container, false);
initViews(view); saleViewModel = new ViewModelProvider(this).get(SaleViewModel.class);
setupSpinner(); setupSpinner();
loadAllSales(); loadAllSales();
@@ -69,59 +67,47 @@ public class RefundFragment extends Fragment {
Bundle args = getArguments(); Bundle args = getArguments();
if (args != null && args.containsKey("saleId")) { if (args != null && args.containsKey("saleId")) {
long saleId = args.getLong("saleId"); long saleId = args.getLong("saleId");
etSaleId.setText(String.valueOf(saleId)); binding.etRefundSaleId.setText(String.valueOf(saleId));
// Auto-load after sales are fetched // Auto-load after sales are fetched
} }
btnLoadSale.setOnClickListener(v -> loadSale()); binding.btnLoadSale.setOnClickListener(v -> loadSale());
btnProcessRefund.setOnClickListener(v -> processRefund()); binding.btnProcessRefund.setOnClickListener(v -> processRefund());
btnBack.setOnClickListener(v -> navigateBack()); binding.btnRefundBack.setOnClickListener(v -> navigateBack());
return view; return binding.getRoot();
}
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);
} }
private void setupSpinner() { private void setupSpinner() {
spinnerPayment.setAdapter(new ArrayAdapter<>(requireContext(), binding.spinnerRefundPayment.setAdapter(new ArrayAdapter<>(requireContext(),
android.R.layout.simple_spinner_item, PAYMENT_METHODS)); android.R.layout.simple_spinner_item, PAYMENT_METHODS));
} }
private void loadAllSales() { private void loadAllSales() {
RetrofitClient.getSaleApi(requireContext()).getAllSales(0, 1000) saleViewModel.getAllSales(0, 1000, null, null, null, "saleDate,desc")
.enqueue(new Callback<PageResponse<SaleDTO>>() { .observe(getViewLifecycleOwner(), resource -> {
public void onResponse(Call<PageResponse<SaleDTO>> c, if (resource != null) {
Response<PageResponse<SaleDTO>> r) { switch (resource.status) {
if (r.isSuccessful() && r.body() != null) { case SUCCESS:
allSales = r.body().getContent(); if (resource.data != null) {
// Auto-load if saleId was pre-filled allSales = resource.data.getContent();
Bundle args = getArguments(); // Auto-load if saleId was pre-filled
if (args != null && args.containsKey("saleId")) { Bundle args = getArguments();
loadSale(); if (args != null && args.containsKey("saleId")) {
} loadSale();
}
}
break;
case ERROR:
Log.e("Refund", "Failed to load sales: " + resource.message);
break;
} }
} }
public void onFailure(Call<PageResponse<SaleDTO>> c, Throwable t) {
Log.e("Refund", "Failed to load sales: " + t.getMessage());
}
}); });
} }
private void loadSale() { private void loadSale() {
String idStr = etSaleId.getText().toString().trim(); String idStr = binding.etRefundSaleId.getText().toString().trim();
if (idStr.isEmpty()) { if (idStr.isEmpty()) {
Toast.makeText(getContext(), "Enter a Sale ID", Toast.LENGTH_SHORT).show(); Toast.makeText(getContext(), "Enter a Sale ID", Toast.LENGTH_SHORT).show();
return; return;
@@ -156,8 +142,8 @@ public class RefundFragment extends Fragment {
currentSale = found; currentSale = found;
// Show sale info // Show sale info
tvSaleInfo.setVisibility(View.VISIBLE); binding.tvSaleInfo.setVisibility(View.VISIBLE);
tvSaleInfo.setText("Sale #" + currentSale.getSaleId() binding.tvSaleInfo.setText("Sale #" + currentSale.getSaleId()
+ " | " + (currentSale.getSaleDate() != null + " | " + (currentSale.getSaleDate() != null
? currentSale.getSaleDate().substring(0, 10) : "") ? currentSale.getSaleDate().substring(0, 10) : "")
+ " | Employee: " + (currentSale.getEmployeeName() != null + " | Employee: " + (currentSale.getEmployeeName() != null
@@ -169,7 +155,7 @@ public class RefundFragment extends Fragment {
if (currentSale.getPaymentMethod() != null) { if (currentSale.getPaymentMethod() != null) {
for (int i = 0; i < PAYMENT_METHODS.length; i++) { for (int i = 0; i < PAYMENT_METHODS.length; i++) {
if (PAYMENT_METHODS[i].equalsIgnoreCase(currentSale.getPaymentMethod())) { 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(); refundCart.clear();
// Show cards // Show cards
cardOriginalItems.setVisibility(View.VISIBLE); binding.cardOriginalItems.setVisibility(View.VISIBLE);
cardRefundItems.setVisibility(View.VISIBLE); binding.cardRefundItems.setVisibility(View.VISIBLE);
cardPayment.setVisibility(View.VISIBLE); binding.cardPayment.setVisibility(View.VISIBLE);
btnProcessRefund.setVisibility(View.VISIBLE); binding.btnProcessRefund.setVisibility(View.VISIBLE);
renderOriginalItems(); renderOriginalItems();
renderRefundCart(); renderRefundCart();
@@ -233,10 +219,10 @@ public class RefundFragment extends Fragment {
} }
private void renderOriginalItems() { private void renderOriginalItems() {
llOriginalItems.removeAllViews(); binding.llOriginalItems.removeAllViews();
// Header // Header
addTableHeader(llOriginalItems); addTableHeader(binding.llOriginalItems);
for (RefundItem item : availableItems) { for (RefundItem item : availableItems) {
// Calculate pending in cart // Calculate pending in cart
@@ -254,23 +240,23 @@ public class RefundFragment extends Fragment {
true, // show add button true, // show add button
() -> showQuantityDialog(item) () -> showQuantityDialog(item)
); );
llOriginalItems.addView(row); binding.llOriginalItems.addView(row);
} }
} }
private void renderRefundCart() { private void renderRefundCart() {
llRefundItems.removeAllViews(); binding.llRefundItems.removeAllViews();
if (refundCart.isEmpty()) { if (refundCart.isEmpty()) {
TextView empty = new TextView(getContext()); TextView empty = new TextView(getContext());
empty.setText("No items added to refund yet"); empty.setText("No items added to refund yet");
empty.setTextColor(0xFF888780); empty.setTextColor(0xFF888780);
empty.setTextSize(13f); empty.setTextSize(13f);
llRefundItems.addView(empty); binding.llRefundItems.addView(empty);
return; return;
} }
addTableHeader(llRefundItems); addTableHeader(binding.llRefundItems);
for (RefundItem item : refundCart) { for (RefundItem item : refundCart) {
LinearLayout row = buildItemRow( LinearLayout row = buildItemRow(
@@ -285,11 +271,12 @@ public class RefundFragment extends Fragment {
updateRefundTotal(); updateRefundTotal();
} }
); );
llRefundItems.addView(row); binding.llRefundItems.addView(row);
} }
} }
private void addTableHeader(LinearLayout parent) { private void addTableHeader(LinearLayout parent) {
if (getContext() == null) return;
LinearLayout header = new LinearLayout(getContext()); LinearLayout header = new LinearLayout(getContext());
header.setOrientation(LinearLayout.HORIZONTAL); header.setOrientation(LinearLayout.HORIZONTAL);
header.setPadding(0, 0, 0, 8); header.setPadding(0, 0, 0, 8);
@@ -310,6 +297,7 @@ public class RefundFragment extends Fragment {
private LinearLayout buildItemRow(String name, int qty, BigDecimal unitPrice, private LinearLayout buildItemRow(String name, int qty, BigDecimal unitPrice,
boolean isAdd, Runnable action) { boolean isAdd, Runnable action) {
if (getContext() == null) return new LinearLayout(getContext());
LinearLayout row = new LinearLayout(getContext()); LinearLayout row = new LinearLayout(getContext());
row.setOrientation(LinearLayout.HORIZONTAL); row.setOrientation(LinearLayout.HORIZONTAL);
row.setGravity(android.view.Gravity.CENTER_VERTICAL); row.setGravity(android.view.Gravity.CENTER_VERTICAL);
@@ -428,7 +416,7 @@ public class RefundFragment extends Fragment {
private void updateRefundTotal() { private void updateRefundTotal() {
BigDecimal total = BigDecimal.ZERO; BigDecimal total = BigDecimal.ZERO;
for (RefundItem item : refundCart) total = total.add(item.getTotal()); 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() { private void processRefund() {
@@ -442,7 +430,7 @@ public class RefundFragment extends Fragment {
return; return;
} }
String payment = PAYMENT_METHODS[spinnerPayment.getSelectedItemPosition()]; String payment = PAYMENT_METHODS[binding.spinnerRefundPayment.getSelectedItemPosition()];
// Confirm dialog // Confirm dialog
BigDecimal total = BigDecimal.ZERO; BigDecimal total = BigDecimal.ZERO;
@@ -478,33 +466,34 @@ public class RefundFragment extends Fragment {
Log.d("REFUND", "Submitting refund for saleId=" + currentSale.getSaleId() Log.d("REFUND", "Submitting refund for saleId=" + currentSale.getSaleId()
+ " items=" + items.size()); + " items=" + items.size());
RetrofitClient.getSaleApi(requireContext()).createSale(dto) saleViewModel.createSale(dto).observe(getViewLifecycleOwner(), resource -> {
.enqueue(new Callback<SaleDTO>() { if (resource != null) {
public void onResponse(Call<SaleDTO> c, Response<SaleDTO> r) { switch (resource.status) {
if (r.isSuccessful() && r.body() != null) { case SUCCESS:
if (resource.data != null) {
Toast.makeText(getContext(), Toast.makeText(getContext(),
"Refund #" + r.body().getSaleId() + " processed successfully!", "Refund #" + resource.data.getSaleId() + " processed successfully!",
Toast.LENGTH_LONG).show(); Toast.LENGTH_LONG).show();
navigateBack(); 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");
}
} }
} break;
public void onFailure(Call<SaleDTO> c, Throwable t) { case ERROR:
Log.e("REFUND", "Failure: " + t.getMessage()); Log.e("REFUND", "Error: " + resource.message);
Toast.makeText(getContext(), "Network error", Toast.LENGTH_SHORT).show(); Toast.makeText(getContext(), "Error: " + resource.message,
} Toast.LENGTH_LONG).show();
}); break;
}
}
});
} }
private void navigateBack() { private void navigateBack() {
NavHostFragment.findNavController(this).popBackStack(); NavHostFragment.findNavController(this).popBackStack();
} }
@Override
public void onDestroyView() {
super.onDestroyView();
binding = null;
}
} }

View File

@@ -5,23 +5,28 @@ import android.util.Log;
import android.view.*; import android.view.*;
import android.widget.*; import android.widget.*;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import androidx.fragment.app.Fragment; import androidx.fragment.app.Fragment;
import androidx.lifecycle.ViewModelProvider;
import androidx.navigation.fragment.NavHostFragment; import androidx.navigation.fragment.NavHostFragment;
import com.example.petstoremobile.R; 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.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.math.BigDecimal;
import java.util.*; import java.util.*;
import retrofit2.*;
@AndroidEntryPoint
public class SaleDetailFragment extends Fragment { public class SaleDetailFragment extends Fragment {
private TextView tvMode, tvSaleDetailId, tvTotal; private FragmentSaleDetailBinding binding;
private Spinner spinnerStore, spinnerCustomer, spinnerPayment, spinnerProduct; private SaleViewModel saleViewModel;
private EditText etQuantity; private StoreViewModel storeViewModel;
private Button btnAddItem, btnSave, btnBack, btnRefund; private CustomerViewModel customerViewModel;
private LinearLayout llItems; private ProductViewModel productViewModel;
private boolean viewOnly = false; private boolean viewOnly = false;
private long saleId = -1; private long saleId = -1;
@@ -31,13 +36,20 @@ public class SaleDetailFragment extends Fragment {
private List<ProductDTO> productList = new ArrayList<>(); private List<ProductDTO> productList = new ArrayList<>();
private List<SaleDTO.SaleItemDTO> cartItems = new ArrayList<>(); private List<SaleDTO.SaleItemDTO> cartItems = new ArrayList<>();
private final String[] PAYMENT_METHODS = { "Cash", "Card" }; private final String[] PAYMENT_METHODS = { "Cash", "Card"};
@Override @Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) { Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_sale_detail, container, false); binding = FragmentSaleDetailBinding.inflate(inflater, container, false);
initViews(view);
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(); handleArguments();
if (!viewOnly) { if (!viewOnly) {
@@ -45,30 +57,11 @@ public class SaleDetailFragment extends Fragment {
setupAddItem(); setupAddItem();
} }
btnBack.setOnClickListener(v -> navigateBack()); binding.btnSaleBack.setOnClickListener(v -> navigateBack());
btnSave.setOnClickListener(v -> saveSale()); binding.btnSaveSale.setOnClickListener(v -> saveSale());
btnRefund.setOnClickListener(v -> showRefundDialog()); binding.btnRefundSale.setOnClickListener(v -> showRefundDialog());
return view; return binding.getRoot();
}
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));
} }
private void handleArguments() { private void handleArguments() {
@@ -76,31 +69,31 @@ public class SaleDetailFragment extends Fragment {
if (a != null && a.containsKey("saleId")) { if (a != null && a.containsKey("saleId")) {
saleId = a.getLong("saleId"); saleId = a.getLong("saleId");
viewOnly = a.getBoolean("viewOnly", false); viewOnly = a.getBoolean("viewOnly", false);
tvMode.setText("Sale #" + saleId); binding.tvSaleMode.setText("Sale #" + saleId);
tvSaleDetailId.setText("ID: " + saleId); binding.tvSaleDetailId.setText("ID: " + saleId);
// Show refund button for existing non-refund sales // Show refund button for existing non-refund sales
if (!a.getBoolean("isRefund", false)) { if (!a.getBoolean("isRefund", false)) {
btnRefund.setVisibility(View.VISIBLE); binding.btnRefundSale.setVisibility(View.VISIBLE);
} }
// Hide save and input controls for view only // Hide save and input controls for view only
if (viewOnly) { if (viewOnly) {
btnSave.setVisibility(View.GONE); binding.btnSaveSale.setVisibility(View.GONE);
spinnerStore.setEnabled(false); binding.spinnerSaleStore.setEnabled(false);
spinnerCustomer.setEnabled(false); binding.spinnerSaleCustomer.setEnabled(false);
spinnerPayment.setEnabled(false); binding.spinnerPaymentMethod.setEnabled(false);
spinnerProduct.setEnabled(false); binding.llAddItemRow.setVisibility(View.GONE);
etQuantity.setEnabled(false); binding.llExtraInfo.setVisibility(View.VISIBLE);
btnAddItem.setEnabled(false);
} }
// Load sale details // Load sale details
loadSaleDetails(); loadSaleDetails();
} else { } else {
tvMode.setText("New Sale"); binding.tvSaleMode.setText("New Sale");
tvSaleDetailId.setVisibility(View.GONE); binding.tvSaleDetailId.setVisibility(View.GONE);
btnRefund.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() { private void loadStores() {
// Hardcoded since store endpoint is admin only storeViewModel.getAllStores(0, 50).observe(getViewLifecycleOwner(), resource -> {
storeList = new ArrayList<>(); if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) {
storeList.add(new StoreDTO(1L, "Downtown Branch")); storeList = resource.data.getContent();
List<String> names = new ArrayList<>(); if (binding != null) {
names.add("-- Select Store --"); SpinnerUtils.populateSpinner(requireContext(), binding.spinnerSaleStore, storeList,
names.add("Downtown Branch"); StoreDTO::getStoreName, "-- Select Store --", -1L, StoreDTO::getStoreId);
spinnerStore.setAdapter(new ArrayAdapter<>(requireContext(), }
android.R.layout.simple_spinner_item, names)); } 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() { private void loadCustomers() {
RetrofitClient.getCustomerApi(requireContext()).getAllCustomers(0, 200) customerViewModel.getAllCustomers(0, 200).observe(getViewLifecycleOwner(), resource -> {
.enqueue(new Callback<PageResponse<CustomerDTO>>() { if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) {
public void onResponse(Call<PageResponse<CustomerDTO>> c, customerList = resource.data.getContent();
Response<PageResponse<CustomerDTO>> r) { if (binding != null) {
if (r.isSuccessful() && r.body() != null) { SpinnerUtils.populateSpinner(requireContext(), binding.spinnerSaleCustomer, customerList,
customerList = r.body().getContent(); CustomerDTO::getFullName, "-- No Customer --", -1L, CustomerDTO::getCustomerId);
List<String> 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<PageResponse<CustomerDTO>> c, Throwable t) {
Log.e("SaleDetail", "Customer load failed: " + t.getMessage());
}
});
} }
private void loadProducts() { private void loadProducts() {
RetrofitClient.getProductApi(requireContext()).getAllProducts(null, null, 0, 200, null) productViewModel.getAllProducts(null, null, 0, 200, null).observe(getViewLifecycleOwner(), resource -> {
.enqueue(new Callback<PageResponse<ProductDTO>>() { if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) {
public void onResponse(Call<PageResponse<ProductDTO>> c, productList = resource.data.getContent();
Response<PageResponse<ProductDTO>> r) { if (binding != null) {
if (r.isSuccessful() && r.body() != null) { SpinnerUtils.populateSpinner(requireContext(), binding.spinnerSaleProduct, productList,
productList = r.body().getContent(); ProductDTO::getProdName, "Select Product", -1L, ProductDTO::getProdId);
List<String> 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<PageResponse<ProductDTO>> c, Throwable t) {
Log.e("SaleDetail", "Product load failed: " + t.getMessage());
}
});
} }
private void loadSaleDetails() { private void loadSaleDetails() {
RetrofitClient.getSaleApi(requireContext()).getSaleById(saleId) saleViewModel.getSaleById(saleId).observe(getViewLifecycleOwner(), resource -> {
.enqueue(new Callback<SaleDTO>() { if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) {
public void onResponse(Call<SaleDTO> c, Response<SaleDTO> r) { SaleDTO sale = resource.data;
if (r.isSuccessful() && r.body() != null) { if (binding != null) {
SaleDTO sale = r.body(); binding.tvSaleDetailTotal.setText("Total: $" + sale.getTotalAmount());
tvTotal.setText("Total: $" + sale.getTotalAmount()); binding.tvSaleSubtotal.setText("$" + (sale.getSubtotalAmount() != null ? sale.getSubtotalAmount() : sale.getTotalAmount()));
// Display items
if (sale.getItems() != null) { if (sale.getCouponDiscountAmount() != null && sale.getCouponDiscountAmount().compareTo(BigDecimal.ZERO) > 0) {
llItems.removeAllViews(); binding.llCouponDiscount.setVisibility(View.VISIBLE);
for (SaleDTO.SaleItemDTO item : sale.getItems()) { binding.tvSaleCouponDiscount.setText("-$" + sale.getCouponDiscountAmount());
addItemRow(item.getProductName(), } else {
Math.abs(item.getQuantity()), binding.llCouponDiscount.setVisibility(View.GONE);
item.getUnitPrice());
}
}
}
} }
public void onFailure(Call<SaleDTO> c, Throwable t) { if (sale.getEmployeeDiscountAmount() != null && sale.getEmployeeDiscountAmount().compareTo(BigDecimal.ZERO) > 0) {
Log.e("SaleDetail", "Load failed: " + t.getMessage()); 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() { private void setupAddItem() {
btnAddItem.setOnClickListener(v -> { binding.btnAddItem.setOnClickListener(v -> {
if (spinnerProduct.getSelectedItemPosition() == 0) { if (binding.spinnerSaleProduct.getSelectedItemPosition() == 0) {
Toast.makeText(getContext(), "Select a product", Toast.LENGTH_SHORT).show(); Toast.makeText(getContext(), "Select a product", Toast.LENGTH_SHORT).show();
return; return;
} }
String qtyStr = etQuantity.getText().toString().trim(); String qtyStr = binding.etSaleQuantity.getText().toString().trim();
if (qtyStr.isEmpty()) { if (qtyStr.isEmpty()) {
etQuantity.setError("Enter quantity"); binding.etSaleQuantity.setError("Enter quantity");
return; return;
} }
int qty; int qty;
try { try {
qty = Integer.parseInt(qtyStr); qty = Integer.parseInt(qtyStr);
} catch (Exception e) { } catch (Exception e) {
etQuantity.setError("Invalid quantity"); binding.etSaleQuantity.setError("Invalid quantity");
return; return;
} }
ProductDTO product = productList.get(spinnerProduct.getSelectedItemPosition() - 1); ProductDTO product = productList.get(binding.spinnerSaleProduct.getSelectedItemPosition() - 1);
// Check if product already in cart // Check if product already in cart
for (SaleDTO.SaleItemDTO existing : cartItems) { for (SaleDTO.SaleItemDTO existing : cartItems) {
@@ -223,11 +219,12 @@ public class SaleDetailFragment extends Fragment {
cartItems.add(item); cartItems.add(item);
addItemRow(product.getProdName(), qty, product.getProdPrice()); addItemRow(product.getProdName(), qty, product.getProdPrice());
updateTotal(); updateTotal();
etQuantity.setText(""); binding.etSaleQuantity.setText("");
}); });
} }
private void addItemRow(String name, int qty, BigDecimal price) { private void addItemRow(String name, int qty, BigDecimal price) {
if (getContext() == null) return;
LinearLayout row = new LinearLayout(getContext()); LinearLayout row = new LinearLayout(getContext());
row.setOrientation(LinearLayout.HORIZONTAL); row.setOrientation(LinearLayout.HORIZONTAL);
row.setPadding(0, 8, 0, 8); row.setPadding(0, 8, 0, 8);
@@ -250,28 +247,26 @@ public class SaleDetailFragment extends Fragment {
row.addView(tvName); row.addView(tvName);
row.addView(tvQty); row.addView(tvQty);
row.addView(tvPrice); row.addView(tvPrice);
llItems.addView(row); binding.llSaleItems.addView(row);
} }
private void updateTotal() { private void updateTotal() {
BigDecimal total = BigDecimal.ZERO; BigDecimal total = BigDecimal.ZERO;
int productIdx = 0;
for (SaleDTO.SaleItemDTO item : cartItems) { for (SaleDTO.SaleItemDTO item : cartItems) {
if (productIdx < productList.size()) { for (ProductDTO p : productList) {
for (ProductDTO p : productList) { if (p.getProdId().equals(item.getProdId()) && p.getProdPrice() != null) {
if (p.getProdId().equals(item.getProdId()) && p.getProdPrice() != null) { total = total.add(p.getProdPrice()
total = total.add(p.getProdPrice() .multiply(BigDecimal.valueOf(item.getQuantity())));
.multiply(BigDecimal.valueOf(item.getQuantity()))); break;
break;
}
} }
} }
} }
tvTotal.setText("Total: $" + total); binding.tvSaleSubtotal.setText("$" + total);
binding.tvSaleDetailTotal.setText("Total: $" + total);
} }
private void saveSale() { private void saveSale() {
if (spinnerStore.getSelectedItemPosition() == 0) { if (binding.spinnerSaleStore.getSelectedItemPosition() == 0) {
Toast.makeText(getContext(), "Select a store", Toast.LENGTH_SHORT).show(); Toast.makeText(getContext(), "Select a store", Toast.LENGTH_SHORT).show();
return; return;
} }
@@ -280,13 +275,13 @@ public class SaleDetailFragment extends Fragment {
return; return;
} }
StoreDTO store = storeList.get(0); // only one store StoreDTO store = storeList.get(binding.spinnerSaleStore.getSelectedItemPosition() - 1);
String payment = PAYMENT_METHODS[spinnerPayment.getSelectedItemPosition()]; String payment = PAYMENT_METHODS[binding.spinnerPaymentMethod.getSelectedItemPosition()];
// Optional customer // Optional customer
Long customerId = null; Long customerId = null;
if (spinnerCustomer.getSelectedItemPosition() > 0) { if (binding.spinnerSaleCustomer.getSelectedItemPosition() > 0) {
customerId = customerList.get(spinnerCustomer.getSelectedItemPosition() - 1) customerId = customerList.get(binding.spinnerSaleCustomer.getSelectedItemPosition() - 1)
.getCustomerId(); .getCustomerId();
} }
@@ -298,71 +293,38 @@ public class SaleDetailFragment extends Fragment {
null, null,
customerId); customerId);
Log.d("SALE_SAVE", "storeId=" + store.getStoreId() saleViewModel.createSale(dto).observe(getViewLifecycleOwner(), resource -> {
+ " payment=" + payment if (resource != null) {
+ " items=" + cartItems.size() switch (resource.status) {
+ " customerId=" + customerId); case SUCCESS:
Toast.makeText(getContext(), "Sale saved!", Toast.LENGTH_SHORT).show();
RetrofitClient.getSaleApi(requireContext()).createSale(dto) navigateBack();
.enqueue(new Callback<SaleDTO>() { break;
public void onResponse(Call<SaleDTO> c, Response<SaleDTO> r) { case ERROR:
if (r.isSuccessful()) { Log.e("SALE_SAVE", "Error: " + resource.message);
Toast.makeText(getContext(), "Sale saved!", Toast.LENGTH_SHORT).show(); DialogUtils.showInfoDialog(requireContext(), "Save Error", resource.message);
navigateBack(); break;
} 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<SaleDTO> c, Throwable t) {
Log.e("SALE_SAVE", "Failure: " + t.getMessage());
Toast.makeText(getContext(), "Network error", Toast.LENGTH_SHORT).show();
}
});
} }
private void showRefundDialog() { private void showRefundDialog() {
Bundle args = new Bundle(); DialogUtils.showConfirmDialog(requireContext(), "Process Refund",
args.putLong("saleId", saleId); "Are you sure you want to process a refund for this sale?", () -> {
NavHostFragment.findNavController(this).navigate(R.id.nav_refund, args); 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<RefundDTO>() {
public void onResponse(Call<RefundDTO> c, Response<RefundDTO> 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<RefundDTO> c, Throwable t) {
Log.e("REFUND", "Failure: " + t.getMessage());
Toast.makeText(getContext(), "Network error", Toast.LENGTH_SHORT).show();
}
});
} }
private void navigateBack() { private void navigateBack() {
NavHostFragment.findNavController(this).popBackStack(); NavHostFragment.findNavController(this).popBackStack();
} }
@Override
public void onDestroyView() {
super.onDestroyView();
binding = null;
}
} }

View File

@@ -7,20 +7,19 @@ import android.widget.*;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AlertDialog;
import androidx.fragment.app.Fragment; import androidx.fragment.app.Fragment;
import androidx.lifecycle.ViewModelProvider;
import androidx.navigation.fragment.NavHostFragment; import androidx.navigation.fragment.NavHostFragment;
import com.example.petstoremobile.R; import com.example.petstoremobile.R;
import com.example.petstoremobile.api.EmployeeApi; import com.example.petstoremobile.databinding.FragmentStaffDetailBinding;
import com.example.petstoremobile.api.RetrofitClient;
import com.example.petstoremobile.dtos.EmployeeDTO; 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 { public class StaffDetailFragment extends Fragment {
private TextView tvMode, tvStaffId; private FragmentStaffDetailBinding binding;
private EditText etUsername, etPassword, etFirstName, etLastName, etEmail, etPhone; private EmployeeViewModel employeeViewModel;
private Spinner spinnerRole, spinnerStatus;
private Button btnSave, btnDelete, btnBack;
private long employeeId = -1; private long employeeId = -1;
private boolean isEditing = false; private boolean isEditing = false;
@@ -30,37 +29,22 @@ public class StaffDetailFragment extends Fragment {
@Override @Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) { Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_staff_detail, container, false); binding = FragmentStaffDetailBinding.inflate(inflater, container, false);
initViews(view); employeeViewModel = new ViewModelProvider(this).get(EmployeeViewModel.class);
setupSpinners(); setupSpinners();
handleArguments(); handleArguments();
btnBack.setOnClickListener(v -> navigateBack()); binding.btnStaffBack.setOnClickListener(v -> navigateBack());
btnSave.setOnClickListener(v -> save()); binding.btnSaveStaff.setOnClickListener(v -> save());
btnDelete.setOnClickListener(v -> confirmDelete()); binding.btnDeleteStaff.setOnClickListener(v -> confirmDelete());
return view; return binding.getRoot();
}
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);
} }
private void setupSpinners() { private void setupSpinners() {
spinnerRole.setAdapter(new ArrayAdapter<>(requireContext(), binding.spinnerStaffRole.setAdapter(new ArrayAdapter<>(requireContext(),
android.R.layout.simple_spinner_item, ROLES)); 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)); android.R.layout.simple_spinner_item, STATUSES));
} }
@@ -70,61 +54,61 @@ public class StaffDetailFragment extends Fragment {
isEditing = true; isEditing = true;
employeeId = a.getLong("employeeId", -1); employeeId = a.getLong("employeeId", -1);
tvMode.setText("Edit Staff Account"); binding.tvStaffMode.setText("Edit Staff Account");
tvStaffId.setText("ID: " + employeeId); binding.tvStaffId.setText("ID: " + employeeId);
tvStaffId.setVisibility(View.VISIBLE); binding.tvStaffId.setVisibility(View.VISIBLE);
etUsername.setText(a.getString("username", "")); binding.etStaffUsername.setText(a.getString("username", ""));
etFirstName.setText(a.getString("firstName", "")); binding.etStaffFirstName.setText(a.getString("firstName", ""));
etLastName.setText(a.getString("lastName", "")); binding.etStaffLastName.setText(a.getString("lastName", ""));
etEmail.setText(a.getString("email", "")); // ← was showing fullName binding.etStaffEmail.setText(a.getString("email", ""));
etPhone.setText(a.getString("phone", "")); binding.etStaffPhone.setText(a.getString("phone", ""));
btnDelete.setVisibility(View.VISIBLE); binding.btnDeleteStaff.setVisibility(View.VISIBLE);
// Pre-fill role // Pre-fill role
String role = a.getString("role", "STAFF"); String role = a.getString("role", "STAFF");
for (int i = 0; i < ROLES.length; i++) { for (int i = 0; i < ROLES.length; i++) {
if (ROLES[i].equals(role)) { if (ROLES[i].equals(role)) {
spinnerRole.setSelection(i); binding.spinnerStaffRole.setSelection(i);
break; break;
} }
} }
// Pre-fill status // Pre-fill status
boolean active = a.getBoolean("active", true); boolean active = a.getBoolean("active", true);
spinnerStatus.setSelection(active ? 0 : 1); binding.spinnerStaffStatus.setSelection(active ? 0 : 1);
} else { } else {
isEditing = false; isEditing = false;
employeeId = -1; employeeId = -1;
tvMode.setText("Add Staff Account"); binding.tvStaffMode.setText("Add Staff Account");
btnDelete.setVisibility(View.GONE); binding.btnDeleteStaff.setVisibility(View.GONE);
tvStaffId.setVisibility(View.GONE); binding.tvStaffId.setVisibility(View.GONE);
} }
} }
private void save() { private void save() {
String username = etUsername.getText() != null ? etUsername.getText().toString().trim() : ""; String username = binding.etStaffUsername.getText() != null ? binding.etStaffUsername.getText().toString().trim() : "";
String password = etPassword.getText() != null ? etPassword.getText().toString().trim() : ""; String password = binding.etStaffPassword.getText() != null ? binding.etStaffPassword.getText().toString().trim() : "";
String firstName = etFirstName.getText() != null ? etFirstName.getText().toString().trim() : ""; String firstName = binding.etStaffFirstName.getText() != null ? binding.etStaffFirstName.getText().toString().trim() : "";
String lastName = etLastName.getText() != null ? etLastName.getText().toString().trim() : ""; String lastName = binding.etStaffLastName.getText() != null ? binding.etStaffLastName.getText().toString().trim() : "";
String email = etEmail.getText() != null ? etEmail.getText().toString().trim() : ""; String email = binding.etStaffEmail.getText() != null ? binding.etStaffEmail.getText().toString().trim() : "";
String phone = etPhone.getText() != null ? etPhone.getText().toString().trim() : ""; String phone = binding.etStaffPhone.getText() != null ? binding.etStaffPhone.getText().toString().trim() : "";
String role = ROLES[spinnerRole.getSelectedItemPosition()]; String role = ROLES[binding.spinnerStaffRole.getSelectedItemPosition()];
boolean active = spinnerStatus.getSelectedItemPosition() == 0; boolean active = binding.spinnerStaffStatus.getSelectedItemPosition() == 0;
// Validation // Validation
if (username.isEmpty()) { etUsername.setError("Required"); return; } if (username.isEmpty()) { binding.etStaffUsername.setError("Required"); return; }
if (!isEditing && password.isEmpty()) { if (!isEditing && password.isEmpty()) {
etPassword.setError("Required for new account"); return; binding.etStaffPassword.setError("Required for new account"); return;
} }
if (!isEditing && password.length() < 6) { 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 (firstName.isEmpty()) { binding.etStaffFirstName.setError("Required"); return; }
if (lastName.isEmpty()) { etLastName.setError("Required"); return; } if (lastName.isEmpty()) { binding.etStaffLastName.setError("Required"); return; }
if (email.isEmpty()) { etEmail.setError("Required"); return; } if (email.isEmpty()) { binding.etStaffEmail.setError("Required"); return; }
if (phone.isEmpty()) { etPhone.setError("Required"); return; } if (phone.isEmpty()) { binding.etStaffPhone.setError("Required"); return; }
EmployeeDTO dto = new EmployeeDTO( EmployeeDTO dto = new EmployeeDTO(
username, username,
@@ -137,41 +121,35 @@ public class StaffDetailFragment extends Fragment {
active active
); );
Log.d("STAFF_SAVE", "isEditing=" + isEditing
+ " employeeId=" + employeeId
+ " username=" + username);
EmployeeApi api = RetrofitClient.getEmployeeApi(requireContext());
if (isEditing && employeeId > 0) { if (isEditing && employeeId > 0) {
api.updateEmployee(employeeId, dto).enqueue(simpleCallback("Updated successfully")); employeeViewModel.updateEmployee(employeeId, dto).observe(getViewLifecycleOwner(), resource -> {
} else { if (resource != null) {
api.createEmployee(dto).enqueue(simpleCallback("Staff account created")); switch (resource.status) {
} case SUCCESS:
} Toast.makeText(getContext(), "Updated successfully", Toast.LENGTH_SHORT).show();
navigateBack();
private Callback<EmployeeDTO> simpleCallback(String msg) { break;
return new Callback<>() { case ERROR:
public void onResponse(Call<EmployeeDTO> c, Response<EmployeeDTO> r) { Toast.makeText(getContext(), "Error: " + resource.message, Toast.LENGTH_LONG).show();
Log.d("STAFF_SAVE", "Response: " + r.code()); break;
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");
} }
} }
} });
public void onFailure(Call<EmployeeDTO> c, Throwable t) { } else {
Log.e("STAFF_SAVE", "Failure: " + t.getMessage()); employeeViewModel.createEmployee(dto).observe(getViewLifecycleOwner(), resource -> {
Toast.makeText(getContext(), "Network error", Toast.LENGTH_SHORT).show(); 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() { private void confirmDelete() {
@@ -179,21 +157,29 @@ public class StaffDetailFragment extends Fragment {
.setTitle("Delete Staff Account?") .setTitle("Delete Staff Account?")
.setMessage("This will permanently delete this staff account.") .setMessage("This will permanently delete this staff account.")
.setPositiveButton("Yes", (d, w) -> .setPositiveButton("Yes", (d, w) ->
RetrofitClient.getEmployeeApi(requireContext()) employeeViewModel.deleteEmployee(employeeId).observe(getViewLifecycleOwner(), resource -> {
.deleteEmployee(employeeId) if (resource != null) {
.enqueue(new Callback<Void>() { switch (resource.status) {
public void onResponse(Call<Void> c, Response<Void> r) { case SUCCESS:
navigateBack(); navigateBack();
} break;
public void onFailure(Call<Void> c, Throwable t) { case ERROR:
Toast.makeText(getContext(), "Delete failed", Toast.makeText(getContext(), "Delete failed: " + resource.message,
Toast.LENGTH_SHORT).show(); Toast.LENGTH_SHORT).show();
} break;
})) }
}
}))
.setNegativeButton("No", null).show(); .setNegativeButton("No", null).show();
} }
private void navigateBack() { private void navigateBack() {
NavHostFragment.findNavController(this).popBackStack(); NavHostFragment.findNavController(this).popBackStack();
} }
@Override
public void onDestroyView() {
super.onDestroyView();
binding = null;
}
} }

View File

@@ -131,11 +131,30 @@ public class PetProfileFragment extends Fragment {
binding.tvPetPrice.setText("$0.00"); binding.tvPetPrice.setText("$0.00");
} }
// Display owner name if available, otherwise show No Owner String status = pet.getPetStatus();
if (pet.getCustomerName() != null && !pet.getCustomerName().isEmpty()) {
binding.tvPetOwner.setText(pet.getCustomerName()); // 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 { } 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) { } else if (resource.status == Resource.Status.ERROR) {
Toast.makeText(getContext(), "Failed to load pet data: " + resource.message, Toast.LENGTH_SHORT).show(); Toast.makeText(getContext(), "Failed to load pet data: " + resource.message, Toast.LENGTH_SHORT).show();

View File

@@ -20,8 +20,8 @@ public class SaleRepository extends BaseRepository {
this.saleApi = saleApi; this.saleApi = saleApi;
} }
public LiveData<Resource<PageResponse<SaleDTO>>> getAllSales(int page, int size) { public LiveData<Resource<PageResponse<SaleDTO>>> getAllSales(int page, int size, String query, String paymentMethod, Long storeId, String sortBy) {
return executeCall(saleApi.getAllSales(page, size)); return executeCall(saleApi.getAllSales(page, size, query, paymentMethod, storeId, sortBy));
} }
public LiveData<Resource<SaleDTO>> getSaleById(Long id) { public LiveData<Resource<SaleDTO>> getSaleById(Long id) {

View File

@@ -1,18 +1,76 @@
package com.example.petstoremobile.utils; package com.example.petstoremobile.utils;
import android.telephony.PhoneNumberFormattingTextWatcher; import android.telephony.PhoneNumberFormattingTextWatcher;
import android.text.Editable;
import android.text.InputFilter; import android.text.InputFilter;
import android.text.TextWatcher;
import android.view.View;
import android.widget.EditText; 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. * Utility class for shared UI component logic and formatting.
*/ */
public class UIUtils { public class UIUtils {
/** /**
* Formats an EditText for to phone format * Formats an EditText to phone format.
*/ */
public static void formatPhoneInput(EditText editText) { public static void formatPhoneInput(EditText editText) {
editText.addTextChangedListener(new PhoneNumberFormattingTextWatcher("CA")); editText.addTextChangedListener(new PhoneNumberFormattingTextWatcher("CA"));
editText.setFilters(new InputFilter[]{new InputFilter.LengthFilter(14)}); 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) {}
});
}
} }

View File

@@ -21,8 +21,8 @@ public class SaleViewModel extends ViewModel {
this.saleRepository = saleRepository; this.saleRepository = saleRepository;
} }
public LiveData<Resource<PageResponse<SaleDTO>>> getAllSales(int page, int size) { public LiveData<Resource<PageResponse<SaleDTO>>> getAllSales(int page, int size, String query, String paymentMethod, Long storeId, String sortBy) {
return saleRepository.getAllSales(page, size); return saleRepository.getAllSales(page, size, query, paymentMethod, storeId, sortBy);
} }
public LiveData<Resource<SaleDTO>> getSaleById(Long id) { public LiveData<Resource<SaleDTO>> getSaleById(Long id) {

View File

@@ -222,13 +222,15 @@
</LinearLayout> </LinearLayout>
<LinearLayout <LinearLayout
android:id="@+id/layoutPetOwner"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="vertical" android:orientation="vertical"
android:gravity="center" android:gravity="center"
android:background="@color/white" android:background="@color/white"
android:layout_marginTop="16dp" android:layout_marginTop="16dp"
android:padding="16dp"> android:padding="16dp"
android:visibility="gone">
<TextView <TextView
android:layout_width="wrap_content" android:layout_width="wrap_content"
@@ -250,6 +252,37 @@
</LinearLayout> </LinearLayout>
<LinearLayout
android:id="@+id/layoutPetStore"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:gravity="center"
android:background="@color/white"
android:layout_marginTop="16dp"
android:padding="16dp"
android:visibility="gone">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="STORE"
android:textSize="11sp"
android:textColor="#888888"
android:textAllCaps="true"
android:layout_marginBottom="4dp"/>
<TextView
android:id="@+id/tvPetStore"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="No Store"
android:textSize="20sp"
android:textStyle="bold"
android:textColor="@color/text_dark"/>
</LinearLayout>
</LinearLayout> </LinearLayout>
</ScrollView> </ScrollView>

View File

@@ -29,35 +29,112 @@
android:contentDescription="Open menu"/> android:contentDescription="Open menu"/>
<TextView <TextView
android:layout_width="match_parent" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Sales" android:text="Sales"
android:textColor="@color/white" android:textColor="@color/white"
android:textSize="20sp" android:textSize="20sp"
android:textStyle="bold"/> android:textStyle="bold"
android:layout_marginStart="8dp"/>
<ImageButton
android:id="@+id/btnToggleFilter"
android:layout_width="48dp"
android:layout_height="48dp"
android:src="@android:drawable/ic_menu_search"
android:background="?attr/selectableItemBackgroundBorderless"
app:tint="@color/white"
android:contentDescription="Toggle filter"/>
</LinearLayout> </LinearLayout>
<EditText <LinearLayout
android:id="@+id/etSearchSale" android:id="@+id/layoutFilter"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_margin="8dp" android:orientation="vertical"
android:hint="Search by employee or store..." android:paddingStart="12dp"
android:inputType="text" android:paddingEnd="12dp"
android:drawableStart="@android:drawable/ic_menu_search" android:paddingTop="10dp"
android:drawablePadding="8dp" android:paddingBottom="10dp"
android:background="@android:color/white" android:visibility="gone"
android:padding="12dp" android:background="@color/primary_dark"
android:textColor="@color/text_dark"/> android:elevation="4dp">
<Button <LinearLayout
android:id="@+id/btnOpenRefund" android:layout_width="match_parent"
android:layout_width="wrap_content" android:layout_height="44dp"
android:layout_height="wrap_content" android:background="@drawable/bg_search_bar"
android:text="Refund" android:gravity="center_vertical"
android:backgroundTint="@color/accent_coral" android:paddingStart="12dp"
android:textColor="@color/white"/> android:paddingEnd="12dp">
<ImageView
android:layout_width="18dp"
android:layout_height="18dp"
android:src="@android:drawable/ic_menu_search"
android:alpha="0.6"/>
<EditText
android:id="@+id/etSearchSale"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:hint="Search by employee or store..."
android:inputType="text"
android:background="@android:color/transparent"
android:textColor="@color/text_dark"
android:textColorHint="#99000000"
android:textSize="14sp"
android:paddingStart="8dp"
android:paddingEnd="8dp"/>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginTop="8dp"
android:gravity="center_vertical">
<Spinner
android:id="@+id/spinnerStore"
android:layout_width="0dp"
android:layout_height="44dp"
android:layout_weight="1"
android:background="@drawable/bg_spinner"
android:paddingStart="12dp"
android:paddingEnd="8dp"
android:layout_marginEnd="4dp"/>
<Spinner
android:id="@+id/spinnerPaymentMethod"
android:layout_width="0dp"
android:layout_height="44dp"
android:layout_weight="1"
android:background="@drawable/bg_spinner"
android:paddingStart="12dp"
android:paddingEnd="8dp"
android:layout_marginEnd="4dp"
android:layout_marginStart="4dp"/>
<Button
android:id="@+id/btnOpenRefund"
android:layout_width="0dp"
android:layout_height="44dp"
android:layout_weight="1"
android:text="Process Refund"
android:backgroundTint="@color/accent_coral"
android:textColor="@color/white"
android:textSize="12sp"
android:insetTop="0dp"
android:insetBottom="0dp"
android:layout_marginStart="4dp"/>
</LinearLayout>
</LinearLayout>
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout <androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/swipeRefreshSale" android:id="@+id/swipeRefreshSale"
@@ -68,7 +145,8 @@
android:id="@+id/recyclerViewSales" android:id="@+id/recyclerViewSales"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:padding="8dp"/> android:padding="8dp"
android:clipToPadding="false"/>
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout> </androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
@@ -85,5 +163,4 @@
app:srcCompat="@android:drawable/ic_input_add" app:srcCompat="@android:drawable/ic_input_add"
app:tint="@color/white"/> app:tint="@color/white"/>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@@ -46,7 +46,6 @@
android:orientation="vertical" android:orientation="vertical"
android:padding="24dp"> android:padding="24dp">
<!-- Sale Info Card -->
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
@@ -65,7 +64,6 @@
android:layout_gravity="end" android:layout_gravity="end"
android:layout_marginBottom="8dp"/> android:layout_marginBottom="8dp"/>
<!-- Store -->
<TextView <TextView
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
@@ -80,7 +78,6 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginBottom="16dp"/> android:layout_marginBottom="16dp"/>
<!-- Customer (optional) -->
<TextView <TextView
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
@@ -110,6 +107,54 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginBottom="16dp"/> android:layout_marginBottom="16dp"/>
<LinearLayout
android:id="@+id/llExtraInfo"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:visibility="gone">
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Channel"
android:textColor="@color/text_dark"
android:textSize="12sp"/>
<TextView
android:id="@+id/tvSaleChannel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="—"
android:textSize="14sp"
android:layout_marginTop="4dp"/>
</LinearLayout>
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Points Earned"
android:textColor="@color/text_dark"
android:textSize="12sp"/>
<TextView
android:id="@+id/tvSalePoints"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="0"
android:textSize="14sp"
android:layout_marginTop="4dp"/>
</LinearLayout>
</LinearLayout>
</LinearLayout> </LinearLayout>
<!-- Items Card --> <!-- Items Card -->
@@ -130,8 +175,8 @@
android:textStyle="bold" android:textStyle="bold"
android:layout_marginBottom="12dp"/> android:layout_marginBottom="12dp"/>
<!-- Item row -->
<LinearLayout <LinearLayout
android:id="@+id/llAddItemRow"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="horizontal" android:orientation="horizontal"
@@ -164,24 +209,91 @@
</LinearLayout> </LinearLayout>
<!-- Items list container -->
<LinearLayout <LinearLayout
android:id="@+id/llSaleItems" android:id="@+id/llSaleItems"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="vertical"/> android:orientation="vertical"/>
<!-- Total --> <View
<TextView android:layout_width="match_parent"
android:id="@+id/tvSaleDetailTotal" android:layout_height="1dp"
android:layout_width="wrap_content" android:background="#EEEEEE"
android:layout_marginTop="12dp"
android:layout_marginBottom="12dp"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="Total: $0.00" android:orientation="vertical">
android:textSize="18sp"
android:textStyle="bold" <LinearLayout
android:textColor="@color/accent_coral" android:layout_width="match_parent"
android:layout_gravity="end" android:layout_height="wrap_content"
android:layout_marginTop="12dp"/> android:orientation="horizontal">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Subtotal:"/>
<TextView
android:id="@+id/tvSaleSubtotal"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="$0.00"/>
</LinearLayout>
<LinearLayout
android:id="@+id/llCouponDiscount"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginTop="4dp"
android:visibility="gone">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Coupon Discount:"/>
<TextView
android:id="@+id/tvSaleCouponDiscount"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="-$0.00"
android:textColor="@color/status_adopted"/>
</LinearLayout>
<LinearLayout
android:id="@+id/llEmployeeDiscount"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginTop="4dp"
android:visibility="gone">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Employee Discount:"/>
<TextView
android:id="@+id/tvSaleEmployeeDiscount"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="-$0.00"
android:textColor="@color/status_adopted"/>
</LinearLayout>
<TextView
android:id="@+id/tvSaleDetailTotal"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Total: $0.00"
android:textSize="18sp"
android:textStyle="bold"
android:textColor="@color/accent_coral"
android:layout_gravity="end"
android:layout_marginTop="12dp"/>
</LinearLayout>
</LinearLayout> </LinearLayout>

View File

@@ -12,6 +12,7 @@
android:orientation="vertical"> android:orientation="vertical">
<LinearLayout <LinearLayout
android:id="@+id/headerStaff"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="56dp" android:layout_height="56dp"
android:background="@color/primary_dark" android:background="@color/primary_dark"
@@ -34,22 +35,63 @@
android:text="Staff Accounts" android:text="Staff Accounts"
android:textColor="@color/white" android:textColor="@color/white"
android:textSize="20sp" android:textSize="20sp"
android:textStyle="bold"/> android:textStyle="bold"
android:layout_marginStart="8dp"/>
<ImageButton
android:id="@+id/btnToggleFilterStaff"
android:layout_width="48dp"
android:layout_height="48dp"
android:src="@android:drawable/ic_menu_search"
android:background="?attr/selectableItemBackgroundBorderless"
app:tint="@color/white"
android:contentDescription="Toggle filter"/>
</LinearLayout> </LinearLayout>
<EditText <LinearLayout
android:id="@+id/etSearchStaff" android:id="@+id/layoutFilterStaff"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_margin="8dp" android:orientation="vertical"
android:hint="Search by name, username or email..." android:paddingStart="12dp"
android:inputType="text" android:paddingEnd="12dp"
android:drawableStart="@android:drawable/ic_menu_search" android:paddingTop="10dp"
android:drawablePadding="8dp" android:paddingBottom="10dp"
android:background="@android:color/white" android:visibility="gone"
android:padding="12dp" android:background="@color/primary_dark"
android:textColor="@color/text_dark"/> android:elevation="4dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="44dp"
android:background="@drawable/bg_search_bar"
android:gravity="center_vertical"
android:paddingStart="12dp"
android:paddingEnd="12dp">
<ImageView
android:layout_width="18dp"
android:layout_height="18dp"
android:src="@android:drawable/ic_menu_search"
android:alpha="0.6"/>
<EditText
android:id="@+id/etSearchStaff"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:hint="Search staff..."
android:inputType="text"
android:background="@android:color/transparent"
android:textColor="@color/text_dark"
android:textColorHint="#99000000"
android:textSize="14sp"
android:paddingStart="8dp"
android:paddingEnd="8dp"/>
</LinearLayout>
</LinearLayout>
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout <androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/swipeRefreshStaff" android:id="@+id/swipeRefreshStaff"
@@ -78,5 +120,3 @@
app:tint="@color/white"/> app:tint="@color/white"/>
</androidx.coordinatorlayout.widget.CoordinatorLayout> </androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@@ -1,99 +1,117 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_margin="8dp" android:orientation="vertical"
app:cardCornerRadius="8dp" android:paddingStart="16dp"
app:cardElevation="2dp"> android:paddingEnd="16dp"
android:paddingTop="16dp"
android:paddingBottom="16dp"
android:background="@color/white">
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="vertical" android:orientation="horizontal"
android:padding="16dp"> android:gravity="center_vertical">
<com.google.android.material.imageview.ShapeableImageView
android:id="@+id/ivEmployeeProfile"
android:layout_width="60dp"
android:layout_height="60dp"
android:layout_marginEnd="16dp"
android:scaleType="centerCrop"
android:src="@drawable/placeholder"
app:shapeAppearanceOverlay="@style/CircleImageView"
app:strokeWidth="2dp"
app:strokeColor="#BDBDBD"
android:padding="2dp"
android:contentDescription="Employee Profile Image" />
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="horizontal"> android:layout_weight="1"
android:orientation="vertical">
<LinearLayout <LinearLayout
android:layout_width="0dp" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_weight="1" android:orientation="horizontal"
android:orientation="vertical"> android:gravity="center_vertical">
<TextView <TextView
android:id="@+id/tvEmployeeFullName" android:id="@+id/tvEmployeeFullName"
android:layout_width="wrap_content" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:textSize="16sp" android:layout_weight="1"
android:textStyle="bold" android:ellipsize="end"
android:textColor="@color/text_dark"/> android:maxLines="1"
android:text="Full Name"
<TextView android:textColor="@color/text_dark"
android:id="@+id/tvEmployeeUsername" android:textSize="18sp"
android:layout_width="wrap_content" android:textStyle="bold" />
android:layout_height="wrap_content"
android:textSize="13sp"
android:textColor="@color/text_light"
android:layout_marginTop="2dp"/>
<TextView
android:id="@+id/tvEmployeeEmail"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="12sp"
android:textColor="@color/text_light"
android:layout_marginTop="2dp"/>
<TextView
android:id="@+id/tvEmployeePhone"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="12sp"
android:textColor="@color/text_light"
android:layout_marginTop="2dp"/>
</LinearLayout>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
android:gravity="end"
android:layout_gravity="center_vertical">
<TextView <TextView
android:id="@+id/tvEmployeeRole" android:id="@+id/tvEmployeeRole"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:paddingStart="10dp" android:paddingStart="8dp"
android:paddingEnd="10dp" android:paddingTop="3dp"
android:paddingTop="4dp" android:paddingEnd="8dp"
android:paddingBottom="4dp" android:paddingBottom="3dp"
android:textSize="11sp" android:text="ROLE"
android:textAllCaps="true"
android:textColor="@color/white" android:textColor="@color/white"
android:layout_marginBottom="4dp"/> android:textSize="11sp" />
</LinearLayout>
<TextView
android:id="@+id/tvEmployeeUsername"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:text="@username"
android:textColor="#888888"
android:textSize="14sp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:layout_marginTop="4dp">
<TextView
android:id="@+id/tvEmployeeEmail"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="email@example.com"
android:textColor="#888888"
android:textSize="13sp"
android:ellipsize="end"
android:maxLines="1"/>
<TextView <TextView
android:id="@+id/tvEmployeeStatus" android:id="@+id/tvEmployeeStatus"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:paddingStart="10dp" android:text="Active"
android:paddingEnd="10dp" android:textColor="@color/accent_coral"
android:paddingTop="4dp" android:textSize="13sp"
android:paddingBottom="4dp" android:textStyle="bold" />
android:textSize="11sp"
android:textColor="@color/white"/>
</LinearLayout> </LinearLayout>
</LinearLayout> </LinearLayout>
</LinearLayout> </LinearLayout>
</androidx.cardview.widget.CardView> <View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="#F0F0F0"
android:layout_marginTop="12dp"/>
</LinearLayout>

View File

@@ -1,23 +1,64 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_margin="8dp" android:orientation="horizontal"
app:cardCornerRadius="8dp" android:background="@color/white"
app:cardElevation="2dp"> android:foreground="?attr/selectableItemBackground"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:paddingTop="16dp"
android:paddingBottom="16dp"
android:gravity="center_vertical">
<CheckBox
android:id="@+id/cbSelectSale"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="12dp"
android:visibility="gone"
android:clickable="false"
android:focusable="false"/>
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="vertical" android:orientation="vertical">
android:padding="16dp">
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="horizontal"> android:orientation="horizontal"
android:gravity="center_vertical">
<TextView
android:id="@+id/tvSaleId"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:ellipsize="end"
android:maxLines="1"
android:text="Sale #—"
android:textColor="@color/text_dark"
android:textSize="18sp"
android:textStyle="bold" />
<TextView
android:id="@+id/tvSaleTotal"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="$0.00"
android:textColor="#4CAF50"
android:textSize="16sp"
android:textStyle="bold" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginTop="4dp">
<LinearLayout <LinearLayout
android:layout_width="0dp" android:layout_width="0dp"
@@ -25,74 +66,58 @@
android:layout_weight="1" android:layout_weight="1"
android:orientation="vertical"> android:orientation="vertical">
<TextView
android:id="@+id/tvSaleId"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="16sp"
android:textStyle="bold"
android:textColor="@color/text_dark"/>
<TextView <TextView
android:id="@+id/tvSaleEmployee" android:id="@+id/tvSaleEmployee"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:textSize="13sp" android:text="By: —"
android:textColor="@color/text_light" android:textColor="#888888"
android:layout_marginTop="2dp"/> android:textSize="14sp" />
<TextView <TextView
android:id="@+id/tvSaleDate" android:id="@+id/tvSaleDate"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:textSize="12sp" android:layout_marginTop="2dp"
android:textColor="@color/text_light" android:text="—"
android:layout_marginTop="2dp"/> android:textColor="#888888"
android:textSize="14sp" />
<TextView <TextView
android:id="@+id/tvSalePayment" android:id="@+id/tvSalePayment"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:textSize="12sp" android:layout_marginTop="2dp"
android:textColor="@color/text_light" android:text="—"
android:layout_marginTop="2dp"/> android:textColor="#888888"
android:textSize="14sp" />
</LinearLayout> </LinearLayout>
<LinearLayout <TextView
android:id="@+id/tvSaleRefundBadge"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="vertical" android:background="@color/status_adopted"
android:gravity="end" android:paddingStart="8dp"
android:layout_gravity="center_vertical"> android:paddingTop="3dp"
android:paddingEnd="8dp"
<TextView android:paddingBottom="3dp"
android:id="@+id/tvSaleTotal" android:text="REFUND"
android:layout_width="wrap_content" android:textAllCaps="true"
android:layout_height="wrap_content" android:textColor="@color/white"
android:textSize="18sp" android:textSize="11sp"
android:textStyle="bold" android:visibility="gone"
android:textColor="@color/accent_coral"/> android:layout_gravity="bottom"/>
<TextView
android:id="@+id/tvSaleRefundBadge"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="REFUND"
android:textSize="10sp"
android:textColor="@color/white"
android:background="@color/accent_coral"
android:paddingStart="6dp"
android:paddingEnd="6dp"
android:paddingTop="2dp"
android:paddingBottom="2dp"
android:layout_marginTop="4dp"
android:visibility="gone"/>
</LinearLayout>
</LinearLayout> </LinearLayout>
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="#F0F0F0"
android:layout_marginTop="12dp" />
</LinearLayout> </LinearLayout>
</androidx.cardview.widget.CardView> </LinearLayout>

View File

@@ -0,0 +1,101 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:background="@color/white"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:paddingTop="16dp"
android:paddingBottom="16dp"
android:gravity="center_vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical">
<TextView
android:id="@+id/tvSaleId"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Sale #123"
android:textSize="18sp"
android:textStyle="bold"
android:textColor="@color/text_dark"/>
<TextView
android:id="@+id/tvSaleTotal"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="$0.00"
android:textSize="18sp"
android:textStyle="bold"
android:textColor="@color/accent_coral"/>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginTop="4dp">
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical">
<TextView
android:id="@+id/tvSaleEmployee"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="By: Employee"
android:textSize="14sp"
android:textColor="#888888"/>
<TextView
android:id="@+id/tvSaleDate"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="2023-01-01"
android:textSize="14sp"
android:textColor="#888888"
android:layout_marginTop="2dp"/>
<TextView
android:id="@+id/tvSalePayment"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Cash"
android:textSize="14sp"
android:textColor="#888888"
android:layout_marginTop="2dp"/>
</LinearLayout>
<TextView
android:id="@+id/tvSaleRefundBadge"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="REFUND"
android:textSize="12sp"
android:textStyle="bold"
android:textColor="@color/white"
android:background="@color/accent_coral"
android:paddingStart="8dp"
android:paddingEnd="8dp"
android:paddingTop="2dp"
android:paddingBottom="2dp"
android:layout_gravity="bottom"
android:visibility="gone"/>
</LinearLayout>
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="#F0F0F0"
android:layout_marginTop="12dp"/>
</LinearLayout>

View File

@@ -25,8 +25,10 @@ public class SaleController {
@PreAuthorize("hasAnyRole('STAFF', 'ADMIN')") @PreAuthorize("hasAnyRole('STAFF', 'ADMIN')")
public ResponseEntity<Page<SaleResponse>> getAllSales( public ResponseEntity<Page<SaleResponse>> getAllSales(
@RequestParam(required = false) String q, @RequestParam(required = false) String q,
@RequestParam(required = false) String paymentMethod,
@RequestParam(required = false) Long storeId,
Pageable pageable) { Pageable pageable) {
return ResponseEntity.ok(saleService.getAllSales(q, pageable)); return ResponseEntity.ok(saleService.getAllSales(q, paymentMethod, storeId, pageable));
} }
@GetMapping("/{id}") @GetMapping("/{id}")

View File

@@ -14,10 +14,14 @@ import java.util.List;
public interface SaleRepository extends JpaRepository<Sale, Long> { public interface SaleRepository extends JpaRepository<Sale, Long> {
@Query("SELECT s FROM Sale s WHERE " + @Query("SELECT s FROM Sale s WHERE " +
"(:q IS NULL OR (" +
"LOWER(s.employee.firstName) LIKE LOWER(CONCAT('%', :q, '%')) OR " + "LOWER(s.employee.firstName) LIKE LOWER(CONCAT('%', :q, '%')) OR " +
"LOWER(s.employee.lastName) LIKE LOWER(CONCAT('%', :q, '%')) OR " + "LOWER(s.employee.lastName) LIKE LOWER(CONCAT('%', :q, '%')) OR " +
"LOWER(s.store.storeName) LIKE LOWER(CONCAT('%', :q, '%'))") "LOWER(s.store.storeName) LIKE LOWER(CONCAT('%', :q, '%'))" +
Page<Sale> searchSales(@Param("q") String query, Pageable pageable); ")) AND " +
"(:paymentMethod IS NULL OR LOWER(s.paymentMethod) = LOWER(:paymentMethod)) AND " +
"(:storeId IS NULL OR s.store.storeId = :storeId)")
Page<Sale> searchSales(@Param("q") String query, @Param("paymentMethod") String paymentMethod, @Param("storeId") Long storeId, Pageable pageable);
List<Sale> findByOriginalSaleSaleId(Long originalSaleId); List<Sale> findByOriginalSaleSaleId(Long originalSaleId);
} }

View File

@@ -39,13 +39,8 @@ public class SaleService {
} }
@Transactional(readOnly = true) @Transactional(readOnly = true)
public Page<SaleResponse> getAllSales(String query, Pageable pageable) { public Page<SaleResponse> getAllSales(String query, String paymentMethod, Long storeId, Pageable pageable) {
Page<Sale> sales; Page<Sale> sales = saleRepository.searchSales(normalizeFilter(query), normalizeFilter(paymentMethod), storeId, pageable);
if (query != null && !query.trim().isEmpty()) {
sales = saleRepository.searchSales(query, pageable);
} else {
sales = saleRepository.findAll(pageable);
}
return sales.map(this::mapToResponse); return sales.map(this::mapToResponse);
} }
@@ -236,6 +231,14 @@ public class SaleService {
return response; return response;
} }
private String normalizeFilter(String value) {
if (value == null) {
return null;
}
String trimmed = value.trim();
return trimmed.isEmpty() ? null : trimmed;
}
String normalizePaymentMethod(String paymentMethod) { String normalizePaymentMethod(String paymentMethod) {
if (paymentMethod == null) { if (paymentMethod == null) {
return null; return null;