Merge branch 'AttachmentsToChat'

This commit is contained in:
Alex
2026-04-13 22:36:53 -06:00
53 changed files with 1285 additions and 390 deletions

View File

@@ -103,11 +103,21 @@ public class PetAdapter extends RecyclerView.Adapter<PetAdapter.PetViewHolder> i
binding.tvPetStatus.setText(pet.getPetStatus()); binding.tvPetStatus.setText(pet.getPetStatus());
//Set the status color depending on availability. If available, green, otherwise red //Set the status color depending on availability. If available, green, If Pending, yellow, otherwise red
if (pet.getPetStatus() != null && pet.getPetStatus().equals("Available")) { if (pet.getPetStatus() != null) {
binding.tvPetStatus.setBackgroundColor(Color.parseColor("#4CAF50")); switch (pet.getPetStatus()) {
case "Available":
binding.tvPetStatus.setBackgroundColor(Color.parseColor("#4CAF50"));
break;
case "Pending":
binding.tvPetStatus.setBackgroundColor(Color.parseColor("#FF9800"));
break;
default:
binding.tvPetStatus.setBackgroundColor(Color.parseColor("#F44336"));
break;
}
} else { } else {
binding.tvPetStatus.setBackgroundColor(Color.parseColor("#F44336")); binding.tvPetStatus.setBackgroundColor(Color.parseColor("#9E9E9E"));
} }
// Load pet image using Glide // Load pet image using Glide

View File

@@ -50,6 +50,9 @@ public interface PetApi {
@GET("api/v1/dropdowns/pet-species") @GET("api/v1/dropdowns/pet-species")
Call<List<DropdownDTO>> getPetSpeciesDropdowns(); Call<List<DropdownDTO>> getPetSpeciesDropdowns();
@GET("api/v1/dropdowns/pet-breeds")
Call<List<DropdownDTO>> getPetBreedsDropdowns(@Query("species") String species);
// Get pet by id // Get pet by id
@GET("api/v1/pets/{id}") @GET("api/v1/pets/{id}")
Call<PetDTO> getPetById(@Path("id") Long id); Call<PetDTO> getPetById(@Path("id") Long id);

View File

@@ -17,6 +17,7 @@ public class CustomerDTO {
private String createdAt; private String createdAt;
private String updatedAt; private String updatedAt;
private String password; private String password;
private String role;
public CustomerDTO() {} public CustomerDTO() {}
@@ -73,4 +74,7 @@ public class CustomerDTO {
public String getPassword() { return password; } public String getPassword() { return password; }
public void setPassword(String password) { this.password = password; } public void setPassword(String password) { this.password = password; }
public String getRole() { return role; }
public void setRole(String role) { this.role = role; }
} }

View File

@@ -17,6 +17,9 @@ public class SaleDTO {
private BigDecimal subtotalAmount; private BigDecimal subtotalAmount;
private BigDecimal couponDiscountAmount; private BigDecimal couponDiscountAmount;
private BigDecimal employeeDiscountAmount; private BigDecimal employeeDiscountAmount;
private BigDecimal loyaltyDiscountAmount;
private BigDecimal pointsDiscountAmount;
private Integer pointsUsed;
private String paymentMethod; private String paymentMethod;
private String channel; private String channel;
private Boolean isRefund; private Boolean isRefund;
@@ -78,6 +81,22 @@ public class SaleDTO {
return employeeDiscountAmount; return employeeDiscountAmount;
} }
public BigDecimal getLoyaltyDiscountAmount() {
return loyaltyDiscountAmount;
}
public void setLoyaltyDiscountAmount(BigDecimal loyaltyDiscountAmount) {
this.loyaltyDiscountAmount = loyaltyDiscountAmount;
}
public Integer getPointsUsed() {
return pointsUsed;
}
public void setPointsUsed(Integer pointsUsed) {
this.pointsUsed = pointsUsed;
}
public String getPaymentMethod() { public String getPaymentMethod() {
return paymentMethod; return paymentMethod;
} }
@@ -126,6 +145,14 @@ public class SaleDTO {
return customerName; return customerName;
} }
public BigDecimal getPointsDiscountAmount() {
return pointsDiscountAmount;
}
public void setPointsDiscountAmount(BigDecimal pointsDiscountAmount) {
this.pointsDiscountAmount = pointsDiscountAmount;
}
// Nested SaleItemDTO // Nested SaleItemDTO
public static class SaleItemDTO { public static class SaleItemDTO {
private Long saleItemId; private Long saleItemId;

View File

@@ -136,7 +136,7 @@ public class PetFragment extends Fragment implements PetAdapter.OnPetClickListen
} }
private void setupStatusFilter() { private void setupStatusFilter() {
String[] statuses = {"All Statuses", "Available", "Adopted", "Owned"}; String[] statuses = {"All Statuses", "Available", "Adopted", "Owned", "Pending"};
SpinnerUtils.setupStringFilterSpinner(requireContext(), binding.spinnerStatus, statuses, this::loadPetData); SpinnerUtils.setupStringFilterSpinner(requireContext(), binding.spinnerStatus, statuses, this::loadPetData);
} }

View File

@@ -65,6 +65,11 @@ public class SaleFragment extends Fragment implements SaleAdapter.OnSaleClickLis
UIUtils.setupHamburgerMenu(binding.btnHamburger, this); UIUtils.setupHamburgerMenu(binding.btnHamburger, this);
if (isAdmin()) {
binding.fabAddSale.setVisibility(View.GONE);
binding.btnOpenRefund.setVisibility(View.GONE);
}
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));
@@ -110,6 +115,10 @@ public class SaleFragment extends Fragment implements SaleAdapter.OnSaleClickLis
return "STAFF".equalsIgnoreCase(tokenManager.getRole()); return "STAFF".equalsIgnoreCase(tokenManager.getRole());
} }
private boolean isAdmin() {
return "ADMIN".equalsIgnoreCase(tokenManager.getRole());
}
private void setupStoreFilter() { private void setupStoreFilter() {
SpinnerUtils.setupFilterSpinner(binding.spinnerStore, () -> loadSales(true)); SpinnerUtils.setupFilterSpinner(binding.spinnerStore, () -> loadSales(true));
} }

View File

