Merge branch 'AttachmentsToChat'
This commit is contained in:
@@ -103,11 +103,21 @@ public class PetAdapter extends RecyclerView.Adapter<PetAdapter.PetViewHolder> i
|
||||
|
||||
binding.tvPetStatus.setText(pet.getPetStatus());
|
||||
|
||||
//Set the status color depending on availability. If available, green, otherwise red
|
||||
if (pet.getPetStatus() != null && pet.getPetStatus().equals("Available")) {
|
||||
binding.tvPetStatus.setBackgroundColor(Color.parseColor("#4CAF50"));
|
||||
//Set the status color depending on availability. If available, green, If Pending, yellow, otherwise red
|
||||
if (pet.getPetStatus() != null) {
|
||||
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 {
|
||||
binding.tvPetStatus.setBackgroundColor(Color.parseColor("#F44336"));
|
||||
binding.tvPetStatus.setBackgroundColor(Color.parseColor("#9E9E9E"));
|
||||
}
|
||||
|
||||
// Load pet image using Glide
|
||||
|
||||
@@ -50,6 +50,9 @@ public interface PetApi {
|
||||
@GET("api/v1/dropdowns/pet-species")
|
||||
Call<List<DropdownDTO>> getPetSpeciesDropdowns();
|
||||
|
||||
@GET("api/v1/dropdowns/pet-breeds")
|
||||
Call<List<DropdownDTO>> getPetBreedsDropdowns(@Query("species") String species);
|
||||
|
||||
// Get pet by id
|
||||
@GET("api/v1/pets/{id}")
|
||||
Call<PetDTO> getPetById(@Path("id") Long id);
|
||||
|
||||
@@ -17,6 +17,7 @@ public class CustomerDTO {
|
||||
private String createdAt;
|
||||
private String updatedAt;
|
||||
private String password;
|
||||
private String role;
|
||||
|
||||
public CustomerDTO() {}
|
||||
|
||||
@@ -73,4 +74,7 @@ public class CustomerDTO {
|
||||
|
||||
public String getPassword() { return password; }
|
||||
public void setPassword(String password) { this.password = password; }
|
||||
|
||||
public String getRole() { return role; }
|
||||
public void setRole(String role) { this.role = role; }
|
||||
}
|
||||
|
||||
@@ -17,6 +17,9 @@ public class SaleDTO {
|
||||
private BigDecimal subtotalAmount;
|
||||
private BigDecimal couponDiscountAmount;
|
||||
private BigDecimal employeeDiscountAmount;
|
||||
private BigDecimal loyaltyDiscountAmount;
|
||||
private BigDecimal pointsDiscountAmount;
|
||||
private Integer pointsUsed;
|
||||
private String paymentMethod;
|
||||
private String channel;
|
||||
private Boolean isRefund;
|
||||
@@ -78,6 +81,22 @@ public class SaleDTO {
|
||||
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() {
|
||||
return paymentMethod;
|
||||
}
|
||||
@@ -126,6 +145,14 @@ public class SaleDTO {
|
||||
return customerName;
|
||||
}
|
||||
|
||||
public BigDecimal getPointsDiscountAmount() {
|
||||
return pointsDiscountAmount;
|
||||
}
|
||||
|
||||
public void setPointsDiscountAmount(BigDecimal pointsDiscountAmount) {
|
||||
this.pointsDiscountAmount = pointsDiscountAmount;
|
||||
}
|
||||
|
||||
// Nested SaleItemDTO
|
||||
public static class SaleItemDTO {
|
||||
private Long saleItemId;
|
||||
|
||||
@@ -136,7 +136,7 @@ public class PetFragment extends Fragment implements PetAdapter.OnPetClickListen
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
@@ -65,6 +65,11 @@ public class SaleFragment extends Fragment implements SaleAdapter.OnSaleClickLis
|
||||
|
||||
UIUtils.setupHamburgerMenu(binding.btnHamburger, this);
|
||||
|
||||
if (isAdmin()) {
|
||||
binding.fabAddSale.setVisibility(View.GONE);
|
||||
binding.btnOpenRefund.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
binding.fabAddSale.setOnClickListener(v ->
|
||||
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());
|
||||
}
|
||||
|
||||
private boolean isAdmin() {
|
||||
return "ADMIN".equalsIgnoreCase(tokenManager.getRole());
|
||||
}
|
||||
|
||||
private void setupStoreFilter() {
|
||||
SpinnerUtils.setupFilterSpinner(binding.spinnerStore, () -> loadSales(true));
|
||||
}
|
||||
|
||||
@@ -14,8 +14,11 @@ import com.example.petstoremobile.utils.InputValidator;
|
||||
import com.example.petstoremobile.utils.SpinnerUtils;
|
||||
import com.example.petstoremobile.utils.UIUtils;
|
||||
import com.example.petstoremobile.viewmodels.CustomerDetailViewModel;
|
||||
import com.example.petstoremobile.api.auth.TokenManager;
|
||||
import com.example.petstoremobile.utils.Resource;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import dagger.hilt.android.AndroidEntryPoint;
|
||||
|
||||
@AndroidEntryPoint
|
||||
@@ -24,6 +27,8 @@ public class CustomerDetailFragment extends Fragment {
|
||||
private FragmentCustomerDetailBinding binding;
|
||||
private CustomerDetailViewModel viewModel;
|
||||
|
||||
@Inject TokenManager tokenManager;
|
||||
|
||||
private final String[] STATUSES = {"Active", "Inactive"};
|
||||
|
||||
@Override
|
||||
@@ -65,7 +70,10 @@ public class CustomerDetailFragment extends Fragment {
|
||||
|
||||
// Show loyalty points
|
||||
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);
|
||||
} else {
|
||||
@@ -74,7 +82,7 @@ public class CustomerDetailFragment extends Fragment {
|
||||
binding.btnDeleteCustomer.setVisibility(View.GONE);
|
||||
binding.tvCustomerId.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.spinnerCustomerStatus.setSelection(Boolean.TRUE.equals(c.getActive()) ? 0 : 1);
|
||||
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.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 password = viewModel.isEditing() ? null : binding.etCustomerPassword.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();
|
||||
boolean active = binding.spinnerCustomerStatus.getSelectedItemPosition() == 0;
|
||||
|
||||
CustomerDTO dto = new CustomerDTO(username, password, firstName, lastName, email, phone);
|
||||
dto.setFullName(firstName + " " + lastName);
|
||||
dto.setActive(active);
|
||||
CustomerDTO dto = viewModel.createCustomerDto(username, password, firstName, lastName, email, phone, active, loyaltyPoints);
|
||||
|
||||
viewModel.saveCustomer(dto).observe(getViewLifecycleOwner(), resource -> {
|
||||
if (resource != null) {
|
||||
|
||||
@@ -101,6 +101,14 @@ public class PetDetailFragment extends Fragment {
|
||||
DropdownDTO::getLabel, "-- Select Species --", null, DropdownDTO::getId);
|
||||
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) {
|
||||
@@ -118,7 +126,7 @@ public class PetDetailFragment extends Fragment {
|
||||
private void savePet() {
|
||||
if (!InputValidator.isNotEmpty(binding.etPetName, "Pet Name")) 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.isPositiveDecimal(binding.etPetPrice, "Price")) return;
|
||||
|
||||
@@ -127,7 +135,11 @@ public class PetDetailFragment extends Fragment {
|
||||
String species = (speciesOptions != null && binding.spinnerPetSpecies.getSelectedItemPosition() > 0)
|
||||
? 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());
|
||||
double price = Double.parseDouble(binding.etPetPrice.getText().toString().trim());
|
||||
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.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.setPetName(name);
|
||||
@@ -236,7 +252,6 @@ public class PetDetailFragment extends Fragment {
|
||||
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
|
||||
PetDTO p = resource.data;
|
||||
binding.etPetName.setText(p.getPetName());
|
||||
binding.etPetBreed.setText(p.getPetBreed());
|
||||
binding.etPetAge.setText(String.valueOf(p.getPetAge()));
|
||||
if (p.getPetPrice() != null) {
|
||||
binding.etPetPrice.setText(String.format(Locale.getDefault(), "%.2f", p.getPetPrice()));
|
||||
@@ -279,6 +294,11 @@ public class PetDetailFragment extends Fragment {
|
||||
viewModel.onSpeciesSelected(p);
|
||||
});
|
||||
|
||||
SpinnerUtils.setOnIndexSelectedListener(binding.spinnerPetBreed, p -> {
|
||||
if (isUpdatingUI) return;
|
||||
viewModel.onBreedSelected(p);
|
||||
});
|
||||
|
||||
SpinnerUtils.setOnIndexSelectedListener(binding.spinnerCustomer, p -> {
|
||||
if (isUpdatingUI) return;
|
||||
viewModel.onCustomerSelected(p);
|
||||
@@ -306,7 +326,7 @@ public class PetDetailFragment extends Fragment {
|
||||
binding.btnSavePet.setText(state.saveButtonText);
|
||||
|
||||
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.isStoreEnabled, binding.spinnerStore);
|
||||
|
||||
@@ -323,6 +343,13 @@ public class PetDetailFragment extends Fragment {
|
||||
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) {
|
||||
binding.spinnerCustomer.setSelection(0);
|
||||
}
|
||||
|
||||
@@ -302,11 +302,7 @@ public class RefundFragment extends Fragment {
|
||||
}
|
||||
|
||||
private void updateRefundTotal() {
|
||||
BigDecimal total = BigDecimal.ZERO;
|
||||
List<RefundViewModel.RefundItem> cart = viewModel.getRefundCart().getValue();
|
||||
if (cart != null) {
|
||||
for (RefundViewModel.RefundItem item : cart) total = total.add(item.getTotal());
|
||||
}
|
||||
BigDecimal total = viewModel.calculateRefundTotal();
|
||||
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()];
|
||||
BigDecimal total = BigDecimal.ZERO;
|
||||
for (RefundViewModel.RefundItem item : viewModel.getRefundCart().getValue()) total = total.add(item.getTotal());
|
||||
final BigDecimal finalTotal = total;
|
||||
final BigDecimal finalTotal = viewModel.calculateRefundTotal();
|
||||
|
||||
DialogUtils.showConfirmDialog(requireContext(), "Confirm Refund",
|
||||
"Process refund for Sale #" + viewModel.getCurrentSale().getSaleId()
|
||||
|
||||
@@ -14,6 +14,7 @@ import com.example.petstoremobile.dtos.*;
|
||||
import com.example.petstoremobile.viewmodels.SaleDetailViewModel;
|
||||
import com.example.petstoremobile.utils.SpinnerUtils;
|
||||
import com.example.petstoremobile.utils.DialogUtils;
|
||||
import com.example.petstoremobile.utils.DateTimeUtils;
|
||||
import com.example.petstoremobile.utils.InputValidator;
|
||||
import com.example.petstoremobile.utils.Resource;
|
||||
import com.example.petstoremobile.utils.UIUtils;
|
||||
@@ -68,6 +69,10 @@ public class SaleDetailFragment extends Fragment {
|
||||
return "STAFF".equalsIgnoreCase(tokenManager.getRole());
|
||||
}
|
||||
|
||||
private boolean isAdmin() {
|
||||
return "ADMIN".equalsIgnoreCase(tokenManager.getRole());
|
||||
}
|
||||
|
||||
private void observeViewModel() {
|
||||
viewModel.getStoreList().observe(getViewLifecycleOwner(), list -> {
|
||||
Long primaryStoreId = tokenManager.getPrimaryStoreId();
|
||||
@@ -77,7 +82,7 @@ public class SaleDetailFragment extends Fragment {
|
||||
if (isStaff()) {
|
||||
UIUtils.setViewsEnabled(false, binding.spinnerSaleStore);
|
||||
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();
|
||||
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() {
|
||||
@@ -105,11 +121,11 @@ public class SaleDetailFragment extends Fragment {
|
||||
boolean viewOnly = a.getBoolean("viewOnly", false);
|
||||
viewModel.setSaleId(saleId, viewOnly);
|
||||
|
||||
binding.tvSaleMode.setText("Sale #" + saleId);
|
||||
binding.tvSaleDetailId.setText("ID: " + saleId);
|
||||
binding.tvSaleMode.setText("Sale #" + DateTimeUtils.formatId(saleId));
|
||||
binding.tvSaleDetailId.setText("ID: " + DateTimeUtils.formatId(saleId));
|
||||
|
||||
boolean isRefund = a.getBoolean("isRefund", false);
|
||||
if (isRefund) {
|
||||
if (isRefund || isAdmin()) {
|
||||
binding.btnRefundSale.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
@@ -128,9 +144,8 @@ public class SaleDetailFragment extends Fragment {
|
||||
binding.spinnerPaymentMethod.setVisibility(View.GONE);
|
||||
binding.tvSaleStore.setVisibility(View.VISIBLE);
|
||||
binding.tvSalePaymentMethod.setVisibility(View.VISIBLE);
|
||||
|
||||
// Show refund button only if it's not already a refund
|
||||
binding.btnRefundSale.setVisibility(isRefund ? View.GONE : View.VISIBLE);
|
||||
|
||||
binding.btnRefundSale.setVisibility((isRefund || isAdmin()) ? View.GONE : View.VISIBLE);
|
||||
}
|
||||
|
||||
loadSaleDetails();
|
||||
@@ -173,23 +188,36 @@ public class SaleDetailFragment extends Fragment {
|
||||
setLoading(resource.status == Resource.Status.LOADING);
|
||||
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
|
||||
SaleDTO sale = resource.data;
|
||||
binding.tvSaleDetailTotal.setText("Total: $" + sale.getTotalAmount());
|
||||
binding.tvSaleSubtotal.setText("$" + (sale.getSubtotalAmount() != null ? sale.getSubtotalAmount() : sale.getTotalAmount()));
|
||||
binding.tvSaleDetailTotal.setText("Total: $" + String.format(Locale.getDefault(), "%.2f", 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) {
|
||||
binding.llCouponDiscount.setVisibility(View.VISIBLE);
|
||||
binding.tvSaleCouponDiscount.setText("-$" + sale.getCouponDiscountAmount());
|
||||
binding.tvSaleCouponDiscount.setText("-$" + String.format(Locale.getDefault(), "%.2f", sale.getCouponDiscountAmount()));
|
||||
} else {
|
||||
binding.llCouponDiscount.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
if (sale.getEmployeeDiscountAmount() != null && sale.getEmployeeDiscountAmount().compareTo(BigDecimal.ZERO) > 0) {
|
||||
binding.llEmployeeDiscount.setVisibility(View.VISIBLE);
|
||||
binding.tvSaleEmployeeDiscount.setText("-$" + sale.getEmployeeDiscountAmount());
|
||||
binding.tvSaleEmployeeDiscount.setText("-$" + String.format(Locale.getDefault(), "%.2f", sale.getEmployeeDiscountAmount()));
|
||||
} else {
|
||||
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.tvSalePoints.setText(String.valueOf(sale.getPointsEarned() != null ? sale.getPointsEarned() : 0));
|
||||
binding.tvSaleStore.setText(sale.getStoreName() != null ? sale.getStoreName() : "—");
|
||||
@@ -216,7 +244,7 @@ public class SaleDetailFragment extends Fragment {
|
||||
binding.btnApplyCoupon.setOnClickListener(v -> {
|
||||
String code = binding.etCouponCode.getText().toString().trim();
|
||||
if (code.isEmpty()) {
|
||||
Toast.makeText(getContext(), "Enter a coupon code", Toast.LENGTH_SHORT).show();
|
||||
UIUtils.showToast(getContext(), "Enter a coupon code");
|
||||
return;
|
||||
}
|
||||
setLoading(true);
|
||||
@@ -286,7 +314,7 @@ public class SaleDetailFragment extends Fragment {
|
||||
|
||||
for (SaleDTO.SaleItemDTO existing : viewModel.getCartItems().getValue()) {
|
||||
if (existing.getProdId().equals(product.getProdId())) {
|
||||
Toast.makeText(getContext(), "Product already added", Toast.LENGTH_SHORT).show();
|
||||
UIUtils.showToast(getContext(), "Product already added");
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -294,6 +322,19 @@ public class SaleDetailFragment extends Fragment {
|
||||
viewModel.addToCart(new SaleDTO.SaleItemDTO(product.getProdId(), qty));
|
||||
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() {
|
||||
@@ -333,7 +374,7 @@ public class SaleDetailFragment extends Fragment {
|
||||
|
||||
TextView tvPrice = new TextView(getContext());
|
||||
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(tvQty);
|
||||
@@ -357,23 +398,35 @@ public class SaleDetailFragment extends Fragment {
|
||||
|
||||
private void updateTotal() {
|
||||
BigDecimal subtotal = viewModel.calculateSubtotal();
|
||||
BigDecimal discount = viewModel.calculateDiscount();
|
||||
BigDecimal total = subtotal.subtract(discount);
|
||||
binding.tvSaleSubtotal.setText("$" + subtotal);
|
||||
if (discount.compareTo(BigDecimal.ZERO) > 0) {
|
||||
BigDecimal couponDiscount = viewModel.calculateCouponDiscount();
|
||||
BigDecimal loyaltyDiscount = viewModel.calculateLoyaltyDiscount();
|
||||
BigDecimal total = subtotal.subtract(couponDiscount).subtract(loyaltyDiscount);
|
||||
|
||||
binding.tvSaleSubtotal.setText("$" + String.format(Locale.getDefault(), "%.2f", subtotal));
|
||||
|
||||
if (couponDiscount.compareTo(BigDecimal.ZERO) > 0) {
|
||||
binding.llCouponDiscount.setVisibility(View.VISIBLE);
|
||||
binding.tvSaleCouponDiscount.setText("-$" + discount);
|
||||
binding.tvSaleCouponDiscount.setText("-$" + String.format(Locale.getDefault(), "%.2f", couponDiscount));
|
||||
} else {
|
||||
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() {
|
||||
if (!InputValidator.isSpinnerSelected(binding.spinnerSaleStore, "Store")) return;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -387,12 +440,16 @@ public class SaleDetailFragment extends Fragment {
|
||||
|
||||
SaleDTO dto = new SaleDTO(store.getId(), payment, viewModel.getCartItems().getValue(), false, null, customerId);
|
||||
dto.setCouponId(viewModel.getAppliedCouponId());
|
||||
|
||||
if (Boolean.TRUE.equals(viewModel.getUseLoyaltyPoints().getValue())) {
|
||||
dto.setPointsUsed(viewModel.calculatePointsToUse());
|
||||
}
|
||||
|
||||
viewModel.createSale(dto).observe(getViewLifecycleOwner(), resource -> {
|
||||
if (resource != null) {
|
||||
setLoading(resource.status == Resource.Status.LOADING);
|
||||
if (resource.status == Resource.Status.SUCCESS) {
|
||||
Toast.makeText(getContext(), "Sale saved!", Toast.LENGTH_SHORT).show();
|
||||
UIUtils.showToast(getContext(), "Sale saved!");
|
||||
navigateBack();
|
||||
} else if (resource.status == Resource.Status.ERROR) {
|
||||
DialogUtils.showInfoDialog(requireContext(), "Save Error", resource.message);
|
||||
|
||||
@@ -161,7 +161,7 @@ public class StaffDetailFragment extends Fragment {
|
||||
List<DropdownDTO> stores = viewModel.getStoreList().getValue();
|
||||
Long storeId = stores.get(binding.spinnerStaffStore.getSelectedItemPosition() - 1).getId();
|
||||
|
||||
EmployeeDTO dto = new EmployeeDTO(
|
||||
EmployeeDTO dto = viewModel.createEmployeeDto(
|
||||
username,
|
||||
password.isEmpty() ? null : password,
|
||||
firstName,
|
||||
|
||||
@@ -126,7 +126,7 @@ public class PetProfileFragment extends Fragment {
|
||||
|
||||
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);
|
||||
if (pet.getCustomerName() != null && !pet.getCustomerName().isEmpty()) {
|
||||
binding.tvPetOwner.setText(pet.getCustomerName());
|
||||
@@ -137,7 +137,7 @@ public class PetProfileFragment extends Fragment {
|
||||
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);
|
||||
if (pet.getStoreName() != null && !pet.getStoreName().isEmpty()) {
|
||||
binding.tvPetStore.setText(pet.getStoreName());
|
||||
|
||||
@@ -58,6 +58,10 @@ public class PetRepository extends BaseRepository {
|
||||
return executeCall(petApi.getPetSpeciesDropdowns());
|
||||
}
|
||||
|
||||
public LiveData<Resource<List<DropdownDTO>>> getPetBreedsDropdowns(String species) {
|
||||
return executeCall(petApi.getPetBreedsDropdowns(species));
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves available pets for a specific store.
|
||||
*/
|
||||
|
||||
@@ -100,12 +100,13 @@ public class InputValidator {
|
||||
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) {
|
||||
String phone = field.getText().toString().trim();
|
||||
// Android built in phone validation pattern
|
||||
if (phone.isEmpty() || !android.util.Patterns.PHONE.matcher(phone).matches()) {
|
||||
field.setError("Enter a valid phone number");
|
||||
// Matches (XXX) XXX-XXXX format
|
||||
String pattern = "^\\(\\d{3}\\) \\d{3}-\\d{4}$";
|
||||
if (phone.isEmpty() || !phone.matches(pattern)) {
|
||||
field.setError("Enter a valid phone number: (XXX) XXX-XXXX");
|
||||
field.requestFocus();
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -31,6 +31,19 @@ public class CustomerDetailViewModel extends ViewModel {
|
||||
public long getCustomerId() { return customerId; }
|
||||
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) {
|
||||
return repository.getCustomerById(id);
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ public class PetDetailViewModel extends ViewModel {
|
||||
private static final String STATUS_AVAILABLE = "Available";
|
||||
private static final String STATUS_ADOPTED = "Adopted";
|
||||
private static final String STATUS_OWNED = "Owned";
|
||||
private static final String STATUS_PENDING = "Pending";
|
||||
|
||||
private final PetRepository petRepository;
|
||||
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>> storeList = 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<ViewState> viewState = new MutableLiveData<>(new ViewState());
|
||||
|
||||
@@ -40,6 +42,7 @@ public class PetDetailViewModel extends ViewModel {
|
||||
private Long selectedCustomerId = null;
|
||||
private Long selectedStoreId = null;
|
||||
private String selectedSpecies = null;
|
||||
private String selectedBreed = null;
|
||||
private boolean isOriginallyOwnedOrAdopted = false;
|
||||
private Long originalCustomerId = null;
|
||||
|
||||
@@ -111,10 +114,33 @@ public class PetDetailViewModel extends ViewModel {
|
||||
List<DropdownDTO> list = speciesList.getValue();
|
||||
if (position > 0 && list != null && position <= list.size()) {
|
||||
selectedSpecies = list.get(position - 1).getLabel();
|
||||
loadBreeds(selectedSpecies);
|
||||
} else {
|
||||
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) {
|
||||
@@ -154,12 +180,15 @@ public class PetDetailViewModel extends ViewModel {
|
||||
selectedCustomerId = null;
|
||||
selectedStoreId = null;
|
||||
selectedSpecies = null;
|
||||
selectedBreed = null;
|
||||
state.selectedCustomerId = null;
|
||||
state.selectedStoreId = null;
|
||||
state.selectedSpecies = null;
|
||||
state.selectedBreed = null;
|
||||
state.selectedStatus = STATUS_AVAILABLE;
|
||||
state.isCustomerEnabled = false;
|
||||
state.isStoreEnabled = true;
|
||||
state.isBreedEnabled = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -172,15 +201,22 @@ public class PetDetailViewModel extends ViewModel {
|
||||
selectedCustomerId = pet.getCustomerId();
|
||||
selectedStoreId = pet.getStoreId();
|
||||
selectedSpecies = pet.getPetSpecies();
|
||||
selectedBreed = pet.getPetBreed();
|
||||
isOriginallyOwnedOrAdopted = STATUS_OWNED.equalsIgnoreCase(pet.getPetStatus())
|
||||
|| STATUS_ADOPTED.equalsIgnoreCase(pet.getPetStatus());
|
||||
originalCustomerId = pet.getCustomerId();
|
||||
|
||||
if (selectedSpecies != null) {
|
||||
loadBreeds(selectedSpecies);
|
||||
}
|
||||
|
||||
updateViewState(state -> {
|
||||
state.selectedCustomerId = selectedCustomerId;
|
||||
state.selectedStoreId = selectedStoreId;
|
||||
state.selectedSpecies = selectedSpecies;
|
||||
state.selectedBreed = selectedBreed;
|
||||
state.selectedStatus = normalizeStatus(pet.getPetStatus());
|
||||
state.isBreedEnabled = !state.isEditing && (selectedSpecies != null);
|
||||
applyStatusRules(state, false);
|
||||
});
|
||||
}
|
||||
@@ -215,6 +251,10 @@ public class PetDetailViewModel extends ViewModel {
|
||||
return speciesList;
|
||||
}
|
||||
|
||||
public LiveData<List<DropdownDTO>> getBreedList() {
|
||||
return breedList;
|
||||
}
|
||||
|
||||
public LiveData<Boolean> getIsLoading() {
|
||||
return isLoading;
|
||||
}
|
||||
@@ -253,6 +293,7 @@ public class PetDetailViewModel extends ViewModel {
|
||||
String normalized = status.trim();
|
||||
if (STATUS_ADOPTED.equalsIgnoreCase(normalized)) return STATUS_ADOPTED;
|
||||
if (STATUS_OWNED.equalsIgnoreCase(normalized)) return STATUS_OWNED;
|
||||
if (STATUS_PENDING.equalsIgnoreCase(normalized)) return STATUS_PENDING;
|
||||
return STATUS_AVAILABLE;
|
||||
}
|
||||
|
||||
@@ -290,9 +331,10 @@ public class PetDetailViewModel extends ViewModel {
|
||||
public boolean isStoreEnabled = true;
|
||||
public String modeTitle = "Add Pet";
|
||||
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 selectedSpecies = null;
|
||||
public String selectedBreed = null;
|
||||
public Long selectedCustomerId = null;
|
||||
public Long selectedStoreId = null;
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import com.example.petstoremobile.repositories.SaleRepository;
|
||||
import com.example.petstoremobile.utils.Resource;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
@@ -125,6 +126,33 @@ public class RefundViewModel extends ViewModel {
|
||||
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) {
|
||||
SaleDTO sale = currentSale.getValue();
|
||||
List<RefundItem> cart = refundCart.getValue();
|
||||
|
||||
@@ -5,6 +5,7 @@ import androidx.lifecycle.MutableLiveData;
|
||||
import androidx.lifecycle.ViewModel;
|
||||
|
||||
import com.example.petstoremobile.dtos.CouponDTO;
|
||||
import com.example.petstoremobile.dtos.CustomerDTO;
|
||||
import com.example.petstoremobile.dtos.DropdownDTO;
|
||||
import com.example.petstoremobile.dtos.ProductDTO;
|
||||
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<SaleDTO.SaleItemDTO>> cartItems = new MutableLiveData<>(new ArrayList<>());
|
||||
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);
|
||||
|
||||
@Inject
|
||||
@@ -111,6 +114,34 @@ public class SaleDetailViewModel extends ViewModel {
|
||||
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() {
|
||||
appliedCoupon.setValue(null);
|
||||
}
|
||||
@@ -125,6 +156,10 @@ public class SaleDetailViewModel extends ViewModel {
|
||||
}
|
||||
|
||||
public BigDecimal calculateDiscount() {
|
||||
return calculateCouponDiscount().add(calculateLoyaltyDiscount());
|
||||
}
|
||||
|
||||
public BigDecimal calculateCouponDiscount() {
|
||||
CouponDTO coupon = appliedCoupon.getValue();
|
||||
if (coupon == null || coupon.getDiscountValue() == null) return BigDecimal.ZERO;
|
||||
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() {
|
||||
BigDecimal total = BigDecimal.ZERO;
|
||||
List<SaleDTO.SaleItemDTO> items = cartItems.getValue();
|
||||
|
||||
@@ -60,6 +60,14 @@ public class StaffDetailViewModel extends ViewModel {
|
||||
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) {
|
||||
if (isEditing && employeeId > 0) {
|
||||
return repository.updateEmployee(employeeId, dto);
|
||||
|
||||
@@ -195,14 +195,13 @@
|
||||
android:textSize="12sp"
|
||||
android:layout_marginBottom="4dp"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvCustomerLoyaltyPoints"
|
||||
<EditText
|
||||
android:id="@+id/etCustomerLoyaltyPoints"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="0"
|
||||
android:textColor="@color/text_dark"
|
||||
android:textSize="16sp"
|
||||
android:layout_marginBottom="8dp"/>
|
||||
android:hint="0"
|
||||
android:inputType="number"
|
||||
android:layout_marginBottom="16dp"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
|
||||
@@ -109,14 +109,11 @@
|
||||
android:textSize="12sp"
|
||||
android:layout_marginBottom="4dp"/>
|
||||
|
||||
<EditText
|
||||
android:id="@+id/etPetBreed"
|
||||
<Spinner
|
||||
android:id="@+id/spinnerPetBreed"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="Enter breed"
|
||||
android:inputType="text"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:textColor="@color/text_dark"/>
|
||||
android:layout_marginBottom="16dp"/>
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
|
||||
@@ -128,7 +128,30 @@
|
||||
android:id="@+id/spinnerSaleCustomer"
|
||||
android:layout_width="match_parent"
|
||||
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 -->
|
||||
<TextView
|
||||
@@ -390,6 +413,27 @@
|
||||
android:textColor="@color/status_adopted"/>
|
||||
</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
|
||||
android:id="@+id/tvSaleDetailTotal"
|
||||
android:layout_width="wrap_content"
|
||||
|
||||
@@ -5,6 +5,7 @@ import jakarta.validation.constraints.NotEmpty;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.math.BigDecimal;
|
||||
|
||||
public class SaleRequest {
|
||||
@NotNull(message = "Store ID is required")
|
||||
@@ -28,7 +29,9 @@ public class SaleRequest {
|
||||
|
||||
private Long cartId;
|
||||
|
||||
private Boolean useLoyaltyPoints = false;
|
||||
private Integer pointsUsed;
|
||||
|
||||
private BigDecimal pointsDiscountAmount;
|
||||
|
||||
public Long getStoreId() {
|
||||
return storeId;
|
||||
@@ -102,12 +105,20 @@ public class SaleRequest {
|
||||
this.cartId = cartId;
|
||||
}
|
||||
|
||||
public Boolean getUseLoyaltyPoints() {
|
||||
return useLoyaltyPoints;
|
||||
public Integer getPointsUsed() {
|
||||
return pointsUsed;
|
||||
}
|
||||
|
||||
public void setUseLoyaltyPoints(Boolean useLoyaltyPoints) {
|
||||
this.useLoyaltyPoints = useLoyaltyPoints;
|
||||
public void setPointsUsed(Integer pointsUsed) {
|
||||
this.pointsUsed = pointsUsed;
|
||||
}
|
||||
|
||||
public BigDecimal getPointsDiscountAmount() {
|
||||
return pointsDiscountAmount;
|
||||
}
|
||||
|
||||
public void setPointsDiscountAmount(BigDecimal pointsDiscountAmount) {
|
||||
this.pointsDiscountAmount = pointsDiscountAmount;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -21,6 +21,8 @@ public class SaleResponse {
|
||||
private BigDecimal loyaltyDiscountAmount;
|
||||
private Integer pointsUsed;
|
||||
private Integer pointsEarned;
|
||||
private Integer pointsUsed;
|
||||
private BigDecimal pointsDiscountAmount;
|
||||
private String channel;
|
||||
private Long couponId;
|
||||
private Long cartId;
|
||||
@@ -153,6 +155,22 @@ public class SaleResponse {
|
||||
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() {
|
||||
return channel;
|
||||
}
|
||||
|
||||
@@ -39,6 +39,8 @@ public class UserRequest {
|
||||
|
||||
private Boolean active = true;
|
||||
|
||||
private Integer loyaltyPoints;
|
||||
|
||||
public String getUsername() {
|
||||
return username;
|
||||
}
|
||||
@@ -127,6 +129,14 @@ public class UserRequest {
|
||||
this.active = active;
|
||||
}
|
||||
|
||||
public Integer getLoyaltyPoints() {
|
||||
return loyaltyPoints;
|
||||
}
|
||||
|
||||
public void setLoyaltyPoints(Integer loyaltyPoints) {
|
||||
this.loyaltyPoints = loyaltyPoints;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
|
||||
@@ -75,6 +75,12 @@ public class Sale {
|
||||
@Column(nullable = false)
|
||||
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)
|
||||
private List<SaleItem> items = new ArrayList<>();
|
||||
|
||||
@@ -233,6 +239,22 @@ public class Sale {
|
||||
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() {
|
||||
return items;
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ public interface PetRepository extends JpaRepository<Pet, Long> {
|
||||
"WHERE LOWER(p.petStatus) = 'available' " +
|
||||
"AND NOT EXISTS (" +
|
||||
" 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")
|
||||
List<Pet> findAdoptablePetsOrderByPetNameAsc();
|
||||
@@ -29,7 +29,7 @@ public interface PetRepository extends JpaRepository<Pet, Long> {
|
||||
"AND (:storeId IS NULL OR p.store.storeId = :storeId) " +
|
||||
"AND NOT EXISTS (" +
|
||||
" 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")
|
||||
List<Pet> findAdoptablePetsByStore(@Param("storeId") Long storeId);
|
||||
|
||||
@@ -32,6 +32,7 @@ public class AdoptionService {
|
||||
private static final String ADOPTION_STATUS_MISSED = "Missed";
|
||||
private static final String PET_STATUS_AVAILABLE = "Available";
|
||||
private static final String PET_STATUS_ADOPTED = "Adopted";
|
||||
private static final String PET_STATUS_PENDING = "Pending";
|
||||
|
||||
private final AdoptionRepository adoptionRepository;
|
||||
private final PetRepository petRepository;
|
||||
@@ -263,10 +264,14 @@ public class AdoptionService {
|
||||
private void syncPetStatus(Pet pet, String adoptionStatus, Long adoptionId, User customer) {
|
||||
boolean completedElsewhere = adoptionId != null
|
||||
&& adoptionRepository.existsByPet_IdAndAdoptionStatusIgnoreCaseAndAdoptionIdNot(pet.getPetId(), ADOPTION_STATUS_COMPLETED, adoptionId);
|
||||
|
||||
if (ADOPTION_STATUS_COMPLETED.equalsIgnoreCase(adoptionStatus) || completedElsewhere) {
|
||||
pet.setPetStatus(PET_STATUS_ADOPTED);
|
||||
pet.setOwner(customer);
|
||||
pet.setStore(null);
|
||||
} else if (ADOPTION_STATUS_PENDING.equalsIgnoreCase(adoptionStatus)) {
|
||||
pet.setPetStatus(PET_STATUS_PENDING);
|
||||
pet.setOwner(customer);
|
||||
} else {
|
||||
pet.setPetStatus(PET_STATUS_AVAILABLE);
|
||||
pet.setOwner(null);
|
||||
|
||||
@@ -160,14 +160,33 @@ public class SaleService {
|
||||
saleItems.add(saleItem);
|
||||
subtotalAmount = subtotalAmount.add(itemTotal);
|
||||
}
|
||||
subtotalAmount = subtotalAmount.negate();
|
||||
sale.setSubtotalAmount(subtotalAmount);
|
||||
sale.setTotalAmount(subtotalAmount);
|
||||
sale.setCouponDiscountAmount(BigDecimal.ZERO);
|
||||
sale.setEmployeeDiscountAmount(BigDecimal.ZERO);
|
||||
sale.setLoyaltyDiscountAmount(BigDecimal.ZERO);
|
||||
sale.setPointsUsed(0);
|
||||
sale.setPointsEarned(0);
|
||||
|
||||
Sale originalSale = sale.getOriginalSale();
|
||||
BigDecimal originalSubtotal = originalSale.getSubtotalAmount() != null
|
||||
? originalSale.getSubtotalAmount()
|
||||
: originalSale.getItems().stream()
|
||||
.map(i -> i.getUnitPrice().multiply(BigDecimal.valueOf(Math.abs(i.getQuantity()))))
|
||||
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||
|
||||
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 {
|
||||
if (request.getItems() == null || request.getItems().isEmpty()) {
|
||||
throw new BusinessException("At least one item is required");
|
||||
@@ -204,14 +223,23 @@ public class SaleService {
|
||||
BigDecimal couponDiscount = calculateCouponDiscount(sale.getCoupon(), subtotalAmount);
|
||||
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);
|
||||
|
||||
BigDecimal loyaltyDiscount = calculateLoyaltyDiscount(customer, subtotalAmount.subtract(couponDiscount).subtract(employeeDiscount), Boolean.TRUE.equals(request.getUseLoyaltyPoints()));
|
||||
sale.setLoyaltyDiscountAmount(loyaltyDiscount);
|
||||
sale.setPointsUsed(toPointsUsed(loyaltyDiscount));
|
||||
|
||||
BigDecimal finalTotal = subtotalAmount.subtract(couponDiscount).subtract(employeeDiscount).subtract(loyaltyDiscount);
|
||||
BigDecimal finalTotal = subtotalAmount.subtract(couponDiscount).subtract(pointsDiscount).subtract(employeeDiscount);
|
||||
sale.setTotalAmount(finalTotal.max(BigDecimal.ZERO));
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
private BigDecimal calculatePointsDiscount(int pointsUsed) {
|
||||
return new BigDecimal(pointsUsed).divide(new BigDecimal("20"), 2, RoundingMode.HALF_UP);
|
||||
}
|
||||
|
||||
private BigDecimal calculateEmployeeDiscount(User customer, BigDecimal remainingAmount) {
|
||||
if (customer == null || remainingAmount.compareTo(BigDecimal.ZERO) <= 0) {
|
||||
return BigDecimal.ZERO;
|
||||
@@ -328,6 +360,8 @@ public class SaleService {
|
||||
response.setLoyaltyDiscountAmount(sale.getLoyaltyDiscountAmount());
|
||||
response.setPointsUsed(sale.getPointsUsed());
|
||||
response.setPointsEarned(sale.getPointsEarned());
|
||||
response.setPointsUsed(sale.getPointsUsed());
|
||||
response.setPointsDiscountAmount(sale.getPointsDiscountAmount());
|
||||
response.setChannel(sale.getChannel());
|
||||
if (sale.getCoupon() != null) {
|
||||
response.setCouponId(sale.getCoupon().getCouponId());
|
||||
|
||||
@@ -75,6 +75,9 @@ public class UserService {
|
||||
user.setStaffRole(trimToNull(request.getStaffRole()));
|
||||
user.setPrimaryStore(resolveStore(request.getPrimaryStoreId()));
|
||||
user.setActive(request.getActive() != null ? request.getActive() : true);
|
||||
if (request.getLoyaltyPoints() != null) {
|
||||
user.setLoyaltyPoints(request.getLoyaltyPoints());
|
||||
}
|
||||
|
||||
validateUniquePhone(user.getPhone(), null);
|
||||
|
||||
@@ -111,6 +114,9 @@ public class UserService {
|
||||
user.setStaffRole(trimToNull(request.getStaffRole()));
|
||||
user.setPrimaryStore(resolveStore(request.getPrimaryStoreId()));
|
||||
user.setActive(request.getActive() != null ? request.getActive() : true);
|
||||
if (request.getLoyaltyPoints() != null) {
|
||||
user.setLoyaltyPoints(request.getLoyaltyPoints());
|
||||
}
|
||||
if (invalidateToken) {
|
||||
user.setTokenVersion(user.getTokenVersion() + 1);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -230,6 +230,8 @@ CREATE TABLE IF NOT EXISTS sale (
|
||||
couponDiscountAmount DECIMAL(10, 2) NOT NULL DEFAULT 0.00,
|
||||
employeeDiscountAmount DECIMAL(10, 2) NOT NULL DEFAULT 0.00,
|
||||
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,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
CONSTRAINT fk_sale_employee FOREIGN KEY (employeeId) REFERENCES users(id),
|
||||
|
||||
@@ -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 name name of input
|
||||
* @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){
|
||||
String msg = "";
|
||||
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;
|
||||
}
|
||||
String regex = "^\\d{3}-\\d{3}-\\d{4}$";
|
||||
String regex = "^\\(\\d{3}\\) \\d{3}-\\d{4}$";
|
||||
|
||||
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;
|
||||
|
||||
@@ -10,6 +10,7 @@ public class UserResponse {
|
||||
private String phone;
|
||||
private String role;
|
||||
private Boolean active;
|
||||
private Integer loyaltyPoints;
|
||||
private LocalDateTime createdAt;
|
||||
private LocalDateTime updatedAt;
|
||||
|
||||
@@ -72,6 +73,14 @@ public class UserResponse {
|
||||
this.active = active;
|
||||
}
|
||||
|
||||
public Integer getLoyaltyPoints() {
|
||||
return loyaltyPoints;
|
||||
}
|
||||
|
||||
public void setLoyaltyPoints(Integer loyaltyPoints) {
|
||||
this.loyaltyPoints = loyaltyPoints;
|
||||
}
|
||||
|
||||
public LocalDateTime getCreatedAt() {
|
||||
return createdAt;
|
||||
}
|
||||
|
||||
@@ -42,6 +42,15 @@ public class DropdownApi {
|
||||
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 {
|
||||
String response = apiClient.getRawResponse("/api/v1/dropdowns/products");
|
||||
if (response == null || response.isEmpty()) {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -89,6 +89,9 @@ public class MainLayoutController {
|
||||
@FXML
|
||||
private Button btnStaffAccounts;
|
||||
|
||||
@FXML
|
||||
private Button btnCustomers;
|
||||
|
||||
@FXML
|
||||
private Button btnAnalytics;
|
||||
|
||||
@@ -179,6 +182,12 @@ public class MainLayoutController {
|
||||
updateButtons(btnStaffAccounts);
|
||||
}
|
||||
|
||||
@FXML
|
||||
void btnCustomersClicked(ActionEvent event) {
|
||||
loadView("customer-accounts-view.fxml");
|
||||
updateButtons(btnCustomers);
|
||||
}
|
||||
|
||||
@FXML
|
||||
void btnAnalyticsClicked(ActionEvent event) {
|
||||
loadView("analytics-view.fxml");
|
||||
@@ -415,8 +424,13 @@ public class MainLayoutController {
|
||||
btnPurchaseOrders.setManaged(isAdmin);
|
||||
|
||||
if (btnStaffAccounts != null) {
|
||||
btnStaffAccounts.setVisible(true);
|
||||
btnStaffAccounts.setManaged(true);
|
||||
btnStaffAccounts.setVisible(isAdmin);
|
||||
btnStaffAccounts.setManaged(isAdmin);
|
||||
}
|
||||
|
||||
if (btnCustomers != null) {
|
||||
btnCustomers.setVisible(true);
|
||||
btnCustomers.setManaged(true);
|
||||
}
|
||||
|
||||
if (lblAdminSection != null) {
|
||||
@@ -493,6 +507,7 @@ public class MainLayoutController {
|
||||
btnProducts,
|
||||
btnPurchaseOrders,
|
||||
btnStaffAccounts,
|
||||
btnCustomers,
|
||||
btnAnalytics,
|
||||
btnActivityLogs,
|
||||
btnCoupons,
|
||||
|
||||
@@ -255,6 +255,8 @@ public class SaleController {
|
||||
boolean isAdmin = UserSession.getInstance().isAdmin();
|
||||
vbCreateSale.setVisible(!isAdmin);
|
||||
vbCreateSale.setManaged(!isAdmin);
|
||||
btnRefund.setVisible(!isAdmin);
|
||||
btnRefund.setManaged(!isAdmin);
|
||||
lblModeNote.setText(isAdmin ? "(View only)" : "(Staff can create sales)");
|
||||
}
|
||||
|
||||
|
||||
@@ -8,26 +8,25 @@ import javafx.event.ActionEvent;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.fxml.FXMLLoader;
|
||||
import javafx.scene.Scene;
|
||||
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.control.*;
|
||||
import javafx.scene.layout.VBox;
|
||||
import javafx.stage.Modality;
|
||||
import javafx.stage.Stage;
|
||||
import org.example.petshopdesktop.api.dto.user.UserResponse;
|
||||
import org.example.petshopdesktop.api.endpoints.UserApi;
|
||||
import org.example.petshopdesktop.api.endpoints.CustomerApi;
|
||||
import org.example.petshopdesktop.auth.UserSession;
|
||||
import org.example.petshopdesktop.util.ActivityLogger;
|
||||
import org.example.petshopdesktop.util.TableViewSupport;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class StaffAccountsController {
|
||||
|
||||
@FXML
|
||||
private VBox staffSection;
|
||||
|
||||
@FXML
|
||||
private TableView<UserResponse> tvStaff;
|
||||
|
||||
@@ -55,12 +54,6 @@ public class StaffAccountsController {
|
||||
@FXML
|
||||
private TextField txtSearch;
|
||||
|
||||
@FXML
|
||||
private Label lblError;
|
||||
|
||||
@FXML
|
||||
private Label lblStatus;
|
||||
|
||||
@FXML
|
||||
private Button btnRefresh;
|
||||
|
||||
@@ -70,8 +63,14 @@ public class StaffAccountsController {
|
||||
@FXML
|
||||
private Button btnEditAccount;
|
||||
|
||||
@FXML
|
||||
private Label lblError;
|
||||
|
||||
@FXML
|
||||
private Label lblStatus;
|
||||
|
||||
private final ObservableList<UserResponse> staffAccounts = FXCollections.observableArrayList();
|
||||
private FilteredList<UserResponse> filtered;
|
||||
private FilteredList<UserResponse> filteredStaff;
|
||||
|
||||
@FXML
|
||||
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"));
|
||||
colCreated.setCellValueFactory(data -> new javafx.beans.property.SimpleObjectProperty<>(data.getValue().getCreatedAt()));
|
||||
|
||||
filtered = new FilteredList<>(staffAccounts, a -> true);
|
||||
TableViewSupport.bindSortedItems(tvStaff, filtered);
|
||||
filteredStaff = new FilteredList<>(staffAccounts, a -> true);
|
||||
TableViewSupport.bindSortedItems(tvStaff, filteredStaff);
|
||||
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) -> {
|
||||
if (btnEditAccount != null) {
|
||||
btnEditAccount.setDisable(newValue == null);
|
||||
}
|
||||
});
|
||||
|
||||
if (btnEditAccount != null) {
|
||||
btnEditAccount.setDisable(true);
|
||||
}
|
||||
txtSearch.textProperty().addListener((obs, o, n) -> applyStaffFilter(n));
|
||||
|
||||
refresh();
|
||||
}
|
||||
@@ -132,8 +125,7 @@ public class StaffAccountsController {
|
||||
@FXML
|
||||
void btnEditAccountClicked(ActionEvent event) {
|
||||
lblError.setText("");
|
||||
UserResponse selected = tvStaff.getSelectionModel().getSelectedItem();
|
||||
openEditDialog(selected);
|
||||
openEditDialog(tvStaff.getSelectionModel().getSelectedItem());
|
||||
}
|
||||
|
||||
private void openEditDialog(UserResponse selected) {
|
||||
@@ -156,7 +148,7 @@ public class StaffAccountsController {
|
||||
Stage dialog = new Stage();
|
||||
dialog.initOwner(tvStaff.getScene().getWindow());
|
||||
dialog.initModality(Modality.APPLICATION_MODAL);
|
||||
dialog.setTitle("Edit User Account");
|
||||
dialog.setTitle("Edit Staff Account");
|
||||
dialog.setScene(new Scene(loader.load()));
|
||||
dialog.setResizable(false);
|
||||
var controller = (org.example.petshopdesktop.controllers.dialogcontrollers.StaffEditDialogController) loader.getController();
|
||||
@@ -164,7 +156,7 @@ public class StaffAccountsController {
|
||||
dialog.showAndWait();
|
||||
refresh();
|
||||
} 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.");
|
||||
}
|
||||
}
|
||||
@@ -175,48 +167,40 @@ public class StaffAccountsController {
|
||||
|
||||
new Thread(() -> {
|
||||
try {
|
||||
UserSession session = UserSession.getInstance();
|
||||
List<UserResponse> users;
|
||||
if (session.isAdmin()) {
|
||||
users = UserApi.getInstance().listUsers(null);
|
||||
} else {
|
||||
users = CustomerApi.getInstance().listCustomers(null);
|
||||
}
|
||||
Comparator<UserResponse> byCreated = Comparator.comparing(
|
||||
UserResponse::getCreatedAt, Comparator.nullsLast(Comparator.reverseOrder()));
|
||||
|
||||
List<UserResponse> sortedUsers = users.stream()
|
||||
.sorted(Comparator.comparing(UserResponse::getCreatedAt, Comparator.nullsLast(Comparator.reverseOrder())))
|
||||
.collect(Collectors.toList());
|
||||
List<UserResponse> staff = UserApi.getInstance().listUsers(null).stream()
|
||||
.filter(u -> !"CUSTOMER".equalsIgnoreCase(u.getRole()))
|
||||
.sorted(byCreated)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
Platform.runLater(() -> {
|
||||
staffAccounts.setAll(sortedUsers);
|
||||
staffAccounts.setAll(staff);
|
||||
tvStaff.setDisable(false);
|
||||
});
|
||||
} catch (Exception e) {
|
||||
ActivityLogger.getInstance().logException("StaffAccountsController.refresh", e, "Loading user accounts");
|
||||
ActivityLogger.getInstance().logException("StaffAccountsController.refresh", e, "Loading staff accounts");
|
||||
Platform.runLater(() -> {
|
||||
String message = e.getMessage();
|
||||
lblError.setText(message == null || message.isBlank()
|
||||
? "Could not load user accounts."
|
||||
: "Could not load user accounts: " + message);
|
||||
lblError.setText("Could not load staff accounts.");
|
||||
tvStaff.setDisable(false);
|
||||
});
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
|
||||
private void applyFilter(String text) {
|
||||
private void applyStaffFilter(String text) {
|
||||
String q = text == null ? "" : text.trim().toLowerCase();
|
||||
if (q.isEmpty()) {
|
||||
filtered.setPredicate(a -> true);
|
||||
filteredStaff.setPredicate(a -> true);
|
||||
return;
|
||||
}
|
||||
|
||||
filtered.setPredicate(a ->
|
||||
safe(a.getUsername()).contains(q)
|
||||
|| safe(a.getFullName()).contains(q)
|
||||
|| safe(a.getEmail()).contains(q)
|
||||
|| safe(a.getPhone()).contains(q)
|
||||
|| safe(a.getRole()).contains(q)
|
||||
filteredStaff.setPredicate(a ->
|
||||
safe(a.getUsername()).contains(q)
|
||||
|| safe(a.getFullName()).contains(q)
|
||||
|| safe(a.getEmail()).contains(q)
|
||||
|| safe(a.getPhone()).contains(q)
|
||||
|| safe(a.getRole()).contains(q)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -62,7 +62,7 @@ public class AdoptionDialogController {
|
||||
private boolean suppressPaymentDialog = false;
|
||||
|
||||
private ObservableList<String> statusList = FXCollections.observableArrayList(
|
||||
"Pending", "Completed", "Cancelled"
|
||||
"Pending", "Completed", "Missed", "Cancelled"
|
||||
);
|
||||
|
||||
@FXML
|
||||
@@ -282,6 +282,7 @@ public class AdoptionDialogController {
|
||||
}
|
||||
|
||||
suppressPaymentDialog = true;
|
||||
cbAdoptionStatus.setItems(statusList);
|
||||
for (String status : cbAdoptionStatus.getItems()) {
|
||||
if (status.equals(adoption.getAdoptionStatus())) {
|
||||
cbAdoptionStatus.getSelectionModel().select(status);
|
||||
@@ -289,13 +290,57 @@ public class AdoptionDialogController {
|
||||
}
|
||||
}
|
||||
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) {
|
||||
this.mode = mode;
|
||||
lblMode.setText(mode + " Adoption");
|
||||
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() {
|
||||
|
||||
@@ -43,24 +43,26 @@ public class AppointmentDialogController {
|
||||
@FXML private Label lblAppointmentId;
|
||||
@FXML private Label lblMode;
|
||||
|
||||
private String mode = null;
|
||||
private String mode = null;
|
||||
private AppointmentDTO selectedAppointment = null;
|
||||
private Long pendingPetSelectionId = null;
|
||||
|
||||
private ObservableList<String> statusList =
|
||||
FXCollections.observableArrayList(
|
||||
"Booked", "Completed", "Missed", "Cancelled"
|
||||
);
|
||||
private boolean isOriginallyCancel = false;
|
||||
private boolean isOriginallyCompletedOrMissed = false;
|
||||
|
||||
public void setMode(String mode) {
|
||||
this.mode = mode;
|
||||
lblMode.setText(mode + " Appointment");
|
||||
lblAppointmentId.setVisible(!mode.equals("Add"));
|
||||
if (mode.equals("Add")) {
|
||||
cbAppointmentStatus.setItems(FXCollections.observableArrayList("Booked"));
|
||||
cbAppointmentStatus.setValue("Booked");
|
||||
cbAppointmentStatus.setDisable(true);
|
||||
}
|
||||
}
|
||||
|
||||
@FXML
|
||||
public void initialize() {
|
||||
cbAppointmentStatus.setItems(statusList);
|
||||
cbAppointmentStatus.setItems(FXCollections.observableArrayList("Booked", "Completed", "Missed", "Cancelled"));
|
||||
cbPet.setDisable(true);
|
||||
cbEmployee.setPromptText("Select an employee");
|
||||
cbPet.setPromptText("Select a customer first");
|
||||
@@ -228,6 +230,46 @@ public class AppointmentDialogController {
|
||||
applySelectedService();
|
||||
applySelectedCustomer();
|
||||
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) {
|
||||
|
||||
@@ -121,7 +121,7 @@ public class InventoryDialogController {
|
||||
//Validate inputs
|
||||
errorMsg += Validator.isPresent(txtQuantity.getText(), "Quantity");
|
||||
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
|
||||
if (errorMsg.isEmpty()) {
|
||||
|
||||
@@ -28,65 +28,34 @@ import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class PetDialogController {
|
||||
|
||||
@FXML
|
||||
private Button btnCancel;
|
||||
private static final String STATUS_AVAILABLE = "Available";
|
||||
private static final String STATUS_ADOPTED = "Adopted";
|
||||
private static final String STATUS_OWNED = "Owned";
|
||||
private static final String STATUS_PENDING = "Pending";
|
||||
|
||||
@FXML
|
||||
private Button btnSave;
|
||||
|
||||
@FXML
|
||||
private Button btnChangeImage;
|
||||
|
||||
@FXML
|
||||
private Button btnRemoveImage;
|
||||
|
||||
@FXML
|
||||
private ComboBox<String> cbPetStatus;
|
||||
|
||||
@FXML
|
||||
private ComboBox<DropdownOption> cbCustomer;
|
||||
|
||||
@FXML
|
||||
private ComboBox<DropdownOption> cbStore;
|
||||
|
||||
@FXML
|
||||
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;
|
||||
@FXML private Button btnCancel;
|
||||
@FXML private Button btnSave;
|
||||
@FXML private Button btnChangeImage;
|
||||
@FXML private Button btnRemoveImage;
|
||||
@FXML private ComboBox<String> cbPetStatus;
|
||||
@FXML private ComboBox<String> cbPetSpecies;
|
||||
@FXML private ComboBox<String> cbPetBreed;
|
||||
@FXML private ComboBox<DropdownOption> cbCustomer;
|
||||
@FXML private ComboBox<DropdownOption> cbStore;
|
||||
@FXML 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 txtPetName;
|
||||
@FXML private TextField txtPetPrice;
|
||||
|
||||
private String mode = null;
|
||||
private File selectedImageFile;
|
||||
@@ -96,15 +65,17 @@ public class PetDialogController {
|
||||
private Long pendingCustomerId = null;
|
||||
private Long pendingStoreId = null;
|
||||
private Long originalCustomerId = null;
|
||||
private boolean isOriginallyOwnedOrAdopted = false;
|
||||
private String pendingBreedValue = null;
|
||||
|
||||
private ObservableList<String> statusList = FXCollections.observableArrayList(
|
||||
"Available", "Adopted", "Owned", "Pending"
|
||||
private final ObservableList<String> statusList = FXCollections.observableArrayList(
|
||||
STATUS_AVAILABLE, STATUS_ADOPTED, STATUS_OWNED, STATUS_PENDING
|
||||
);
|
||||
|
||||
@FXML
|
||||
void initialize() {
|
||||
|
||||
cbPetStatus.setItems(statusList);
|
||||
cbPetBreed.setDisable(true);
|
||||
|
||||
cbCustomer.setCellFactory(param -> new ListCell<>() {
|
||||
@Override protected void updateItem(DropdownOption o, boolean empty) {
|
||||
@@ -132,14 +103,26 @@ public class PetDialogController {
|
||||
}
|
||||
});
|
||||
|
||||
setFieldVisibility(vbCustomerField, false);
|
||||
setFieldVisibility(vbStoreField, false);
|
||||
setFieldVisibility(vbPriceField, true);
|
||||
applyStatusRules(STATUS_AVAILABLE, false);
|
||||
|
||||
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) -> {
|
||||
updateStatusFieldVisibility(newVal);
|
||||
if (newVal != null) applyStatusRules(newVal, true);
|
||||
});
|
||||
|
||||
btnSave.setOnMouseClicked(new EventHandler<MouseEvent>() {
|
||||
@@ -159,52 +142,81 @@ public class PetDialogController {
|
||||
btnChangeImage.setOnMouseClicked(mouseEvent -> handleChangeImage());
|
||||
btnRemoveImage.setOnMouseClicked(mouseEvent -> handleRemoveImage());
|
||||
refreshImagePreview();
|
||||
}
|
||||
|
||||
loadCustomers();
|
||||
loadStores();
|
||||
private void loadBreeds(String species) {
|
||||
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) {
|
||||
String errorMsg = "";
|
||||
|
||||
//Check validation (input required)
|
||||
errorMsg += Validator.isPresent(txtPetName.getText(), "Pet Name");
|
||||
errorMsg += Validator.isPresent(txtPetAge.getText(), "Age");
|
||||
errorMsg += Validator.isPresent(txtPetBreed.getText(), "Breed");
|
||||
String speciesValue = cbPetSpecies.getValue() != null ? cbPetSpecies.getValue().trim() : "";
|
||||
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();
|
||||
boolean needsPrice = !("Owned".equalsIgnoreCase(selectedStatus) || "Adopted".equalsIgnoreCase(selectedStatus));
|
||||
if (needsPrice) {
|
||||
errorMsg += Validator.isPresent(txtPetPrice.getText(), "Price");
|
||||
if (STATUS_AVAILABLE.equalsIgnoreCase(selectedStatus) && cbStore.getValue() == null) {
|
||||
errorMsg += "Store is required for Available status\n";
|
||||
}
|
||||
if (cbPetStatus.getSelectionModel().getSelectedItem() == null){
|
||||
errorMsg += "Status is required";
|
||||
}
|
||||
if ("Owned".equalsIgnoreCase(selectedStatus) && cbCustomer.getValue() == null && UserSession.getInstance().isAdmin()) {
|
||||
if (STATUS_OWNED.equalsIgnoreCase(selectedStatus) && cbCustomer.getValue() == null) {
|
||||
errorMsg += "Customer is required for Owned status\n";
|
||||
}
|
||||
boolean storeRequired = requiresStore(selectedStatus) && !"Adopted".equalsIgnoreCase(selectedStatus);
|
||||
if (storeRequired && cbStore.getValue() == null) {
|
||||
errorMsg += "Store is required for " + selectedStatus + " status\n";
|
||||
if (STATUS_ADOPTED.equalsIgnoreCase(selectedStatus)) {
|
||||
if (cbCustomer.getValue() == null) errorMsg += "Customer is required for Adopted 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(speciesValue, "Species", 50);
|
||||
errorMsg += Validator.isLessThanVarChars(txtPetBreed.getText(), "Breed", 50);
|
||||
if (needsPrice) {
|
||||
errorMsg += Validator.isLessThanVarChars(txtPetPrice.getText(), "Price", 12);
|
||||
}
|
||||
errorMsg += Validator.isLessThanVarChars(breedValue, "Breed", 50);
|
||||
errorMsg += Validator.isLessThanVarChars(txtPetPrice.getText(), "Price", 12);
|
||||
errorMsg += Validator.isLessThanVarChars(txtPetAge.getText(), "Age", 11);
|
||||
|
||||
//Check validation (format)
|
||||
if (needsPrice) {
|
||||
errorMsg += Validator.isNonNegativeDouble(txtPetPrice.getText(), "Price");
|
||||
}
|
||||
errorMsg += Validator.isNonNegativeDouble(txtPetPrice.getText(), "Price");
|
||||
errorMsg += Validator.isPositiveInteger(txtPetAge.getText(), "Age");
|
||||
|
||||
if(errorMsg.isEmpty()){
|
||||
if (errorMsg.isEmpty()) {
|
||||
if ("Edit".equals(mode) && UserSession.getInstance().isAdmin()) {
|
||||
Long newCustomerId = cbCustomer.getValue() != null ? cbCustomer.getValue().getId() : null;
|
||||
if (!Objects.equals(originalCustomerId, newCustomerId)) {
|
||||
@@ -212,44 +224,34 @@ public class PetDialogController {
|
||||
confirm.setHeaderText("Confirm Owner Change");
|
||||
confirm.setContentText("Are you sure you want to reassign this pet to a different owner?");
|
||||
Optional<ButtonType> result = confirm.showAndWait();
|
||||
if (result.isEmpty() || result.get() != ButtonType.OK) {
|
||||
return;
|
||||
}
|
||||
if (result.isEmpty() || result.get() != ButtonType.OK) return;
|
||||
}
|
||||
}
|
||||
PetRequest request = buildPetRequest();
|
||||
try {
|
||||
if(mode.equals("Add")) {
|
||||
if (mode.equals("Add")) {
|
||||
PetResponse response = PetApi.getInstance().createPet(request);
|
||||
applyImageChanges(response.getPetId());
|
||||
} else {
|
||||
String[] parts = lblPetId.getText().split(": ");
|
||||
if (parts.length < 2) {
|
||||
throw new IllegalStateException("Invalid pet ID format");
|
||||
}
|
||||
if (parts.length < 2) throw new IllegalStateException("Invalid pet ID format");
|
||||
Long petId = Long.parseLong(parts[1]);
|
||||
PetApi.getInstance().updatePet(petId, request);
|
||||
applyImageChanges(petId);
|
||||
}
|
||||
|
||||
//tell the user operation was successful
|
||||
Alert alert = new Alert(Alert.AlertType.INFORMATION);
|
||||
alert.setHeaderText("Saved");
|
||||
alert.setContentText(mode + " succeeded");
|
||||
alert.showAndWait();
|
||||
closeStage(mouseEvent);
|
||||
} catch (Exception e) {
|
||||
ActivityLogger.getInstance().logException(
|
||||
"PetDialogController.buttonSaveClicked",
|
||||
e,
|
||||
mode + " pet record");
|
||||
ActivityLogger.getInstance().logException("PetDialogController.buttonSaveClicked", e, mode + " pet record");
|
||||
Alert alert = new Alert(Alert.AlertType.ERROR);
|
||||
alert.setHeaderText("Operation Error");
|
||||
alert.setContentText(mode + " failed: " + e.getMessage());
|
||||
alert.showAndWait();
|
||||
}
|
||||
}
|
||||
else{
|
||||
} else {
|
||||
Alert alert = new Alert(Alert.AlertType.ERROR);
|
||||
alert.setHeaderText("Input Error");
|
||||
alert.setContentText(errorMsg);
|
||||
@@ -261,11 +263,10 @@ public class PetDialogController {
|
||||
PetRequest request = new PetRequest();
|
||||
request.setPetName(txtPetName.getText());
|
||||
request.setPetSpecies(cbPetSpecies.getValue() != null ? cbPetSpecies.getValue().trim() : "");
|
||||
request.setPetBreed(txtPetBreed.getText());
|
||||
request.setPetBreed(cbPetBreed.getValue() != null ? cbPetBreed.getValue().trim() : "");
|
||||
request.setPetStatus(cbPetStatus.getValue());
|
||||
String buildStatus = cbPetStatus.getValue();
|
||||
boolean buildNeedsPrice = !("Owned".equalsIgnoreCase(buildStatus) || "Adopted".equalsIgnoreCase(buildStatus));
|
||||
if (buildNeedsPrice && txtPetPrice.getText() != null && !txtPetPrice.getText().isBlank()) {
|
||||
|
||||
if (txtPetPrice.getText() != null && !txtPetPrice.getText().isBlank()) {
|
||||
try {
|
||||
request.setPetPrice(new BigDecimal(txtPetPrice.getText()));
|
||||
} catch (NumberFormatException e) {
|
||||
@@ -273,21 +274,14 @@ public class PetDialogController {
|
||||
}
|
||||
}
|
||||
|
||||
int age;
|
||||
try {
|
||||
age = Integer.parseInt(txtPetAge.getText());
|
||||
request.setPetAge(Integer.parseInt(txtPetAge.getText()));
|
||||
} catch (NumberFormatException e) {
|
||||
throw new IllegalArgumentException("Invalid age format");
|
||||
}
|
||||
request.setPetAge(age);
|
||||
|
||||
String status = cbPetStatus.getValue();
|
||||
if (("Owned".equalsIgnoreCase(status) || "Adopted".equalsIgnoreCase(status)) && cbCustomer.getValue() != null) {
|
||||
request.setCustomerId(cbCustomer.getValue().getId());
|
||||
}
|
||||
if (requiresStore(status) && cbStore.getValue() != null) {
|
||||
request.setStoreId(cbStore.getValue().getId());
|
||||
}
|
||||
if (cbCustomer.getValue() != null) request.setCustomerId(cbCustomer.getValue().getId());
|
||||
if (cbStore.getValue() != null) request.setStoreId(cbStore.getValue().getId());
|
||||
|
||||
return request;
|
||||
}
|
||||
@@ -296,15 +290,11 @@ public class PetDialogController {
|
||||
new Thread(() -> {
|
||||
try {
|
||||
List<DropdownOption> options = DropdownApi.getInstance().getPetSpecies();
|
||||
List<String> species = options.stream()
|
||||
.map(DropdownOption::getLabel)
|
||||
.collect(java.util.stream.Collectors.toList());
|
||||
List<String> species = options.stream().map(DropdownOption::getLabel).collect(Collectors.toList());
|
||||
Platform.runLater(() -> {
|
||||
String current = cbPetSpecies.getValue();
|
||||
cbPetSpecies.setItems(FXCollections.observableArrayList(species));
|
||||
if (current != null && !current.isBlank()) {
|
||||
cbPetSpecies.setValue(current);
|
||||
}
|
||||
if (current != null && !current.isBlank()) cbPetSpecies.setValue(current);
|
||||
});
|
||||
} catch (Exception e) {
|
||||
Platform.runLater(() -> ActivityLogger.getInstance().logException(
|
||||
@@ -323,8 +313,7 @@ public class PetDialogController {
|
||||
});
|
||||
} catch (Exception e) {
|
||||
Platform.runLater(() -> {
|
||||
ActivityLogger.getInstance().logException(
|
||||
"PetDialogController.loadCustomers", e, "Loading customers");
|
||||
ActivityLogger.getInstance().logException("PetDialogController.loadCustomers", e, "Loading customers");
|
||||
cbCustomer.setDisable(true);
|
||||
cbCustomer.setPromptText("Unable to load customers");
|
||||
});
|
||||
@@ -342,8 +331,7 @@ public class PetDialogController {
|
||||
});
|
||||
} catch (Exception e) {
|
||||
Platform.runLater(() -> {
|
||||
ActivityLogger.getInstance().logException(
|
||||
"PetDialogController.loadStores", e, "Loading stores");
|
||||
ActivityLogger.getInstance().logException("PetDialogController.loadStores", e, "Loading stores");
|
||||
cbStore.setDisable(true);
|
||||
cbStore.setPromptText("Unable to load stores");
|
||||
});
|
||||
@@ -383,55 +371,78 @@ public class PetDialogController {
|
||||
stage.close();
|
||||
}
|
||||
|
||||
public void displayPetDetails(Pet pet){
|
||||
if (pet!=null){
|
||||
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();
|
||||
public void displayPetDetails(Pet pet) {
|
||||
if (pet == null) return;
|
||||
|
||||
pendingCustomerId = pet.getCustomerId() > 0 ? pet.getCustomerId() : null;
|
||||
originalCustomerId = pendingCustomerId;
|
||||
pendingStoreId = pet.getStoreId() > 0 ? pet.getStoreId() : null;
|
||||
lblPetId.setText("ID: " + pet.getPetId());
|
||||
txtPetName.setText(pet.getPetName());
|
||||
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()) {
|
||||
if(status.equals(pet.getPetStatus())){
|
||||
cbPetStatus.getSelectionModel().select(status);
|
||||
break;
|
||||
}
|
||||
pendingCustomerId = pet.getCustomerId() > 0 ? pet.getCustomerId() : null;
|
||||
originalCustomerId = pendingCustomerId;
|
||||
pendingStoreId = pet.getStoreId() > 0 ? pet.getStoreId() : null;
|
||||
|
||||
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) {
|
||||
this.mode = mode;
|
||||
lblMode.setText(mode + " Pet");
|
||||
if(mode.equals("Add")) {
|
||||
|
||||
if (mode.equals("Add")) {
|
||||
lblPetId.setVisible(false);
|
||||
currentImageUrl = null;
|
||||
selectedImageFile = null;
|
||||
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();
|
||||
}
|
||||
else if(mode.equals("Edit")) {
|
||||
} else if (mode.equals("Edit")) {
|
||||
lblPetId.setVisible(true);
|
||||
refreshImagePreview();
|
||||
}
|
||||
updateStatusFieldVisibility(cbPetStatus.getValue());
|
||||
}
|
||||
|
||||
private void handleChangeImage() {
|
||||
File file = FilePickerSupport.pickImageFile(btnSave.getScene().getWindow());
|
||||
if (file == null) {
|
||||
return;
|
||||
}
|
||||
if (file == null) return;
|
||||
selectedImageFile = file;
|
||||
removeImageRequested = false;
|
||||
lblImageStatus.setText("Selected: " + file.getName());
|
||||
@@ -468,9 +479,7 @@ public class PetDialogController {
|
||||
}
|
||||
|
||||
private void refreshImagePreview() {
|
||||
if (imgPetPreview == null || lblImageStatus == null || btnRemoveImage == null) {
|
||||
return;
|
||||
}
|
||||
if (imgPetPreview == null || lblImageStatus == null || btnRemoveImage == null) return;
|
||||
imgPetPreview.setImage(null);
|
||||
if (selectedImageFile != null) {
|
||||
lblImageStatus.setText("Selected: " + selectedImageFile.getName());
|
||||
@@ -487,30 +496,4 @@ public class PetDialogController {
|
||||
lblImageStatus.setText("No image selected");
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import javafx.fxml.FXMLLoader;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.stage.Modality;
|
||||
import org.example.petshopdesktop.util.ActivityLogger;
|
||||
import org.example.petshopdesktop.auth.UserSession;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
|
||||
@@ -46,6 +47,12 @@ public class SaleDetailDialogController {
|
||||
colQuantity.setCellValueFactory(new PropertyValueFactory<>("quantity"));
|
||||
colUnitPrice.setCellValueFactory(new PropertyValueFactory<>("unitPrice"));
|
||||
colLineTotal.setCellValueFactory(new PropertyValueFactory<>("total"));
|
||||
|
||||
if (btnRefund != null) {
|
||||
boolean isAdmin = UserSession.getInstance().isAdmin();
|
||||
btnRefund.setVisible(!isAdmin);
|
||||
btnRefund.setManaged(!isAdmin);
|
||||
}
|
||||
}
|
||||
|
||||
public void displaySaleDetails(SaleDetail sale) {
|
||||
@@ -57,7 +64,10 @@ public class SaleDetailDialogController {
|
||||
lblTotal.setText(currency.format(sale.getTotalAmount()));
|
||||
tvItems.setItems(sale.getItems());
|
||||
if (btnRefund != null) {
|
||||
btnRefund.setDisable(sale.isRefund());
|
||||
boolean isAdmin = UserSession.getInstance().isAdmin();
|
||||
if (!isAdmin) {
|
||||
btnRefund.setDisable(sale.isRefund());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ import org.example.petshopdesktop.api.endpoints.UserApi;
|
||||
import org.example.petshopdesktop.api.endpoints.CustomerApi;
|
||||
import org.example.petshopdesktop.auth.UserSession;
|
||||
import org.example.petshopdesktop.util.ActivityLogger;
|
||||
import org.example.petshopdesktop.util.TextFieldFormatSupport;
|
||||
|
||||
public class StaffEditDialogController {
|
||||
|
||||
@@ -47,6 +48,11 @@ public class StaffEditDialogController {
|
||||
|
||||
private UserResponse user;
|
||||
|
||||
@FXML
|
||||
void initialize() {
|
||||
TextFieldFormatSupport.applyPhoneNumberFormat(txtPhone);
|
||||
}
|
||||
|
||||
public void setUser(UserResponse user) {
|
||||
this.user = user;
|
||||
String fullName = user.getFullName() == null ? "" : user.getFullName();
|
||||
|
||||
@@ -15,6 +15,7 @@ import org.example.petshopdesktop.api.endpoints.CustomerApi;
|
||||
import org.example.petshopdesktop.auth.UserSession;
|
||||
import org.example.petshopdesktop.Validator;
|
||||
import org.example.petshopdesktop.util.ActivityLogger;
|
||||
import org.example.petshopdesktop.util.TextFieldFormatSupport;
|
||||
|
||||
public class StaffRegisterDialogController {
|
||||
|
||||
@@ -45,6 +46,11 @@ public class StaffRegisterDialogController {
|
||||
@FXML
|
||||
private Button btnCreate;
|
||||
|
||||
@FXML
|
||||
void initialize() {
|
||||
TextFieldFormatSupport.applyPhoneNumberFormat(txtPhone);
|
||||
}
|
||||
|
||||
@FXML
|
||||
void btnCreateClicked(ActionEvent event) {
|
||||
lblError.setText("");
|
||||
@@ -82,6 +88,10 @@ public class StaffRegisterDialogController {
|
||||
lblError.setText("Password is required.");
|
||||
return;
|
||||
}
|
||||
if (password.length() < 6) {
|
||||
lblError.setText("Password must be at least 6 characters.");
|
||||
return;
|
||||
}
|
||||
if (!password.equals(confirm)) {
|
||||
lblError.setText("Passwords do not match.");
|
||||
return;
|
||||
|
||||
@@ -15,6 +15,7 @@ import org.example.petshopdesktop.api.dto.supplier.SupplierResponse;
|
||||
import org.example.petshopdesktop.api.endpoints.SupplierApi;
|
||||
import org.example.petshopdesktop.models.Supplier;
|
||||
import org.example.petshopdesktop.util.ActivityLogger;
|
||||
import org.example.petshopdesktop.util.TextFieldFormatSupport;
|
||||
|
||||
public class SupplierDialogController {
|
||||
|
||||
@@ -52,6 +53,8 @@ public class SupplierDialogController {
|
||||
*/
|
||||
@FXML
|
||||
void initialize() {
|
||||
TextFieldFormatSupport.applyPhoneNumberFormat(txtPhone);
|
||||
|
||||
//Set up mouse handlers for buttons
|
||||
btnSave.setOnMouseClicked(new EventHandler<MouseEvent>() {
|
||||
@Override
|
||||
|
||||
@@ -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());
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -91,7 +91,7 @@
|
||||
<Font name="System Bold" size="16.0" />
|
||||
</font>
|
||||
</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>
|
||||
<Insets bottom="3.0" left="10.0" right="10.0" top="3.0" />
|
||||
</padding>
|
||||
@@ -105,11 +105,11 @@
|
||||
<Font name="System Bold" size="16.0" />
|
||||
</font>
|
||||
</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>
|
||||
<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>
|
||||
</TextField>
|
||||
</ComboBox>
|
||||
</children>
|
||||
</VBox>
|
||||
<VBox prefHeight="200.0" prefWidth="100.0" spacing="8.0" GridPane.columnIndex="1" GridPane.rowIndex="1">
|
||||
|
||||
@@ -176,6 +176,14 @@
|
||||
<Insets bottom="8.0" left="10.0" right="10.0" top="8.0" />
|
||||
</padding>
|
||||
</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;" />
|
||||
|
||||
@@ -220,7 +228,7 @@
|
||||
<Insets bottom="8.0" left="10.0" right="10.0" top="8.0" />
|
||||
</padding>
|
||||
</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 name="System" size="12.0" />
|
||||
</font>
|
||||
|
||||
@@ -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>
|
||||
@@ -48,7 +48,7 @@
|
||||
</HBox>
|
||||
</children>
|
||||
</VBox>
|
||||
<FlowPane hgap="8.0" maxWidth="Infinity" prefWrapLength="260.0" vgap="8.0">
|
||||
<HBox alignment="CENTER_RIGHT" spacing="8.0">
|
||||
<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">
|
||||
<font>
|
||||
@@ -67,7 +67,7 @@
|
||||
</padding>
|
||||
</Button>
|
||||
</children>
|
||||
</FlowPane>
|
||||
</HBox>
|
||||
</children>
|
||||
</HBox>
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
<?import javafx.scene.layout.VBox?>
|
||||
<?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>
|
||||
<Insets bottom="20.0" left="20.0" right="20.0" top="20.0" />
|
||||
</padding>
|
||||
@@ -19,28 +19,12 @@
|
||||
<children>
|
||||
<HBox alignment="CENTER_LEFT" spacing="20.0">
|
||||
<children>
|
||||
<Label text="User Accounts" textFill="#2c3e50">
|
||||
<Label text="Staff Accounts" textFill="#2c3e50">
|
||||
<font>
|
||||
<Font name="System Bold" size="30.0" />
|
||||
</font>
|
||||
</Label>
|
||||
<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">
|
||||
<font>
|
||||
<Font name="System Bold" size="14.0" />
|
||||
@@ -52,31 +36,56 @@
|
||||
</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>
|
||||
<VBox fx:id="staffSection" spacing="10.0" VBox.vgrow="ALWAYS">
|
||||
<children>
|
||||
<TextField fx:id="txtSearch" promptText="Search users..." style="-fx-border-width: 0; -fx-background-color: transparent;" HBox.hgrow="ALWAYS">
|
||||
<font>
|
||||
<Font size="15.0" />
|
||||
</font>
|
||||
</TextField>
|
||||
<HBox alignment="CENTER_LEFT" spacing="12.0">
|
||||
<children>
|
||||
<Region HBox.hgrow="ALWAYS" />
|
||||
<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">
|
||||
<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>
|
||||
</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>
|
||||
|
||||
</VBox>
|
||||
|
||||
<Label fx:id="lblStatus" text="" textFill="#64748b" visible="false" managed="true">
|
||||
<font>
|
||||
|
||||
Reference in New Issue
Block a user