added Analytics filter

This commit is contained in:
Alex
2026-04-10 05:03:36 -06:00
parent 49ee40b912
commit 9d7c577f85
8 changed files with 730 additions and 103 deletions

View File

@@ -2,8 +2,7 @@ package com.example.petstoremobile.dtos;
public class EmployeeDTO { public class EmployeeDTO {
private long EmployeeId; private Long id;
private Long userId;
private String username; private String username;
private String firstName; private String firstName;
private String lastName; private String lastName;
@@ -11,16 +10,18 @@ public class EmployeeDTO {
private String email; private String email;
private String phone; private String phone;
private String role; private String role;
private String staffRole;
private Boolean active; private Boolean active;
private String createAt; private Integer loyaltyPoints;
private Long primaryStoreId;
private String createdAt;
private String updatedAt; private String updatedAt;
private String password;
public EmployeeDTO() {}
// Constructor for create and update the employee
public EmployeeDTO(String username, String password, String firstName, String lastName, public EmployeeDTO(String username, String password, String firstName, String lastName,
String email, String phone, String role, boolean active) { String email, String phone, String role, String staffRole, boolean active, Long primaryStoreId) {
this.username = username; this.username = username;
this.password = password; this.password = password;
this.firstName = firstName; this.firstName = firstName;
@@ -28,75 +29,128 @@ public class EmployeeDTO {
this.email = email; this.email = email;
this.phone = phone; this.phone = phone;
this.role = role; this.role = role;
this.staffRole = staffRole;
this.active = active; this.active = active;
} this.primaryStoreId = primaryStoreId;
// password field for request only
private String password;
public long getEmployeeId() {
return EmployeeId;
} }
public Long getUserId() { public Long getId() {
return id;
}
return userId; public void setId(Long id) {
this.id = id;
} }
public String getUsername() { public String getUsername() {
return username; return username;
} }
public String getFirstName() { public void setUsername(String username) {
this.username = username;
}
public String getFirstName() {
return firstName; return firstName;
} }
public String getLastName() { public void setFirstName(String firstName) {
this.firstName = firstName;
}
public String getLastName() {
return lastName; return lastName;
} }
public String getFullName() { public void setLastName(String lastName) {
this.lastName = lastName;
}
public String getFullName() {
return fullName; return fullName;
} }
public String getEmail() { public void setFullName(String fullName) {
this.fullName = fullName;
}
public String getEmail() {
return email; return email;
} }
public String getPhone() {
public void setEmail(String email) {
this.email = email;
}
public String getPhone() {
return phone; return phone;
} }
public String getRole() { public void setPhone(String phone) {
this.phone = phone;
}
public String getRole() {
return role; return role;
} }
public Boolean getActive() { public void setRole(String role) {
this.role = role;
}
public String getStaffRole() {
return staffRole;
}
public void setStaffRole(String staffRole) {
this.staffRole = staffRole;
}
public Boolean getActive() {
return active; return active;
} }
public String getCreateAt() { public void setActive(Boolean active) {
this.active = active;
}
return createAt; public Integer getLoyaltyPoints() {
return loyaltyPoints;
}
public void setLoyaltyPoints(Integer loyaltyPoints) {
this.loyaltyPoints = loyaltyPoints;
}
public Long getPrimaryStoreId() {
return primaryStoreId;
}
public void setPrimaryStoreId(Long primaryStoreId) {
this.primaryStoreId = primaryStoreId;
}
public String getCreatedAt() {
return createdAt;
}
public void setCreatedAt(String createdAt) {
this.createdAt = createdAt;
} }
public String getUpdatedAt() { public String getUpdatedAt() {
return updatedAt; return updatedAt;
} }
public String getPassword() { public void setUpdatedAt(String updatedAt) {
this.updatedAt = updatedAt;
}
public String getPassword() {
return password; return password;
} }
public void setPassword(String password) {
this.password = password;
}
} }

View File

@@ -8,6 +8,7 @@ import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment; import androidx.fragment.app.Fragment;
import androidx.lifecycle.ViewModelProvider; import androidx.lifecycle.ViewModelProvider;
import com.example.petstoremobile.databinding.FragmentAnalyticsBinding; import com.example.petstoremobile.databinding.FragmentAnalyticsBinding;
import com.example.petstoremobile.utils.SpinnerUtils;
import com.example.petstoremobile.utils.UIUtils; import com.example.petstoremobile.utils.UIUtils;
import com.example.petstoremobile.viewmodels.AnalyticsViewModel; import com.example.petstoremobile.viewmodels.AnalyticsViewModel;
import dagger.hilt.android.AndroidEntryPoint; import dagger.hilt.android.AndroidEntryPoint;
@@ -20,6 +21,10 @@ public class AnalyticsFragment extends Fragment {
private FragmentAnalyticsBinding binding; private FragmentAnalyticsBinding binding;
private AnalyticsViewModel viewModel; private AnalyticsViewModel viewModel;
private boolean filtersExpanded = false;
private static final String[] TOP_N_OPTIONS = {"5", "10", "15", "20"};
private static final int[] TOP_N_VALUES = { 5, 10, 15, 20 };
@Override @Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
@@ -27,16 +32,111 @@ public class AnalyticsFragment extends Fragment {
binding = FragmentAnalyticsBinding.inflate(inflater, container, false); binding = FragmentAnalyticsBinding.inflate(inflater, container, false);
viewModel = new ViewModelProvider(this).get(AnalyticsViewModel.class); viewModel = new ViewModelProvider(this).get(AnalyticsViewModel.class);
setupFilterPanel();
observeViewModel(); observeViewModel();
viewModel.loadAnalytics(); viewModel.loadAnalytics();
binding.btnRefreshAnalytics.setOnClickListener(v -> viewModel.loadAnalytics()); binding.btnRefreshAnalytics.setOnClickListener(v -> viewModel.loadAnalytics());
UIUtils.setupHamburgerMenu(binding.btnHamburgerAnalytics, this); UIUtils.setupHamburgerMenu(binding.btnHamburgerAnalytics, this);
return binding.getRoot(); return binding.getRoot();
} }
// Filter Panel
private void setupFilterPanel() {
SpinnerUtils.setupStringSpinner(requireContext(), binding.spinnerTopN, TOP_N_OPTIONS);
// Toggle expand/collapse
binding.rowFilterHeader.setOnClickListener(v -> toggleFilters());
// Date pickers
binding.etFilterStartDate.setOnClickListener(v ->
UIUtils.showDatePicker(requireContext(), binding.etFilterStartDate, this::updateFilterSummary));
binding.etFilterEndDate.setOnClickListener(v ->
UIUtils.showDatePicker(requireContext(), binding.etFilterEndDate, this::updateFilterSummary));
// Quick presets
binding.btnPresetToday.setOnClickListener(v -> applyPreset(0, 0));
binding.btnPreset7D.setOnClickListener(v -> applyPreset(-6, 0));
binding.btnPreset30D.setOnClickListener(v -> applyPreset(-29, 0));
binding.btnPreset3M.setOnClickListener(v -> applyPreset(-89, 0));
binding.btnPreset1Y.setOnClickListener(v -> applyPreset(-364, 0));
binding.btnPresetAll.setOnClickListener(v -> {
binding.etFilterStartDate.setText("");
binding.etFilterEndDate.setText("");
updateFilterSummary();
});
binding.btnFilterApply.setOnClickListener(v -> applyFiltersFromUI());
binding.btnFilterReset.setOnClickListener(v -> resetFilters());
}
private void toggleFilters() {
filtersExpanded = !filtersExpanded;
binding.llFilterContent.setVisibility(filtersExpanded ? View.VISIBLE : View.GONE);
binding.tvFilterToggleIcon.setText(filtersExpanded ? "" : "");
}
private void applyPreset(int startOffset, int endOffset) {
binding.etFilterStartDate.setText(getDateString(startOffset));
binding.etFilterEndDate.setText(getDateString(endOffset));
updateFilterSummary();
applyFiltersFromUI();
}
private void applyFiltersFromUI() {
AnalyticsViewModel.FilterState filter = new AnalyticsViewModel.FilterState();
filter.startDate = binding.etFilterStartDate.getText().toString().trim();
filter.endDate = binding.etFilterEndDate.getText().toString().trim();
Object pm = binding.spinnerFilterPayment.getSelectedItem();
filter.paymentMethod = pm != null ? pm.toString() : "All";
int topNPos = binding.spinnerTopN.getSelectedItemPosition();
filter.topN = (topNPos >= 0 && topNPos < TOP_N_VALUES.length) ? TOP_N_VALUES[topNPos] : 5;
updateFilterSummary();
viewModel.applyFilter(filter);
}
private void resetFilters() {
binding.etFilterStartDate.setText("");
binding.etFilterEndDate.setText("");
binding.spinnerTopN.setSelection(0);
// Reset payment method to "All"
SpinnerUtils.setSelectionByValue(binding.spinnerFilterPayment, "All");
updateFilterSummary();
viewModel.resetFilter();
}
private void updateFilterSummary() {
String start = binding.etFilterStartDate.getText().toString().trim();
String end = binding.etFilterEndDate.getText().toString().trim();
if (start.isEmpty() && end.isEmpty()) {
binding.tvFilterSummary.setText("All time");
} else if (start.isEmpty()) {
binding.tvFilterSummary.setText("Up to " + shortDate(end));
} else if (end.isEmpty()) {
binding.tvFilterSummary.setText("From " + shortDate(start));
} else {
binding.tvFilterSummary.setText(shortDate(start) + " " + shortDate(end));
}
}
private String shortDate(String date) {
return (date != null && date.length() >= 10) ? date.substring(5) : date;
}
private String getDateString(int offsetDays) {
Calendar c = Calendar.getInstance();
c.add(Calendar.DAY_OF_YEAR, offsetDays);
return String.format(Locale.US, "%04d-%02d-%02d",
c.get(Calendar.YEAR), c.get(Calendar.MONTH) + 1, c.get(Calendar.DAY_OF_MONTH));
}
// ViewModel Observation
private void observeViewModel() { private void observeViewModel() {
viewModel.getAnalyticsData().observe(getViewLifecycleOwner(), this::computeAndDisplay); viewModel.getAnalyticsData().observe(getViewLifecycleOwner(), this::computeAndDisplay);
@@ -53,6 +153,15 @@ public class AnalyticsFragment extends Fragment {
viewModel.getErrorMessage().observe(getViewLifecycleOwner(), error -> { viewModel.getErrorMessage().observe(getViewLifecycleOwner(), error -> {
if (error != null) showError(error); if (error != null) showError(error);
}); });
viewModel.getAvailablePaymentMethods().observe(getViewLifecycleOwner(), methods -> {
if (methods == null || methods.isEmpty()) return;
String currentSelection = binding.spinnerFilterPayment.getSelectedItem() != null
? binding.spinnerFilterPayment.getSelectedItem().toString() : "All";
SpinnerUtils.setupStringSpinner(requireContext(), binding.spinnerFilterPayment,
methods.toArray(new String[0]));
SpinnerUtils.setSelectionByValue(binding.spinnerFilterPayment, currentSelection);
});
} }
@Override @Override
@@ -61,10 +170,12 @@ public class AnalyticsFragment extends Fragment {
binding = null; binding = null;
} }
// Display
private void computeAndDisplay(AnalyticsViewModel.AnalyticsData data) { private void computeAndDisplay(AnalyticsViewModel.AnalyticsData data) {
if (data == null) return; if (data == null) return;
// Summary // Summary cards
binding.tvTotalRevenue.setText("$" + data.totalRevenue.setScale(2, RoundingMode.HALF_UP)); binding.tvTotalRevenue.setText("$" + data.totalRevenue.setScale(2, RoundingMode.HALF_UP));
binding.tvTotalTransactions.setText(String.valueOf(data.totalTransactions)); binding.tvTotalTransactions.setText(String.valueOf(data.totalTransactions));
binding.tvAvgTransaction.setText("$" + data.avgTransaction); binding.tvAvgTransaction.setText("$" + data.avgTransaction);
@@ -73,11 +184,12 @@ public class AnalyticsFragment extends Fragment {
// Top Revenue Products // Top Revenue Products
binding.llTopRevenue.removeAllViews(); binding.llTopRevenue.removeAllViews();
if (data.topRevenueProducts != null && !data.topRevenueProducts.isEmpty()) { if (data.topRevenueProducts != null && !data.topRevenueProducts.isEmpty()) {
BigDecimal maxRevenue = data.topRevenueProducts.get(0).getValue(); BigDecimal maxRev = data.topRevenueProducts.get(0).getValue();
if (maxRevenue.compareTo(BigDecimal.ZERO) == 0) maxRevenue = BigDecimal.ONE; if (maxRev.compareTo(BigDecimal.ZERO) == 0) maxRev = BigDecimal.ONE;
for (Map.Entry<String, BigDecimal> e : data.topRevenueProducts) { for (Map.Entry<String, BigDecimal> e : data.topRevenueProducts) {
addBarRow(binding.llTopRevenue, e.getKey(), "$" + e.getValue().setScale(2, RoundingMode.HALF_UP), addBarRow(binding.llTopRevenue, e.getKey(),
e.getValue().floatValue() / maxRevenue.floatValue(), "#ff6b35"); "$" + e.getValue().setScale(2, RoundingMode.HALF_UP),
e.getValue().floatValue() / maxRev.floatValue(), "#ff6b35");
} }
} else { } else {
addEmptyRow(binding.llTopRevenue, "No data"); addEmptyRow(binding.llTopRevenue, "No data");
@@ -99,15 +211,13 @@ public class AnalyticsFragment extends Fragment {
// Payment Methods // Payment Methods
binding.llPaymentMethods.removeAllViews(); binding.llPaymentMethods.removeAllViews();
if (data.paymentMethodStats != null && !data.paymentMethodStats.isEmpty()) { if (data.paymentMethodStats != null && !data.paymentMethodStats.isEmpty()) {
int maxPayment = data.paymentMethodStats.stream().mapToInt(Map.Entry::getValue).max().orElse(1); int maxPay = data.paymentMethodStats.stream().mapToInt(Map.Entry::getValue).max().orElse(1);
String[] paymentColors = { "#1a759f", "#ff9f1c", "#577590", "#90be6d" }; String[] payColors = { "#1a759f", "#ff9f1c", "#577590", "#90be6d" };
int ci = 0; int ci = 0;
for (Map.Entry<String, Integer> e : data.paymentMethodStats) { for (Map.Entry<String, Integer> e : data.paymentMethodStats) {
addBarRow(binding.llPaymentMethods, e.getKey(), addBarRow(binding.llPaymentMethods, e.getKey(),
e.getValue() + " transactions", e.getValue() + " transactions",
(float) e.getValue() / maxPayment, (float) e.getValue() / maxPay, payColors[ci++ % payColors.length]);
paymentColors[ci % paymentColors.length]);
ci++;
} }
} else { } else {
addEmptyRow(binding.llPaymentMethods, "No data"); addEmptyRow(binding.llPaymentMethods, "No data");
@@ -116,36 +226,37 @@ public class AnalyticsFragment extends Fragment {
// Employee Performance // Employee Performance
binding.llEmployeePerformance.removeAllViews(); binding.llEmployeePerformance.removeAllViews();
if (data.employeePerformance != null && !data.employeePerformance.isEmpty()) { if (data.employeePerformance != null && !data.employeePerformance.isEmpty()) {
BigDecimal maxEmp = data.employeePerformance.get(data.employeePerformance.size() - 1).getValue(); BigDecimal maxEmp = data.employeePerformance.get(0).getValue();
if (maxEmp.compareTo(BigDecimal.ZERO) == 0) maxEmp = BigDecimal.ONE; if (maxEmp.compareTo(BigDecimal.ZERO) == 0) maxEmp = BigDecimal.ONE;
maxEmp = data.employeePerformance.get(0).getValue();
if (maxEmp.compareTo(BigDecimal.ZERO) == 0) maxEmp = BigDecimal.ONE;
for (Map.Entry<String, BigDecimal> e : data.employeePerformance) { for (Map.Entry<String, BigDecimal> e : data.employeePerformance) {
addBarRow(binding.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");
} }
} else { } else {
addEmptyRow(binding.llEmployeePerformance, "No data"); addEmptyRow(binding.llEmployeePerformance, "No data");
} }
// Daily Revenue // Daily Revenue
binding.tvDailyRevenueTitle.setText(data.dailyRevenueTitle);
binding.llDailyRevenue.removeAllViews(); binding.llDailyRevenue.removeAllViews();
if (data.dailyRevenue != null && !data.dailyRevenue.isEmpty()) { if (data.dailyRevenue != null && !data.dailyRevenue.isEmpty()) {
BigDecimal maxDaily = data.dailyRevenue.stream().map(Map.Entry::getValue).max(BigDecimal::compareTo).orElse(BigDecimal.ONE); BigDecimal maxDaily = data.dailyRevenue.stream()
.map(Map.Entry::getValue).max(BigDecimal::compareTo).orElse(BigDecimal.ONE);
if (maxDaily.compareTo(BigDecimal.ZERO) == 0) maxDaily = BigDecimal.ONE; if (maxDaily.compareTo(BigDecimal.ZERO) == 0) maxDaily = BigDecimal.ONE;
for (Map.Entry<String, BigDecimal> e : data.dailyRevenue) { for (Map.Entry<String, BigDecimal> e : data.dailyRevenue) {
String label = e.getKey().length() >= 10 ? e.getKey().substring(5) : e.getKey(); String label = e.getKey().length() >= 10 ? e.getKey().substring(5) : e.getKey();
addBarRow(binding.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");
} }
} else {
addEmptyRow(binding.llDailyRevenue, "No data");
} }
} }
// Chart Helpers
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; if (getContext() == null) return;
LinearLayout row = new LinearLayout(getContext()); LinearLayout row = new LinearLayout(getContext());
@@ -156,8 +267,7 @@ public class AnalyticsFragment extends Fragment {
labelRow.setOrientation(LinearLayout.HORIZONTAL); labelRow.setOrientation(LinearLayout.HORIZONTAL);
TextView tvLabel = new TextView(getContext()); TextView tvLabel = new TextView(getContext());
tvLabel.setLayoutParams(new LinearLayout.LayoutParams( tvLabel.setLayoutParams(new LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f));
0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f));
tvLabel.setText(label); tvLabel.setText(label);
tvLabel.setTextColor(Color.parseColor("#444441")); tvLabel.setTextColor(Color.parseColor("#444441"));
tvLabel.setTextSize(13f); tvLabel.setTextSize(13f);
@@ -172,22 +282,19 @@ public class AnalyticsFragment extends Fragment {
labelRow.addView(tvValue); labelRow.addView(tvValue);
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);
bgParams.setMargins(0, 4, 0, 0); bgParams.setMargins(0, 4, 0, 0);
barBg.setLayoutParams(bgParams); barBg.setLayoutParams(bgParams);
barBg.setBackgroundColor(Color.parseColor("#EEEEEE")); barBg.setBackgroundColor(Color.parseColor("#EEEEEE"));
float safeRatio = Math.max(0f, Math.min(1f, ratio));
View barFill = new View(getContext()); View barFill = new View(getContext());
LinearLayout.LayoutParams fillParams = new LinearLayout.LayoutParams( barFill.setLayoutParams(new LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.MATCH_PARENT, safeRatio));
0, LinearLayout.LayoutParams.MATCH_PARENT, ratio);
barFill.setLayoutParams(fillParams);
barFill.setBackgroundColor(Color.parseColor(color)); barFill.setBackgroundColor(Color.parseColor(color));
barBg.addView(barFill); barBg.addView(barFill);
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 - safeRatio));
0, LinearLayout.LayoutParams.MATCH_PARENT, 1f - ratio));
barBg.addView(spacer); barBg.addView(spacer);
row.addView(labelRow); row.addView(labelRow);
@@ -205,8 +312,7 @@ public class AnalyticsFragment extends Fragment {
} }
private void showError(String msg) { private void showError(String msg) {
if (getContext() == null || binding == null) if (getContext() == null || binding == null) return;
return;
binding.tvTotalRevenue.setText("Error"); binding.tvTotalRevenue.setText("Error");
binding.tvTotalTransactions.setText(""); binding.tvTotalTransactions.setText("");
binding.tvAvgTransaction.setText(""); binding.tvAvgTransaction.setText("");

View File

@@ -76,7 +76,7 @@ public class StaffFragment extends Fragment implements EmployeeAdapter.OnEmploye
Bundle args = new Bundle(); Bundle args = new Bundle();
if (position != -1) { if (position != -1) {
EmployeeDTO e = staffList.get(position); EmployeeDTO e = staffList.get(position);
args.putLong("employeeId", e.getEmployeeId()); args.putLong("employeeId", e.getId());
args.putString("username", e.getUsername() != null ? e.getUsername() : ""); args.putString("username", e.getUsername() != null ? e.getUsername() : "");
args.putString("firstName", e.getFirstName() != null ? e.getFirstName() : ""); args.putString("firstName", e.getFirstName() != null ? e.getFirstName() : "");
args.putString("lastName", e.getLastName() != null ? e.getLastName() : ""); args.putString("lastName", e.getLastName() != null ? e.getLastName() : "");

View File

@@ -9,6 +9,7 @@ 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.databinding.FragmentStaffDetailBinding; import com.example.petstoremobile.databinding.FragmentStaffDetailBinding;
import com.example.petstoremobile.dtos.DropdownDTO;
import com.example.petstoremobile.dtos.EmployeeDTO; import com.example.petstoremobile.dtos.EmployeeDTO;
import com.example.petstoremobile.utils.DialogUtils; import com.example.petstoremobile.utils.DialogUtils;
import com.example.petstoremobile.utils.InputValidator; import com.example.petstoremobile.utils.InputValidator;
@@ -16,6 +17,9 @@ import com.example.petstoremobile.utils.SpinnerUtils;
import com.example.petstoremobile.utils.UIUtils; import com.example.petstoremobile.utils.UIUtils;
import com.example.petstoremobile.viewmodels.StaffDetailViewModel; import com.example.petstoremobile.viewmodels.StaffDetailViewModel;
import com.example.petstoremobile.utils.Resource; import com.example.petstoremobile.utils.Resource;
import java.util.List;
import dagger.hilt.android.AndroidEntryPoint; import dagger.hilt.android.AndroidEntryPoint;
@AndroidEntryPoint @AndroidEntryPoint
@@ -25,8 +29,11 @@ public class StaffDetailFragment extends Fragment {
private StaffDetailViewModel viewModel; private StaffDetailViewModel viewModel;
private final String[] ROLES = {"STAFF", "ADMIN"}; private final String[] ROLES = {"STAFF", "ADMIN"};
private final String[] STAFF_ROLES = {"STORE_MANAGER", "SALES_ASSOCIATE", "GROOMER", "VETERINARIAN"};
private final String[] STATUSES = {"Active", "Inactive"}; private final String[] STATUSES = {"Active", "Inactive"};
private long preselectedStoreId = -1;
@Override @Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) { Bundle savedInstanceState) {
@@ -34,6 +41,8 @@ public class StaffDetailFragment extends Fragment {
viewModel = new ViewModelProvider(this).get(StaffDetailViewModel.class); viewModel = new ViewModelProvider(this).get(StaffDetailViewModel.class);
setupSpinners(); setupSpinners();
observeViewModel();
loadStores();
handleArguments(); handleArguments();
binding.btnStaffBack.setOnClickListener(v -> navigateBack()); binding.btnStaffBack.setOnClickListener(v -> navigateBack());
@@ -45,11 +54,30 @@ public class StaffDetailFragment extends Fragment {
return binding.getRoot(); return binding.getRoot();
} }
private void observeViewModel() {
viewModel.getStoreList().observe(getViewLifecycleOwner(), list -> refreshStoreSpinner());
}
private void setupSpinners() { private void setupSpinners() {
SpinnerUtils.setupStringSpinner(requireContext(), binding.spinnerStaffRole, ROLES); SpinnerUtils.setupStringSpinner(requireContext(), binding.spinnerStaffRole, ROLES);
SpinnerUtils.setupStringSpinner(requireContext(), binding.spinnerStaffType, STAFF_ROLES);
SpinnerUtils.setupStringSpinner(requireContext(), binding.spinnerStaffStatus, STATUSES); SpinnerUtils.setupStringSpinner(requireContext(), binding.spinnerStaffStatus, STATUSES);
} }
private void loadStores() {
viewModel.loadStores().observe(getViewLifecycleOwner(), resource -> {
if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) {
viewModel.setStoreList(resource.data);
}
});
}
private void refreshStoreSpinner() {
SpinnerUtils.populateSpinner(requireContext(), binding.spinnerStaffStore, viewModel.getStoreList().getValue(),
DropdownDTO::getLabel, "-- Select Store --",
preselectedStoreId, DropdownDTO::getId);
}
private void handleArguments() { private void handleArguments() {
Bundle a = getArguments(); Bundle a = getArguments();
if (a != null && a.getBoolean("isEditing", false)) { if (a != null && a.getBoolean("isEditing", false)) {
@@ -59,16 +87,9 @@ public class StaffDetailFragment extends Fragment {
binding.tvStaffMode.setText("Edit Staff Account"); binding.tvStaffMode.setText("Edit Staff Account");
binding.tvStaffId.setText("ID: " + employeeId); binding.tvStaffId.setText("ID: " + employeeId);
binding.tvStaffId.setVisibility(View.VISIBLE); binding.tvStaffId.setVisibility(View.VISIBLE);
binding.etStaffUsername.setText(a.getString("username", ""));
binding.etStaffFirstName.setText(a.getString("firstName", ""));
binding.etStaffLastName.setText(a.getString("lastName", ""));
binding.etStaffEmail.setText(a.getString("email", ""));
binding.etStaffPhone.setText(a.getString("phone", ""));
binding.btnDeleteStaff.setVisibility(View.VISIBLE); binding.btnDeleteStaff.setVisibility(View.VISIBLE);
SpinnerUtils.setSelectionByValue(binding.spinnerStaffRole, a.getString("role", "STAFF")); loadEmployeeData(employeeId);
binding.spinnerStaffStatus.setSelection(a.getBoolean("active", true) ? 0 : 1);
} else { } else {
viewModel.setEmployeeId(-1, false); viewModel.setEmployeeId(-1, false);
binding.tvStaffMode.setText("Add Staff Account"); binding.tvStaffMode.setText("Add Staff Account");
@@ -77,6 +98,29 @@ public class StaffDetailFragment extends Fragment {
} }
} }
private void loadEmployeeData(long id) {
viewModel.loadEmployee(id).observe(getViewLifecycleOwner(), resource -> {
if (resource != null) {
setLoading(resource.status == Resource.Status.LOADING);
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
EmployeeDTO e = resource.data;
binding.etStaffUsername.setText(e.getUsername());
binding.etStaffFirstName.setText(e.getFirstName());
binding.etStaffLastName.setText(e.getLastName());
binding.etStaffEmail.setText(e.getEmail());
binding.etStaffPhone.setText(e.getPhone());
SpinnerUtils.setSelectionByValue(binding.spinnerStaffRole, e.getRole());
SpinnerUtils.setSelectionByValue(binding.spinnerStaffType, e.getStaffRole());
binding.spinnerStaffStatus.setSelection(Boolean.TRUE.equals(e.getActive()) ? 0 : 1);
preselectedStoreId = e.getPrimaryStoreId() != null ? e.getPrimaryStoreId() : -1;
refreshStoreSpinner();
}
}
});
}
private void setLoading(boolean loading) { private void setLoading(boolean loading) {
if (binding != null && binding.progressBar != null) { if (binding != null && binding.progressBar != null) {
binding.progressBar.setVisibility(loading ? View.VISIBLE : View.GONE); binding.progressBar.setVisibility(loading ? View.VISIBLE : View.GONE);
@@ -100,6 +144,7 @@ public class StaffDetailFragment extends Fragment {
if (!InputValidator.isNotEmpty(binding.etStaffLastName, "Last Name")) return; if (!InputValidator.isNotEmpty(binding.etStaffLastName, "Last Name")) return;
if (!InputValidator.isValidEmail(binding.etStaffEmail)) return; if (!InputValidator.isValidEmail(binding.etStaffEmail)) return;
if (!InputValidator.isValidPhone(binding.etStaffPhone)) return; if (!InputValidator.isValidPhone(binding.etStaffPhone)) return;
if (!InputValidator.isSpinnerSelected(binding.spinnerStaffStore, "Primary Store")) return;
String username = binding.etStaffUsername.getText().toString().trim(); String username = binding.etStaffUsername.getText().toString().trim();
String password = binding.etStaffPassword.getText().toString().trim(); String password = binding.etStaffPassword.getText().toString().trim();
@@ -108,8 +153,12 @@ public class StaffDetailFragment extends Fragment {
String email = binding.etStaffEmail.getText().toString().trim(); String email = binding.etStaffEmail.getText().toString().trim();
String phone = binding.etStaffPhone.getText().toString().trim(); String phone = binding.etStaffPhone.getText().toString().trim();
String role = ROLES[binding.spinnerStaffRole.getSelectedItemPosition()]; String role = ROLES[binding.spinnerStaffRole.getSelectedItemPosition()];
String staffRole = STAFF_ROLES[binding.spinnerStaffType.getSelectedItemPosition()];
boolean active = binding.spinnerStaffStatus.getSelectedItemPosition() == 0; boolean active = binding.spinnerStaffStatus.getSelectedItemPosition() == 0;
List<DropdownDTO> stores = viewModel.getStoreList().getValue();
Long storeId = stores.get(binding.spinnerStaffStore.getSelectedItemPosition() - 1).getId();
EmployeeDTO dto = new EmployeeDTO( EmployeeDTO dto = new EmployeeDTO(
username, username,
password.isEmpty() ? null : password, password.isEmpty() ? null : password,
@@ -118,7 +167,9 @@ public class StaffDetailFragment extends Fragment {
email, email,
phone, phone,
role, role,
active staffRole,
active,
storeId
); );
viewModel.saveEmployee(dto).observe(getViewLifecycleOwner(), resource -> { viewModel.saveEmployee(dto).observe(getViewLifecycleOwner(), resource -> {

View File

@@ -16,6 +16,7 @@ import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Locale;
import java.util.Map; import java.util.Map;
import java.util.TreeMap; import java.util.TreeMap;
@@ -30,6 +31,10 @@ public class AnalyticsViewModel extends ViewModel {
private final MutableLiveData<AnalyticsData> analyticsData = new MutableLiveData<>(); private final MutableLiveData<AnalyticsData> analyticsData = new MutableLiveData<>();
private final MutableLiveData<Boolean> isLoading = new MutableLiveData<>(false); private final MutableLiveData<Boolean> isLoading = new MutableLiveData<>(false);
private final MutableLiveData<String> errorMessage = new MutableLiveData<>(); private final MutableLiveData<String> errorMessage = new MutableLiveData<>();
private final MutableLiveData<List<String>> availablePaymentMethods = new MutableLiveData<>(new ArrayList<>());
private List<SaleDTO> cachedSales = new ArrayList<>();
private FilterState currentFilter = new FilterState();
@Inject @Inject
public AnalyticsViewModel(SaleRepository saleRepository) { public AnalyticsViewModel(SaleRepository saleRepository) {
@@ -39,14 +44,17 @@ public class AnalyticsViewModel extends ViewModel {
public LiveData<AnalyticsData> getAnalyticsData() { return analyticsData; } public LiveData<AnalyticsData> getAnalyticsData() { return analyticsData; }
public LiveData<Boolean> getIsLoading() { return isLoading; } public LiveData<Boolean> getIsLoading() { return isLoading; }
public LiveData<String> getErrorMessage() { return errorMessage; } public LiveData<String> getErrorMessage() { return errorMessage; }
public LiveData<List<String>> getAvailablePaymentMethods() { return availablePaymentMethods; }
public void loadAnalytics() { public void loadAnalytics() {
isLoading.setValue(true); isLoading.setValue(true);
errorMessage.setValue(null); errorMessage.setValue(null);
saleRepository.getAllSales(0, 1000, null, null, null, "saleDate,desc").observeForever(resource -> { saleRepository.getAllSales(0, 2000, null, null, null, "saleDate,desc").observeForever(resource -> {
if (resource != null) { if (resource != null) {
if (resource.status == Resource.Status.SUCCESS && resource.data != null) { if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
computeAnalytics(resource.data.getContent()); cachedSales = resource.data.getContent();
derivePaymentMethods();
applyCurrentFilter();
isLoading.setValue(false); isLoading.setValue(false);
} else if (resource.status == Resource.Status.ERROR) { } else if (resource.status == Resource.Status.ERROR) {
errorMessage.setValue(resource.message); errorMessage.setValue(resource.message);
@@ -56,11 +64,53 @@ public class AnalyticsViewModel extends ViewModel {
}); });
} }
private void computeAnalytics(List<SaleDTO> sales) { public void applyFilter(FilterState filter) {
currentFilter = filter;
applyCurrentFilter();
}
public void resetFilter() {
currentFilter = new FilterState();
applyCurrentFilter();
}
private void applyCurrentFilter() {
List<SaleDTO> filtered = filterSales(cachedSales, currentFilter);
computeAnalytics(filtered, currentFilter);
}
private void derivePaymentMethods() {
java.util.Set<String> methods = new java.util.TreeSet<>();
for (SaleDTO s : cachedSales) {
if (s.getPaymentMethod() != null && !s.getPaymentMethod().isEmpty()) {
methods.add(s.getPaymentMethod());
}
}
List<String> result = new ArrayList<>();
result.add("All");
result.addAll(methods);
availablePaymentMethods.setValue(result);
}
private List<SaleDTO> filterSales(List<SaleDTO> sales, FilterState filter) {
List<SaleDTO> result = new ArrayList<>();
for (SaleDTO s : sales) {
String date = s.getSaleDate() != null && s.getSaleDate().length() >= 10
? s.getSaleDate().substring(0, 10) : "";
if (!filter.startDate.isEmpty() && !date.isEmpty() && date.compareTo(filter.startDate) < 0) continue;
if (!filter.endDate.isEmpty() && !date.isEmpty() && date.compareTo(filter.endDate) > 0) continue;
if (!filter.paymentMethod.equals("All") && !filter.paymentMethod.isEmpty()) {
if (!filter.paymentMethod.equalsIgnoreCase(s.getPaymentMethod())) continue;
}
result.add(s);
}
return result;
}
private void computeAnalytics(List<SaleDTO> sales, FilterState filter) {
List<SaleDTO> regularSales = new ArrayList<>(); List<SaleDTO> regularSales = new ArrayList<>();
for (SaleDTO s : sales) { for (SaleDTO s : sales) {
if (!Boolean.TRUE.equals(s.getIsRefund())) if (!Boolean.TRUE.equals(s.getIsRefund())) regularSales.add(s);
regularSales.add(s);
} }
AnalyticsData data = new AnalyticsData(); AnalyticsData data = new AnalyticsData();
@@ -83,72 +133,127 @@ public class AnalyticsViewModel extends ViewModel {
: BigDecimal.ZERO; : BigDecimal.ZERO;
data.totalItems = totalItems; data.totalItems = totalItems;
// Product Maps
Map<String, BigDecimal> revenueByProduct = new LinkedHashMap<>(); Map<String, BigDecimal> revenueByProduct = new LinkedHashMap<>();
Map<String, Integer> quantityByProduct = new LinkedHashMap<>(); Map<String, Integer> quantityByProduct = new LinkedHashMap<>();
Map<String, Integer> paymentCount = new LinkedHashMap<>(); Map<String, Integer> paymentCount = new LinkedHashMap<>();
Map<String, BigDecimal> employeeRevenue = new LinkedHashMap<>(); Map<String, BigDecimal> employeeRevenue = new LinkedHashMap<>();
for (SaleDTO s : regularSales) { for (SaleDTO s : regularSales) {
// Payments
String method = s.getPaymentMethod() != null ? s.getPaymentMethod() : "Unknown"; String method = s.getPaymentMethod() != null ? s.getPaymentMethod() : "Unknown";
paymentCount.merge(method, 1, Integer::sum); paymentCount.merge(method, 1, Integer::sum);
// Employee
String emp = s.getEmployeeName() != null ? s.getEmployeeName() : "Unknown"; String emp = s.getEmployeeName() != null ? s.getEmployeeName() : "Unknown";
if (s.getTotalAmount() != null) employeeRevenue.merge(emp, s.getTotalAmount(), BigDecimal::add); if (s.getTotalAmount() != null) employeeRevenue.merge(emp, s.getTotalAmount(), BigDecimal::add);
// Items
if (s.getItems() != null) { if (s.getItems() != null) {
for (SaleDTO.SaleItemDTO item : s.getItems()) { for (SaleDTO.SaleItemDTO item : s.getItems()) {
String name = item.getProductName() != null ? item.getProductName() : "Unknown"; String name = item.getProductName() != null ? item.getProductName() : "Unknown";
int qty = item.getQuantity() != null ? Math.abs(item.getQuantity()) : 0; int qty = item.getQuantity() != null ? Math.abs(item.getQuantity()) : 0;
BigDecimal lineTotal = item.getUnitPrice() != null BigDecimal lineTotal = item.getUnitPrice() != null
? item.getUnitPrice().multiply(BigDecimal.valueOf(qty)) ? item.getUnitPrice().multiply(BigDecimal.valueOf(qty)) : BigDecimal.ZERO;
: BigDecimal.ZERO;
revenueByProduct.merge(name, lineTotal, BigDecimal::add); revenueByProduct.merge(name, lineTotal, BigDecimal::add);
quantityByProduct.merge(name, qty, Integer::sum); quantityByProduct.merge(name, qty, Integer::sum);
} }
} }
} }
// Sort Top Revenue int topN = filter.topN > 0 ? filter.topN : 5;
data.topRevenueProducts = new ArrayList<>(revenueByProduct.entrySet()); data.topRevenueProducts = new ArrayList<>(revenueByProduct.entrySet());
data.topRevenueProducts.sort((a, b) -> b.getValue().compareTo(a.getValue())); data.topRevenueProducts.sort((a, b) -> b.getValue().compareTo(a.getValue()));
if (data.topRevenueProducts.size() > 5) data.topRevenueProducts = data.topRevenueProducts.subList(0, 5); if (data.topRevenueProducts.size() > topN) data.topRevenueProducts = data.topRevenueProducts.subList(0, topN);
// Sort Top Quantity
data.topQuantityProducts = new ArrayList<>(quantityByProduct.entrySet()); data.topQuantityProducts = new ArrayList<>(quantityByProduct.entrySet());
data.topQuantityProducts.sort((a, b) -> b.getValue() - a.getValue()); data.topQuantityProducts.sort((a, b) -> b.getValue() - a.getValue());
if (data.topQuantityProducts.size() > 5) data.topQuantityProducts = data.topQuantityProducts.subList(0, 5); if (data.topQuantityProducts.size() > topN) data.topQuantityProducts = data.topQuantityProducts.subList(0, topN);
// Payment Stats
data.paymentMethodStats = new ArrayList<>(paymentCount.entrySet()); data.paymentMethodStats = new ArrayList<>(paymentCount.entrySet());
data.paymentMethodStats.sort((a, b) -> b.getValue() - a.getValue());
// Employee Performance
data.employeePerformance = new ArrayList<>(employeeRevenue.entrySet()); data.employeePerformance = new ArrayList<>(employeeRevenue.entrySet());
data.employeePerformance.sort((a, b) -> b.getValue().compareTo(a.getValue())); data.employeePerformance.sort((a, b) -> b.getValue().compareTo(a.getValue()));
// Daily Revenue (last 7 days) // Daily revenue display to filter date range, max 60 days
Map<String, BigDecimal> dailyMap = new TreeMap<>(); String rangeStart = filter.startDate;
for (int i = 6; i >= 0; i--) { String rangeEnd = filter.endDate;
Calendar day = Calendar.getInstance(); if (rangeStart.isEmpty() && rangeEnd.isEmpty()) {
day.add(Calendar.DAY_OF_YEAR, -i); rangeEnd = todayString(0);
String key = String.format("%04d-%02d-%02d", rangeStart = todayString(-6);
day.get(Calendar.YEAR), day.get(Calendar.MONTH) + 1, day.get(Calendar.DAY_OF_MONTH)); } else if (rangeStart.isEmpty()) {
dailyMap.put(key, BigDecimal.ZERO); rangeStart = shiftDate(rangeEnd, -6);
} else if (rangeEnd.isEmpty()) {
rangeEnd = todayString(0);
} }
List<String> dateRange = buildDateRange(rangeStart, rangeEnd, 60);
Map<String, BigDecimal> dailyMap = new TreeMap<>();
for (String d : dateRange) dailyMap.put(d, BigDecimal.ZERO);
for (SaleDTO s : regularSales) { for (SaleDTO s : regularSales) {
if (s.getSaleDate() != null && s.getTotalAmount() != null) { if (s.getSaleDate() != null && s.getTotalAmount() != null) {
String date = s.getSaleDate().length() >= 10 ? s.getSaleDate().substring(0, 10) : s.getSaleDate(); String d = s.getSaleDate().length() >= 10 ? s.getSaleDate().substring(0, 10) : s.getSaleDate();
if (dailyMap.containsKey(date)) dailyMap.merge(date, s.getTotalAmount(), BigDecimal::add); if (dailyMap.containsKey(d)) dailyMap.merge(d, s.getTotalAmount(), BigDecimal::add);
} }
} }
data.dailyRevenue = new ArrayList<>(dailyMap.entrySet()); data.dailyRevenue = new ArrayList<>(dailyMap.entrySet());
data.dailyRevenueTitle = buildDailyTitle(filter, rangeStart, rangeEnd);
analyticsData.setValue(data); analyticsData.setValue(data);
} }
private String todayString(int offsetDays) {
Calendar c = Calendar.getInstance();
c.add(Calendar.DAY_OF_YEAR, offsetDays);
return String.format(Locale.US, "%04d-%02d-%02d",
c.get(Calendar.YEAR), c.get(Calendar.MONTH) + 1, c.get(Calendar.DAY_OF_MONTH));
}
private String shiftDate(String date, int offsetDays) {
try {
String[] p = date.split("-");
Calendar c = Calendar.getInstance();
c.set(Integer.parseInt(p[0]), Integer.parseInt(p[1]) - 1, Integer.parseInt(p[2]), 0, 0, 0);
c.add(Calendar.DAY_OF_YEAR, offsetDays);
return String.format(Locale.US, "%04d-%02d-%02d",
c.get(Calendar.YEAR), c.get(Calendar.MONTH) + 1, c.get(Calendar.DAY_OF_MONTH));
} catch (Exception e) {
return date;
}
}
private List<String> buildDateRange(String start, String end, int maxDays) {
List<String> dates = new ArrayList<>();
try {
String[] sp = start.split("-");
String[] ep = end.split("-");
Calendar cur = Calendar.getInstance();
cur.set(Integer.parseInt(sp[0]), Integer.parseInt(sp[1]) - 1, Integer.parseInt(sp[2]), 0, 0, 0);
Calendar endCal = Calendar.getInstance();
endCal.set(Integer.parseInt(ep[0]), Integer.parseInt(ep[1]) - 1, Integer.parseInt(ep[2]), 0, 0, 0);
int count = 0;
while (!cur.after(endCal) && count < maxDays) {
dates.add(String.format(Locale.US, "%04d-%02d-%02d",
cur.get(Calendar.YEAR), cur.get(Calendar.MONTH) + 1, cur.get(Calendar.DAY_OF_MONTH)));
cur.add(Calendar.DAY_OF_YEAR, 1);
count++;
}
} catch (Exception ignored) {}
return dates;
}
private String buildDailyTitle(FilterState filter, String rangeStart, String rangeEnd) {
if (filter.startDate.isEmpty() && filter.endDate.isEmpty()) return "Daily Revenue (Last 7 Days)";
String s = rangeStart.length() >= 10 ? rangeStart.substring(5) : rangeStart;
String e = rangeEnd.length() >= 10 ? rangeEnd.substring(5) : rangeEnd;
return "Daily Revenue (" + s + " " + e + ")";
}
public static class FilterState {
public String startDate = "";
public String endDate = "";
public String paymentMethod = "All";
public int topN = 5;
}
public static class AnalyticsData { public static class AnalyticsData {
public BigDecimal totalRevenue; public BigDecimal totalRevenue;
public int totalTransactions; public int totalTransactions;
@@ -159,5 +264,6 @@ public class AnalyticsViewModel extends ViewModel {
public List<Map.Entry<String, Integer>> paymentMethodStats; public List<Map.Entry<String, Integer>> paymentMethodStats;
public List<Map.Entry<String, BigDecimal>> employeePerformance; public List<Map.Entry<String, BigDecimal>> employeePerformance;
public List<Map.Entry<String, BigDecimal>> dailyRevenue; public List<Map.Entry<String, BigDecimal>> dailyRevenue;
public String dailyRevenueTitle = "Daily Revenue";
} }
} }

View File

@@ -1,12 +1,17 @@
package com.example.petstoremobile.viewmodels; package com.example.petstoremobile.viewmodels;
import androidx.lifecycle.LiveData; import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.ViewModel; import androidx.lifecycle.ViewModel;
import com.example.petstoremobile.dtos.DropdownDTO;
import com.example.petstoremobile.dtos.EmployeeDTO; import com.example.petstoremobile.dtos.EmployeeDTO;
import com.example.petstoremobile.repositories.EmployeeRepository; import com.example.petstoremobile.repositories.EmployeeRepository;
import com.example.petstoremobile.repositories.StoreRepository;
import com.example.petstoremobile.utils.Resource; import com.example.petstoremobile.utils.Resource;
import java.util.List;
import javax.inject.Inject; import javax.inject.Inject;
import dagger.hilt.android.lifecycle.HiltViewModel; import dagger.hilt.android.lifecycle.HiltViewModel;
@@ -14,12 +19,31 @@ import dagger.hilt.android.lifecycle.HiltViewModel;
@HiltViewModel @HiltViewModel
public class StaffDetailViewModel extends ViewModel { public class StaffDetailViewModel extends ViewModel {
private final EmployeeRepository repository; private final EmployeeRepository repository;
private final StoreRepository storeRepository;
private final MutableLiveData<List<DropdownDTO>> storeList = new MutableLiveData<>();
private long employeeId = -1; private long employeeId = -1;
private boolean isEditing = false; private boolean isEditing = false;
@Inject @Inject
public StaffDetailViewModel(EmployeeRepository repository) { public StaffDetailViewModel(EmployeeRepository repository, StoreRepository storeRepository) {
this.repository = repository; this.repository = repository;
this.storeRepository = storeRepository;
}
public LiveData<Resource<List<DropdownDTO>>> loadStores() {
return storeRepository.getStoreDropdowns();
}
public LiveData<List<DropdownDTO>> getStoreList() {
return storeList;
}
public void setStoreList(List<DropdownDTO> list) {
storeList.setValue(list);
}
public LiveData<Resource<EmployeeDTO>> loadEmployee(long id) {
return repository.getEmployeeById(id);
} }
public void setEmployeeId(long id, boolean isEditing) { public void setEmployeeId(long id, boolean isEditing) {

View File

@@ -45,9 +45,267 @@
</LinearLayout> </LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:background="@drawable/rounded_card"
android:padding="16dp"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:layout_marginTop="8dp"
android:layout_marginBottom="4dp">
<LinearLayout
android:id="@+id/rowFilterHeader"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:clickable="true"
android:focusable="true"
android:background="?attr/selectableItemBackground">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Filters"
android:textColor="@color/text_dark"
android:textSize="14sp"
android:textStyle="bold"/>
<TextView
android:id="@+id/tvFilterSummary"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="All time"
android:textColor="@color/text_light"
android:textSize="12sp"
android:layout_marginEnd="8dp"/>
<TextView
android:id="@+id/tvFilterToggleIcon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="▼"
android:textColor="@color/text_light"
android:textSize="12sp"/>
</LinearLayout>
<LinearLayout
android:id="@+id/llFilterContent"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_marginTop="12dp"
android:visibility="gone">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Quick Range"
android:textColor="@color/text_light"
android:textSize="11sp"
android:layout_marginBottom="6dp"/>
<HorizontalScrollView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:scrollbars="none"
android:layout_marginBottom="12dp">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal">
<Button
android:id="@+id/btnPresetToday"
android:layout_width="wrap_content"
android:layout_height="32dp"
android:text="Today"
android:textSize="11sp"
android:backgroundTint="@color/primary_medium"
android:textColor="@color/white"
android:layout_marginEnd="6dp"
android:paddingStart="10dp"
android:paddingEnd="10dp"/>
<Button
android:id="@+id/btnPreset7D"
android:layout_width="wrap_content"
android:layout_height="32dp"
android:text="7D"
android:textSize="11sp"
android:backgroundTint="@color/primary_medium"
android:textColor="@color/white"
android:layout_marginEnd="6dp"
android:paddingStart="10dp"
android:paddingEnd="10dp"/>
<Button
android:id="@+id/btnPreset30D"
android:layout_width="wrap_content"
android:layout_height="32dp"
android:text="30D"
android:textSize="11sp"
android:backgroundTint="@color/primary_medium"
android:textColor="@color/white"
android:layout_marginEnd="6dp"
android:paddingStart="10dp"
android:paddingEnd="10dp"/>
<Button
android:id="@+id/btnPreset3M"
android:layout_width="wrap_content"
android:layout_height="32dp"
android:text="3M"
android:textSize="11sp"
android:backgroundTint="@color/primary_medium"
android:textColor="@color/white"
android:layout_marginEnd="6dp"
android:paddingStart="10dp"
android:paddingEnd="10dp"/>
<Button
android:id="@+id/btnPreset1Y"
android:layout_width="wrap_content"
android:layout_height="32dp"
android:text="1Y"
android:textSize="11sp"
android:backgroundTint="@color/primary_medium"
android:textColor="@color/white"
android:layout_marginEnd="6dp"
android:paddingStart="10dp"
android:paddingEnd="10dp"/>
<Button
android:id="@+id/btnPresetAll"
android:layout_width="wrap_content"
android:layout_height="32dp"
android:text="All"
android:textSize="11sp"
android:backgroundTint="@color/text_light"
android:textColor="@color/white"
android:paddingStart="10dp"
android:paddingEnd="10dp"/>
</LinearLayout>
</HorizontalScrollView>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Date Range"
android:textColor="@color/text_light"
android:textSize="11sp"
android:layout_marginBottom="6dp"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:layout_marginBottom="12dp">
<EditText
android:id="@+id/etFilterStartDate"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:hint="Start date"
android:inputType="none"
android:focusable="false"
android:clickable="true"
android:drawableEnd="@android:drawable/ic_menu_my_calendar"
android:textSize="13sp"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text=" "
android:textColor="@color/text_light"
android:layout_marginStart="4dp"
android:layout_marginEnd="4dp"/>
<EditText
android:id="@+id/etFilterEndDate"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:hint="End date"
android:inputType="none"
android:focusable="false"
android:clickable="true"
android:drawableEnd="@android:drawable/ic_menu_my_calendar"
android:textSize="13sp"/>
</LinearLayout>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Payment Method"
android:textColor="@color/text_light"
android:textSize="11sp"
android:layout_marginBottom="4dp"/>
<Spinner
android:id="@+id/spinnerFilterPayment"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="12dp"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Top N Products"
android:textColor="@color/text_light"
android:textSize="11sp"
android:layout_marginBottom="4dp"/>
<Spinner
android:id="@+id/spinnerTopN"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<Button
android:id="@+id/btnFilterApply"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginEnd="6dp"
android:text="Apply"
android:backgroundTint="@color/accent_coral"
android:textColor="@color/white"/>
<Button
android:id="@+id/btnFilterReset"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Reset"
android:backgroundTint="@color/primary_medium"
android:textColor="@color/white"/>
</LinearLayout>
</LinearLayout>
</LinearLayout>
<ScrollView <ScrollView
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="0dp"
android:layout_weight="1">
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
@@ -299,6 +557,7 @@
android:layout_marginBottom="16dp"> android:layout_marginBottom="16dp">
<TextView <TextView
android:id="@+id/tvDailyRevenueTitle"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="Daily Revenue (Last 7 Days)" android:text="Daily Revenue (Last 7 Days)"

View File

@@ -173,7 +173,7 @@
<TextView <TextView
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="Role" android:text="User Role"
android:textColor="@color/text_dark" android:textColor="@color/text_dark"
android:textSize="12sp" android:textSize="12sp"
android:layout_marginBottom="4dp"/> android:layout_marginBottom="4dp"/>
@@ -184,7 +184,34 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginBottom="16dp"/> android:layout_marginBottom="16dp"/>
<!-- Status --> <TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Staff Role"
android:textColor="@color/text_dark"
android:textSize="12sp"
android:layout_marginBottom="4dp"/>
<Spinner
android:id="@+id/spinnerStaffType"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Primary Store"
android:textColor="@color/text_dark"
android:textSize="12sp"
android:layout_marginBottom="4dp"/>
<Spinner
android:id="@+id/spinnerStaffStore"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"/>
<TextView <TextView
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"