@@ -14,8 +14,11 @@ import com.example.petstoremobile.utils.InputValidator;
import com.example.petstoremobile.utils.SpinnerUtils; import com.example.petstoremobile.utils.SpinnerUtils;
import com.example.petstoremobile.utils.UIUtils; import com.example.petstoremobile.utils.UIUtils;
import com.example.petstoremobile.viewmodels.CustomerDetailViewModel; import com.example.petstoremobile.viewmodels.CustomerDetailViewModel;
import com.example.petstoremobile.api.auth.TokenManager;
import com.example.petstoremobile.utils.Resource; import com.example.petstoremobile.utils.Resource;
import javax.inject.Inject;
import dagger.hilt.android.AndroidEntryPoint; import dagger.hilt.android.AndroidEntryPoint;
@AndroidEntryPoint @AndroidEntryPoint
@@ -24,6 +27,8 @@ public class CustomerDetailFragment extends Fragment {
private FragmentCustomerDetailBinding binding; private FragmentCustomerDetailBinding binding;
private CustomerDetailViewModel viewModel; private CustomerDetailViewModel viewModel;
@Inject TokenManager tokenManager;
private final String[] STATUSES = {"Active", "Inactive"}; private final String[] STATUSES = {"Active", "Inactive"};
@Override @Override
@@ -65,7 +70,10 @@ public class CustomerDetailFragment extends Fragment {
// Show loyalty points // Show loyalty points
binding.tvLoyaltyPointsLabel.setVisibility(View.VISIBLE); binding.tvLoyaltyPointsLabel.setVisibility(View.VISIBLE);
binding.tvCustomerLoyaltyPoints.setVisibility(View.VISIBLE); binding.etCustomerLoyaltyPoints.setVisibility(View.VISIBLE);
boolean isAdmin = "ADMIN".equalsIgnoreCase(tokenManager.getRole());
binding.etCustomerLoyaltyPoints.setEnabled(isAdmin);
loadCustomerData(customerId); loadCustomerData(customerId);
} else { } else {
@@ -74,7 +82,7 @@ public class CustomerDetailFragment extends Fragment {
binding.btnDeleteCustomer.setVisibility(View.GONE); binding.btnDeleteCustomer.setVisibility(View.GONE);
binding.tvCustomerId.setVisibility(View.GONE); binding.tvCustomerId.setVisibility(View.GONE);
binding.tvLoyaltyPointsLabel.setVisibility(View.GONE); binding.tvLoyaltyPointsLabel.setVisibility(View.GONE);
binding.tvCustomerLoyaltyPoints.setVisibility(View.GONE); binding.etCustomerLoyaltyPoints.setVisibility(View.GONE);
} }
} }
@@ -91,7 +99,7 @@ public class CustomerDetailFragment extends Fragment {
binding.etCustomerPhone.setText(c.getPhone() != null ? c.getPhone() : ""); binding.etCustomerPhone.setText(c.getPhone() != null ? c.getPhone() : "");
binding.spinnerCustomerStatus.setSelection(Boolean.TRUE.equals(c.getActive()) ? 0 : 1); binding.spinnerCustomerStatus.setSelection(Boolean.TRUE.equals(c.getActive()) ? 0 : 1);
int pts = c.getLoyaltyPoints() != null ? c.getLoyaltyPoints() : 0; int pts = c.getLoyaltyPoints() != null ? c.getLoyaltyPoints() : 0;
binding.tvCustomerLoyaltyPoints.setText(String.valueOf(pts)); binding.etCustomerLoyaltyPoints.setText(String.valueOf(pts));
} }
} }
}); });
@@ -121,6 +129,12 @@ public class CustomerDetailFragment extends Fragment {
if (!InputValidator.isValidEmail(binding.etCustomerEmail)) return; if (!InputValidator.isValidEmail(binding.etCustomerEmail)) return;
if (!InputValidator.isValidPhone(binding.etCustomerPhone)) return; if (!InputValidator.isValidPhone(binding.etCustomerPhone)) return;
Integer loyaltyPoints = null;
if (viewModel.isEditing()) {
if (!InputValidator.isPositiveInteger(binding.etCustomerLoyaltyPoints, "Loyalty Points")) return;
loyaltyPoints = Integer.parseInt(binding.etCustomerLoyaltyPoints.getText().toString().trim());
}
String username = binding.etCustomerUsername.getText().toString().trim(); String username = binding.etCustomerUsername.getText().toString().trim();
String password = viewModel.isEditing() ? null : binding.etCustomerPassword.getText().toString().trim(); String password = viewModel.isEditing() ? null : binding.etCustomerPassword.getText().toString().trim();
String firstName = binding.etCustomerFirstName.getText().toString().trim(); String firstName = binding.etCustomerFirstName.getText().toString().trim();
@@ -129,9 +143,7 @@ public class CustomerDetailFragment extends Fragment {
String phone = binding.etCustomerPhone.getText().toString().trim(); String phone = binding.etCustomerPhone.getText().toString().trim();
boolean active = binding.spinnerCustomerStatus.getSelectedItemPosition() == 0; boolean active = binding.spinnerCustomerStatus.getSelectedItemPosition() == 0;
CustomerDTO dto = new CustomerDTO(username, password, firstName, lastName, email, phone); CustomerDTO dto = viewModel.createCustomerDto(username, password, firstName, lastName, email, phone, active, loyaltyPoints);
dto.setFullName(firstName + " " + lastName);
dto.setActive(active);
viewModel.saveCustomer(dto).observe(getViewLifecycleOwner(), resource -> { viewModel.saveCustomer(dto).observe(getViewLifecycleOwner(), resource -> {
if (resource != null) { if (resource != null) {

View File

@@ -101,6 +101,14 @@ public class PetDetailFragment extends Fragment {
DropdownDTO::getLabel, "-- Select Species --", null, DropdownDTO::getId); DropdownDTO::getLabel, "-- Select Species --", null, DropdownDTO::getId);
SpinnerUtils.setSelectionByValue(binding.spinnerPetSpecies, selectedSpecies); SpinnerUtils.setSelectionByValue(binding.spinnerPetSpecies, selectedSpecies);
}); });
viewModel.getBreedList().observe(getViewLifecycleOwner(), list -> {
PetDetailViewModel.ViewState state = viewModel.getViewState().getValue();
String selectedBreed = state != null ? state.selectedBreed : null;
SpinnerUtils.populateSpinner(requireContext(), binding.spinnerPetBreed, list,
DropdownDTO::getLabel, "-- Select Breed --", null, DropdownDTO::getId);
SpinnerUtils.setSelectionByValue(binding.spinnerPetBreed, selectedBreed);
});
} }
private void setLoading(boolean loading) { private void setLoading(boolean loading) {
@@ -118,7 +126,7 @@ public class PetDetailFragment extends Fragment {
private void savePet() { private void savePet() {
if (!InputValidator.isNotEmpty(binding.etPetName, "Pet Name")) return; if (!InputValidator.isNotEmpty(binding.etPetName, "Pet Name")) return;
if (!InputValidator.isSpinnerSelected(binding.spinnerPetSpecies, "Species")) return; if (!InputValidator.isSpinnerSelected(binding.spinnerPetSpecies, "Species")) return;
if (!InputValidator.isNotEmpty(binding.etPetBreed, "Breed")) return; if (!InputValidator.isSpinnerSelected(binding.spinnerPetBreed, "Breed")) return;
if (!InputValidator.isPositiveInteger(binding.etPetAge, "Age")) return; if (!InputValidator.isPositiveInteger(binding.etPetAge, "Age")) return;
if (!InputValidator.isPositiveDecimal(binding.etPetPrice, "Price")) return; if (!InputValidator.isPositiveDecimal(binding.etPetPrice, "Price")) return;
@@ -127,7 +135,11 @@ public class PetDetailFragment extends Fragment {
String species = (speciesOptions != null && binding.spinnerPetSpecies.getSelectedItemPosition() > 0) String species = (speciesOptions != null && binding.spinnerPetSpecies.getSelectedItemPosition() > 0)
? speciesOptions.get(binding.spinnerPetSpecies.getSelectedItemPosition() - 1).getLabel() ? speciesOptions.get(binding.spinnerPetSpecies.getSelectedItemPosition() - 1).getLabel()
: ""; : "";
String breed = binding.etPetBreed.getText().toString().trim();
List<DropdownDTO> breedOptions = viewModel.getBreedList().getValue();
String breed = (breedOptions != null && binding.spinnerPetBreed.getSelectedItemPosition() > 0)
? breedOptions.get(binding.spinnerPetBreed.getSelectedItemPosition() - 1).getLabel()
: "";
int age = Integer.parseInt(binding.etPetAge.getText().toString().trim()); int age = Integer.parseInt(binding.etPetAge.getText().toString().trim());
double price = Double.parseDouble(binding.etPetPrice.getText().toString().trim()); double price = Double.parseDouble(binding.etPetPrice.getText().toString().trim());
String status = binding.spinnerPetStatus.getSelectedItem().toString(); String status = binding.spinnerPetStatus.getSelectedItem().toString();
@@ -152,6 +164,10 @@ public class PetDetailFragment extends Fragment {
if (!InputValidator.isSpinnerSelected(binding.spinnerCustomer, "Owner")) return; if (!InputValidator.isSpinnerSelected(binding.spinnerCustomer, "Owner")) return;
if (!InputValidator.isSpinnerSelected(binding.spinnerStore, "Store")) return; if (!InputValidator.isSpinnerSelected(binding.spinnerStore, "Store")) return;
} }
if ("Pending".equalsIgnoreCase(status)) {
if (!InputValidator.isSpinnerSelected(binding.spinnerCustomer, "Owner")) return;
if (!InputValidator.isSpinnerSelected(binding.spinnerStore, "Store")) return;
}
PetDTO petDTO = new PetDTO(); PetDTO petDTO = new PetDTO();
petDTO.setPetName(name); petDTO.setPetName(name);
@@ -236,7 +252,6 @@ public class PetDetailFragment extends Fragment {
if (resource.status == Resource.Status.SUCCESS && resource.data != null) { if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
PetDTO p = resource.data; PetDTO p = resource.data;
binding.etPetName.setText(p.getPetName()); binding.etPetName.setText(p.getPetName());
binding.etPetBreed.setText(p.getPetBreed());
binding.etPetAge.setText(String.valueOf(p.getPetAge())); binding.etPetAge.setText(String.valueOf(p.getPetAge()));
if (p.getPetPrice() != null) { if (p.getPetPrice() != null) {
binding.etPetPrice.setText(String.format(Locale.getDefault(), "%.2f", p.getPetPrice())); binding.etPetPrice.setText(String.format(Locale.getDefault(), "%.2f", p.getPetPrice()));
@@ -279,6 +294,11 @@ public class PetDetailFragment extends Fragment {
viewModel.onSpeciesSelected(p); viewModel.onSpeciesSelected(p);
}); });
SpinnerUtils.setOnIndexSelectedListener(binding.spinnerPetBreed, p -> {
if (isUpdatingUI) return;
viewModel.onBreedSelected(p);
});
SpinnerUtils.setOnIndexSelectedListener(binding.spinnerCustomer, p -> { SpinnerUtils.setOnIndexSelectedListener(binding.spinnerCustomer, p -> {
if (isUpdatingUI) return; if (isUpdatingUI) return;
viewModel.onCustomerSelected(p); viewModel.onCustomerSelected(p);
@@ -306,7 +326,7 @@ public class PetDetailFragment extends Fragment {
binding.btnSavePet.setText(state.saveButtonText); binding.btnSavePet.setText(state.saveButtonText);
UIUtils.setViewsEnabled(state.isSpeciesEnabled, binding.spinnerPetSpecies); UIUtils.setViewsEnabled(state.isSpeciesEnabled, binding.spinnerPetSpecies);
UIUtils.setViewsEnabled(state.isBreedEnabled, binding.etPetBreed); UIUtils.setViewsEnabled(state.isBreedEnabled, binding.spinnerPetBreed);
UIUtils.setViewsEnabled(state.isCustomerEnabled, binding.spinnerCustomer); UIUtils.setViewsEnabled(state.isCustomerEnabled, binding.spinnerCustomer);
UIUtils.setViewsEnabled(state.isStoreEnabled, binding.spinnerStore); UIUtils.setViewsEnabled(state.isStoreEnabled, binding.spinnerStore);
@@ -323,6 +343,13 @@ public class PetDetailFragment extends Fragment {
SpinnerUtils.setSelectionByValue(binding.spinnerPetSpecies, state.selectedSpecies); SpinnerUtils.setSelectionByValue(binding.spinnerPetSpecies, state.selectedSpecies);
} }
List<DropdownDTO> breeds = viewModel.getBreedList().getValue();
if (breeds != null) {
SpinnerUtils.populateSpinner(requireContext(), binding.spinnerPetBreed, breeds,
DropdownDTO::getLabel, "-- Select Breed --", null, DropdownDTO::getId);
SpinnerUtils.setSelectionByValue(binding.spinnerPetBreed, state.selectedBreed);
}
if (!state.isCustomerEnabled && binding.spinnerCustomer.getSelectedItemPosition() != 0) { if (!state.isCustomerEnabled && binding.spinnerCustomer.getSelectedItemPosition() != 0) {
binding.spinnerCustomer.setSelection(0); binding.spinnerCustomer.setSelection(0);
} }

View File

@@ -302,11 +302,7 @@ public class RefundFragment extends Fragment {
} }
private void updateRefundTotal() { private void updateRefundTotal() {
BigDecimal total = BigDecimal.ZERO; BigDecimal total = viewModel.calculateRefundTotal();
List<RefundViewModel.RefundItem> cart = viewModel.getRefundCart().getValue();
if (cart != null) {
for (RefundViewModel.RefundItem item : cart) total = total.add(item.getTotal());
}
binding.tvRefundTotal.setText("Refund Total: $" + total.setScale(2, RoundingMode.HALF_UP)); binding.tvRefundTotal.setText("Refund Total: $" + total.setScale(2, RoundingMode.HALF_UP));
} }
@@ -321,9 +317,7 @@ public class RefundFragment extends Fragment {
} }
String payment = PAYMENT_METHODS[binding.spinnerRefundPayment.getSelectedItemPosition()]; String payment = PAYMENT_METHODS[binding.spinnerRefundPayment.getSelectedItemPosition()];
BigDecimal total = BigDecimal.ZERO; final BigDecimal finalTotal = viewModel.calculateRefundTotal();
for (RefundViewModel.RefundItem item : viewModel.getRefundCart().getValue()) total = total.add(item.getTotal());
final BigDecimal finalTotal = total;
DialogUtils.showConfirmDialog(requireContext(), "Confirm Refund", DialogUtils.showConfirmDialog(requireContext(), "Confirm Refund",
"Process refund for Sale #" + viewModel.getCurrentSale().getSaleId() "Process refund for Sale #" + viewModel.getCurrentSale().getSaleId()

View File

@@ -14,6 +14,7 @@ import com.example.petstoremobile.dtos.*;
import com.example.petstoremobile.viewmodels.SaleDetailViewModel; import com.example.petstoremobile.viewmodels.SaleDetailViewModel;
import com.example.petstoremobile.utils.SpinnerUtils; import com.example.petstoremobile.utils.SpinnerUtils;
import com.example.petstoremobile.utils.DialogUtils; import com.example.petstoremobile.utils.DialogUtils;
import com.example.petstoremobile.utils.DateTimeUtils;
import com.example.petstoremobile.utils.InputValidator; import com.example.petstoremobile.utils.InputValidator;
import com.example.petstoremobile.utils.Resource; import com.example.petstoremobile.utils.Resource;
import com.example.petstoremobile.utils.UIUtils; import com.example.petstoremobile.utils.UIUtils;
@@ -68,6 +69,10 @@ public class SaleDetailFragment extends Fragment {
return "STAFF".equalsIgnoreCase(tokenManager.getRole()); return "STAFF".equalsIgnoreCase(tokenManager.getRole());
} }
private boolean isAdmin() {
return "ADMIN".equalsIgnoreCase(tokenManager.getRole());
}
private void observeViewModel() { private void observeViewModel() {
viewModel.getStoreList().observe(getViewLifecycleOwner(), list -> { viewModel.getStoreList().observe(getViewLifecycleOwner(), list -> {
Long primaryStoreId = tokenManager.getPrimaryStoreId(); Long primaryStoreId = tokenManager.getPrimaryStoreId();
@@ -77,7 +82,7 @@ public class SaleDetailFragment extends Fragment {
if (isStaff()) { if (isStaff()) {
UIUtils.setViewsEnabled(false, binding.spinnerSaleStore); UIUtils.setViewsEnabled(false, binding.spinnerSaleStore);
if (primaryStoreId == null) { if (primaryStoreId == null) {
Toast.makeText(requireContext(), "No store assigned to your account. Contact an admin.", Toast.LENGTH_LONG).show(); UIUtils.showToast(requireContext(), "No store assigned to your account. Contact an admin.");
} }
} }
}); });
@@ -96,6 +101,17 @@ public class SaleDetailFragment extends Fragment {
renderCartItems(); renderCartItems();
updateTotal(); updateTotal();
}); });
viewModel.getSelectedCustomerData().observe(getViewLifecycleOwner(), customer -> {
if (customer != null && !viewModel.isViewOnly()) {
binding.llLoyaltyPoints.setVisibility(View.VISIBLE);
binding.tvAvailablePoints.setText("(Available: " + customer.getLoyaltyPoints() + ")");
binding.cbUseLoyaltyPoints.setEnabled(customer.getLoyaltyPoints() >= 20);
} else {
binding.llLoyaltyPoints.setVisibility(View.GONE);
binding.cbUseLoyaltyPoints.setChecked(false);
}
});
} }
private void handleArguments() { private void handleArguments() {
@@ -105,11 +121,11 @@ public class SaleDetailFragment extends Fragment {
boolean viewOnly = a.getBoolean("viewOnly", false); boolean viewOnly = a.getBoolean("viewOnly", false);
viewModel.setSaleId(saleId, viewOnly); viewModel.setSaleId(saleId, viewOnly);
binding.tvSaleMode.setText("Sale #" + saleId); binding.tvSaleMode.setText("Sale #" + DateTimeUtils.formatId(saleId));
binding.tvSaleDetailId.setText("ID: " + saleId); binding.tvSaleDetailId.setText("ID: " + DateTimeUtils.formatId(saleId));
boolean isRefund = a.getBoolean("isRefund", false); boolean isRefund = a.getBoolean("isRefund", false);
if (isRefund) { if (isRefund || isAdmin()) {
binding.btnRefundSale.setVisibility(View.GONE); binding.btnRefundSale.setVisibility(View.GONE);
} }
@@ -128,9 +144,8 @@ public class SaleDetailFragment extends Fragment {
binding.spinnerPaymentMethod.setVisibility(View.GONE); binding.spinnerPaymentMethod.setVisibility(View.GONE);
binding.tvSaleStore.setVisibility(View.VISIBLE); binding.tvSaleStore.setVisibility(View.VISIBLE);
binding.tvSalePaymentMethod.setVisibility(View.VISIBLE); binding.tvSalePaymentMethod.setVisibility(View.VISIBLE);
// Show refund button only if it's not already a refund binding.btnRefundSale.setVisibility((isRefund || isAdmin()) ? View.GONE : View.VISIBLE);
binding.btnRefundSale.setVisibility(isRefund ? View.GONE : View.VISIBLE);
} }
loadSaleDetails(); loadSaleDetails();
@@ -173,23 +188,36 @@ public class SaleDetailFragment extends Fragment {
setLoading(resource.status == Resource.Status.LOADING); setLoading(resource.status == Resource.Status.LOADING);
if (resource.status == Resource.Status.SUCCESS && resource.data != null) { if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
SaleDTO sale = resource.data; SaleDTO sale = resource.data;
binding.tvSaleDetailTotal.setText("Total: $" + sale.getTotalAmount()); binding.tvSaleDetailTotal.setText("Total: $" + String.format(Locale.getDefault(), "%.2f", sale.getTotalAmount()));
binding.tvSaleSubtotal.setText("$" + (sale.getSubtotalAmount() != null ? sale.getSubtotalAmount() : sale.getTotalAmount())); binding.tvSaleSubtotal.setText("$" + String.format(Locale.getDefault(), "%.2f", (sale.getSubtotalAmount() != null ? sale.getSubtotalAmount() : sale.getTotalAmount())));
if (sale.getCouponDiscountAmount() != null && sale.getCouponDiscountAmount().compareTo(BigDecimal.ZERO) > 0) { if (sale.getCouponDiscountAmount() != null && sale.getCouponDiscountAmount().compareTo(BigDecimal.ZERO) > 0) {
binding.llCouponDiscount.setVisibility(View.VISIBLE); binding.llCouponDiscount.setVisibility(View.VISIBLE);
binding.tvSaleCouponDiscount.setText("-$" + sale.getCouponDiscountAmount()); binding.tvSaleCouponDiscount.setText("-$" + String.format(Locale.getDefault(), "%.2f", sale.getCouponDiscountAmount()));
} else { } else {
binding.llCouponDiscount.setVisibility(View.GONE); binding.llCouponDiscount.setVisibility(View.GONE);
} }
if (sale.getEmployeeDiscountAmount() != null && sale.getEmployeeDiscountAmount().compareTo(BigDecimal.ZERO) > 0) { if (sale.getEmployeeDiscountAmount() != null && sale.getEmployeeDiscountAmount().compareTo(BigDecimal.ZERO) > 0) {
binding.llEmployeeDiscount.setVisibility(View.VISIBLE); binding.llEmployeeDiscount.setVisibility(View.VISIBLE);
binding.tvSaleEmployeeDiscount.setText("-$" + sale.getEmployeeDiscountAmount()); binding.tvSaleEmployeeDiscount.setText("-$" + String.format(Locale.getDefault(), "%.2f", sale.getEmployeeDiscountAmount()));
} else { } else {
binding.llEmployeeDiscount.setVisibility(View.GONE); binding.llEmployeeDiscount.setVisibility(View.GONE);
} }
if (sale.getPointsDiscountAmount() != null && sale.getPointsDiscountAmount().compareTo(BigDecimal.ZERO) > 0) {
binding.llLoyaltyDiscount.setVisibility(View.VISIBLE);
binding.tvSaleLoyaltyDiscount.setText("-$" + String.format(Locale.getDefault(), "%.2f", sale.getPointsDiscountAmount()));
if (sale.getPointsUsed() != null) {
binding.tvLoyaltyDiscountLabel.setText("Loyalty Discount (" + sale.getPointsUsed() + " pts):");
}
} else if (sale.getLoyaltyDiscountAmount() != null && sale.getLoyaltyDiscountAmount().compareTo(BigDecimal.ZERO) > 0) {
binding.llLoyaltyDiscount.setVisibility(View.VISIBLE);
binding.tvSaleLoyaltyDiscount.setText("-$" + String.format(Locale.getDefault(), "%.2f", sale.getLoyaltyDiscountAmount()));
} else {
binding.llLoyaltyDiscount.setVisibility(View.GONE);
}
binding.tvSaleChannel.setText(sale.getChannel() != null ? sale.getChannel() : ""); binding.tvSaleChannel.setText(sale.getChannel() != null ? sale.getChannel() : "");
binding.tvSalePoints.setText(String.valueOf(sale.getPointsEarned() != null ? sale.getPointsEarned() : 0)); binding.tvSalePoints.setText(String.valueOf(sale.getPointsEarned() != null ? sale.getPointsEarned() : 0));
binding.tvSaleStore.setText(sale.getStoreName() != null ? sale.getStoreName() : ""); binding.tvSaleStore.setText(sale.getStoreName() != null ? sale.getStoreName() : "");
@@ -216,7 +244,7 @@ public class SaleDetailFragment extends Fragment {
binding.btnApplyCoupon.setOnClickListener(v -> { binding.btnApplyCoupon.setOnClickListener(v -> {
String code = binding.etCouponCode.getText().toString().trim(); String code = binding.etCouponCode.getText().toString().trim();
if (code.isEmpty()) { if (code.isEmpty()) {
Toast.makeText(getContext(), "Enter a coupon code", Toast.LENGTH_SHORT).show(); UIUtils.showToast(getContext(), "Enter a coupon code");
return; return;
} }
setLoading(true); setLoading(true);
@@ -286,7 +314,7 @@ public class SaleDetailFragment extends Fragment {
for (SaleDTO.SaleItemDTO existing : viewModel.getCartItems().getValue()) { for (SaleDTO.SaleItemDTO existing : viewModel.getCartItems().getValue()) {
if (existing.getProdId().equals(product.getProdId())) { if (existing.getProdId().equals(product.getProdId())) {
Toast.makeText(getContext(), "Product already added", Toast.LENGTH_SHORT).show(); UIUtils.showToast(getContext(), "Product already added");
return; return;
} }
} }
@@ -294,6 +322,19 @@ public class SaleDetailFragment extends Fragment {
viewModel.addToCart(new SaleDTO.SaleItemDTO(product.getProdId(), qty)); viewModel.addToCart(new SaleDTO.SaleItemDTO(product.getProdId(), qty));
binding.etSaleQuantity.setText(""); binding.etSaleQuantity.setText("");
}); });
SpinnerUtils.setOnIndexSelectedListener(binding.spinnerSaleCustomer, p -> {
if (p > 0) {
Long id = viewModel.getCustomerList().getValue().get(p - 1).getId();
viewModel.selectCustomer(id);
} else {
viewModel.selectCustomer(null);
}
});
binding.cbUseLoyaltyPoints.setOnCheckedChangeListener((v, checked) -> {
viewModel.setUseLoyaltyPoints(checked);
updateTotal();
});
} }
private void renderCartItems() { private void renderCartItems() {
@@ -333,7 +374,7 @@ public class SaleDetailFragment extends Fragment {
TextView tvPrice = new TextView(getContext()); TextView tvPrice = new TextView(getContext());
tvPrice.setLayoutParams(new LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f)); tvPrice.setLayoutParams(new LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f));
tvPrice.setText(price != null ? "$" + price : ""); tvPrice.setText(price != null ? "$" + String.format(Locale.getDefault(), "%.2f", price) : "");
row.addView(tvName); row.addView(tvName);
row.addView(tvQty); row.addView(tvQty);
@@ -357,23 +398,35 @@ public class SaleDetailFragment extends Fragment {
private void updateTotal() { private void updateTotal() {
BigDecimal subtotal = viewModel.calculateSubtotal(); BigDecimal subtotal = viewModel.calculateSubtotal();
BigDecimal discount = viewModel.calculateDiscount(); BigDecimal couponDiscount = viewModel.calculateCouponDiscount();
BigDecimal total = subtotal.subtract(discount); BigDecimal loyaltyDiscount = viewModel.calculateLoyaltyDiscount();
binding.tvSaleSubtotal.setText("$" + subtotal); BigDecimal total = subtotal.subtract(couponDiscount).subtract(loyaltyDiscount);
if (discount.compareTo(BigDecimal.ZERO) > 0) {
binding.tvSaleSubtotal.setText("$" + String.format(Locale.getDefault(), "%.2f", subtotal));
if (couponDiscount.compareTo(BigDecimal.ZERO) > 0) {
binding.llCouponDiscount.setVisibility(View.VISIBLE); binding.llCouponDiscount.setVisibility(View.VISIBLE);
binding.tvSaleCouponDiscount.setText("-$" + discount); binding.tvSaleCouponDiscount.setText("-$" + String.format(Locale.getDefault(), "%.2f", couponDiscount));
} else { } else {
binding.llCouponDiscount.setVisibility(View.GONE); binding.llCouponDiscount.setVisibility(View.GONE);
} }
binding.tvSaleDetailTotal.setText("Total: $" + total);
if (loyaltyDiscount.compareTo(BigDecimal.ZERO) > 0) {
binding.llLoyaltyDiscount.setVisibility(View.VISIBLE);
binding.tvLoyaltyDiscountLabel.setText("Loyalty Discount (" + viewModel.calculatePointsToUse() + " pts):");
binding.tvSaleLoyaltyDiscount.setText("-$" + String.format(Locale.getDefault(), "%.2f", loyaltyDiscount));
} else {
binding.llLoyaltyDiscount.setVisibility(View.GONE);
}
binding.tvSaleDetailTotal.setText("Total: $" + String.format(Locale.getDefault(), "%.2f", total));
} }
private void saveSale() { private void saveSale() {
if (!InputValidator.isSpinnerSelected(binding.spinnerSaleStore, "Store")) return; if (!InputValidator.isSpinnerSelected(binding.spinnerSaleStore, "Store")) return;
if (viewModel.getCartItems().getValue() == null || viewModel.getCartItems().getValue().isEmpty()) { if (viewModel.getCartItems().getValue() == null || viewModel.getCartItems().getValue().isEmpty()) {
Toast.makeText(getContext(), "Add at least one item", Toast.LENGTH_SHORT).show(); UIUtils.showToast(getContext(), "Add at least one item");
return; return;
} }
@@ -387,12 +440,16 @@ public class SaleDetailFragment extends Fragment {
SaleDTO dto = new SaleDTO(store.getId(), payment, viewModel.getCartItems().getValue(), false, null, customerId); SaleDTO dto = new SaleDTO(store.getId(), payment, viewModel.getCartItems().getValue(), false, null, customerId);
dto.setCouponId(viewModel.getAppliedCouponId()); dto.setCouponId(viewModel.getAppliedCouponId());
if (Boolean.TRUE.equals(viewModel.getUseLoyaltyPoints().getValue())) {
dto.setPointsUsed(viewModel.calculatePointsToUse());
}
viewModel.createSale(dto).observe(getViewLifecycleOwner(), resource -> { viewModel.createSale(dto).observe(getViewLifecycleOwner(), resource -> {
if (resource != null) { if (resource != null) {
setLoading(resource.status == Resource.Status.LOADING); setLoading(resource.status == Resource.Status.LOADING);
if (resource.status == Resource.Status.SUCCESS) { if (resource.status == Resource.Status.SUCCESS) {
Toast.makeText(getContext(), "Sale saved!", Toast.LENGTH_SHORT).show(); UIUtils.showToast(getContext(), "Sale saved!");
navigateBack(); navigateBack();
} else if (resource.status == Resource.Status.ERROR) { } else if (resource.status == Resource.Status.ERROR) {
DialogUtils.showInfoDialog(requireContext(), "Save Error", resource.message); DialogUtils.showInfoDialog(requireContext(), "Save Error", resource.message);

View File

@@ -161,7 +161,7 @@ public class StaffDetailFragment extends Fragment {
List<DropdownDTO> stores = viewModel.getStoreList().getValue(); List<DropdownDTO> stores = viewModel.getStoreList().getValue();
Long storeId = stores.get(binding.spinnerStaffStore.getSelectedItemPosition() - 1).getId(); Long storeId = stores.get(binding.spinnerStaffStore.getSelectedItemPosition() - 1).getId();
EmployeeDTO dto = new EmployeeDTO( EmployeeDTO dto = viewModel.createEmployeeDto(
username, username,
password.isEmpty() ? null : password, password.isEmpty() ? null : password,
firstName, firstName,

View File

@@ -126,7 +126,7 @@ public class PetProfileFragment extends Fragment {
String status = pet.getPetStatus(); String status = pet.getPetStatus();
if ("Adopted".equalsIgnoreCase(status) || "Owned".equalsIgnoreCase(status)) { if ("Adopted".equalsIgnoreCase(status) || "Owned".equalsIgnoreCase(status) || "Pending".equalsIgnoreCase(status)) {
binding.layoutPetOwner.setVisibility(View.VISIBLE); binding.layoutPetOwner.setVisibility(View.VISIBLE);
if (pet.getCustomerName() != null && !pet.getCustomerName().isEmpty()) { if (pet.getCustomerName() != null && !pet.getCustomerName().isEmpty()) {
binding.tvPetOwner.setText(pet.getCustomerName()); binding.tvPetOwner.setText(pet.getCustomerName());
@@ -137,7 +137,7 @@ public class PetProfileFragment extends Fragment {
binding.layoutPetOwner.setVisibility(View.GONE); binding.layoutPetOwner.setVisibility(View.GONE);
} }
if ("Available".equalsIgnoreCase(status) || "Adopted".equalsIgnoreCase(status)) { if ("Available".equalsIgnoreCase(status) || "Adopted".equalsIgnoreCase(status) || "Pending".equalsIgnoreCase(status)) {
binding.layoutPetStore.setVisibility(View.VISIBLE); binding.layoutPetStore.setVisibility(View.VISIBLE);
if (pet.getStoreName() != null && !pet.getStoreName().isEmpty()) { if (pet.getStoreName() != null && !pet.getStoreName().isEmpty()) {
binding.tvPetStore.setText(pet.getStoreName()); binding.tvPetStore.setText(pet.getStoreName());

View File

@@ -58,6 +58,10 @@ public class PetRepository extends BaseRepository {
return executeCall(petApi.getPetSpeciesDropdowns()); return executeCall(petApi.getPetSpeciesDropdowns());
} }
public LiveData<Resource<List<DropdownDTO>>> getPetBreedsDropdowns(String species) {
return executeCall(petApi.getPetBreedsDropdowns(species));
}
/** /**
* Retrieves available pets for a specific store. * Retrieves available pets for a specific store.
*/ */

View File

@@ -100,12 +100,13 @@ public class InputValidator {
return true; return true;
} }
// Checks if the phone number is valid // Checks if the phone number is valid in (XXX) XXX-XXXX format
public static boolean isValidPhone(EditText field) { public static boolean isValidPhone(EditText field) {
String phone = field.getText().toString().trim(); String phone = field.getText().toString().trim();
// Android built in phone validation pattern // Matches (XXX) XXX-XXXX format
if (phone.isEmpty() || !android.util.Patterns.PHONE.matcher(phone).matches()) { String pattern = "^\\(\\d{3}\\) \\d{3}-\\d{4}$";
field.setError("Enter a valid phone number"); if (phone.isEmpty() || !phone.matches(pattern)) {
field.setError("Enter a valid phone number: (XXX) XXX-XXXX");
field.requestFocus(); field.requestFocus();
return false; return false;
} }

View File

@@ -31,6 +31,19 @@ public class CustomerDetailViewModel extends ViewModel {
public long getCustomerId() { return customerId; } public long getCustomerId() { return customerId; }
public boolean isEditing() { return isEditing; } public boolean isEditing() { return isEditing; }
public CustomerDTO createCustomerDto(String username, String password, String firstName,
String lastName, String email, String phone,
boolean active, Integer loyaltyPoints) {
CustomerDTO dto = new CustomerDTO(username, password, firstName, lastName, email, phone);
dto.setFullName(firstName + " " + lastName);
dto.setActive(active);
dto.setRole("CUSTOMER");
if (isEditing && loyaltyPoints != null) {
dto.setLoyaltyPoints(loyaltyPoints);
}
return dto;
}
public LiveData<Resource<CustomerDTO>> loadCustomer(long id) { public LiveData<Resource<CustomerDTO>> loadCustomer(long id) {
return repository.getCustomerById(id); return repository.getCustomerById(id);
} }

View File

@@ -25,6 +25,7 @@ public class PetDetailViewModel extends ViewModel {
private static final String STATUS_AVAILABLE = "Available"; private static final String STATUS_AVAILABLE = "Available";
private static final String STATUS_ADOPTED = "Adopted"; private static final String STATUS_ADOPTED = "Adopted";
private static final String STATUS_OWNED = "Owned"; private static final String STATUS_OWNED = "Owned";
private static final String STATUS_PENDING = "Pending";
private final PetRepository petRepository; private final PetRepository petRepository;
private final CustomerRepository customerRepository; private final CustomerRepository customerRepository;
@@ -33,6 +34,7 @@ public class PetDetailViewModel extends ViewModel {
private final MutableLiveData<List<DropdownDTO>> customerList = new MutableLiveData<>(new ArrayList<>()); private final MutableLiveData<List<DropdownDTO>> customerList = new MutableLiveData<>(new ArrayList<>());
private final MutableLiveData<List<DropdownDTO>> storeList = new MutableLiveData<>(new ArrayList<>()); private final MutableLiveData<List<DropdownDTO>> storeList = new MutableLiveData<>(new ArrayList<>());
private final MutableLiveData<List<DropdownDTO>> speciesList = new MutableLiveData<>(new ArrayList<>()); private final MutableLiveData<List<DropdownDTO>> speciesList = new MutableLiveData<>(new ArrayList<>());
private final MutableLiveData<List<DropdownDTO>> breedList = new MutableLiveData<>(new ArrayList<>());
private final MutableLiveData<Boolean> isLoading = new MutableLiveData<>(false); private final MutableLiveData<Boolean> isLoading = new MutableLiveData<>(false);
private final MutableLiveData<ViewState> viewState = new MutableLiveData<>(new ViewState()); private final MutableLiveData<ViewState> viewState = new MutableLiveData<>(new ViewState());
@@ -40,6 +42,7 @@ public class PetDetailViewModel extends ViewModel {
private Long selectedCustomerId = null; private Long selectedCustomerId = null;
private Long selectedStoreId = null; private Long selectedStoreId = null;
private String selectedSpecies = null; private String selectedSpecies = null;
private String selectedBreed = null;
private boolean isOriginallyOwnedOrAdopted = false; private boolean isOriginallyOwnedOrAdopted = false;
private Long originalCustomerId = null; private Long originalCustomerId = null;
@@ -111,10 +114,33 @@ public class PetDetailViewModel extends ViewModel {
List<DropdownDTO> list = speciesList.getValue(); List<DropdownDTO> list = speciesList.getValue();
if (position > 0 && list != null && position <= list.size()) { if (position > 0 && list != null && position <= list.size()) {
selectedSpecies = list.get(position - 1).getLabel(); selectedSpecies = list.get(position - 1).getLabel();
loadBreeds(selectedSpecies);
} else { } else {
selectedSpecies = null; selectedSpecies = null;
breedList.setValue(new ArrayList<>());
} }
updateViewState(state -> state.selectedSpecies = selectedSpecies); updateViewState(state -> {
state.selectedSpecies = selectedSpecies;
state.isBreedEnabled = !state.isEditing && (selectedSpecies != null);
});
}
public void onBreedSelected(int position) {
List<DropdownDTO> list = breedList.getValue();
if (position > 0 && list != null && position <= list.size()) {
selectedBreed = list.get(position - 1).getLabel();
} else {
selectedBreed = null;
}
updateViewState(state -> state.selectedBreed = selectedBreed);
}
private void loadBreeds(String species) {
observeOnce(petRepository.getPetBreedsDropdowns(species), resource -> {
if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) {
breedList.setValue(resource.data);
}
});
} }
public void onStoreSelected(int position) { public void onStoreSelected(int position) {
@@ -154,12 +180,15 @@ public class PetDetailViewModel extends ViewModel {
selectedCustomerId = null; selectedCustomerId = null;
selectedStoreId = null; selectedStoreId = null;
selectedSpecies = null; selectedSpecies = null;
selectedBreed = null;
state.selectedCustomerId = null; state.selectedCustomerId = null;
state.selectedStoreId = null; state.selectedStoreId = null;
state.selectedSpecies = null; state.selectedSpecies = null;
state.selectedBreed = null;
state.selectedStatus = STATUS_AVAILABLE; state.selectedStatus = STATUS_AVAILABLE;
state.isCustomerEnabled = false; state.isCustomerEnabled = false;
state.isStoreEnabled = true; state.isStoreEnabled = true;
state.isBreedEnabled = false;
} }
}); });
} }
@@ -172,15 +201,22 @@ public class PetDetailViewModel extends ViewModel {
selectedCustomerId = pet.getCustomerId(); selectedCustomerId = pet.getCustomerId();
selectedStoreId = pet.getStoreId(); selectedStoreId = pet.getStoreId();
selectedSpecies = pet.getPetSpecies(); selectedSpecies = pet.getPetSpecies();
selectedBreed = pet.getPetBreed();
isOriginallyOwnedOrAdopted = STATUS_OWNED.equalsIgnoreCase(pet.getPetStatus()) isOriginallyOwnedOrAdopted = STATUS_OWNED.equalsIgnoreCase(pet.getPetStatus())
|| STATUS_ADOPTED.equalsIgnoreCase(pet.getPetStatus()); || STATUS_ADOPTED.equalsIgnoreCase(pet.getPetStatus());
originalCustomerId = pet.getCustomerId(); originalCustomerId = pet.getCustomerId();
if (selectedSpecies != null) {
loadBreeds(selectedSpecies);
}
updateViewState(state -> { updateViewState(state -> {
state.selectedCustomerId = selectedCustomerId; state.selectedCustomerId = selectedCustomerId;
state.selectedStoreId = selectedStoreId; state.selectedStoreId = selectedStoreId;
state.selectedSpecies = selectedSpecies; state.selectedSpecies = selectedSpecies;
state.selectedBreed = selectedBreed;
state.selectedStatus = normalizeStatus(pet.getPetStatus()); state.selectedStatus = normalizeStatus(pet.getPetStatus());
state.isBreedEnabled = !state.isEditing && (selectedSpecies != null);
applyStatusRules(state, false); applyStatusRules(state, false);
}); });
} }
@@ -215,6 +251,10 @@ public class PetDetailViewModel extends ViewModel {
return speciesList; return speciesList;
} }
public LiveData<List<DropdownDTO>> getBreedList() {
return breedList;
}
public LiveData<Boolean> getIsLoading() { public LiveData<Boolean> getIsLoading() {
return isLoading; return isLoading;
} }
@@ -253,6 +293,7 @@ public class PetDetailViewModel extends ViewModel {
String normalized = status.trim(); String normalized = status.trim();
if (STATUS_ADOPTED.equalsIgnoreCase(normalized)) return STATUS_ADOPTED; if (STATUS_ADOPTED.equalsIgnoreCase(normalized)) return STATUS_ADOPTED;
if (STATUS_OWNED.equalsIgnoreCase(normalized)) return STATUS_OWNED; if (STATUS_OWNED.equalsIgnoreCase(normalized)) return STATUS_OWNED;
if (STATUS_PENDING.equalsIgnoreCase(normalized)) return STATUS_PENDING;
return STATUS_AVAILABLE; return STATUS_AVAILABLE;
} }
@@ -290,9 +331,10 @@ public class PetDetailViewModel extends ViewModel {
public boolean isStoreEnabled = true; public boolean isStoreEnabled = true;
public String modeTitle = "Add Pet"; public String modeTitle = "Add Pet";
public String saveButtonText = "Add"; public String saveButtonText = "Add";
public String[] availableStatuses = new String[]{STATUS_AVAILABLE, STATUS_ADOPTED, STATUS_OWNED}; public String[] availableStatuses = new String[]{STATUS_AVAILABLE, STATUS_ADOPTED, STATUS_OWNED, STATUS_PENDING};
public String selectedStatus = STATUS_AVAILABLE; public String selectedStatus = STATUS_AVAILABLE;
public String selectedSpecies = null; public String selectedSpecies = null;
public String selectedBreed = null;
public Long selectedCustomerId = null; public Long selectedCustomerId = null;
public Long selectedStoreId = null; public Long selectedStoreId = null;
} }

View File

@@ -10,6 +10,7 @@ import com.example.petstoremobile.repositories.SaleRepository;
import com.example.petstoremobile.utils.Resource; import com.example.petstoremobile.utils.Resource;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
@@ -125,6 +126,33 @@ public class RefundViewModel extends ViewModel {
refundCart.setValue(cart); refundCart.setValue(cart);
} }
public BigDecimal calculateRefundTotal() {
SaleDTO sale = currentSale.getValue();
List<RefundItem> cart = refundCart.getValue();
if (sale == null || cart == null || cart.isEmpty()) return BigDecimal.ZERO;
BigDecimal cartSubtotal = BigDecimal.ZERO;
for (RefundItem item : cart) cartSubtotal = cartSubtotal.add(item.getTotal());
BigDecimal originalSubtotal = sale.getSubtotalAmount();
if (originalSubtotal == null || originalSubtotal.compareTo(BigDecimal.ZERO) == 0) {
if (sale.getItems() != null) {
originalSubtotal = BigDecimal.ZERO;
for (SaleDTO.SaleItemDTO item : sale.getItems()) {
if (item.getUnitPrice() != null && item.getQuantity() != null)
originalSubtotal = originalSubtotal.add(item.getUnitPrice().multiply(BigDecimal.valueOf(Math.abs(item.getQuantity()))));
}
}
}
if (originalSubtotal == null || originalSubtotal.compareTo(BigDecimal.ZERO) == 0) return cartSubtotal;
BigDecimal originalTotal = sale.getTotalAmount();
if (originalTotal == null) return cartSubtotal;
BigDecimal ratio = cartSubtotal.divide(originalSubtotal, 10, RoundingMode.HALF_UP);
return originalTotal.abs().multiply(ratio).setScale(2, RoundingMode.HALF_UP);
}
public LiveData<Resource<SaleDTO>> submitRefund(String paymentMethod) { public LiveData<Resource<SaleDTO>> submitRefund(String paymentMethod) {
SaleDTO sale = currentSale.getValue(); SaleDTO sale = currentSale.getValue();
List<RefundItem> cart = refundCart.getValue(); List<RefundItem> cart = refundCart.getValue();

View File

@@ -5,6 +5,7 @@ import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.ViewModel; import androidx.lifecycle.ViewModel;
import com.example.petstoremobile.dtos.CouponDTO; import com.example.petstoremobile.dtos.CouponDTO;
import com.example.petstoremobile.dtos.CustomerDTO;
import com.example.petstoremobile.dtos.DropdownDTO; import com.example.petstoremobile.dtos.DropdownDTO;
import com.example.petstoremobile.dtos.ProductDTO; import com.example.petstoremobile.dtos.ProductDTO;
import com.example.petstoremobile.dtos.SaleDTO; import com.example.petstoremobile.dtos.SaleDTO;
@@ -39,6 +40,8 @@ public class SaleDetailViewModel extends ViewModel {
private final MutableLiveData<List<ProductDTO>> productList = new MutableLiveData<>(new ArrayList<>()); private final MutableLiveData<List<ProductDTO>> productList = new MutableLiveData<>(new ArrayList<>());
private final MutableLiveData<List<SaleDTO.SaleItemDTO>> cartItems = new MutableLiveData<>(new ArrayList<>()); private final MutableLiveData<List<SaleDTO.SaleItemDTO>> cartItems = new MutableLiveData<>(new ArrayList<>());
private final MutableLiveData<CouponDTO> appliedCoupon = new MutableLiveData<>(null); private final MutableLiveData<CouponDTO> appliedCoupon = new MutableLiveData<>(null);
private final MutableLiveData<CustomerDTO> selectedCustomerData = new MutableLiveData<>(null);
private final MutableLiveData<Boolean> useLoyaltyPoints = new MutableLiveData<>(false);
private final MutableLiveData<Boolean> isLoading = new MutableLiveData<>(false); private final MutableLiveData<Boolean> isLoading = new MutableLiveData<>(false);
@Inject @Inject
@@ -111,6 +114,34 @@ public class SaleDetailViewModel extends ViewModel {
appliedCoupon.setValue(coupon); appliedCoupon.setValue(coupon);
} }
public void setUseLoyaltyPoints(boolean use) {
useLoyaltyPoints.setValue(use);
}
public LiveData<Boolean> getUseLoyaltyPoints() {
return useLoyaltyPoints;
}
public LiveData<CustomerDTO> getSelectedCustomerData() {
return selectedCustomerData;
}
public void selectCustomer(Long customerId) {
if (customerId == null) {
selectedCustomerData.setValue(null);
useLoyaltyPoints.setValue(false);
return;
}
customerRepository.getCustomerById(customerId).observeForever(new androidx.lifecycle.Observer<Resource<CustomerDTO>>() {
@Override
public void onChanged(Resource<CustomerDTO> resource) {
if (resource != null && resource.status == Resource.Status.SUCCESS) {
selectedCustomerData.setValue(resource.data);
}
}
});
}
public void clearCoupon() { public void clearCoupon() {
appliedCoupon.setValue(null); appliedCoupon.setValue(null);
} }
@@ -125,6 +156,10 @@ public class SaleDetailViewModel extends ViewModel {
} }
public BigDecimal calculateDiscount() { public BigDecimal calculateDiscount() {
return calculateCouponDiscount().add(calculateLoyaltyDiscount());
}
public BigDecimal calculateCouponDiscount() {
CouponDTO coupon = appliedCoupon.getValue(); CouponDTO coupon = appliedCoupon.getValue();
if (coupon == null || coupon.getDiscountValue() == null) return BigDecimal.ZERO; if (coupon == null || coupon.getDiscountValue() == null) return BigDecimal.ZERO;
BigDecimal subtotal = calculateSubtotal(); BigDecimal subtotal = calculateSubtotal();
@@ -135,6 +170,25 @@ public class SaleDetailViewModel extends ViewModel {
} }
} }
public BigDecimal calculateLoyaltyDiscount() {
if (Boolean.FALSE.equals(useLoyaltyPoints.getValue())) return BigDecimal.ZERO;
CustomerDTO customer = selectedCustomerData.getValue();
if (customer == null || customer.getLoyaltyPoints() == null || customer.getLoyaltyPoints() < 20) {
return BigDecimal.ZERO;
}
BigDecimal subtotalAfterCoupon = calculateSubtotal().subtract(calculateCouponDiscount());
int maxPointsNeeded = subtotalAfterCoupon.multiply(BigDecimal.valueOf(20)).intValue();
int pointsToUse = Math.min(customer.getLoyaltyPoints(), maxPointsNeeded);
return BigDecimal.valueOf(pointsToUse).multiply(BigDecimal.valueOf(0.05)).setScale(2, java.math.RoundingMode.HALF_UP);
}
public int calculatePointsToUse() {
BigDecimal loyaltyDiscount = calculateLoyaltyDiscount();
return loyaltyDiscount.divide(BigDecimal.valueOf(0.05), 0, java.math.RoundingMode.HALF_UP).intValue();
}
public BigDecimal calculateSubtotal() { public BigDecimal calculateSubtotal() {
BigDecimal total = BigDecimal.ZERO; BigDecimal total = BigDecimal.ZERO;
List<SaleDTO.SaleItemDTO> items = cartItems.getValue(); List<SaleDTO.SaleItemDTO> items = cartItems.getValue();

View File

@@ -60,6 +60,14 @@ public class StaffDetailViewModel extends ViewModel {
return isEditing; return isEditing;
} }
public EmployeeDTO createEmployeeDto(String username, String password, String firstName,
String lastName, String email, String phone,
String role, String staffRole, boolean active, Long storeId) {
EmployeeDTO dto = new EmployeeDTO(username, password, firstName, lastName, email, phone, role, staffRole, active, storeId);
dto.setFullName(firstName + " " + lastName);
return dto;
}
public LiveData<Resource<EmployeeDTO>> saveEmployee(EmployeeDTO dto) { public LiveData<Resource<EmployeeDTO>> saveEmployee(EmployeeDTO dto) {
if (isEditing && employeeId > 0) { if (isEditing && employeeId > 0) {
return repository.updateEmployee(employeeId, dto); return repository.updateEmployee(employeeId, dto);

View File

@@ -195,14 +195,13 @@
android:textSize="12sp" android:textSize="12sp"
android:layout_marginBottom="4dp"/> android:layout_marginBottom="4dp"/>
<TextView <EditText
android:id="@+id/tvCustomerLoyaltyPoints" android:id="@+id/etCustomerLoyaltyPoints"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="0" android:hint="0"
android:textColor="@color/text_dark" android:inputType="number"
android:textSize="16sp" android:layout_marginBottom="16dp"/>
android:layout_marginBottom="8dp"/>
</LinearLayout> </LinearLayout>

View File

@@ -109,14 +109,11 @@
android:textSize="12sp" android:textSize="12sp"
android:layout_marginBottom="4dp"/> android:layout_marginBottom="4dp"/>
<EditText <Spinner
android:id="@+id/etPetBreed" android:id="@+id/spinnerPetBreed"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:hint="Enter breed" android:layout_marginBottom="16dp"/>
android:inputType="text"
android:layout_marginBottom="16dp"
android:textColor="@color/text_dark"/>
<TextView <TextView
android:layout_width="wrap_content" android:layout_width="wrap_content"

View File

@@ -128,7 +128,30 @@
android:id="@+id/spinnerSaleCustomer" android:id="@+id/spinnerSaleCustomer"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginBottom="16dp"/> android:layout_marginBottom="8dp"/>
<LinearLayout
android:id="@+id/llLoyaltyPoints"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:layout_marginBottom="16dp"
android:visibility="gone">
<CheckBox
android:id="@+id/cbUseLoyaltyPoints"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Use Loyalty Points"/>
<TextView
android:id="@+id/tvAvailablePoints"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:textColor="@color/text_light"
android:textSize="12sp"
android:text="(Available: 0)"/>
</LinearLayout>
<!-- Payment Method --> <!-- Payment Method -->
<TextView <TextView
@@ -390,6 +413,27 @@
android:textColor="@color/status_adopted"/> android:textColor="@color/status_adopted"/>
</LinearLayout> </LinearLayout>
<LinearLayout
android:id="@+id/llLoyaltyDiscount"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginTop="4dp"
android:visibility="gone">
<TextView
android:id="@+id/tvLoyaltyDiscountLabel"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Loyalty Discount:"/>
<TextView
android:id="@+id/tvSaleLoyaltyDiscount"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="-$0.00"
android:textColor="@color/status_adopted"/>
</LinearLayout>
<TextView <TextView
android:id="@+id/tvSaleDetailTotal" android:id="@+id/tvSaleDetailTotal"
android:layout_width="wrap_content" android:layout_width="wrap_content"

View File

@@ -5,6 +5,7 @@ import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.NotNull;
import java.util.List; import java.util.List;
import java.util.Objects; import java.util.Objects;
import java.math.BigDecimal;
public class SaleRequest { public class SaleRequest {
@NotNull(message = "Store ID is required") @NotNull(message = "Store ID is required")
@@ -28,7 +29,9 @@ public class SaleRequest {
private Long cartId; private Long cartId;
private Boolean useLoyaltyPoints = false; private Integer pointsUsed;
private BigDecimal pointsDiscountAmount;
public Long getStoreId() { public Long getStoreId() {
return storeId; return storeId;
@@ -102,12 +105,20 @@ public class SaleRequest {
this.cartId = cartId; this.cartId = cartId;
} }
public Boolean getUseLoyaltyPoints() { public Integer getPointsUsed() {
return useLoyaltyPoints; return pointsUsed;
} }
public void setUseLoyaltyPoints(Boolean useLoyaltyPoints) { public void setPointsUsed(Integer pointsUsed) {
this.useLoyaltyPoints = useLoyaltyPoints; this.pointsUsed = pointsUsed;
}
public BigDecimal getPointsDiscountAmount() {
return pointsDiscountAmount;
}
public void setPointsDiscountAmount(BigDecimal pointsDiscountAmount) {
this.pointsDiscountAmount = pointsDiscountAmount;
} }
@Override @Override

View File

@@ -21,6 +21,8 @@ public class SaleResponse {
private BigDecimal loyaltyDiscountAmount; private BigDecimal loyaltyDiscountAmount;
private Integer pointsUsed; private Integer pointsUsed;
private Integer pointsEarned; private Integer pointsEarned;
private Integer pointsUsed;
private BigDecimal pointsDiscountAmount;
private String channel; private String channel;
private Long couponId; private Long couponId;
private Long cartId; private Long cartId;
@@ -153,6 +155,22 @@ public class SaleResponse {
this.pointsEarned = pointsEarned; this.pointsEarned = pointsEarned;
} }
public Integer getPointsUsed() {
return pointsUsed;
}
public void setPointsUsed(Integer pointsUsed) {
this.pointsUsed = pointsUsed;
}
public BigDecimal getPointsDiscountAmount() {
return pointsDiscountAmount;
}
public void setPointsDiscountAmount(BigDecimal pointsDiscountAmount) {
this.pointsDiscountAmount = pointsDiscountAmount;
}
public String getChannel() { public String getChannel() {
return channel; return channel;
} }

View File

@@ -39,6 +39,8 @@ public class UserRequest {
private Boolean active = true; private Boolean active = true;
private Integer loyaltyPoints;
public String getUsername() { public String getUsername() {
return username; return username;
} }
@@ -127,6 +129,14 @@ public class UserRequest {
this.active = active; this.active = active;
} }
public Integer getLoyaltyPoints() {
return loyaltyPoints;
}
public void setLoyaltyPoints(Integer loyaltyPoints) {
this.loyaltyPoints = loyaltyPoints;
}
@Override @Override
public boolean equals(Object o) { public boolean equals(Object o) {
if (this == o) return true; if (this == o) return true;

View File

@@ -75,6 +75,12 @@ public class Sale {
@Column(nullable = false) @Column(nullable = false)
private Integer pointsEarned = 0; private Integer pointsEarned = 0;
@Column(nullable = false)
private Integer pointsUsed = 0;
@Column(nullable = false, precision = 10, scale = 2)
private BigDecimal pointsDiscountAmount = BigDecimal.ZERO;
@OneToMany(mappedBy = "sale", cascade = CascadeType.ALL) @OneToMany(mappedBy = "sale", cascade = CascadeType.ALL)
private List<SaleItem> items = new ArrayList<>(); private List<SaleItem> items = new ArrayList<>();
@@ -233,6 +239,22 @@ public class Sale {
this.pointsEarned = pointsEarned; this.pointsEarned = pointsEarned;
} }
public Integer getPointsUsed() {
return pointsUsed;
}
public void setPointsUsed(Integer pointsUsed) {
this.pointsUsed = pointsUsed;
}
public BigDecimal getPointsDiscountAmount() {
return pointsDiscountAmount;
}
public void setPointsDiscountAmount(BigDecimal pointsDiscountAmount) {
this.pointsDiscountAmount = pointsDiscountAmount;
}
public List<SaleItem> getItems() { public List<SaleItem> getItems() {
return items; return items;
} }

View File

@@ -19,7 +19,7 @@ public interface PetRepository extends JpaRepository<Pet, Long> {
"WHERE LOWER(p.petStatus) = 'available' " + "WHERE LOWER(p.petStatus) = 'available' " +
"AND NOT EXISTS (" + "AND NOT EXISTS (" +
" SELECT 1 FROM Adoption a " + " SELECT 1 FROM Adoption a " +
" WHERE a.pet = p AND LOWER(a.adoptionStatus) = 'completed'" + " WHERE a.pet = p AND (LOWER(a.adoptionStatus) = 'completed' OR LOWER(a.adoptionStatus) = 'pending')" +
") " + ") " +
"ORDER BY p.petName ASC") "ORDER BY p.petName ASC")
List<Pet> findAdoptablePetsOrderByPetNameAsc(); List<Pet> findAdoptablePetsOrderByPetNameAsc();
@@ -29,7 +29,7 @@ public interface PetRepository extends JpaRepository<Pet, Long> {
"AND (:storeId IS NULL OR p.store.storeId = :storeId) " + "AND (:storeId IS NULL OR p.store.storeId = :storeId) " +
"AND NOT EXISTS (" + "AND NOT EXISTS (" +
" SELECT 1 FROM Adoption a " + " SELECT 1 FROM Adoption a " +
" WHERE a.pet = p AND LOWER(a.adoptionStatus) = 'completed'" + " WHERE a.pet = p AND (LOWER(a.adoptionStatus) = 'completed' OR LOWER(a.adoptionStatus) = 'pending')" +
") " + ") " +
"ORDER BY p.petName ASC") "ORDER BY p.petName ASC")
List<Pet> findAdoptablePetsByStore(@Param("storeId") Long storeId); List<Pet> findAdoptablePetsByStore(@Param("storeId") Long storeId);

View File

@@ -32,6 +32,7 @@ public class AdoptionService {
private static final String ADOPTION_STATUS_MISSED = "Missed"; private static final String ADOPTION_STATUS_MISSED = "Missed";
private static final String PET_STATUS_AVAILABLE = "Available"; private static final String PET_STATUS_AVAILABLE = "Available";
private static final String PET_STATUS_ADOPTED = "Adopted"; private static final String PET_STATUS_ADOPTED = "Adopted";
private static final String PET_STATUS_PENDING = "Pending";
private final AdoptionRepository adoptionRepository; private final AdoptionRepository adoptionRepository;
private final PetRepository petRepository; private final PetRepository petRepository;
@@ -263,10 +264,14 @@ public class AdoptionService {
private void syncPetStatus(Pet pet, String adoptionStatus, Long adoptionId, User customer) { private void syncPetStatus(Pet pet, String adoptionStatus, Long adoptionId, User customer) {
boolean completedElsewhere = adoptionId != null boolean completedElsewhere = adoptionId != null
&& adoptionRepository.existsByPet_IdAndAdoptionStatusIgnoreCaseAndAdoptionIdNot(pet.getPetId(), ADOPTION_STATUS_COMPLETED, adoptionId); && adoptionRepository.existsByPet_IdAndAdoptionStatusIgnoreCaseAndAdoptionIdNot(pet.getPetId(), ADOPTION_STATUS_COMPLETED, adoptionId);
if (ADOPTION_STATUS_COMPLETED.equalsIgnoreCase(adoptionStatus) || completedElsewhere) { if (ADOPTION_STATUS_COMPLETED.equalsIgnoreCase(adoptionStatus) || completedElsewhere) {
pet.setPetStatus(PET_STATUS_ADOPTED); pet.setPetStatus(PET_STATUS_ADOPTED);
pet.setOwner(customer); pet.setOwner(customer);
pet.setStore(null); pet.setStore(null);
} else if (ADOPTION_STATUS_PENDING.equalsIgnoreCase(adoptionStatus)) {
pet.setPetStatus(PET_STATUS_PENDING);
pet.setOwner(customer);
} else { } else {
pet.setPetStatus(PET_STATUS_AVAILABLE); pet.setPetStatus(PET_STATUS_AVAILABLE);
pet.setOwner(null); pet.setOwner(null);

View File

@@ -160,14 +160,33 @@ public class SaleService {
saleItems.add(saleItem); saleItems.add(saleItem);
subtotalAmount = subtotalAmount.add(itemTotal); subtotalAmount = subtotalAmount.add(itemTotal);
} }
subtotalAmount = subtotalAmount.negate();
sale.setSubtotalAmount(subtotalAmount); Sale originalSale = sale.getOriginalSale();
sale.setTotalAmount(subtotalAmount); BigDecimal originalSubtotal = originalSale.getSubtotalAmount() != null
sale.setCouponDiscountAmount(BigDecimal.ZERO); ? originalSale.getSubtotalAmount()
sale.setEmployeeDiscountAmount(BigDecimal.ZERO); : originalSale.getItems().stream()
sale.setLoyaltyDiscountAmount(BigDecimal.ZERO); .map(i -> i.getUnitPrice().multiply(BigDecimal.valueOf(Math.abs(i.getQuantity()))))
sale.setPointsUsed(0); .reduce(BigDecimal.ZERO, BigDecimal::add);
sale.setPointsEarned(0);
BigDecimal refundRatio = originalSubtotal.compareTo(BigDecimal.ZERO) != 0
? subtotalAmount.divide(originalSubtotal, 10, RoundingMode.HALF_UP)
: BigDecimal.ONE;
BigDecimal refundTotal = originalSale.getTotalAmount().abs()
.multiply(refundRatio).setScale(2, RoundingMode.HALF_UP);
sale.setSubtotalAmount(subtotalAmount.negate());
sale.setTotalAmount(refundTotal.negate());
User refundCustomer = customer != null ? customer : originalSale.getCustomer();
if (refundCustomer != null) {
int pointsToRestore = BigDecimal.valueOf(originalSale.getPointsUsed())
.multiply(refundRatio).setScale(0, RoundingMode.FLOOR).intValue();
int pointsToDeduct = BigDecimal.valueOf(originalSale.getPointsEarned())
.multiply(refundRatio).setScale(0, RoundingMode.FLOOR).intValue();
refundCustomer.setLoyaltyPoints(refundCustomer.getLoyaltyPoints() + pointsToRestore - pointsToDeduct);
userRepository.save(refundCustomer);
}
} else { } else {
if (request.getItems() == null || request.getItems().isEmpty()) { if (request.getItems() == null || request.getItems().isEmpty()) {
throw new BusinessException("At least one item is required"); throw new BusinessException("At least one item is required");
@@ -204,14 +223,23 @@ public class SaleService {
BigDecimal couponDiscount = calculateCouponDiscount(sale.getCoupon(), subtotalAmount); BigDecimal couponDiscount = calculateCouponDiscount(sale.getCoupon(), subtotalAmount);
sale.setCouponDiscountAmount(couponDiscount); sale.setCouponDiscountAmount(couponDiscount);
BigDecimal employeeDiscount = calculateEmployeeDiscount(customer, subtotalAmount.subtract(couponDiscount)); BigDecimal pointsDiscount = BigDecimal.ZERO;
int pointsUsed = 0;
if (customer != null && request.getPointsUsed() != null && request.getPointsUsed() > 0) {
if (customer.getLoyaltyPoints() < request.getPointsUsed()) {
throw new BusinessException("Customer does not have enough loyalty points");
}
pointsUsed = request.getPointsUsed();
pointsDiscount = calculatePointsDiscount(pointsUsed);
customer.setLoyaltyPoints(customer.getLoyaltyPoints() - pointsUsed);
}
sale.setPointsUsed(pointsUsed);
sale.setPointsDiscountAmount(pointsDiscount);
BigDecimal employeeDiscount = calculateEmployeeDiscount(customer, subtotalAmount.subtract(couponDiscount).subtract(pointsDiscount));
sale.setEmployeeDiscountAmount(employeeDiscount); sale.setEmployeeDiscountAmount(employeeDiscount);
BigDecimal loyaltyDiscount = calculateLoyaltyDiscount(customer, subtotalAmount.subtract(couponDiscount).subtract(employeeDiscount), Boolean.TRUE.equals(request.getUseLoyaltyPoints())); BigDecimal finalTotal = subtotalAmount.subtract(couponDiscount).subtract(pointsDiscount).subtract(employeeDiscount);
sale.setLoyaltyDiscountAmount(loyaltyDiscount);
sale.setPointsUsed(toPointsUsed(loyaltyDiscount));
BigDecimal finalTotal = subtotalAmount.subtract(couponDiscount).subtract(employeeDiscount).subtract(loyaltyDiscount);
sale.setTotalAmount(finalTotal.max(BigDecimal.ZERO)); sale.setTotalAmount(finalTotal.max(BigDecimal.ZERO));
sale.setPointsEarned(sale.getTotalAmount().setScale(0, RoundingMode.FLOOR).intValue()); sale.setPointsEarned(sale.getTotalAmount().setScale(0, RoundingMode.FLOOR).intValue());
@@ -262,6 +290,10 @@ public class SaleService {
return discount.min(subtotal).setScale(2, RoundingMode.HALF_UP); return discount.min(subtotal).setScale(2, RoundingMode.HALF_UP);
} }
private BigDecimal calculatePointsDiscount(int pointsUsed) {
return new BigDecimal(pointsUsed).divide(new BigDecimal("20"), 2, RoundingMode.HALF_UP);
}
private BigDecimal calculateEmployeeDiscount(User customer, BigDecimal remainingAmount) { private BigDecimal calculateEmployeeDiscount(User customer, BigDecimal remainingAmount) {
if (customer == null || remainingAmount.compareTo(BigDecimal.ZERO) <= 0) { if (customer == null || remainingAmount.compareTo(BigDecimal.ZERO) <= 0) {
return BigDecimal.ZERO; return BigDecimal.ZERO;
@@ -328,6 +360,8 @@ public class SaleService {
response.setLoyaltyDiscountAmount(sale.getLoyaltyDiscountAmount()); response.setLoyaltyDiscountAmount(sale.getLoyaltyDiscountAmount());
response.setPointsUsed(sale.getPointsUsed()); response.setPointsUsed(sale.getPointsUsed());
response.setPointsEarned(sale.getPointsEarned()); response.setPointsEarned(sale.getPointsEarned());
response.setPointsUsed(sale.getPointsUsed());
response.setPointsDiscountAmount(sale.getPointsDiscountAmount());
response.setChannel(sale.getChannel()); response.setChannel(sale.getChannel());
if (sale.getCoupon() != null) { if (sale.getCoupon() != null) {
response.setCouponId(sale.getCoupon().getCouponId()); response.setCouponId(sale.getCoupon().getCouponId());

View File

@@ -75,6 +75,9 @@ public class UserService {
user.setStaffRole(trimToNull(request.getStaffRole())); user.setStaffRole(trimToNull(request.getStaffRole()));
user.setPrimaryStore(resolveStore(request.getPrimaryStoreId())); user.setPrimaryStore(resolveStore(request.getPrimaryStoreId()));
user.setActive(request.getActive() != null ? request.getActive() : true); user.setActive(request.getActive() != null ? request.getActive() : true);
if (request.getLoyaltyPoints() != null) {
user.setLoyaltyPoints(request.getLoyaltyPoints());
}
validateUniquePhone(user.getPhone(), null); validateUniquePhone(user.getPhone(), null);
@@ -111,6 +114,9 @@ public class UserService {
user.setStaffRole(trimToNull(request.getStaffRole())); user.setStaffRole(trimToNull(request.getStaffRole()));
user.setPrimaryStore(resolveStore(request.getPrimaryStoreId())); user.setPrimaryStore(resolveStore(request.getPrimaryStoreId()));
user.setActive(request.getActive() != null ? request.getActive() : true); user.setActive(request.getActive() != null ? request.getActive() : true);
if (request.getLoyaltyPoints() != null) {
user.setLoyaltyPoints(request.getLoyaltyPoints());
}
if (invalidateToken) { if (invalidateToken) {
user.setTokenVersion(user.getTokenVersion() + 1); user.setTokenVersion(user.getTokenVersion() + 1);
} }

View File

@@ -0,0 +1,3 @@
ALTER TABLE sale
ADD COLUMN pointsUsed INT NOT NULL DEFAULT 0,
ADD COLUMN pointsDiscountAmount DECIMAL(10, 2) NOT NULL DEFAULT 0.00;

View File

@@ -230,6 +230,8 @@ CREATE TABLE IF NOT EXISTS sale (
couponDiscountAmount DECIMAL(10, 2) NOT NULL DEFAULT 0.00, couponDiscountAmount DECIMAL(10, 2) NOT NULL DEFAULT 0.00,
employeeDiscountAmount DECIMAL(10, 2) NOT NULL DEFAULT 0.00, employeeDiscountAmount DECIMAL(10, 2) NOT NULL DEFAULT 0.00,
pointsEarned INT NOT NULL DEFAULT 0, pointsEarned INT NOT NULL DEFAULT 0,
pointsUsed INT NOT NULL DEFAULT 0,
pointsDiscountAmount DECIMAL(10, 2) NOT NULL DEFAULT 0.00,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
CONSTRAINT fk_sale_employee FOREIGN KEY (employeeId) REFERENCES users(id), CONSTRAINT fk_sale_employee FOREIGN KEY (employeeId) REFERENCES users(id),

View File

@@ -183,7 +183,7 @@ public class Validator {
} }
/** /**
* Checks if the input is a valid phone number in format XXX-XXX-XXXX * Checks if the input is a valid phone number in format (XXX) XXX-XXXX
* @param value input of string * @param value input of string
* @param name name of input * @param name name of input
* @return error msg if input is not in valid phone format, otherwise empty * @return error msg if input is not in valid phone format, otherwise empty
@@ -191,14 +191,14 @@ public class Validator {
public static String isValidPhoneNumber(String value, String name){ public static String isValidPhoneNumber(String value, String name){
String msg = ""; String msg = "";
if (value == null) { if (value == null) {
msg += name + " must be in format XXX-XXX-XXXX. \n"; msg += name + " must be in format (XXX) XXX-XXXX. \n";
return msg; return msg;
} }
String regex = "^\\d{3}-\\d{3}-\\d{4}$"; String regex = "^\\(\\d{3}\\) \\d{3}-\\d{4}$";
if (!value.matches(regex)){ if (!value.matches(regex)){
msg += name + " must be in format XXX-XXX-XXXX. \n"; msg += name + " must be in format (XXX) XXX-XXXX. \n";
} }
return msg; return msg;

View File

@@ -10,6 +10,7 @@ public class UserResponse {
private String phone; private String phone;
private String role; private String role;
private Boolean active; private Boolean active;
private Integer loyaltyPoints;
private LocalDateTime createdAt; private LocalDateTime createdAt;
private LocalDateTime updatedAt; private LocalDateTime updatedAt;
@@ -72,6 +73,14 @@ public class UserResponse {
this.active = active; this.active = active;
} }
public Integer getLoyaltyPoints() {
return loyaltyPoints;
}
public void setLoyaltyPoints(Integer loyaltyPoints) {
this.loyaltyPoints = loyaltyPoints;
}
public LocalDateTime getCreatedAt() { public LocalDateTime getCreatedAt() {
return createdAt; return createdAt;
} }

View File

@@ -42,6 +42,15 @@ public class DropdownApi {
return apiClient.getObjectMapper().readValue(response, new TypeReference<List<DropdownOption>>() {}); return apiClient.getObjectMapper().readValue(response, new TypeReference<List<DropdownOption>>() {});
} }
public List<DropdownOption> getPetBreeds(String species) throws Exception {
String encoded = java.net.URLEncoder.encode(species, java.nio.charset.StandardCharsets.UTF_8);
String response = apiClient.getRawResponse("/api/v1/dropdowns/pet-breeds?species=" + encoded);
if (response == null || response.isEmpty()) {
throw new IllegalStateException("Empty response from pet breeds endpoint");
}
return apiClient.getObjectMapper().readValue(response, new TypeReference<List<DropdownOption>>() {});
}
public List<DropdownOption> getProducts() throws Exception { public List<DropdownOption> getProducts() throws Exception {
String response = apiClient.getRawResponse("/api/v1/dropdowns/products"); String response = apiClient.getRawResponse("/api/v1/dropdowns/products");
if (response == null || response.isEmpty()) { if (response == null || response.isEmpty()) {

View File

@@ -0,0 +1,172 @@
package org.example.petshopdesktop.controllers;
import javafx.application.Platform;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.collections.transformation.FilteredList;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.stage.Modality;
import javafx.stage.Stage;
import org.example.petshopdesktop.api.dto.user.UserResponse;
import org.example.petshopdesktop.api.endpoints.CustomerApi;
import org.example.petshopdesktop.util.ActivityLogger;
import org.example.petshopdesktop.util.TableViewSupport;
import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;
public class CustomerAccountsController {
@FXML
private TableView<UserResponse> tvCustomers;
@FXML
private TableColumn<UserResponse, String> colCustomerUsername;
@FXML
private TableColumn<UserResponse, String> colCustomerName;
@FXML
private TableColumn<UserResponse, String> colCustomerEmail;
@FXML
private TableColumn<UserResponse, String> colCustomerPhone;
@FXML
private TableColumn<UserResponse, Object> colCustomerLoyaltyPoints;
@FXML
private TableColumn<UserResponse, String> colCustomerStatus;
@FXML
private TableColumn<UserResponse, Object> colCustomerCreated;
@FXML
private TextField txtSearchCustomer;
@FXML
private Button btnEditCustomer;
@FXML
private Button btnRefresh;
@FXML
private Label lblError;
@FXML
private Label lblStatus;
private final ObservableList<UserResponse> customerAccounts = FXCollections.observableArrayList();
private FilteredList<UserResponse> filteredCustomers;
@FXML
public void initialize() {
colCustomerUsername.setCellValueFactory(data -> new javafx.beans.property.SimpleStringProperty(data.getValue().getUsername()));
colCustomerName.setCellValueFactory(data -> new javafx.beans.property.SimpleStringProperty(data.getValue().getFullName()));
colCustomerEmail.setCellValueFactory(data -> new javafx.beans.property.SimpleStringProperty(data.getValue().getEmail()));
colCustomerPhone.setCellValueFactory(data -> new javafx.beans.property.SimpleStringProperty(data.getValue().getPhone()));
colCustomerLoyaltyPoints.setCellValueFactory(data -> new javafx.beans.property.SimpleObjectProperty<>(data.getValue().getLoyaltyPoints()));
colCustomerStatus.setCellValueFactory(data -> new javafx.beans.property.SimpleStringProperty(data.getValue().getActive() != null && data.getValue().getActive() ? "Active" : "Inactive"));
colCustomerCreated.setCellValueFactory(data -> new javafx.beans.property.SimpleObjectProperty<>(data.getValue().getCreatedAt()));
filteredCustomers = new FilteredList<>(customerAccounts, a -> true);
TableViewSupport.bindSortedItems(tvCustomers, filteredCustomers);
TableViewSupport.installDoubleClickAction(tvCustomers, this::openEditDialog);
tvCustomers.getSelectionModel().selectedItemProperty().addListener((obs, oldVal, newVal) ->
btnEditCustomer.setDisable(newVal == null));
btnEditCustomer.setDisable(true);
txtSearchCustomer.textProperty().addListener((obs, o, n) -> applyCustomerFilter(n));
refresh();
}
@FXML
void btnRefreshClicked(ActionEvent event) {
txtSearchCustomer.clear();
TableViewSupport.clearSort(tvCustomers);
refresh();
TableViewSupport.flashStatus(lblStatus, "Refreshed");
}
@FXML
void btnEditCustomerClicked(ActionEvent event) {
lblError.setText("");
openEditDialog(tvCustomers.getSelectionModel().getSelectedItem());
}
private void openEditDialog(UserResponse selected) {
if (selected == null) {
lblError.setText("Select a customer to edit.");
return;
}
try {
FXMLLoader loader = new FXMLLoader(getClass().getResource("/org/example/petshopdesktop/dialogviews/staff-edit-dialog-view.fxml"));
Stage dialog = new Stage();
dialog.initOwner(tvCustomers.getScene().getWindow());
dialog.initModality(Modality.APPLICATION_MODAL);
dialog.setTitle("Edit Customer Account");
dialog.setScene(new Scene(loader.load()));
dialog.setResizable(false);
var controller = (org.example.petshopdesktop.controllers.dialogcontrollers.StaffEditDialogController) loader.getController();
controller.setUser(selected);
dialog.showAndWait();
refresh();
} catch (Exception e) {
ActivityLogger.getInstance().logException("CustomerAccountsController.openEditDialog", e, "Opening customer edit dialog");
lblError.setText("Could not open customer account editor.");
}
}
private void refresh() {
lblError.setText("");
tvCustomers.setDisable(true);
new Thread(() -> {
try {
Comparator<UserResponse> byCreated = Comparator.comparing(
UserResponse::getCreatedAt, Comparator.nullsLast(Comparator.reverseOrder()));
List<UserResponse> customers = CustomerApi.getInstance().listCustomers(null).stream()
.sorted(byCreated)
.collect(Collectors.toList());
Platform.runLater(() -> {
customerAccounts.setAll(customers);
tvCustomers.setDisable(false);
});
} catch (Exception e) {
ActivityLogger.getInstance().logException("CustomerAccountsController.refresh", e, "Loading customer accounts");
Platform.runLater(() -> {
lblError.setText("Could not load customer accounts.");
tvCustomers.setDisable(false);
});
}
}).start();
}
private void applyCustomerFilter(String text) {
String q = text == null ? "" : text.trim().toLowerCase();
if (q.isEmpty()) {
filteredCustomers.setPredicate(a -> true);
return;
}
filteredCustomers.setPredicate(a ->
safe(a.getUsername()).contains(q)
|| safe(a.getFullName()).contains(q)
|| safe(a.getEmail()).contains(q)
|| safe(a.getPhone()).contains(q)
);
}
private static String safe(String v) {
return v == null ? "" : v.toLowerCase();
}
}

View File

@@ -89,6 +89,9 @@ public class MainLayoutController {
@FXML @FXML
private Button btnStaffAccounts; private Button btnStaffAccounts;
@FXML
private Button btnCustomers;
@FXML @FXML
private Button btnAnalytics; private Button btnAnalytics;
@@ -179,6 +182,12 @@ public class MainLayoutController {
updateButtons(btnStaffAccounts); updateButtons(btnStaffAccounts);
} }
@FXML
void btnCustomersClicked(ActionEvent event) {
loadView("customer-accounts-view.fxml");
updateButtons(btnCustomers);
}
@FXML @FXML
void btnAnalyticsClicked(ActionEvent event) { void btnAnalyticsClicked(ActionEvent event) {
loadView("analytics-view.fxml"); loadView("analytics-view.fxml");
@@ -415,8 +424,13 @@ public class MainLayoutController {
btnPurchaseOrders.setManaged(isAdmin); btnPurchaseOrders.setManaged(isAdmin);
if (btnStaffAccounts != null) { if (btnStaffAccounts != null) {
btnStaffAccounts.setVisible(true); btnStaffAccounts.setVisible(isAdmin);
btnStaffAccounts.setManaged(true); btnStaffAccounts.setManaged(isAdmin);
}
if (btnCustomers != null) {
btnCustomers.setVisible(true);
btnCustomers.setManaged(true);
} }
if (lblAdminSection != null) { if (lblAdminSection != null) {
@@ -493,6 +507,7 @@ public class MainLayoutController {
btnProducts, btnProducts,
btnPurchaseOrders, btnPurchaseOrders,
btnStaffAccounts, btnStaffAccounts,
btnCustomers,
btnAnalytics, btnAnalytics,
btnActivityLogs, btnActivityLogs,
btnCoupons, btnCoupons,

View File

@@ -255,6 +255,8 @@ public class SaleController {
boolean isAdmin = UserSession.getInstance().isAdmin(); boolean isAdmin = UserSession.getInstance().isAdmin();
vbCreateSale.setVisible(!isAdmin); vbCreateSale.setVisible(!isAdmin);
vbCreateSale.setManaged(!isAdmin); vbCreateSale.setManaged(!isAdmin);
btnRefund.setVisible(!isAdmin);
btnRefund.setManaged(!isAdmin);
lblModeNote.setText(isAdmin ? "(View only)" : "(Staff can create sales)"); lblModeNote.setText(isAdmin ? "(View only)" : "(Staff can create sales)");
} }

View File

@@ -8,26 +8,25 @@ import javafx.event.ActionEvent;
import javafx.fxml.FXML; import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader; import javafx.fxml.FXMLLoader;
import javafx.scene.Scene; import javafx.scene.Scene;
import javafx.scene.control.Button; import javafx.scene.control.*;
import javafx.scene.control.Label; import javafx.scene.layout.VBox;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.TextField;
import javafx.stage.Modality; import javafx.stage.Modality;
import javafx.stage.Stage; import javafx.stage.Stage;
import org.example.petshopdesktop.api.dto.user.UserResponse; import org.example.petshopdesktop.api.dto.user.UserResponse;
import org.example.petshopdesktop.api.endpoints.UserApi; import org.example.petshopdesktop.api.endpoints.UserApi;
import org.example.petshopdesktop.api.endpoints.CustomerApi;
import org.example.petshopdesktop.auth.UserSession; import org.example.petshopdesktop.auth.UserSession;
import org.example.petshopdesktop.util.ActivityLogger; import org.example.petshopdesktop.util.ActivityLogger;
import org.example.petshopdesktop.util.TableViewSupport; import org.example.petshopdesktop.util.TableViewSupport;
import java.util.List;
import java.util.Comparator; import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors; import java.util.stream.Collectors;
public class StaffAccountsController { public class StaffAccountsController {
@FXML
private VBox staffSection;
@FXML @FXML
private TableView<UserResponse> tvStaff; private TableView<UserResponse> tvStaff;
@@ -55,12 +54,6 @@ public class StaffAccountsController {
@FXML @FXML
private TextField txtSearch; private TextField txtSearch;
@FXML
private Label lblError;
@FXML
private Label lblStatus;
@FXML @FXML
private Button btnRefresh; private Button btnRefresh;
@@ -70,8 +63,14 @@ public class StaffAccountsController {
@FXML @FXML
private Button btnEditAccount; private Button btnEditAccount;
@FXML
private Label lblError;
@FXML
private Label lblStatus;
private final ObservableList<UserResponse> staffAccounts = FXCollections.observableArrayList(); private final ObservableList<UserResponse> staffAccounts = FXCollections.observableArrayList();
private FilteredList<UserResponse> filtered; private FilteredList<UserResponse> filteredStaff;
@FXML @FXML
public void initialize() { public void initialize() {
@@ -83,21 +82,15 @@ public class StaffAccountsController {
colStatus.setCellValueFactory(data -> new javafx.beans.property.SimpleStringProperty(data.getValue().getActive() != null && data.getValue().getActive() ? "Active" : "Inactive")); colStatus.setCellValueFactory(data -> new javafx.beans.property.SimpleStringProperty(data.getValue().getActive() != null && data.getValue().getActive() ? "Active" : "Inactive"));
colCreated.setCellValueFactory(data -> new javafx.beans.property.SimpleObjectProperty<>(data.getValue().getCreatedAt())); colCreated.setCellValueFactory(data -> new javafx.beans.property.SimpleObjectProperty<>(data.getValue().getCreatedAt()));
filtered = new FilteredList<>(staffAccounts, a -> true); filteredStaff = new FilteredList<>(staffAccounts, a -> true);
TableViewSupport.bindSortedItems(tvStaff, filtered); TableViewSupport.bindSortedItems(tvStaff, filteredStaff);
TableViewSupport.installDoubleClickAction(tvStaff, this::openEditDialog); TableViewSupport.installDoubleClickAction(tvStaff, this::openEditDialog);
txtSearch.textProperty().addListener((obs, o, n) -> applyFilter(n)); tvStaff.getSelectionModel().selectedItemProperty().addListener((obs, oldVal, newVal) ->
btnEditAccount.setDisable(newVal == null));
btnEditAccount.setDisable(true);
tvStaff.getSelectionModel().selectedItemProperty().addListener((obs, oldValue, newValue) -> { txtSearch.textProperty().addListener((obs, o, n) -> applyStaffFilter(n));
if (btnEditAccount != null) {
btnEditAccount.setDisable(newValue == null);
}
});
if (btnEditAccount != null) {
btnEditAccount.setDisable(true);
}
refresh(); refresh();
} }
@@ -132,8 +125,7 @@ public class StaffAccountsController {
@FXML @FXML
void btnEditAccountClicked(ActionEvent event) { void btnEditAccountClicked(ActionEvent event) {
lblError.setText(""); lblError.setText("");
UserResponse selected = tvStaff.getSelectionModel().getSelectedItem(); openEditDialog(tvStaff.getSelectionModel().getSelectedItem());
openEditDialog(selected);
} }
private void openEditDialog(UserResponse selected) { private void openEditDialog(UserResponse selected) {
@@ -156,7 +148,7 @@ public class StaffAccountsController {
Stage dialog = new Stage(); Stage dialog = new Stage();
dialog.initOwner(tvStaff.getScene().getWindow()); dialog.initOwner(tvStaff.getScene().getWindow());
dialog.initModality(Modality.APPLICATION_MODAL); dialog.initModality(Modality.APPLICATION_MODAL);
dialog.setTitle("Edit User Account"); dialog.setTitle("Edit Staff Account");
dialog.setScene(new Scene(loader.load())); dialog.setScene(new Scene(loader.load()));
dialog.setResizable(false); dialog.setResizable(false);
var controller = (org.example.petshopdesktop.controllers.dialogcontrollers.StaffEditDialogController) loader.getController(); var controller = (org.example.petshopdesktop.controllers.dialogcontrollers.StaffEditDialogController) loader.getController();
@@ -164,7 +156,7 @@ public class StaffAccountsController {
dialog.showAndWait(); dialog.showAndWait();
refresh(); refresh();
} catch (Exception e) { } catch (Exception e) {
ActivityLogger.getInstance().logException("StaffAccountsController.btnEditAccountClicked", e, "Opening user edit dialog"); ActivityLogger.getInstance().logException("StaffAccountsController.openEditDialog", e, "Opening user edit dialog");
lblError.setText("Could not open user account editor."); lblError.setText("Could not open user account editor.");
} }
} }
@@ -175,48 +167,40 @@ public class StaffAccountsController {
new Thread(() -> { new Thread(() -> {
try { try {
UserSession session = UserSession.getInstance(); Comparator<UserResponse> byCreated = Comparator.comparing(
List<UserResponse> users; UserResponse::getCreatedAt, Comparator.nullsLast(Comparator.reverseOrder()));
if (session.isAdmin()) {
users = UserApi.getInstance().listUsers(null);
} else {
users = CustomerApi.getInstance().listCustomers(null);
}
List<UserResponse> sortedUsers = users.stream() List<UserResponse> staff = UserApi.getInstance().listUsers(null).stream()
.sorted(Comparator.comparing(UserResponse::getCreatedAt, Comparator.nullsLast(Comparator.reverseOrder()))) .filter(u -> !"CUSTOMER".equalsIgnoreCase(u.getRole()))
.collect(Collectors.toList()); .sorted(byCreated)
.collect(Collectors.toList());
Platform.runLater(() -> { Platform.runLater(() -> {
staffAccounts.setAll(sortedUsers); staffAccounts.setAll(staff);
tvStaff.setDisable(false); tvStaff.setDisable(false);
}); });
} catch (Exception e) { } catch (Exception e) {
ActivityLogger.getInstance().logException("StaffAccountsController.refresh", e, "Loading user accounts"); ActivityLogger.getInstance().logException("StaffAccountsController.refresh", e, "Loading staff accounts");
Platform.runLater(() -> { Platform.runLater(() -> {
String message = e.getMessage(); lblError.setText("Could not load staff accounts.");
lblError.setText(message == null || message.isBlank()
? "Could not load user accounts."
: "Could not load user accounts: " + message);
tvStaff.setDisable(false); tvStaff.setDisable(false);
}); });
} }
}).start(); }).start();
} }
private void applyFilter(String text) { private void applyStaffFilter(String text) {
String q = text == null ? "" : text.trim().toLowerCase(); String q = text == null ? "" : text.trim().toLowerCase();
if (q.isEmpty()) { if (q.isEmpty()) {
filtered.setPredicate(a -> true); filteredStaff.setPredicate(a -> true);
return; return;
} }
filteredStaff.setPredicate(a ->
filtered.setPredicate(a -> safe(a.getUsername()).contains(q)
safe(a.getUsername()).contains(q) || safe(a.getFullName()).contains(q)
|| safe(a.getFullName()).contains(q) || safe(a.getEmail()).contains(q)
|| safe(a.getEmail()).contains(q) || safe(a.getPhone()).contains(q)
|| safe(a.getPhone()).contains(q) || safe(a.getRole()).contains(q)
|| safe(a.getRole()).contains(q)
); );
} }

View File

@@ -62,7 +62,7 @@ public class AdoptionDialogController {
private boolean suppressPaymentDialog = false; private boolean suppressPaymentDialog = false;
private ObservableList<String> statusList = FXCollections.observableArrayList( private ObservableList<String> statusList = FXCollections.observableArrayList(
"Pending", "Completed", "Cancelled" "Pending", "Completed", "Missed", "Cancelled"
); );
@FXML @FXML
@@ -282,6 +282,7 @@ public class AdoptionDialogController {
} }
suppressPaymentDialog = true; suppressPaymentDialog = true;
cbAdoptionStatus.setItems(statusList);
for (String status : cbAdoptionStatus.getItems()) { for (String status : cbAdoptionStatus.getItems()) {
if (status.equals(adoption.getAdoptionStatus())) { if (status.equals(adoption.getAdoptionStatus())) {
cbAdoptionStatus.getSelectionModel().select(status); cbAdoptionStatus.getSelectionModel().select(status);
@@ -289,13 +290,57 @@ public class AdoptionDialogController {
} }
} }
suppressPaymentDialog = false; suppressPaymentDialog = false;
applyEditModeLock();
} }
} }
private void applyEditModeLock() {
String status = cbAdoptionStatus.getValue();
if ("Cancelled".equalsIgnoreCase(status)) {
cbPet.setDisable(true);
cbCustomer.setDisable(true);
cbEmployee.setDisable(true);
dpAdoptionDate.setDisable(true);
cbAdoptionStatus.setDisable(true);
cbAdoptionStatus.setItems(FXCollections.observableArrayList("Cancelled"));
btnSave.setDisable(true);
return;
}
LocalDate adoptionDate = dpAdoptionDate.getValue();
boolean isPast = adoptionDate != null && adoptionDate.isBefore(LocalDate.now());
cbPet.setDisable(true);
cbCustomer.setDisable(true);
dpAdoptionDate.setDisable(false);
cbEmployee.setDisable(false);
cbAdoptionStatus.setDisable(false);
suppressPaymentDialog = true;
if (isPast) {
cbAdoptionStatus.setItems(FXCollections.observableArrayList("Completed", "Missed"));
dpAdoptionDate.setDisable(true);
} else {
cbAdoptionStatus.setItems(FXCollections.observableArrayList("Pending", "Cancelled"));
}
if (!cbAdoptionStatus.getItems().contains(cbAdoptionStatus.getValue())) {
cbAdoptionStatus.getSelectionModel().selectFirst();
}
suppressPaymentDialog = false;
}
public void setMode(String mode) { public void setMode(String mode) {
this.mode = mode; this.mode = mode;
lblMode.setText(mode + " Adoption"); lblMode.setText(mode + " Adoption");
lblAdoptionId.setVisible(mode.equals("Edit")); lblAdoptionId.setVisible(mode.equals("Edit"));
if (mode.equals("Add")) {
suppressPaymentDialog = true;
cbAdoptionStatus.setItems(FXCollections.observableArrayList("Pending"));
cbAdoptionStatus.setValue("Pending");
cbAdoptionStatus.setDisable(true);
suppressPaymentDialog = false;
}
} }
private void applySelectedPet() { private void applySelectedPet() {

View File

@@ -43,24 +43,26 @@ public class AppointmentDialogController {
@FXML private Label lblAppointmentId; @FXML private Label lblAppointmentId;
@FXML private Label lblMode; @FXML private Label lblMode;
private String mode = null; private String mode = null;
private AppointmentDTO selectedAppointment = null; private AppointmentDTO selectedAppointment = null;
private Long pendingPetSelectionId = null; private Long pendingPetSelectionId = null;
private boolean isOriginallyCancel = false;
private ObservableList<String> statusList = private boolean isOriginallyCompletedOrMissed = false;
FXCollections.observableArrayList(
"Booked", "Completed", "Missed", "Cancelled"
);
public void setMode(String mode) { public void setMode(String mode) {
this.mode = mode; this.mode = mode;
lblMode.setText(mode + " Appointment"); lblMode.setText(mode + " Appointment");
lblAppointmentId.setVisible(!mode.equals("Add")); lblAppointmentId.setVisible(!mode.equals("Add"));
if (mode.equals("Add")) {
cbAppointmentStatus.setItems(FXCollections.observableArrayList("Booked"));
cbAppointmentStatus.setValue("Booked");
cbAppointmentStatus.setDisable(true);
}
} }
@FXML @FXML
public void initialize() { public void initialize() {
cbAppointmentStatus.setItems(statusList); cbAppointmentStatus.setItems(FXCollections.observableArrayList("Booked", "Completed", "Missed", "Cancelled"));
cbPet.setDisable(true); cbPet.setDisable(true);
cbEmployee.setPromptText("Select an employee"); cbEmployee.setPromptText("Select an employee");
cbPet.setPromptText("Select a customer first"); cbPet.setPromptText("Select a customer first");
@@ -228,6 +230,46 @@ public class AppointmentDialogController {
applySelectedService(); applySelectedService();
applySelectedCustomer(); applySelectedCustomer();
applySelectedEmployee(); applySelectedEmployee();
applyEditModeLock();
}
private void applyEditModeLock() {
String status = cbAppointmentStatus.getValue();
isOriginallyCancel = "Cancelled".equalsIgnoreCase(status);
isOriginallyCompletedOrMissed = "Completed".equalsIgnoreCase(status) || "Missed".equalsIgnoreCase(status);
if (isOriginallyCancel) {
cbService.setDisable(true);
cbCustomer.setDisable(true);
cbPet.setDisable(true);
cbEmployee.setDisable(true);
cbHour.setDisable(true);
cbMinute.setDisable(true);
dpAppointmentDate.setDisable(true);
cbAppointmentStatus.setDisable(true);
cbAppointmentStatus.setItems(FXCollections.observableArrayList("Cancelled"));
btnSave.setDisable(true);
} else if (isOriginallyCompletedOrMissed) {
cbService.setDisable(true);
cbCustomer.setDisable(true);
cbPet.setDisable(true);
cbEmployee.setDisable(true);
cbHour.setDisable(true);
cbMinute.setDisable(true);
dpAppointmentDate.setDisable(true);
cbAppointmentStatus.setDisable(false);
cbAppointmentStatus.setItems(FXCollections.observableArrayList("Completed", "Missed"));
} else {
cbService.setDisable(true);
cbCustomer.setDisable(true);
cbPet.setDisable(true);
cbEmployee.setDisable(false);
cbHour.setDisable(false);
cbMinute.setDisable(false);
dpAppointmentDate.setDisable(false);
cbAppointmentStatus.setDisable(false);
cbAppointmentStatus.setItems(FXCollections.observableArrayList("Booked", "Cancelled"));
}
} }
private void buttonSaveClicked(MouseEvent e) { private void buttonSaveClicked(MouseEvent e) {

View File

@@ -121,7 +121,7 @@ public class InventoryDialogController {
//Validate inputs //Validate inputs
errorMsg += Validator.isPresent(txtQuantity.getText(), "Quantity"); errorMsg += Validator.isPresent(txtQuantity.getText(), "Quantity");
errorMsg += Validator.isLessThanVarChars(txtQuantity.getText(), "Quantity", 11); errorMsg += Validator.isLessThanVarChars(txtQuantity.getText(), "Quantity", 11);
errorMsg += Validator.isNonNegativeInteger(txtQuantity.getText(), "Quantity"); errorMsg += Validator.isPositiveInteger(txtQuantity.getText(), "Quantity");
//Operation only occurs if there are no errors //Operation only occurs if there are no errors
if (errorMsg.isEmpty()) { if (errorMsg.isEmpty()) {

View File

@@ -28,65 +28,34 @@ import java.math.BigDecimal;
import java.util.List; import java.util.List;
import java.util.Objects; import java.util.Objects;
import java.util.Optional; import java.util.Optional;
import java.util.stream.Collectors;
public class PetDialogController { public class PetDialogController {
@FXML private static final String STATUS_AVAILABLE = "Available";
private Button btnCancel; private static final String STATUS_ADOPTED = "Adopted";
private static final String STATUS_OWNED = "Owned";
private static final String STATUS_PENDING = "Pending";
@FXML @FXML private Button btnCancel;
private Button btnSave; @FXML private Button btnSave;
@FXML private Button btnChangeImage;
@FXML @FXML private Button btnRemoveImage;
private Button btnChangeImage; @FXML private ComboBox<String> cbPetStatus;
@FXML private ComboBox<String> cbPetSpecies;
@FXML @FXML private ComboBox<String> cbPetBreed;
private Button btnRemoveImage; @FXML private ComboBox<DropdownOption> cbCustomer;
@FXML private ComboBox<DropdownOption> cbStore;
@FXML @FXML private VBox vbCustomerField;
private ComboBox<String> cbPetStatus; @FXML private VBox vbStoreField;
@FXML private VBox vbPriceField;
@FXML @FXML private Label lblMode;
private ComboBox<DropdownOption> cbCustomer; @FXML private Label lblPetId;
@FXML private Label lblImageStatus;
@FXML @FXML private ImageView imgPetPreview;
private ComboBox<DropdownOption> cbStore; @FXML private TextField txtPetAge;
@FXML private TextField txtPetName;
@FXML @FXML private TextField txtPetPrice;
private VBox vbCustomerField;
@FXML
private VBox vbStoreField;
@FXML
private VBox vbPriceField;
@FXML
private Label lblMode;
@FXML
private Label lblPetId;
@FXML
private Label lblImageStatus;
@FXML
private ImageView imgPetPreview;
@FXML
private TextField txtPetAge;
@FXML
private TextField txtPetBreed;
@FXML
private TextField txtPetName;
@FXML
private TextField txtPetPrice;
@FXML
private ComboBox<String> cbPetSpecies;
private String mode = null; private String mode = null;
private File selectedImageFile; private File selectedImageFile;
@@ -96,15 +65,17 @@ public class PetDialogController {
private Long pendingCustomerId = null; private Long pendingCustomerId = null;
private Long pendingStoreId = null; private Long pendingStoreId = null;
private Long originalCustomerId = null; private Long originalCustomerId = null;
private boolean isOriginallyOwnedOrAdopted = false;
private String pendingBreedValue = null;
private ObservableList<String> statusList = FXCollections.observableArrayList( private final ObservableList<String> statusList = FXCollections.observableArrayList(
"Available", "Adopted", "Owned", "Pending" STATUS_AVAILABLE, STATUS_ADOPTED, STATUS_OWNED, STATUS_PENDING
); );
@FXML @FXML
void initialize() { void initialize() {
cbPetStatus.setItems(statusList); cbPetStatus.setItems(statusList);
cbPetBreed.setDisable(true);
cbCustomer.setCellFactory(param -> new ListCell<>() { cbCustomer.setCellFactory(param -> new ListCell<>() {
@Override protected void updateItem(DropdownOption o, boolean empty) { @Override protected void updateItem(DropdownOption o, boolean empty) {
@@ -132,14 +103,26 @@ public class PetDialogController {
} }
}); });
setFieldVisibility(vbCustomerField, false); applyStatusRules(STATUS_AVAILABLE, false);
setFieldVisibility(vbStoreField, false);
setFieldVisibility(vbPriceField, true);
loadSpecies(); loadSpecies();
loadCustomers();
loadStores();
cbPetSpecies.valueProperty().addListener((obs, oldVal, newVal) -> {
if (newVal != null && !newVal.isBlank()) {
if (!cbPetSpecies.isDisabled()) cbPetBreed.setDisable(false);
loadBreeds(newVal);
} else {
cbPetBreed.setItems(FXCollections.observableArrayList());
cbPetBreed.setValue(null);
cbPetBreed.setPromptText("Select Species first");
if (!cbPetSpecies.isDisabled()) cbPetBreed.setDisable(true);
}
});
cbPetStatus.valueProperty().addListener((obs, oldVal, newVal) -> { cbPetStatus.valueProperty().addListener((obs, oldVal, newVal) -> {
updateStatusFieldVisibility(newVal); if (newVal != null) applyStatusRules(newVal, true);
}); });
btnSave.setOnMouseClicked(new EventHandler<MouseEvent>() { btnSave.setOnMouseClicked(new EventHandler<MouseEvent>() {
@@ -159,52 +142,81 @@ public class PetDialogController {
btnChangeImage.setOnMouseClicked(mouseEvent -> handleChangeImage()); btnChangeImage.setOnMouseClicked(mouseEvent -> handleChangeImage());
btnRemoveImage.setOnMouseClicked(mouseEvent -> handleRemoveImage()); btnRemoveImage.setOnMouseClicked(mouseEvent -> handleRemoveImage());
refreshImagePreview(); refreshImagePreview();
}
loadCustomers(); private void loadBreeds(String species) {
loadStores(); cbPetBreed.setPromptText("Loading breeds...");
new Thread(() -> {
try {
List<DropdownOption> options = DropdownApi.getInstance().getPetBreeds(species);
List<String> breeds = options.stream().map(DropdownOption::getLabel).collect(Collectors.toList());
Platform.runLater(() -> {
cbPetBreed.setItems(FXCollections.observableArrayList(breeds));
cbPetBreed.setPromptText("Select Breed");
if (pendingBreedValue != null) {
cbPetBreed.setValue(pendingBreedValue);
pendingBreedValue = null;
}
});
} catch (Exception e) {
Platform.runLater(() -> {
ActivityLogger.getInstance().logException("PetDialogController.loadBreeds", e, "Loading breeds for species: " + species);
cbPetBreed.setPromptText("Unable to load breeds");
});
}
}).start();
}
private void applyStatusRules(String status, boolean clearInvalidSelections) {
if (STATUS_AVAILABLE.equalsIgnoreCase(status)) {
vbCustomerField.setDisable(true);
vbStoreField.setDisable(false);
if (clearInvalidSelections) cbCustomer.setValue(null);
return;
}
if (STATUS_OWNED.equalsIgnoreCase(status)) {
vbCustomerField.setDisable(false);
vbStoreField.setDisable(true);
if (clearInvalidSelections) cbStore.setValue(null);
return;
}
vbCustomerField.setDisable(false);
vbStoreField.setDisable(false);
} }
private void buttonSaveClicked(MouseEvent mouseEvent) { private void buttonSaveClicked(MouseEvent mouseEvent) {
String errorMsg = ""; String errorMsg = "";
//Check validation (input required)
errorMsg += Validator.isPresent(txtPetName.getText(), "Pet Name"); errorMsg += Validator.isPresent(txtPetName.getText(), "Pet Name");
errorMsg += Validator.isPresent(txtPetAge.getText(), "Age"); errorMsg += Validator.isPresent(txtPetAge.getText(), "Age");
errorMsg += Validator.isPresent(txtPetBreed.getText(), "Breed");
String speciesValue = cbPetSpecies.getValue() != null ? cbPetSpecies.getValue().trim() : ""; String speciesValue = cbPetSpecies.getValue() != null ? cbPetSpecies.getValue().trim() : "";
if (speciesValue.isEmpty()) errorMsg += "Species is required\n"; if (speciesValue.isEmpty()) errorMsg += "Species is required\n";
String breedValue = cbPetBreed.getValue() != null ? cbPetBreed.getValue().trim() : "";
if (breedValue.isEmpty()) errorMsg += "Breed is required\n";
if (cbPetStatus.getSelectionModel().getSelectedItem() == null) errorMsg += "Status is required\n";
errorMsg += Validator.isPresent(txtPetPrice.getText(), "Price");
String selectedStatus = cbPetStatus.getValue(); String selectedStatus = cbPetStatus.getValue();
boolean needsPrice = !("Owned".equalsIgnoreCase(selectedStatus) || "Adopted".equalsIgnoreCase(selectedStatus)); if (STATUS_AVAILABLE.equalsIgnoreCase(selectedStatus) && cbStore.getValue() == null) {
if (needsPrice) { errorMsg += "Store is required for Available status\n";
errorMsg += Validator.isPresent(txtPetPrice.getText(), "Price");
} }
if (cbPetStatus.getSelectionModel().getSelectedItem() == null){ if (STATUS_OWNED.equalsIgnoreCase(selectedStatus) && cbCustomer.getValue() == null) {
errorMsg += "Status is required";
}
if ("Owned".equalsIgnoreCase(selectedStatus) && cbCustomer.getValue() == null && UserSession.getInstance().isAdmin()) {
errorMsg += "Customer is required for Owned status\n"; errorMsg += "Customer is required for Owned status\n";
} }
boolean storeRequired = requiresStore(selectedStatus) && !"Adopted".equalsIgnoreCase(selectedStatus); if (STATUS_ADOPTED.equalsIgnoreCase(selectedStatus)) {
if (storeRequired && cbStore.getValue() == null) { if (cbCustomer.getValue() == null) errorMsg += "Customer is required for Adopted status\n";
errorMsg += "Store is required for " + selectedStatus + " status\n"; if (cbStore.getValue() == null) errorMsg += "Store is required for Adopted status\n";
} }
//Check validation (length size)
errorMsg += Validator.isLessThanVarChars(txtPetName.getText(), "Pet Name", 50); errorMsg += Validator.isLessThanVarChars(txtPetName.getText(), "Pet Name", 50);
errorMsg += Validator.isLessThanVarChars(speciesValue, "Species", 50); errorMsg += Validator.isLessThanVarChars(speciesValue, "Species", 50);
errorMsg += Validator.isLessThanVarChars(txtPetBreed.getText(), "Breed", 50); errorMsg += Validator.isLessThanVarChars(breedValue, "Breed", 50);
if (needsPrice) { errorMsg += Validator.isLessThanVarChars(txtPetPrice.getText(), "Price", 12);
errorMsg += Validator.isLessThanVarChars(txtPetPrice.getText(), "Price", 12);
}
errorMsg += Validator.isLessThanVarChars(txtPetAge.getText(), "Age", 11); errorMsg += Validator.isLessThanVarChars(txtPetAge.getText(), "Age", 11);
errorMsg += Validator.isNonNegativeDouble(txtPetPrice.getText(), "Price");
//Check validation (format)
if (needsPrice) {
errorMsg += Validator.isNonNegativeDouble(txtPetPrice.getText(), "Price");
}
errorMsg += Validator.isPositiveInteger(txtPetAge.getText(), "Age"); errorMsg += Validator.isPositiveInteger(txtPetAge.getText(), "Age");
if(errorMsg.isEmpty()){ if (errorMsg.isEmpty()) {
if ("Edit".equals(mode) && UserSession.getInstance().isAdmin()) { if ("Edit".equals(mode) && UserSession.getInstance().isAdmin()) {
Long newCustomerId = cbCustomer.getValue() != null ? cbCustomer.getValue().getId() : null; Long newCustomerId = cbCustomer.getValue() != null ? cbCustomer.getValue().getId() : null;
if (!Objects.equals(originalCustomerId, newCustomerId)) { if (!Objects.equals(originalCustomerId, newCustomerId)) {
@@ -212,44 +224,34 @@ public class PetDialogController {
confirm.setHeaderText("Confirm Owner Change"); confirm.setHeaderText("Confirm Owner Change");
confirm.setContentText("Are you sure you want to reassign this pet to a different owner?"); confirm.setContentText("Are you sure you want to reassign this pet to a different owner?");
Optional<ButtonType> result = confirm.showAndWait(); Optional<ButtonType> result = confirm.showAndWait();
if (result.isEmpty() || result.get() != ButtonType.OK) { if (result.isEmpty() || result.get() != ButtonType.OK) return;
return;
}
} }
} }
PetRequest request = buildPetRequest(); PetRequest request = buildPetRequest();
try { try {
if(mode.equals("Add")) { if (mode.equals("Add")) {
PetResponse response = PetApi.getInstance().createPet(request); PetResponse response = PetApi.getInstance().createPet(request);
applyImageChanges(response.getPetId()); applyImageChanges(response.getPetId());
} else { } else {
String[] parts = lblPetId.getText().split(": "); String[] parts = lblPetId.getText().split(": ");
if (parts.length < 2) { if (parts.length < 2) throw new IllegalStateException("Invalid pet ID format");
throw new IllegalStateException("Invalid pet ID format");
}
Long petId = Long.parseLong(parts[1]); Long petId = Long.parseLong(parts[1]);
PetApi.getInstance().updatePet(petId, request); PetApi.getInstance().updatePet(petId, request);
applyImageChanges(petId); applyImageChanges(petId);
} }
//tell the user operation was successful
Alert alert = new Alert(Alert.AlertType.INFORMATION); Alert alert = new Alert(Alert.AlertType.INFORMATION);
alert.setHeaderText("Saved"); alert.setHeaderText("Saved");
alert.setContentText(mode + " succeeded"); alert.setContentText(mode + " succeeded");
alert.showAndWait(); alert.showAndWait();
closeStage(mouseEvent); closeStage(mouseEvent);
} catch (Exception e) { } catch (Exception e) {
ActivityLogger.getInstance().logException( ActivityLogger.getInstance().logException("PetDialogController.buttonSaveClicked", e, mode + " pet record");
"PetDialogController.buttonSaveClicked",
e,
mode + " pet record");
Alert alert = new Alert(Alert.AlertType.ERROR); Alert alert = new Alert(Alert.AlertType.ERROR);
alert.setHeaderText("Operation Error"); alert.setHeaderText("Operation Error");
alert.setContentText(mode + " failed: " + e.getMessage()); alert.setContentText(mode + " failed: " + e.getMessage());
alert.showAndWait(); alert.showAndWait();
} }
} } else {
else{
Alert alert = new Alert(Alert.AlertType.ERROR); Alert alert = new Alert(Alert.AlertType.ERROR);
alert.setHeaderText("Input Error"); alert.setHeaderText("Input Error");
alert.setContentText(errorMsg); alert.setContentText(errorMsg);
@@ -261,11 +263,10 @@ public class PetDialogController {
PetRequest request = new PetRequest(); PetRequest request = new PetRequest();
request.setPetName(txtPetName.getText()); request.setPetName(txtPetName.getText());
request.setPetSpecies(cbPetSpecies.getValue() != null ? cbPetSpecies.getValue().trim() : ""); request.setPetSpecies(cbPetSpecies.getValue() != null ? cbPetSpecies.getValue().trim() : "");
request.setPetBreed(txtPetBreed.getText()); request.setPetBreed(cbPetBreed.getValue() != null ? cbPetBreed.getValue().trim() : "");
request.setPetStatus(cbPetStatus.getValue()); request.setPetStatus(cbPetStatus.getValue());
String buildStatus = cbPetStatus.getValue();
boolean buildNeedsPrice = !("Owned".equalsIgnoreCase(buildStatus) || "Adopted".equalsIgnoreCase(buildStatus)); if (txtPetPrice.getText() != null && !txtPetPrice.getText().isBlank()) {
if (buildNeedsPrice && txtPetPrice.getText() != null && !txtPetPrice.getText().isBlank()) {
try { try {
request.setPetPrice(new BigDecimal(txtPetPrice.getText())); request.setPetPrice(new BigDecimal(txtPetPrice.getText()));
} catch (NumberFormatException e) { } catch (NumberFormatException e) {
@@ -273,21 +274,14 @@ public class PetDialogController {
} }
} }
int age;
try { try {
age = Integer.parseInt(txtPetAge.getText()); request.setPetAge(Integer.parseInt(txtPetAge.getText()));
} catch (NumberFormatException e) { } catch (NumberFormatException e) {
throw new IllegalArgumentException("Invalid age format"); throw new IllegalArgumentException("Invalid age format");
} }
request.setPetAge(age);
String status = cbPetStatus.getValue(); if (cbCustomer.getValue() != null) request.setCustomerId(cbCustomer.getValue().getId());
if (("Owned".equalsIgnoreCase(status) || "Adopted".equalsIgnoreCase(status)) && cbCustomer.getValue() != null) { if (cbStore.getValue() != null) request.setStoreId(cbStore.getValue().getId());
request.setCustomerId(cbCustomer.getValue().getId());
}
if (requiresStore(status) && cbStore.getValue() != null) {
request.setStoreId(cbStore.getValue().getId());
}
return request; return request;
} }
@@ -296,15 +290,11 @@ public class PetDialogController {
new Thread(() -> { new Thread(() -> {
try { try {
List<DropdownOption> options = DropdownApi.getInstance().getPetSpecies(); List<DropdownOption> options = DropdownApi.getInstance().getPetSpecies();
List<String> species = options.stream() List<String> species = options.stream().map(DropdownOption::getLabel).collect(Collectors.toList());
.map(DropdownOption::getLabel)
.collect(java.util.stream.Collectors.toList());
Platform.runLater(() -> { Platform.runLater(() -> {
String current = cbPetSpecies.getValue(); String current = cbPetSpecies.getValue();
cbPetSpecies.setItems(FXCollections.observableArrayList(species)); cbPetSpecies.setItems(FXCollections.observableArrayList(species));
if (current != null && !current.isBlank()) { if (current != null && !current.isBlank()) cbPetSpecies.setValue(current);
cbPetSpecies.setValue(current);
}
}); });
} catch (Exception e) { } catch (Exception e) {
Platform.runLater(() -> ActivityLogger.getInstance().logException( Platform.runLater(() -> ActivityLogger.getInstance().logException(
@@ -323,8 +313,7 @@ public class PetDialogController {
}); });
} catch (Exception e) { } catch (Exception e) {
Platform.runLater(() -> { Platform.runLater(() -> {
ActivityLogger.getInstance().logException( ActivityLogger.getInstance().logException("PetDialogController.loadCustomers", e, "Loading customers");
"PetDialogController.loadCustomers", e, "Loading customers");
cbCustomer.setDisable(true); cbCustomer.setDisable(true);
cbCustomer.setPromptText("Unable to load customers"); cbCustomer.setPromptText("Unable to load customers");
}); });
@@ -342,8 +331,7 @@ public class PetDialogController {
}); });
} catch (Exception e) { } catch (Exception e) {
Platform.runLater(() -> { Platform.runLater(() -> {
ActivityLogger.getInstance().logException( ActivityLogger.getInstance().logException("PetDialogController.loadStores", e, "Loading stores");
"PetDialogController.loadStores", e, "Loading stores");
cbStore.setDisable(true); cbStore.setDisable(true);
cbStore.setPromptText("Unable to load stores"); cbStore.setPromptText("Unable to load stores");
}); });
@@ -383,55 +371,78 @@ public class PetDialogController {
stage.close(); stage.close();
} }
public void displayPetDetails(Pet pet){ public void displayPetDetails(Pet pet) {
if (pet!=null){ if (pet == null) return;
lblPetId.setText("ID: " + pet.getPetId());
txtPetName.setText(pet.getPetName());
cbPetSpecies.setValue(pet.getPetSpecies());
txtPetBreed.setText(pet.getPetBreed());
txtPetAge.setText(pet.getPetAge() + "");
txtPetPrice.setText(pet.getPetPrice() + "");
currentImageUrl = pet.getImageUrl();
selectedImageFile = null;
removeImageRequested = false;
refreshImagePreview();
pendingCustomerId = pet.getCustomerId() > 0 ? pet.getCustomerId() : null; lblPetId.setText("ID: " + pet.getPetId());
originalCustomerId = pendingCustomerId; txtPetName.setText(pet.getPetName());
pendingStoreId = pet.getStoreId() > 0 ? pet.getStoreId() : null; txtPetAge.setText(String.valueOf(pet.getPetAge()));
txtPetPrice.setText(String.valueOf(pet.getPetPrice()));
currentImageUrl = pet.getImageUrl();
selectedImageFile = null;
removeImageRequested = false;
refreshImagePreview();
for (String status : cbPetStatus.getItems()) { pendingCustomerId = pet.getCustomerId() > 0 ? pet.getCustomerId() : null;
if(status.equals(pet.getPetStatus())){ originalCustomerId = pendingCustomerId;
cbPetStatus.getSelectionModel().select(status); pendingStoreId = pet.getStoreId() > 0 ? pet.getStoreId() : null;
break;
} isOriginallyOwnedOrAdopted = STATUS_OWNED.equalsIgnoreCase(pet.getPetStatus())
|| STATUS_ADOPTED.equalsIgnoreCase(pet.getPetStatus());
pendingBreedValue = pet.getPetBreed();
cbPetSpecies.setValue(pet.getPetSpecies());
for (String status : cbPetStatus.getItems()) {
if (status.equals(pet.getPetStatus())) {
cbPetStatus.getSelectionModel().select(status);
break;
} }
updateStatusFieldVisibility(cbPetStatus.getValue()); }
applyStatusRules(cbPetStatus.getValue(), false);
applyEditModeLock();
}
private void applyEditModeLock() {
cbPetSpecies.setDisable(true);
cbPetBreed.setDisable(true);
boolean isStaff = !UserSession.getInstance().isAdmin();
if (isStaff && isOriginallyOwnedOrAdopted) {
cbPetStatus.setDisable(true);
vbCustomerField.setDisable(true);
vbStoreField.setDisable(true);
} }
} }
public void setMode(String mode) { public void setMode(String mode) {
this.mode = mode; this.mode = mode;
lblMode.setText(mode + " Pet"); lblMode.setText(mode + " Pet");
if(mode.equals("Add")) {
if (mode.equals("Add")) {
lblPetId.setVisible(false); lblPetId.setVisible(false);
currentImageUrl = null; currentImageUrl = null;
selectedImageFile = null; selectedImageFile = null;
removeImageRequested = false; removeImageRequested = false;
cbPetSpecies.setDisable(false);
cbPetBreed.setDisable(true);
cbPetBreed.setItems(FXCollections.observableArrayList());
cbPetBreed.setValue(null);
cbPetBreed.setPromptText("Select Species first");
cbPetStatus.setDisable(false);
cbPetStatus.getSelectionModel().select(STATUS_AVAILABLE);
applyStatusRules(STATUS_AVAILABLE, false);
refreshImagePreview(); refreshImagePreview();
} } else if (mode.equals("Edit")) {
else if(mode.equals("Edit")) {
lblPetId.setVisible(true); lblPetId.setVisible(true);
refreshImagePreview(); refreshImagePreview();
} }
updateStatusFieldVisibility(cbPetStatus.getValue());
} }
private void handleChangeImage() { private void handleChangeImage() {
File file = FilePickerSupport.pickImageFile(btnSave.getScene().getWindow()); File file = FilePickerSupport.pickImageFile(btnSave.getScene().getWindow());
if (file == null) { if (file == null) return;
return;
}
selectedImageFile = file; selectedImageFile = file;
removeImageRequested = false; removeImageRequested = false;
lblImageStatus.setText("Selected: " + file.getName()); lblImageStatus.setText("Selected: " + file.getName());
@@ -468,9 +479,7 @@ public class PetDialogController {
} }
private void refreshImagePreview() { private void refreshImagePreview() {
if (imgPetPreview == null || lblImageStatus == null || btnRemoveImage == null) { if (imgPetPreview == null || lblImageStatus == null || btnRemoveImage == null) return;
return;
}
imgPetPreview.setImage(null); imgPetPreview.setImage(null);
if (selectedImageFile != null) { if (selectedImageFile != null) {
lblImageStatus.setText("Selected: " + selectedImageFile.getName()); lblImageStatus.setText("Selected: " + selectedImageFile.getName());
@@ -487,30 +496,4 @@ public class PetDialogController {
lblImageStatus.setText("No image selected"); lblImageStatus.setText("No image selected");
btnRemoveImage.setDisable(true); btnRemoveImage.setDisable(true);
} }
private void updateStatusFieldVisibility(String status) {
boolean statusNeedsCustomer = "Owned".equalsIgnoreCase(status) || "Adopted".equalsIgnoreCase(status);
boolean needsCustomer = statusNeedsCustomer && UserSession.getInstance().isAdmin();
boolean storeBased = requiresStore(status);
boolean needsPrice = !statusNeedsCustomer;
setFieldVisibility(vbCustomerField, needsCustomer);
setFieldVisibility(vbStoreField, storeBased);
setFieldVisibility(vbPriceField, needsPrice);
}
private boolean requiresStore(String status) {
return "Available".equalsIgnoreCase(status)
|| "Pending".equalsIgnoreCase(status)
|| "Unadopted".equalsIgnoreCase(status)
|| "Adopted".equalsIgnoreCase(status);
}
private void setFieldVisibility(VBox field, boolean visible) {
if (field == null) {
return;
}
field.setVisible(visible);
field.setManaged(visible);
}
} }

View File

@@ -13,6 +13,7 @@ import javafx.fxml.FXMLLoader;
import javafx.scene.Scene; import javafx.scene.Scene;
import javafx.stage.Modality; import javafx.stage.Modality;
import org.example.petshopdesktop.util.ActivityLogger; import org.example.petshopdesktop.util.ActivityLogger;
import org.example.petshopdesktop.auth.UserSession;
import java.util.function.Consumer; import java.util.function.Consumer;
@@ -46,6 +47,12 @@ public class SaleDetailDialogController {
colQuantity.setCellValueFactory(new PropertyValueFactory<>("quantity")); colQuantity.setCellValueFactory(new PropertyValueFactory<>("quantity"));
colUnitPrice.setCellValueFactory(new PropertyValueFactory<>("unitPrice")); colUnitPrice.setCellValueFactory(new PropertyValueFactory<>("unitPrice"));
colLineTotal.setCellValueFactory(new PropertyValueFactory<>("total")); colLineTotal.setCellValueFactory(new PropertyValueFactory<>("total"));
if (btnRefund != null) {
boolean isAdmin = UserSession.getInstance().isAdmin();
btnRefund.setVisible(!isAdmin);
btnRefund.setManaged(!isAdmin);
}
} }
public void displaySaleDetails(SaleDetail sale) { public void displaySaleDetails(SaleDetail sale) {
@@ -57,7 +64,10 @@ public class SaleDetailDialogController {
lblTotal.setText(currency.format(sale.getTotalAmount())); lblTotal.setText(currency.format(sale.getTotalAmount()));
tvItems.setItems(sale.getItems()); tvItems.setItems(sale.getItems());
if (btnRefund != null) { if (btnRefund != null) {
btnRefund.setDisable(sale.isRefund()); boolean isAdmin = UserSession.getInstance().isAdmin();
if (!isAdmin) {
btnRefund.setDisable(sale.isRefund());
}
} }
} }

View File

@@ -15,6 +15,7 @@ import org.example.petshopdesktop.api.endpoints.UserApi;
import org.example.petshopdesktop.api.endpoints.CustomerApi; import org.example.petshopdesktop.api.endpoints.CustomerApi;
import org.example.petshopdesktop.auth.UserSession; import org.example.petshopdesktop.auth.UserSession;
import org.example.petshopdesktop.util.ActivityLogger; import org.example.petshopdesktop.util.ActivityLogger;
import org.example.petshopdesktop.util.TextFieldFormatSupport;
public class StaffEditDialogController { public class StaffEditDialogController {
@@ -47,6 +48,11 @@ public class StaffEditDialogController {
private UserResponse user; private UserResponse user;
@FXML
void initialize() {
TextFieldFormatSupport.applyPhoneNumberFormat(txtPhone);
}
public void setUser(UserResponse user) { public void setUser(UserResponse user) {
this.user = user; this.user = user;
String fullName = user.getFullName() == null ? "" : user.getFullName(); String fullName = user.getFullName() == null ? "" : user.getFullName();

View File

@@ -15,6 +15,7 @@ import org.example.petshopdesktop.api.endpoints.CustomerApi;
import org.example.petshopdesktop.auth.UserSession; import org.example.petshopdesktop.auth.UserSession;
import org.example.petshopdesktop.Validator; import org.example.petshopdesktop.Validator;
import org.example.petshopdesktop.util.ActivityLogger; import org.example.petshopdesktop.util.ActivityLogger;
import org.example.petshopdesktop.util.TextFieldFormatSupport;
public class StaffRegisterDialogController { public class StaffRegisterDialogController {
@@ -45,6 +46,11 @@ public class StaffRegisterDialogController {
@FXML @FXML
private Button btnCreate; private Button btnCreate;
@FXML
void initialize() {
TextFieldFormatSupport.applyPhoneNumberFormat(txtPhone);
}
@FXML @FXML
void btnCreateClicked(ActionEvent event) { void btnCreateClicked(ActionEvent event) {
lblError.setText(""); lblError.setText("");
@@ -82,6 +88,10 @@ public class StaffRegisterDialogController {
lblError.setText("Password is required."); lblError.setText("Password is required.");
return; return;
} }
if (password.length() < 6) {
lblError.setText("Password must be at least 6 characters.");
return;
}
if (!password.equals(confirm)) { if (!password.equals(confirm)) {
lblError.setText("Passwords do not match."); lblError.setText("Passwords do not match.");
return; return;

View File

@@ -15,6 +15,7 @@ import org.example.petshopdesktop.api.dto.supplier.SupplierResponse;
import org.example.petshopdesktop.api.endpoints.SupplierApi; import org.example.petshopdesktop.api.endpoints.SupplierApi;
import org.example.petshopdesktop.models.Supplier; import org.example.petshopdesktop.models.Supplier;
import org.example.petshopdesktop.util.ActivityLogger; import org.example.petshopdesktop.util.ActivityLogger;
import org.example.petshopdesktop.util.TextFieldFormatSupport;
public class SupplierDialogController { public class SupplierDialogController {
@@ -52,6 +53,8 @@ public class SupplierDialogController {
*/ */
@FXML @FXML
void initialize() { void initialize() {
TextFieldFormatSupport.applyPhoneNumberFormat(txtPhone);
//Set up mouse handlers for buttons //Set up mouse handlers for buttons
btnSave.setOnMouseClicked(new EventHandler<MouseEvent>() { btnSave.setOnMouseClicked(new EventHandler<MouseEvent>() {
@Override @Override

View File

@@ -0,0 +1,55 @@
package org.example.petshopdesktop.util;
import javafx.application.Platform;
import javafx.scene.control.TextField;
import javafx.scene.control.TextFormatter;
import java.util.function.UnaryOperator;
public class TextFieldFormatSupport {
/**
* Applies a phone number formatter to a TextField.
* The formatter only allows digits and automatically formats the input as (XXX) XXX-XXXX.
*
* @param textField The TextField to apply the formatter to.
*/
public static void applyPhoneNumberFormat(TextField textField) {
textField.textProperty().addListener((observable, oldValue, newValue) -> {
if (newValue == null) return;
// Remove all non-digit characters
String digits = newValue.replaceAll("\\D", "");
// Limit to 10 digits
if (digits.length() > 10) {
digits = digits.substring(0, 10);
}
StringBuilder formatted = new StringBuilder();
int len = digits.length();
if (len > 0) {
formatted.append("(");
if (len <= 3) {
formatted.append(digits);
} else {
formatted.append(digits, 0, 3).append(") ");
if (len <= 6) {
formatted.append(digits.substring(3));
} else {
formatted.append(digits, 3, 6).append("-").append(digits.substring(6));
}
}
}
String result = formatted.toString();
if (!result.equals(newValue)) {
Platform.runLater(() -> {
textField.setText(result);
textField.positionCaret(result.length());
});
}
});
}
}

View File

@@ -91,7 +91,7 @@
<Font name="System Bold" size="16.0" /> <Font name="System Bold" size="16.0" />
</font> </font>
</Label> </Label>
<ComboBox fx:id="cbPetSpecies" editable="true" prefHeight="29.0" prefWidth="336.0" promptText="Select or enter species" style="-fx-border-color: #E8EBED; -fx-border-width: 2; -fx-border-radius: 10; -fx-background-radius: 10; -fx-background-color: white;"> <ComboBox fx:id="cbPetSpecies" prefHeight="29.0" prefWidth="336.0" promptText="Select Species" style="-fx-border-color: #E8EBED; -fx-border-width: 2; -fx-border-radius: 10; -fx-background-radius: 10; -fx-background-color: white;">
<padding> <padding>
<Insets bottom="3.0" left="10.0" right="10.0" top="3.0" /> <Insets bottom="3.0" left="10.0" right="10.0" top="3.0" />
</padding> </padding>
@@ -105,11 +105,11 @@
<Font name="System Bold" size="16.0" /> <Font name="System Bold" size="16.0" />
</font> </font>
</Label> </Label>
<TextField fx:id="txtPetBreed" style="-fx-border-color: #E8EBED; -fx-border-width: 2; -fx-border-radius: 10; -fx-background-radius: 10;"> <ComboBox fx:id="cbPetBreed" prefHeight="29.0" prefWidth="336.0" promptText="Select Species first" style="-fx-border-color: #E8EBED; -fx-border-width: 2; -fx-border-radius: 10; -fx-background-radius: 10; -fx-background-color: white;">
<padding> <padding>
<Insets bottom="7.0" left="10.0" right="10.0" top="7.0" /> <Insets bottom="3.0" left="10.0" right="10.0" top="3.0" />
</padding> </padding>
</TextField> </ComboBox>
</children> </children>
</VBox> </VBox>
<VBox prefHeight="200.0" prefWidth="100.0" spacing="8.0" GridPane.columnIndex="1" GridPane.rowIndex="1"> <VBox prefHeight="200.0" prefWidth="100.0" spacing="8.0" GridPane.columnIndex="1" GridPane.rowIndex="1">

View File

@@ -176,6 +176,14 @@
<Insets bottom="8.0" left="10.0" right="10.0" top="8.0" /> <Insets bottom="8.0" left="10.0" right="10.0" top="8.0" />
</padding> </padding>
</Button> </Button>
<Button fx:id="btnCustomers" alignment="CENTER_LEFT" maxWidth="Infinity" mnemonicParsing="false" onAction="#btnCustomersClicked" style="-fx-background-color: transparent; -fx-background-radius: 8; -fx-cursor: hand; -fx-focus-color: transparent; -fx-faint-focus-color: transparent;" text="Customers" textFill="#cbd5e1">
<font>
<Font name="System" size="12.0" />
</font>
<padding>
<Insets bottom="8.0" left="10.0" right="10.0" top="8.0" />
</padding>
</Button>
<Separator fx:id="separatorAdmin" prefWidth="200.0" style="-fx-background-color: #444444; -fx-opacity: 0.35;" /> <Separator fx:id="separatorAdmin" prefWidth="200.0" style="-fx-background-color: #444444; -fx-opacity: 0.35;" />
@@ -220,7 +228,7 @@
<Insets bottom="8.0" left="10.0" right="10.0" top="8.0" /> <Insets bottom="8.0" left="10.0" right="10.0" top="8.0" />
</padding> </padding>
</Button> </Button>
<Button fx:id="btnStaffAccounts" alignment="CENTER_LEFT" maxWidth="Infinity" mnemonicParsing="false" onAction="#btnStaffAccountsClicked" style="-fx-background-color: transparent; -fx-background-radius: 8; -fx-cursor: hand; -fx-focus-color: transparent; -fx-faint-focus-color: transparent;" text="User Accounts" textFill="#cbd5e1"> <Button fx:id="btnStaffAccounts" alignment="CENTER_LEFT" maxWidth="Infinity" mnemonicParsing="false" onAction="#btnStaffAccountsClicked" style="-fx-background-color: transparent; -fx-background-radius: 8; -fx-cursor: hand; -fx-focus-color: transparent; -fx-faint-focus-color: transparent;" text="Staff Accounts" textFill="#cbd5e1">
<font> <font>
<Font name="System" size="12.0" /> <Font name="System" size="12.0" />
</font> </font>

View File

@@ -0,0 +1,89 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.TableColumn?>
<?import javafx.scene.control.TableView?>
<?import javafx.scene.control.TextField?>
<?import javafx.scene.layout.HBox?>
<?import javafx.scene.layout.Region?>
<?import javafx.scene.layout.VBox?>
<?import javafx.scene.text.Font?>
<VBox spacing="16.0" style="-fx-font-size: 14px;" xmlns="http://javafx.com/javafx/25" xmlns:fx="http://javafx.com/fxml/1" fx:controller="org.example.petshopdesktop.controllers.CustomerAccountsController">
<padding>
<Insets bottom="20.0" left="20.0" right="20.0" top="20.0" />
</padding>
<children>
<HBox alignment="CENTER_LEFT" spacing="20.0">
<children>
<Label text="Customers" textFill="#2c3e50">
<font>
<Font name="System Bold" size="30.0" />
</font>
</Label>
<Region HBox.hgrow="ALWAYS" />
<Button fx:id="btnRefresh" mnemonicParsing="false" onAction="#btnRefreshClicked" prefHeight="44.0" prefWidth="118.0" style="-fx-background-color: #4ECDC4; -fx-cursor: hand; -fx-background-radius: 8;" text="Refresh" textFill="WHITE">
<font>
<Font name="System Bold" size="14.0" />
</font>
<padding>
<Insets bottom="12.0" left="24.0" right="24.0" top="12.0" />
</padding>
</Button>
</children>
</HBox>
<VBox spacing="10.0" VBox.vgrow="ALWAYS">
<children>
<HBox alignment="CENTER_LEFT" spacing="12.0">
<children>
<Region HBox.hgrow="ALWAYS" />
<Button fx:id="btnEditCustomer" mnemonicParsing="false" onAction="#btnEditCustomerClicked" prefHeight="40.0" style="-fx-background-color: #F4A261; -fx-cursor: hand; -fx-background-radius: 8;" text="Edit Customer" textFill="WHITE">
<font>
<Font name="System Bold" size="14.0" />
</font>
<padding>
<Insets bottom="10.0" left="20.0" right="20.0" top="10.0" />
</padding>
</Button>
</children>
</HBox>
<HBox alignment="CENTER_LEFT" spacing="10.0" style="-fx-background-color: white; -fx-background-radius: 14; -fx-border-width: 1; -fx-border-radius: 14; -fx-border-color: #e6e6e6;">
<padding>
<Insets bottom="10.0" left="15.0" right="15.0" top="10.0" />
</padding>
<children>
<TextField fx:id="txtSearchCustomer" promptText="Search customers..." style="-fx-border-width: 0; -fx-background-color: transparent;" HBox.hgrow="ALWAYS">
<font>
<Font size="15.0" />
</font>
</TextField>
</children>
</HBox>
<TableView fx:id="tvCustomers" style="-fx-background-color: white; -fx-background-radius: 12;" VBox.vgrow="ALWAYS">
<columns>
<TableColumn fx:id="colCustomerUsername" prefWidth="130.0" text="Username" />
<TableColumn fx:id="colCustomerName" prefWidth="160.0" text="Name" />
<TableColumn fx:id="colCustomerEmail" prefWidth="200.0" text="Email" />
<TableColumn fx:id="colCustomerPhone" prefWidth="130.0" text="Phone" />
<TableColumn fx:id="colCustomerLoyaltyPoints" prefWidth="120.0" text="Loyalty Points" />
<TableColumn fx:id="colCustomerStatus" prefWidth="90.0" text="Status" />
<TableColumn fx:id="colCustomerCreated" prefWidth="150.0" text="Created" />
</columns>
</TableView>
</children>
</VBox>
<Label fx:id="lblStatus" text="" textFill="#64748b" visible="false" managed="true">
<font>
<Font size="13.0" />
</font>
</Label>
<Label fx:id="lblError" text="" textFill="#FF6B6B" wrapText="true" />
</children>
</VBox>

View File

@@ -48,7 +48,7 @@
</HBox> </HBox>
</children> </children>
</VBox> </VBox>
<FlowPane hgap="8.0" maxWidth="Infinity" prefWrapLength="260.0" vgap="8.0"> <HBox alignment="CENTER_RIGHT" spacing="8.0">
<children> <children>
<Button fx:id="btnRefund" mnemonicParsing="false" onAction="#btnRefund" prefHeight="32.0" style="-fx-background-color: #FF6b6b; -fx-cursor: hand; -fx-background-radius: 8;" text="Process Refund" textFill="WHITE"> <Button fx:id="btnRefund" mnemonicParsing="false" onAction="#btnRefund" prefHeight="32.0" style="-fx-background-color: #FF6b6b; -fx-cursor: hand; -fx-background-radius: 8;" text="Process Refund" textFill="WHITE">
<font> <font>
@@ -67,7 +67,7 @@
</padding> </padding>
</Button> </Button>
</children> </children>
</FlowPane> </HBox>
</children> </children>
</HBox> </HBox>

View File

@@ -11,7 +11,7 @@
<?import javafx.scene.layout.VBox?> <?import javafx.scene.layout.VBox?>
<?import javafx.scene.text.Font?> <?import javafx.scene.text.Font?>
<VBox spacing="20.0" style="-fx-font-size: 14px;" xmlns="http://javafx.com/javafx/25" xmlns:fx="http://javafx.com/fxml/1" fx:controller="org.example.petshopdesktop.controllers.StaffAccountsController"> <VBox spacing="16.0" style="-fx-font-size: 14px;" xmlns="http://javafx.com/javafx/25" xmlns:fx="http://javafx.com/fxml/1" fx:controller="org.example.petshopdesktop.controllers.StaffAccountsController">
<padding> <padding>
<Insets bottom="20.0" left="20.0" right="20.0" top="20.0" /> <Insets bottom="20.0" left="20.0" right="20.0" top="20.0" />
</padding> </padding>
@@ -19,28 +19,12 @@
<children> <children>
<HBox alignment="CENTER_LEFT" spacing="20.0"> <HBox alignment="CENTER_LEFT" spacing="20.0">
<children> <children>
<Label text="User Accounts" textFill="#2c3e50"> <Label text="Staff Accounts" textFill="#2c3e50">
<font> <font>
<Font name="System Bold" size="30.0" /> <Font name="System Bold" size="30.0" />
</font> </font>
</Label> </Label>
<Region HBox.hgrow="ALWAYS" /> <Region HBox.hgrow="ALWAYS" />
<Button fx:id="btnCreateAccount" mnemonicParsing="false" onAction="#btnCreateAccountClicked" prefHeight="44.0" style="-fx-background-color: #FF6B6B; -fx-cursor: hand; -fx-background-radius: 8;" text="Create Account" textFill="WHITE">
<font>
<Font name="System Bold" size="14.0" />
</font>
<padding>
<Insets bottom="12.0" left="24.0" right="24.0" top="12.0" />
</padding>
</Button>
<Button fx:id="btnEditAccount" mnemonicParsing="false" onAction="#btnEditAccountClicked" prefHeight="44.0" style="-fx-background-color: #F4A261; -fx-cursor: hand; -fx-background-radius: 8;" text="Edit Account" textFill="WHITE">
<font>
<Font name="System Bold" size="14.0" />
</font>
<padding>
<Insets bottom="12.0" left="24.0" right="24.0" top="12.0" />
</padding>
</Button>
<Button fx:id="btnRefresh" mnemonicParsing="false" onAction="#btnRefreshClicked" prefHeight="44.0" prefWidth="118.0" style="-fx-background-color: #4ECDC4; -fx-cursor: hand; -fx-background-radius: 8;" text="Refresh" textFill="WHITE"> <Button fx:id="btnRefresh" mnemonicParsing="false" onAction="#btnRefreshClicked" prefHeight="44.0" prefWidth="118.0" style="-fx-background-color: #4ECDC4; -fx-cursor: hand; -fx-background-radius: 8;" text="Refresh" textFill="WHITE">
<font> <font>
<Font name="System Bold" size="14.0" /> <Font name="System Bold" size="14.0" />
@@ -52,31 +36,56 @@
</children> </children>
</HBox> </HBox>
<HBox alignment="CENTER_LEFT" spacing="10.0" style="-fx-background-color: white; -fx-background-radius: 14; -fx-border-width: 1; -fx-border-radius: 14; -fx-border-color: #e6e6e6;"> <VBox fx:id="staffSection" spacing="10.0" VBox.vgrow="ALWAYS">
<padding>
<Insets bottom="10.0" left="15.0" right="15.0" top="10.0" />
</padding>
<children> <children>
<TextField fx:id="txtSearch" promptText="Search users..." style="-fx-border-width: 0; -fx-background-color: transparent;" HBox.hgrow="ALWAYS"> <HBox alignment="CENTER_LEFT" spacing="12.0">
<font> <children>
<Font size="15.0" /> <Region HBox.hgrow="ALWAYS" />
</font> <Button fx:id="btnCreateAccount" mnemonicParsing="false" onAction="#btnCreateAccountClicked" prefHeight="40.0" style="-fx-background-color: #FF6B6B; -fx-cursor: hand; -fx-background-radius: 8;" text="Create Account" textFill="WHITE">
</TextField> <font>
<Font name="System Bold" size="14.0" />
</font>
<padding>
<Insets bottom="10.0" left="20.0" right="20.0" top="10.0" />
</padding>
</Button>
<Button fx:id="btnEditAccount" mnemonicParsing="false" onAction="#btnEditAccountClicked" prefHeight="40.0" style="-fx-background-color: #F4A261; -fx-cursor: hand; -fx-background-radius: 8;" text="Edit Account" textFill="WHITE">
<font>
<Font name="System Bold" size="14.0" />
</font>
<padding>
<Insets bottom="10.0" left="20.0" right="20.0" top="10.0" />
</padding>
</Button>
</children>
</HBox>
<HBox alignment="CENTER_LEFT" spacing="10.0" style="-fx-background-color: white; -fx-background-radius: 14; -fx-border-width: 1; -fx-border-radius: 14; -fx-border-color: #e6e6e6;">
<padding>
<Insets bottom="10.0" left="15.0" right="15.0" top="10.0" />
</padding>
<children>
<TextField fx:id="txtSearch" promptText="Search staff..." style="-fx-border-width: 0; -fx-background-color: transparent;" HBox.hgrow="ALWAYS">
<font>
<Font size="15.0" />
</font>
</TextField>
</children>
</HBox>
<TableView fx:id="tvStaff" style="-fx-background-color: white; -fx-background-radius: 12;" VBox.vgrow="ALWAYS">
<columns>
<TableColumn fx:id="colUsername" prefWidth="140.0" text="Username" />
<TableColumn fx:id="colName" prefWidth="170.0" text="Name" />
<TableColumn fx:id="colEmail" prefWidth="210.0" text="Email" />
<TableColumn fx:id="colPhone" prefWidth="130.0" text="Phone" />
<TableColumn fx:id="colRole" prefWidth="100.0" text="Role" />
<TableColumn fx:id="colStatus" prefWidth="90.0" text="Status" />
<TableColumn fx:id="colCreated" prefWidth="150.0" text="Created" />
</columns>
</TableView>
</children> </children>
</HBox> </VBox>
<TableView fx:id="tvStaff" style="-fx-background-color: white; -fx-background-radius: 12;" VBox.vgrow="ALWAYS">
<columns>
<TableColumn fx:id="colUsername" prefWidth="140.0" text="Username" />
<TableColumn fx:id="colName" prefWidth="170.0" text="Name" />
<TableColumn fx:id="colEmail" prefWidth="210.0" text="Email" />
<TableColumn fx:id="colPhone" prefWidth="130.0" text="Phone" />
<TableColumn fx:id="colRole" prefWidth="100.0" text="Role" />
<TableColumn fx:id="colStatus" prefWidth="90.0" text="Status" />
<TableColumn fx:id="colCreated" prefWidth="150.0" text="Created" />
</columns>
</TableView>
<Label fx:id="lblStatus" text="" textFill="#64748b" visible="false" managed="true"> <Label fx:id="lblStatus" text="" textFill="#64748b" visible="false" managed="true">
<font> <font>