diff --git a/android/app/src/main/java/com/example/petstoremobile/activities/ForgotPasswordActivity.java b/android/app/src/main/java/com/example/petstoremobile/activities/ForgotPasswordActivity.java index 385444ed..82ea9934 100644 --- a/android/app/src/main/java/com/example/petstoremobile/activities/ForgotPasswordActivity.java +++ b/android/app/src/main/java/com/example/petstoremobile/activities/ForgotPasswordActivity.java @@ -1,6 +1,7 @@ package com.example.petstoremobile.activities; import android.os.Bundle; +import android.view.View; import android.widget.Toast; import androidx.activity.EdgeToEdge; @@ -9,14 +10,26 @@ import androidx.core.graphics.Insets; import androidx.core.view.ViewCompat; import androidx.core.view.WindowInsetsCompat; +import com.example.petstoremobile.api.auth.AuthApi; import com.example.petstoremobile.databinding.ActivityForgotPasswordBinding; import com.example.petstoremobile.utils.InputValidator; +import java.util.HashMap; +import java.util.Map; + +import javax.inject.Inject; + import dagger.hilt.android.AndroidEntryPoint; +import retrofit2.Call; +import retrofit2.Callback; +import retrofit2.Response; @AndroidEntryPoint public class ForgotPasswordActivity extends AppCompatActivity { + @Inject + AuthApi authApi; + private ActivityForgotPasswordBinding binding; @Override @@ -33,14 +46,39 @@ public class ForgotPasswordActivity extends AppCompatActivity { }); binding.btnSubmit.setOnClickListener(v -> { - if (InputValidator.isValidEmail(binding.etEmail)) { - String email = binding.etEmail.getText().toString().trim(); - // TODO: Implement password reset logic here - Toast.makeText(this, "If this email is linked, a reset email will be sent.", Toast.LENGTH_LONG).show(); - finish(); - } + if (!InputValidator.isValidEmail(binding.etEmail)) return; + String email = binding.etEmail.getText().toString().trim(); + sendResetLink(email); }); binding.btnBackToLogin.setOnClickListener(v -> finish()); } + + private void sendResetLink(String email) { + binding.btnSubmit.setEnabled(false); + + Map body = new HashMap<>(); + body.put("usernameOrEmail", email); + + authApi.forgotPassword(body).enqueue(new Callback() { + @Override + public void onResponse(Call call, Response response) { + if (binding == null) return; + binding.btnSubmit.setEnabled(true); + Toast.makeText(ForgotPasswordActivity.this, + "If this email is registered, a reset link will be sent.", + Toast.LENGTH_LONG).show(); + finish(); + } + + @Override + public void onFailure(Call call, Throwable t) { + if (binding == null) return; + binding.btnSubmit.setEnabled(true); + Toast.makeText(ForgotPasswordActivity.this, + "Could not send reset link. Please try again.", + Toast.LENGTH_LONG).show(); + } + }); + } } diff --git a/android/app/src/main/java/com/example/petstoremobile/adapters/MessageAdapter.java b/android/app/src/main/java/com/example/petstoremobile/adapters/MessageAdapter.java index 0c3a51a0..eb9d9c19 100644 --- a/android/app/src/main/java/com/example/petstoremobile/adapters/MessageAdapter.java +++ b/android/app/src/main/java/com/example/petstoremobile/adapters/MessageAdapter.java @@ -11,6 +11,7 @@ import com.bumptech.glide.Glide; import com.bumptech.glide.load.engine.DiskCacheStrategy; import com.bumptech.glide.load.model.GlideUrl; import com.bumptech.glide.load.model.LazyHeaders; +import com.bumptech.glide.signature.ObjectKey; import com.example.petstoremobile.R; import com.example.petstoremobile.databinding.ItemMessageReceivedBinding; import com.example.petstoremobile.databinding.ItemMessageSentBinding; @@ -35,6 +36,13 @@ public class MessageAdapter extends RecyclerView.Adapter messages, Long currentUserId) { this.messages = messages; this.currentUserId = currentUserId; + setHasStableIds(true); + } + + @Override + public long getItemId(int position) { + Message m = messages.get(position); + return m.getId() != null ? m.getId() : position; } public void setCurrentUserId(Long id) { @@ -150,6 +158,7 @@ public class MessageAdapter extends RecyclerView.Adapter deleteAvatar(); + //forgot password endpoint + @POST("api/v1/auth/forgot-password") + Call forgotPassword(@Body Map body); + } diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/ChatFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/ChatFragment.java index c9171db2..47f755a2 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/ChatFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/ChatFragment.java @@ -182,6 +182,7 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis LinearLayoutManager lm = new LinearLayoutManager(getContext()); lm.setStackFromEnd(true); binding.rvMessages.setLayoutManager(lm); + binding.rvMessages.setItemAnimator(null); binding.rvMessages.setAdapter(messageAdapter); setConversationActive(false, null); } @@ -285,9 +286,14 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis }); viewModel.getMessageList().observe(getViewLifecycleOwner(), list -> { + int prevSize = messageList.size(); messageList.clear(); messageList.addAll(list); - messageAdapter.notifyDataSetChanged(); + if (prevSize > 0 && list.size() == prevSize + 1) { + messageAdapter.notifyItemInserted(list.size() - 1); + } else { + messageAdapter.notifyDataSetChanged(); + } scrollToBottom(); }); diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AdoptionFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AdoptionFragment.java index cfc05233..125d92d8 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AdoptionFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AdoptionFragment.java @@ -194,7 +194,7 @@ public class AdoptionFragment extends Fragment implements AdoptionAdapter.OnAdop } private void setupStatusFilter() { - String[] statuses = {"All Statuses", "Completed", "Pending", "Cancelled"}; + String[] statuses = {"All Statuses", "Completed", "Pending", "Missed", "Cancelled"}; SpinnerUtils.setupStringFilterSpinner(requireContext(), binding.spinnerStatusAdoption, statuses, this::loadAdoptions); } diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AnalyticsFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AnalyticsFragment.java index b8656b60..89aeb098 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AnalyticsFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AnalyticsFragment.java @@ -7,11 +7,13 @@ import android.widget.*; import androidx.annotation.NonNull; import androidx.fragment.app.Fragment; import androidx.lifecycle.ViewModelProvider; +import com.example.petstoremobile.api.auth.TokenManager; import com.example.petstoremobile.databinding.FragmentAnalyticsBinding; import com.example.petstoremobile.utils.SpinnerUtils; import com.example.petstoremobile.utils.UIUtils; import com.example.petstoremobile.viewmodels.AnalyticsViewModel; import dagger.hilt.android.AndroidEntryPoint; +import javax.inject.Inject; import java.math.BigDecimal; import java.math.RoundingMode; import java.util.*; @@ -19,6 +21,9 @@ import java.util.*; @AndroidEntryPoint public class AnalyticsFragment extends Fragment { + @Inject + TokenManager tokenManager; + private FragmentAnalyticsBinding binding; private AnalyticsViewModel viewModel; private boolean filtersExpanded = false; @@ -33,6 +38,7 @@ public class AnalyticsFragment extends Fragment { viewModel = new ViewModelProvider(this).get(AnalyticsViewModel.class); setupFilterPanel(); + setupViewModeToggle(); observeViewModel(); viewModel.loadAnalytics(); @@ -42,6 +48,39 @@ public class AnalyticsFragment extends Fragment { return binding.getRoot(); } + private static final int COLOR_SELECTED = 0xFF4ECDC4; + private static final int COLOR_UNSELECTED = 0xFFCBD5E1; + + private void setupViewModeToggle() { + updateViewModeButtonStyles(viewModel.getViewMode()); + + binding.btnMyAnalytics.setOnClickListener(v -> { + viewModel.setViewMode("mine"); + updateViewModeButtonStyles("mine"); + updateStoreFilterVisibility("mine"); + }); + + binding.btnStoreAnalytics.setOnClickListener(v -> { + viewModel.setViewMode("store"); + updateViewModeButtonStyles("store"); + updateStoreFilterVisibility("store"); + }); + } + + private void updateViewModeButtonStyles(String mode) { + binding.btnMyAnalytics.setBackgroundTintList( + android.content.res.ColorStateList.valueOf(mode.equals("mine") ? COLOR_SELECTED : COLOR_UNSELECTED)); + binding.btnStoreAnalytics.setBackgroundTintList( + android.content.res.ColorStateList.valueOf(mode.equals("store") ? COLOR_SELECTED : COLOR_UNSELECTED)); + } + + private void updateStoreFilterVisibility(String mode) { + boolean isAdmin = "ADMIN".equalsIgnoreCase(tokenManager.getRole()); + int vis = (isAdmin && mode.equals("store")) ? View.VISIBLE : View.GONE; + binding.tvStoreFilterLabel.setVisibility(vis); + binding.spinnerFilterStore.setVisibility(vis); + } + // Filter Panel private void setupFilterPanel() { @@ -96,6 +135,9 @@ public class AnalyticsFragment extends Fragment { int topNPos = binding.spinnerTopN.getSelectedItemPosition(); filter.topN = (topNPos >= 0 && topNPos < TOP_N_VALUES.length) ? TOP_N_VALUES[topNPos] : 5; + Object store = binding.spinnerFilterStore.getSelectedItem(); + viewModel.setStoreFilter(store != null ? store.toString() : "All Stores"); + updateFilterSummary(); viewModel.applyFilter(filter); } @@ -104,8 +146,8 @@ public class AnalyticsFragment extends Fragment { binding.etFilterStartDate.setText(""); binding.etFilterEndDate.setText(""); binding.spinnerTopN.setSelection(0); - // Reset payment method to "All" SpinnerUtils.setSelectionByValue(binding.spinnerFilterPayment, "All"); + SpinnerUtils.setSelectionByValue(binding.spinnerFilterStore, "All Stores"); updateFilterSummary(); viewModel.resetFilter(); } @@ -162,6 +204,16 @@ public class AnalyticsFragment extends Fragment { methods.toArray(new String[0])); SpinnerUtils.setSelectionByValue(binding.spinnerFilterPayment, currentSelection); }); + + viewModel.getAvailableStores().observe(getViewLifecycleOwner(), stores -> { + if (stores == null || stores.isEmpty()) return; + String currentSelection = binding.spinnerFilterStore.getSelectedItem() != null + ? binding.spinnerFilterStore.getSelectedItem().toString() : "All Stores"; + SpinnerUtils.setupStringSpinner(requireContext(), binding.spinnerFilterStore, + stores.toArray(new String[0])); + SpinnerUtils.setSelectionByValue(binding.spinnerFilterStore, currentSelection); + updateStoreFilterVisibility(viewModel.getViewMode()); + }); } @Override @@ -224,17 +276,22 @@ public class AnalyticsFragment extends Fragment { } // Employee Performance - binding.llEmployeePerformance.removeAllViews(); - if (data.employeePerformance != null && !data.employeePerformance.isEmpty()) { - BigDecimal maxEmp = data.employeePerformance.get(0).getValue(); - if (maxEmp.compareTo(BigDecimal.ZERO) == 0) maxEmp = BigDecimal.ONE; - for (Map.Entry e : data.employeePerformance) { - addBarRow(binding.llEmployeePerformance, e.getKey(), - "$" + e.getValue().setScale(2, RoundingMode.HALF_UP), - e.getValue().floatValue() / maxEmp.floatValue(), "#1a759f"); + boolean showEmployeeSection = viewModel.getViewMode().equals("store"); + View empParent = (View) binding.llEmployeePerformance.getParent(); + if (empParent != null) empParent.setVisibility(showEmployeeSection ? View.VISIBLE : View.GONE); + if (showEmployeeSection) { + binding.llEmployeePerformance.removeAllViews(); + if (data.employeePerformance != null && !data.employeePerformance.isEmpty()) { + BigDecimal maxEmp = data.employeePerformance.get(0).getValue(); + if (maxEmp.compareTo(BigDecimal.ZERO) == 0) maxEmp = BigDecimal.ONE; + for (Map.Entry e : data.employeePerformance) { + addBarRow(binding.llEmployeePerformance, e.getKey(), + "$" + e.getValue().setScale(2, RoundingMode.HALF_UP), + e.getValue().floatValue() / maxEmp.floatValue(), "#1a759f"); + } + } else { + addEmptyRow(binding.llEmployeePerformance, "No data"); } - } else { - addEmptyRow(binding.llEmployeePerformance, "No data"); } // Daily Revenue diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/SaleFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/SaleFragment.java index 5c6589f3..f6d0c388 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/SaleFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/SaleFragment.java @@ -17,6 +17,7 @@ import com.example.petstoremobile.R; import com.example.petstoremobile.adapters.SaleAdapter; import com.example.petstoremobile.api.auth.TokenManager; import com.example.petstoremobile.databinding.FragmentSaleBinding; +import com.example.petstoremobile.dtos.CustomerDTO; import com.example.petstoremobile.dtos.SaleDTO; import com.example.petstoremobile.dtos.StoreDTO; import com.example.petstoremobile.utils.SpinnerUtils; @@ -57,10 +58,11 @@ public class SaleFragment extends Fragment implements SaleAdapter.OnSaleClickLis setupStoreFilter(); setupPaymentMethodFilter(); setupRefundStatusFilter(); + setupCustomerFilter(); setupSwipeRefresh(); setupFilterToggle(); observeViewModel(); - + loadSales(true); UIUtils.setupHamburgerMenu(binding.btnHamburger, this); @@ -89,6 +91,11 @@ public class SaleFragment extends Fragment implements SaleAdapter.OnSaleClickLis StoreDTO::getStoreName, "All Stores", null, StoreDTO::getStoreId); }); + viewModel.getCustomers().observe(getViewLifecycleOwner(), list -> { + SpinnerUtils.populateWhiteSpinner(requireContext(), binding.spinnerCustomer, list, + CustomerDTO::getFullName, "All Customers", null, CustomerDTO::getCustomerId); + }); + viewModel.getIsLoading().observe(getViewLifecycleOwner(), loading -> { binding.swipeRefreshSale.setRefreshing(loading); }); @@ -98,16 +105,17 @@ public class SaleFragment extends Fragment implements SaleAdapter.OnSaleClickLis public void onResume() { super.onResume(); if (!isStaff()) viewModel.loadStores(); + viewModel.loadCustomers(); } private void setupFilterToggle() { if (isStaff()) { UIUtils.setupFilterToggle(binding.btnToggleFilter, binding.layoutFilter, binding.etSearchSale, - binding.spinnerPaymentMethod, binding.spinnerRefundStatus); + binding.spinnerPaymentMethod, binding.spinnerRefundStatus, binding.spinnerCustomer); binding.spinnerStore.setVisibility(View.GONE); } else { UIUtils.setupFilterToggle(binding.btnToggleFilter, binding.layoutFilter, binding.etSearchSale, - binding.spinnerPaymentMethod, binding.spinnerStore, binding.spinnerRefundStatus); + binding.spinnerPaymentMethod, binding.spinnerStore, binding.spinnerRefundStatus, binding.spinnerCustomer); } } @@ -133,6 +141,10 @@ public class SaleFragment extends Fragment implements SaleAdapter.OnSaleClickLis SpinnerUtils.setupStringFilterSpinner(requireContext(), binding.spinnerRefundStatus, refundStatuses, () -> loadSales(true)); } + private void setupCustomerFilter() { + SpinnerUtils.setupFilterSpinner(binding.spinnerCustomer, () -> loadSales(true)); + } + private void setupRecyclerView() { adapter = new SaleAdapter(saleList, this); binding.recyclerViewSales.setLayoutManager(new LinearLayoutManager(getContext())); @@ -189,7 +201,13 @@ public class SaleFragment extends Fragment implements SaleAdapter.OnSaleClickLis isRefund = binding.spinnerRefundStatus.getSelectedItemPosition() == 2; } - viewModel.loadSales(reset, query, paymentMethod, storeId, isRefund); + Long customerId = null; + List customerList = viewModel.getCustomers().getValue(); + if (binding.spinnerCustomer.getSelectedItemPosition() > 0 && customerList != null && !customerList.isEmpty()) { + customerId = customerList.get(binding.spinnerCustomer.getSelectedItemPosition() - 1).getCustomerId(); + } + + viewModel.loadSales(reset, query, paymentMethod, storeId, isRefund, customerId); } @Override diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/SaleDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/SaleDetailFragment.java index 8a4dff16..4963b272 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/SaleDetailFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/SaleDetailFragment.java @@ -111,6 +111,7 @@ public class SaleDetailFragment extends Fragment { binding.llLoyaltyPoints.setVisibility(View.GONE); binding.cbUseLoyaltyPoints.setChecked(false); } + updateTotal(); }); } @@ -420,6 +421,15 @@ public class SaleDetailFragment extends Fragment { } binding.tvSaleDetailTotal.setText("Total: $" + String.format(Locale.getDefault(), "%.2f", total)); + + CustomerDTO customer = viewModel.getSelectedCustomerData().getValue(); + if (customer != null && !viewModel.isViewOnly()) { + int pointsToEarn = total.max(BigDecimal.ZERO).intValue(); + binding.tvPointsToEarn.setText("+" + pointsToEarn + " pts"); + binding.llPointsToEarn.setVisibility(View.VISIBLE); + } else { + binding.llPointsToEarn.setVisibility(View.GONE); + } } private void saveSale() { diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/StaffDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/StaffDetailFragment.java index 94b2b307..9bb3435d 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/StaffDetailFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/StaffDetailFragment.java @@ -28,8 +28,6 @@ public class StaffDetailFragment extends Fragment { private FragmentStaffDetailBinding binding; private StaffDetailViewModel viewModel; - private final String[] ROLES = {"STAFF", "ADMIN"}; - private final String[] STAFF_ROLES = {"STORE_MANAGER", "SALES_ASSOCIATE", "GROOMER", "VETERINARIAN"}; private final String[] STATUSES = {"Active", "Inactive"}; private long preselectedStoreId = -1; @@ -59,8 +57,6 @@ public class StaffDetailFragment extends Fragment { } private void setupSpinners() { - SpinnerUtils.setupStringSpinner(requireContext(), binding.spinnerStaffRole, ROLES); - SpinnerUtils.setupStringSpinner(requireContext(), binding.spinnerStaffType, STAFF_ROLES); SpinnerUtils.setupStringSpinner(requireContext(), binding.spinnerStaffStatus, STATUSES); } @@ -112,8 +108,6 @@ public class StaffDetailFragment extends Fragment { 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; @@ -131,17 +125,35 @@ public class StaffDetailFragment extends Fragment { private void save() { if (!InputValidator.isNotEmpty(binding.etStaffUsername, "Username")) return; - + + String password = binding.etStaffPassword.getText().toString().trim(); + String confirmPassword = binding.etStaffConfirmPassword.getText().toString().trim(); + if (!viewModel.isEditing()) { if (!InputValidator.isNotEmpty(binding.etStaffPassword, "Password")) return; - String pass = binding.etStaffPassword.getText().toString(); - if (pass.length() < 6) { + if (password.length() < 6) { binding.etStaffPassword.setError("At least 6 characters"); binding.etStaffPassword.requestFocus(); return; } + if (!password.equals(confirmPassword)) { + binding.etStaffConfirmPassword.setError("Passwords do not match"); + binding.etStaffConfirmPassword.requestFocus(); + return; + } + } else if (!password.isEmpty()) { + if (password.length() < 6) { + binding.etStaffPassword.setError("At least 6 characters"); + binding.etStaffPassword.requestFocus(); + return; + } + if (!password.equals(confirmPassword)) { + binding.etStaffConfirmPassword.setError("Passwords do not match"); + binding.etStaffConfirmPassword.requestFocus(); + return; + } } - + if (!InputValidator.isNotEmpty(binding.etStaffFirstName, "First Name")) return; if (!InputValidator.isNotEmpty(binding.etStaffLastName, "Last Name")) return; if (!InputValidator.isValidEmail(binding.etStaffEmail)) return; @@ -149,13 +161,12 @@ public class StaffDetailFragment extends Fragment { if (!InputValidator.isSpinnerSelected(binding.spinnerStaffStore, "Primary Store")) return; String username = binding.etStaffUsername.getText().toString().trim(); - String password = binding.etStaffPassword.getText().toString().trim(); String firstName = binding.etStaffFirstName.getText().toString().trim(); String lastName = binding.etStaffLastName.getText().toString().trim(); String email = binding.etStaffEmail.getText().toString().trim(); String phone = binding.etStaffPhone.getText().toString().trim(); - String role = ROLES[binding.spinnerStaffRole.getSelectedItemPosition()]; - String staffRole = STAFF_ROLES[binding.spinnerStaffType.getSelectedItemPosition()]; + String role = "STAFF"; + String staffRole = null; boolean active = binding.spinnerStaffStatus.getSelectedItemPosition() == 0; List stores = viewModel.getStoreList().getValue(); diff --git a/android/app/src/main/java/com/example/petstoremobile/repositories/SaleRepository.java b/android/app/src/main/java/com/example/petstoremobile/repositories/SaleRepository.java index 36ac8b30..182a7f0e 100644 --- a/android/app/src/main/java/com/example/petstoremobile/repositories/SaleRepository.java +++ b/android/app/src/main/java/com/example/petstoremobile/repositories/SaleRepository.java @@ -20,8 +20,8 @@ public class SaleRepository extends BaseRepository { this.saleApi = saleApi; } - public LiveData>> getAllSales(int page, int size, String query, String paymentMethod, Long storeId, Boolean isRefund, String sortBy) { - return executeCall(saleApi.getAllSales(page, size, query, paymentMethod, storeId, isRefund, sortBy)); + public LiveData>> getAllSales(int page, int size, String query, String paymentMethod, Long storeId, Boolean isRefund, Long customerId, String sortBy) { + return executeCall(saleApi.getAllSales(page, size, query, paymentMethod, storeId, isRefund, customerId, sortBy)); } public LiveData> getSaleById(Long id) { diff --git a/android/app/src/main/java/com/example/petstoremobile/utils/InputValidator.java b/android/app/src/main/java/com/example/petstoremobile/utils/InputValidator.java index 0d45ad12..9ad2d5f4 100644 --- a/android/app/src/main/java/com/example/petstoremobile/utils/InputValidator.java +++ b/android/app/src/main/java/com/example/petstoremobile/utils/InputValidator.java @@ -100,13 +100,12 @@ public class InputValidator { return true; } - // Checks if the phone number is valid in (XXX) XXX-XXXX format + // Checks if the phone number is valid: XXX-XXX-XXXX, (XXX) XXX-XXXX, or XXXXXXXXXX public static boolean isValidPhone(EditText field) { String phone = field.getText().toString().trim(); - // Matches (XXX) XXX-XXXX format - String pattern = "^\\(\\d{3}\\) \\d{3}-\\d{4}$"; + String pattern = "^(\\(\\d{3}\\) \\d{3}-\\d{4}|\\d{3}-\\d{3}-\\d{4}|\\d{10})$"; if (phone.isEmpty() || !phone.matches(pattern)) { - field.setError("Enter a valid phone number: (XXX) XXX-XXXX"); + field.setError("Enter a valid phone number"); field.requestFocus(); return false; } diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/AnalyticsViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/AnalyticsViewModel.java index 3e5082ec..4a9b94d9 100644 --- a/android/app/src/main/java/com/example/petstoremobile/viewmodels/AnalyticsViewModel.java +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/AnalyticsViewModel.java @@ -4,6 +4,7 @@ import androidx.lifecycle.LiveData; import androidx.lifecycle.MutableLiveData; import androidx.lifecycle.ViewModel; +import com.example.petstoremobile.api.auth.TokenManager; import com.example.petstoremobile.dtos.SaleDTO; import com.example.petstoremobile.repositories.SaleRepository; import com.example.petstoremobile.utils.Resource; @@ -21,6 +22,7 @@ import java.util.List; import java.util.Locale; import java.util.Map; import java.util.TreeMap; +import java.util.stream.Collectors; import javax.inject.Inject; @@ -29,33 +31,40 @@ import dagger.hilt.android.lifecycle.HiltViewModel; @HiltViewModel public class AnalyticsViewModel extends ViewModel { private final SaleRepository saleRepository; + private final TokenManager tokenManager; private final MutableLiveData analyticsData = new MutableLiveData<>(); private final MutableLiveData isLoading = new MutableLiveData<>(false); private final MutableLiveData errorMessage = new MutableLiveData<>(); private final MutableLiveData> availablePaymentMethods = new MutableLiveData<>(new ArrayList<>()); + private final MutableLiveData> availableStores = new MutableLiveData<>(new ArrayList<>()); private List cachedSales = new ArrayList<>(); private FilterState currentFilter = new FilterState(); + private String viewMode = "store"; + private String storeFilter = "All Stores"; @Inject - public AnalyticsViewModel(SaleRepository saleRepository) { + public AnalyticsViewModel(SaleRepository saleRepository, TokenManager tokenManager) { this.saleRepository = saleRepository; + this.tokenManager = tokenManager; } public LiveData getAnalyticsData() { return analyticsData; } public LiveData getIsLoading() { return isLoading; } public LiveData getErrorMessage() { return errorMessage; } public LiveData> getAvailablePaymentMethods() { return availablePaymentMethods; } + public LiveData> getAvailableStores() { return availableStores; } public void loadAnalytics() { isLoading.setValue(true); errorMessage.setValue(null); - observeOnce(saleRepository.getAllSales(0, 2000, null, null, null, null, "saleDate,desc"), resource -> { + observeOnce(saleRepository.getAllSales(0, 2000, null, null, null, null, null, "saleDate,desc"), resource -> { if (resource != null) { if (resource.status == Resource.Status.SUCCESS && resource.data != null) { cachedSales = resource.data.getContent(); derivePaymentMethods(); + deriveStores(); applyCurrentFilter(); isLoading.setValue(false); } else if (resource.status == Resource.Status.ERROR) { @@ -73,14 +82,61 @@ public class AnalyticsViewModel extends ViewModel { public void resetFilter() { currentFilter = new FilterState(); + storeFilter = "All Stores"; applyCurrentFilter(); } + public void setViewMode(String mode) { + viewMode = mode; + applyCurrentFilter(); + } + + public String getViewMode() { + return viewMode; + } + + public void setStoreFilter(String store) { + storeFilter = (store != null && !store.isEmpty()) ? store : "All Stores"; + applyCurrentFilter(); + } + + public String getStoreFilter() { + return storeFilter; + } + private void applyCurrentFilter() { - List filtered = filterSales(cachedSales, currentFilter); + List salesForMode; + if (viewMode.equals("mine")) { + String currentUser = tokenManager.getUsername(); + salesForMode = cachedSales.stream() + .filter(s -> currentUser != null && currentUser.equalsIgnoreCase(s.getEmployeeName() != null ? s.getEmployeeName() : "")) + .collect(Collectors.toList()); + } else { + salesForMode = cachedSales; + } + if (!storeFilter.equals("All Stores") && !storeFilter.isEmpty()) { + final String sf = storeFilter; + salesForMode = salesForMode.stream() + .filter(s -> sf.equalsIgnoreCase(s.getStoreName() != null ? s.getStoreName() : "")) + .collect(Collectors.toList()); + } + List filtered = filterSales(salesForMode, currentFilter); computeAnalytics(filtered, currentFilter); } + private void deriveStores() { + java.util.Set stores = new java.util.TreeSet<>(); + for (SaleDTO s : cachedSales) { + if (s.getStoreName() != null && !s.getStoreName().isEmpty()) { + stores.add(s.getStoreName()); + } + } + List result = new ArrayList<>(); + result.add("All Stores"); + result.addAll(stores); + availableStores.setValue(result); + } + private void derivePaymentMethods() { java.util.Set methods = new java.util.TreeSet<>(); for (SaleDTO s : cachedSales) { diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/RefundViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/RefundViewModel.java index fab83dd9..ff3d069e 100644 --- a/android/app/src/main/java/com/example/petstoremobile/viewmodels/RefundViewModel.java +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/RefundViewModel.java @@ -35,7 +35,7 @@ public class RefundViewModel extends ViewModel { } public LiveData>> loadAllSales() { - return saleRepository.getAllSales(0, 1000, null, null, null, null, "saleDate,desc"); + return saleRepository.getAllSales(0, 1000, null, null, null, null, null, "saleDate,desc"); } public void setAllSales(List sales) { diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/SaleListViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/SaleListViewModel.java index 7df7269a..aee0f948 100644 --- a/android/app/src/main/java/com/example/petstoremobile/viewmodels/SaleListViewModel.java +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/SaleListViewModel.java @@ -4,9 +4,11 @@ import androidx.lifecycle.LiveData; import androidx.lifecycle.MutableLiveData; import androidx.lifecycle.ViewModel; +import com.example.petstoremobile.dtos.CustomerDTO; import com.example.petstoremobile.dtos.PageResponse; import com.example.petstoremobile.dtos.SaleDTO; import com.example.petstoremobile.dtos.StoreDTO; +import com.example.petstoremobile.repositories.CustomerRepository; import com.example.petstoremobile.repositories.SaleRepository; import com.example.petstoremobile.repositories.StoreRepository; import com.example.petstoremobile.utils.Resource; @@ -24,27 +26,31 @@ import dagger.hilt.android.lifecycle.HiltViewModel; public class SaleListViewModel extends ViewModel { private final SaleRepository saleRepository; private final StoreRepository storeRepository; + private final CustomerRepository customerRepository; private final MutableLiveData> sales = new MutableLiveData<>(new ArrayList<>()); private final MutableLiveData> stores = new MutableLiveData<>(new ArrayList<>()); + private final MutableLiveData> customers = new MutableLiveData<>(new ArrayList<>()); private final MutableLiveData isLoading = new MutableLiveData<>(false); - + private int currentPage = 0; private boolean isLastPage = false; private static final int PAGE_SIZE = 20; @Inject - public SaleListViewModel(SaleRepository saleRepository, StoreRepository storeRepository) { + public SaleListViewModel(SaleRepository saleRepository, StoreRepository storeRepository, CustomerRepository customerRepository) { this.saleRepository = saleRepository; this.storeRepository = storeRepository; + this.customerRepository = customerRepository; } public LiveData> getSales() { return sales; } public LiveData> getStores() { return stores; } + public LiveData> getCustomers() { return customers; } public LiveData getIsLoading() { return isLoading; } public boolean isLastPage() { return isLastPage; } - public void loadSales(boolean reset, String query, String paymentMethod, Long storeId, Boolean isRefund) { + public void loadSales(boolean reset, String query, String paymentMethod, Long storeId, Boolean isRefund, Long customerId) { if (isLoading.getValue() != null && isLoading.getValue() && !reset) return; if (reset) { @@ -53,7 +59,7 @@ public class SaleListViewModel extends ViewModel { } isLoading.setValue(true); - observeOnce(saleRepository.getAllSales(currentPage, PAGE_SIZE, query, paymentMethod, storeId, isRefund, "saleDate,desc"), resource -> { + observeOnce(saleRepository.getAllSales(currentPage, PAGE_SIZE, query, paymentMethod, storeId, isRefund, customerId, "saleDate,desc"), resource -> { if (resource != null) { if (resource.status == Resource.Status.SUCCESS && resource.data != null) { List currentList = reset ? new ArrayList<>() : new ArrayList<>(sales.getValue()); @@ -77,6 +83,14 @@ public class SaleListViewModel extends ViewModel { }); } + public void loadCustomers() { + observeOnce(customerRepository.getAllCustomers(0, 500), resource -> { + if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) { + customers.setValue(resource.data.getContent()); + } + }); + } + private void observeOnce(LiveData> liveData, Observer> handler) { liveData.observeForever(new Observer>() { @Override diff --git a/android/app/src/main/res/layout/fragment_analytics.xml b/android/app/src/main/res/layout/fragment_analytics.xml index 19a7a51b..9e59b7d7 100644 --- a/android/app/src/main/res/layout/fragment_analytics.xml +++ b/android/app/src/main/res/layout/fragment_analytics.xml @@ -45,6 +45,40 @@ + + + + + + + + + diff --git a/desktop/src/main/resources/org/example/petshopdesktop/main-layout-view.fxml b/desktop/src/main/resources/org/example/petshopdesktop/main-layout-view.fxml index 7192ea69..674020aa 100644 --- a/desktop/src/main/resources/org/example/petshopdesktop/main-layout-view.fxml +++ b/desktop/src/main/resources/org/example/petshopdesktop/main-layout-view.fxml @@ -59,14 +59,26 @@ - + + + + + + diff --git a/desktop/src/main/resources/org/example/petshopdesktop/modelviews/activity-log-view.fxml b/desktop/src/main/resources/org/example/petshopdesktop/modelviews/activity-log-view.fxml index 7caa317a..c474f8c9 100644 --- a/desktop/src/main/resources/org/example/petshopdesktop/modelviews/activity-log-view.fxml +++ b/desktop/src/main/resources/org/example/petshopdesktop/modelviews/activity-log-view.fxml @@ -2,6 +2,8 @@ + + @@ -30,10 +32,15 @@ +