Merge branch 'main' into AttachmentsToChat

This commit is contained in:
Alex
2026-04-09 13:44:24 -06:00
13 changed files with 270 additions and 73 deletions

View File

@@ -11,7 +11,9 @@ import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
@RestController
@@ -25,12 +27,14 @@ public class DropdownController {
private final StoreRepository storeRepository;
private final SupplierRepository supplierRepository;
private final UserRepository userRepository;
private final AdoptionRepository adoptionRepository;
public DropdownController(PetRepository petRepository,
ServiceRepository serviceRepository, ProductRepository productRepository,
CategoryRepository categoryRepository, StoreRepository storeRepository,
SupplierRepository supplierRepository,
UserRepository userRepository) {
UserRepository userRepository,
AdoptionRepository adoptionRepository) {
this.petRepository = petRepository;
this.serviceRepository = serviceRepository;
this.productRepository = productRepository;
@@ -38,6 +42,7 @@ public class DropdownController {
this.storeRepository = storeRepository;
this.supplierRepository = supplierRepository;
this.userRepository = userRepository;
this.adoptionRepository = adoptionRepository;
}
@GetMapping("/pets")
@@ -74,8 +79,19 @@ public class DropdownController {
@GetMapping("/appointment-customers")
@PreAuthorize("hasAnyRole('STAFF', 'ADMIN')")
public ResponseEntity<List<DropdownOption>> getAppointmentCustomers() {
Set<Long> ownersWithPets = petRepository.findAll().stream()
.filter(p -> p.getOwner() != null)
.map(p -> p.getOwner().getId())
.collect(Collectors.toSet());
Set<Long> customersWithAdoptions = adoptionRepository.findAll().stream()
.filter(a -> "Completed".equalsIgnoreCase(a.getAdoptionStatus()))
.map(a -> a.getCustomer().getId())
.collect(Collectors.toSet());
Set<Long> customersWithPets = new HashSet<>(ownersWithPets);
customersWithPets.addAll(customersWithAdoptions);
return ResponseEntity.ok(
userRepository.findByRoleAndActiveTrue(User.Role.CUSTOMER).stream()
.filter(u -> customersWithPets.contains(u.getId()))
.map(u -> new DropdownOption(u.getId(), u.getFirstName() + " " + u.getLastName()))
.sorted(Comparator.comparing(DropdownOption::getLabel, String.CASE_INSENSITIVE_ORDER))
.collect(Collectors.toList())
@@ -166,11 +182,16 @@ public class DropdownController {
@GetMapping("/customers/{customerId}/pets")
@PreAuthorize("hasAnyRole('STAFF', 'ADMIN')")
public ResponseEntity<List<DropdownOption>> getCustomerPets(@PathVariable Long customerId) {
return ResponseEntity.ok(
Set<Long> seen = new HashSet<>();
List<DropdownOption> pets = new java.util.ArrayList<>();
petRepository.findAllByOwner_IdOrderByPetNameAsc(customerId).stream()
.map(p -> new DropdownOption(p.getPetId(), p.getPetName()))
.collect(Collectors.toList())
);
.forEach(o -> { if (seen.add(o.getId())) pets.add(o); });
adoptionRepository.findByCustomer_IdAndAdoptionStatusIgnoreCase(customerId, "Completed").stream()
.map(a -> new DropdownOption(a.getPet().getPetId(), a.getPet().getPetName()))
.forEach(o -> { if (seen.add(o.getId())) pets.add(o); });
pets.sort(java.util.Comparator.comparing(DropdownOption::getLabel, String.CASE_INSENSITIVE_ORDER));
return ResponseEntity.ok(pets);
}
@GetMapping("/suppliers")

View File

@@ -22,6 +22,8 @@ public class AdoptionRequest {
private Long sourceStoreId;
private String paymentMethod;
public Long getPetId() {
return petId;
}
@@ -70,6 +72,14 @@ public class AdoptionRequest {
this.sourceStoreId = sourceStoreId;
}
public String getPaymentMethod() {
return paymentMethod;
}
public void setPaymentMethod(String paymentMethod) {
this.paymentMethod = paymentMethod;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;

View File

@@ -9,6 +9,7 @@ import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import java.time.LocalDate;
import java.util.List;
import java.util.Optional;
@Repository
@@ -37,4 +38,6 @@ public interface AdoptionRepository extends JpaRepository<Adoption, Long> {
boolean existsByPet_IdAndAdoptionStatusIgnoreCaseAndAdoptionIdNot(Long petId, String adoptionStatus, Long adoptionId);
boolean existsByPet_IdAndAdoptionStatusIgnoreCase(Long petId, String adoptionStatus);
List<Adoption> findByCustomer_IdAndAdoptionStatusIgnoreCase(Long customerId, String adoptionStatus);
}

View File

@@ -5,11 +5,13 @@ import com.petshop.backend.dto.adoption.AdoptionResponse;
import com.petshop.backend.dto.common.BulkDeleteRequest;
import com.petshop.backend.entity.Adoption;
import com.petshop.backend.entity.Pet;
import com.petshop.backend.entity.Sale;
import com.petshop.backend.entity.StoreLocation;
import com.petshop.backend.entity.User;
import com.petshop.backend.exception.ResourceNotFoundException;
import com.petshop.backend.repository.AdoptionRepository;
import com.petshop.backend.repository.PetRepository;
import com.petshop.backend.repository.SaleRepository;
import com.petshop.backend.repository.StoreRepository;
import com.petshop.backend.repository.UserRepository;
import org.springframework.data.domain.Page;
@@ -17,7 +19,9 @@ import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
@Service
public class AdoptionService {
@@ -32,12 +36,14 @@ public class AdoptionService {
private final PetRepository petRepository;
private final UserRepository userRepository;
private final StoreRepository storeRepository;
private final SaleRepository saleRepository;
public AdoptionService(AdoptionRepository adoptionRepository, PetRepository petRepository, UserRepository userRepository, StoreRepository storeRepository) {
public AdoptionService(AdoptionRepository adoptionRepository, PetRepository petRepository, UserRepository userRepository, StoreRepository storeRepository, SaleRepository saleRepository) {
this.adoptionRepository = adoptionRepository;
this.petRepository = petRepository;
this.userRepository = userRepository;
this.storeRepository = storeRepository;
this.saleRepository = saleRepository;
}
public Page<AdoptionResponse> getAllAdoptions(String query, Long customerId, String status, Long storeId, LocalDate date, Pageable pageable) {
@@ -81,6 +87,9 @@ public class AdoptionService {
: null;
String adoptionStatus = normalizeAdoptionStatus(request.getAdoptionStatus());
validatePetAvailability(pet, null, null);
if (ADOPTION_STATUS_COMPLETED.equalsIgnoreCase(adoptionStatus) && request.getAdoptionDate() != null && request.getAdoptionDate().isAfter(LocalDate.now())) {
throw new IllegalArgumentException("Cannot mark a future-dated adoption as Completed");
}
Adoption adoption = new Adoption();
adoption.setPet(pet);
@@ -91,7 +100,10 @@ public class AdoptionService {
adoption.setAdoptionStatus(adoptionStatus);
adoption = adoptionRepository.save(adoption);
syncPetStatus(pet, adoptionStatus, adoption.getAdoptionId());
syncPetStatus(pet, adoptionStatus, adoption.getAdoptionId(), customer);
if (ADOPTION_STATUS_COMPLETED.equalsIgnoreCase(adoptionStatus)) {
createSaleForAdoption(adoption, request.getPaymentMethod());
}
return mapToResponse(adoption);
}
@@ -110,9 +122,13 @@ public class AdoptionService {
? storeRepository.findById(request.getSourceStoreId())
.orElseThrow(() -> new ResourceNotFoundException("Store not found with id: " + request.getSourceStoreId()))
: null;
boolean wasCompleted = ADOPTION_STATUS_COMPLETED.equalsIgnoreCase(adoption.getAdoptionStatus());
String adoptionStatus = normalizeAdoptionStatus(request.getAdoptionStatus());
Long currentPetId = adoption.getPet() != null ? adoption.getPet().getPetId() : null;
validatePetAvailability(pet, adoption.getAdoptionId(), currentPetId);
if (ADOPTION_STATUS_COMPLETED.equalsIgnoreCase(adoptionStatus) && request.getAdoptionDate() != null && request.getAdoptionDate().isAfter(LocalDate.now())) {
throw new IllegalArgumentException("Cannot mark a future-dated adoption as Completed");
}
adoption.setPet(pet);
adoption.setCustomer(customer);
@@ -122,7 +138,10 @@ public class AdoptionService {
adoption.setAdoptionStatus(adoptionStatus);
adoption = adoptionRepository.save(adoption);
syncPetStatus(pet, adoptionStatus, adoption.getAdoptionId());
syncPetStatus(pet, adoptionStatus, adoption.getAdoptionId(), customer);
if (!wasCompleted && ADOPTION_STATUS_COMPLETED.equalsIgnoreCase(adoptionStatus)) {
createSaleForAdoption(adoption, request.getPaymentMethod());
}
return mapToResponse(adoption);
}
@@ -216,13 +235,37 @@ public class AdoptionService {
}
}
private void syncPetStatus(Pet pet, String adoptionStatus, Long adoptionId) {
private void createSaleForAdoption(Adoption adoption, String paymentMethod) {
if (adoption.getEmployee() == null || adoption.getSourceStore() == null) {
return;
}
BigDecimal amount = adoption.getPet().getPetPrice();
if (amount == null || amount.compareTo(BigDecimal.ZERO) <= 0) {
amount = BigDecimal.ZERO;
}
Sale sale = new Sale();
sale.setSaleDate(LocalDateTime.now());
sale.setEmployee(adoption.getEmployee());
sale.setStore(adoption.getSourceStore());
sale.setCustomer(adoption.getCustomer());
sale.setTotalAmount(amount);
sale.setSubtotalAmount(amount);
sale.setPaymentMethod(paymentMethod != null && !paymentMethod.isBlank() ? paymentMethod : "Cash");
sale.setIsRefund(false);
sale.setChannel("ADOPTION");
saleRepository.save(sale);
}
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 {
pet.setPetStatus(PET_STATUS_AVAILABLE);
pet.setOwner(null);
}
petRepository.save(pet);
}

View File

@@ -9,6 +9,7 @@ public class AdoptionRequest {
private Long sourceStoreId;
private LocalDate adoptionDate;
private String adoptionStatus;
private String paymentMethod;
public AdoptionRequest() {
}
@@ -60,4 +61,12 @@ public class AdoptionRequest {
public void setAdoptionStatus(String adoptionStatus) {
this.adoptionStatus = adoptionStatus;
}
public String getPaymentMethod() {
return paymentMethod;
}
public void setPaymentMethod(String paymentMethod) {
this.paymentMethod = paymentMethod;
}
}

View File

@@ -241,16 +241,16 @@ public class AppointmentController {
return new AppointmentDTO(
response.getAppointmentId().intValue(),
response.getCustomerId() != null ? response.getCustomerId().intValue() : 0,
response.getCustomerName(),
response.getCustomerName() != null ? response.getCustomerName() : "",
response.getPetId() != null ? response.getPetId().intValue() : 0,
response.getPetName(),
response.getPetName() != null ? response.getPetName() : "",
response.getServiceId() != null ? response.getServiceId().intValue() : 0,
response.getServiceName(),
response.getServiceName() != null ? response.getServiceName() : "",
response.getEmployeeId() != null ? response.getEmployeeId().intValue() : 0,
response.getEmployeeName(),
response.getAppointmentDate().toString(),
response.getAppointmentTime().toString(),
response.getAppointmentStatus()
response.getEmployeeName() != null ? response.getEmployeeName() : "",
response.getAppointmentDate() != null ? response.getAppointmentDate().toString() : "",
response.getAppointmentTime() != null ? response.getAppointmentTime().toString() : "",
response.getAppointmentStatus() != null ? response.getAppointmentStatus() : ""
);
}
}

View File

@@ -105,7 +105,12 @@ public class ChatController {
});
realtimeClient.setConversationListener(conversation -> Platform.runLater(() -> upsertConversation(conversation)));
realtimeClient.setMessageListener(message -> Platform.runLater(() -> appendMessageIfSelected(message)));
realtimeClient.setMessageListener(message -> Platform.runLater(() -> {
Long myId = UserSession.getInstance().getUserId();
if (message.getSenderId() == null || !message.getSenderId().equals(myId)) {
appendMessageIfSelected(message);
}
}));
realtimeClient.setStatusListener(status -> Platform.runLater(() -> lblChatStatus.setText(status)));
realtimeClient.subscribeToConversations();
@@ -208,9 +213,7 @@ public class ChatController {
MessageResponse response = ChatApi.getInstance().sendMessage(conversationId, new MessageRequest(content));
Platform.runLater(() -> {
btnSend.setDisable(false);
if (!realtimeClient.isConnected()) {
appendMessageIfSelected(response);
}
if (selectedConversation != null && selectedConversation.getId().equals(conversationId)) {
lblChatStatus.setText("Message sent");
}

View File

@@ -327,12 +327,12 @@ public class PetController {
private Pet mapToPet(PetResponse response) {
Pet pet = new Pet(
response.getPetId().intValue(),
response.getPetName(),
response.getPetSpecies(),
response.getPetBreed(),
response.getPetName() != null ? response.getPetName() : "",
response.getPetSpecies() != null ? response.getPetSpecies() : "",
response.getPetBreed() != null ? response.getPetBreed() : "",
response.getPetAge() != null ? response.getPetAge() : 0,
response.getPetStatus(),
response.getPetPrice().doubleValue(),
response.getPetStatus() != null ? response.getPetStatus() : "",
response.getPetPrice() != null ? response.getPetPrice().doubleValue() : 0.0,
response.getImageUrl()
);
pet.setCustomerName(response.getCustomerName());

View File

@@ -8,6 +8,7 @@ import javafx.fxml.FXML;
import javafx.scene.Node;
import javafx.scene.control.Alert;
import javafx.scene.control.Button;
import javafx.scene.control.ChoiceDialog;
import javafx.scene.control.ComboBox;
import javafx.scene.control.DatePicker;
import javafx.scene.control.Label;
@@ -57,6 +58,8 @@ public class AdoptionDialogController {
private String mode = null;
private Adoption selectedAdoption = null;
private String selectedPaymentMethod = null;
private boolean suppressPaymentDialog = false;
private ObservableList<String> statusList = FXCollections.observableArrayList(
"Pending", "Completed", "Cancelled"
@@ -66,6 +69,23 @@ public class AdoptionDialogController {
void initialize() {
cbAdoptionStatus.setItems(statusList);
cbAdoptionStatus.valueProperty().addListener((obs, oldVal, newVal) -> {
if ("Completed".equals(newVal) && !"Completed".equals(oldVal) && !suppressPaymentDialog) {
ChoiceDialog<String> dialog = new ChoiceDialog<>("Cash", "Cash", "Credit Card", "Debit Card", "E-Transfer");
dialog.setTitle("Payment Method");
dialog.setHeaderText("Confirm payment received");
dialog.setContentText("Select payment method:");
dialog.showAndWait().ifPresentOrElse(
method -> selectedPaymentMethod = method,
() -> {
selectedPaymentMethod = null;
cbAdoptionStatus.setValue(oldVal);
}
);
} else if (!"Completed".equals(newVal)) {
selectedPaymentMethod = null;
}
});
cbEmployee.setPromptText("Select an employee");
new Thread(() -> {
@@ -92,15 +112,11 @@ public class AdoptionDialogController {
new Thread(() -> {
try {
Long storeId = UserSession.getInstance().getStoreId();
List<DropdownOption> employees;
if (storeId != null && storeId > 0) {
employees = DropdownApi.getInstance().getStoreEmployees(storeId);
} else {
employees = DropdownApi.getInstance().getEmployees();
}
List<DropdownOption> employees = DropdownApi.getInstance().getEmployees();
Platform.runLater(() -> {
cbEmployee.setItems(FXCollections.observableArrayList(employees));
ObservableList<DropdownOption> employeesObs = FXCollections.observableArrayList(employees);
ensureSelectedEmployeeOption(employeesObs);
cbEmployee.setItems(employeesObs);
applySelectedEmployee();
});
} catch (Exception e) {
@@ -203,6 +219,7 @@ public class AdoptionDialogController {
request.setSourceStoreId(storeId);
request.setAdoptionDate(dpAdoptionDate.getValue());
request.setAdoptionStatus(cbAdoptionStatus.getValue());
request.setPaymentMethod(selectedPaymentMethod);
if (mode.equals("Add")) {
AdoptionApi.getInstance().createAdoption(request);
@@ -263,12 +280,14 @@ public class AdoptionDialogController {
}
}
suppressPaymentDialog = true;
for (String status : cbAdoptionStatus.getItems()) {
if (status.equals(adoption.getAdoptionStatus())) {
cbAdoptionStatus.getSelectionModel().select(status);
break;
}
}
suppressPaymentDialog = false;
}
}
@@ -320,6 +339,19 @@ public class AdoptionDialogController {
return null;
}
private void ensureSelectedEmployeeOption(ObservableList<DropdownOption> options) {
if (selectedAdoption == null || selectedAdoption.getEmployeeId() <= 0 || options == null) {
return;
}
DropdownOption existing = findOptionById(options, (long) selectedAdoption.getEmployeeId());
if (existing == null) {
DropdownOption option = new DropdownOption();
option.setId((long) selectedAdoption.getEmployeeId());
option.setLabel(selectedAdoption.getEmployeeName() != null ? selectedAdoption.getEmployeeName() : "Employee #" + selectedAdoption.getEmployeeId());
options.add(0, option);
}
}
private void ensureSelectedPetOption(ObservableList<DropdownOption> options) {
if (selectedAdoption == null || selectedAdoption.getPetId() <= 0 || options == null) {
return;

View File

@@ -18,6 +18,7 @@ import org.example.petshopdesktop.api.endpoints.DropdownApi;
import org.example.petshopdesktop.auth.UserSession;
import org.example.petshopdesktop.util.ActivityLogger;
import java.time.DayOfWeek;
import java.time.LocalTime;
import java.time.LocalDate;
import java.util.List;
@@ -65,7 +66,15 @@ public class AppointmentDialogController {
cbPet.setPromptText("Select a customer first");
cbCustomer.setPromptText("Select a customer");
cbService.setPromptText("Select a service");
dpAppointmentDate.setValue(LocalDate.now().plusDays(1));
LocalDate minDate = minAppointmentDate();
dpAppointmentDate.setValue(minDate);
dpAppointmentDate.setDayCellFactory(picker -> new javafx.scene.control.DateCell() {
@Override
public void updateItem(LocalDate item, boolean empty) {
super.updateItem(item, empty);
setDisable(empty || item.isBefore(minDate));
}
});
cbAppointmentStatus.setValue("Booked");
for (int i = 9; i <= 17; i++) {
@@ -315,9 +324,20 @@ public class AppointmentDialogController {
try {
List<DropdownOption> pets = DropdownApi.getInstance().getCustomerPets(customerId);
Platform.runLater(() -> {
cbPet.setItems(FXCollections.observableArrayList(pets));
cbPet.setDisable(pets == null || pets.isEmpty());
cbPet.setPromptText(pets == null || pets.isEmpty() ? "No pets for selected customer" : "Select a pet");
ObservableList<DropdownOption> petOptions = FXCollections.observableArrayList(pets != null ? pets : List.of());
if (pendingPetSelectionId != null) {
boolean found = petOptions.stream().anyMatch(p -> pendingPetSelectionId.equals(p.getId()));
if (!found && selectedAppointment != null && selectedAppointment.getPetId() > 0) {
DropdownOption placeholder = new DropdownOption();
placeholder.setId((long) selectedAppointment.getPetId());
placeholder.setLabel(selectedAppointment.getPetName() != null && !selectedAppointment.getPetName().isBlank()
? selectedAppointment.getPetName() : "Pet #" + selectedAppointment.getPetId());
petOptions.add(0, placeholder);
}
}
cbPet.setItems(petOptions);
cbPet.setDisable(petOptions.isEmpty());
cbPet.setPromptText(petOptions.isEmpty() ? "No pets for selected customer" : "Select a pet");
if (pendingPetSelectionId != null) {
for (DropdownOption pet : cbPet.getItems()) {
if (pet.getId() != null && pet.getId().equals(pendingPetSelectionId)) {
@@ -392,6 +412,19 @@ public class AppointmentDialogController {
}).start();
}
private LocalDate minAppointmentDate() {
LocalDate date = LocalDate.now();
int businessDaysAdded = 0;
while (businessDaysAdded < 2) {
date = date.plusDays(1);
DayOfWeek dow = date.getDayOfWeek();
if (dow != DayOfWeek.SATURDAY && dow != DayOfWeek.SUNDAY) {
businessDaysAdded++;
}
}
return date;
}
private void loadEmployees() {
new Thread(() -> {
try {

View File

@@ -55,6 +55,9 @@ public class PetDialogController {
@FXML
private VBox vbStoreField;
@FXML
private VBox vbPriceField;
@FXML
private Label lblMode;
@@ -80,7 +83,7 @@ public class PetDialogController {
private TextField txtPetPrice;
@FXML
private TextField txtPetSpecies;
private ComboBox<String> cbPetSpecies;
private String mode = null;
private File selectedImageFile;
@@ -127,6 +130,9 @@ public class PetDialogController {
setFieldVisibility(vbCustomerField, false);
setFieldVisibility(vbStoreField, false);
setFieldVisibility(vbPriceField, true);
loadSpecies();
cbPetStatus.valueProperty().addListener((obs, oldVal, newVal) -> {
updateStatusFieldVisibility(newVal);
@@ -161,28 +167,37 @@ public class PetDialogController {
errorMsg += Validator.isPresent(txtPetName.getText(), "Pet Name");
errorMsg += Validator.isPresent(txtPetAge.getText(), "Age");
errorMsg += Validator.isPresent(txtPetBreed.getText(), "Breed");
errorMsg += Validator.isPresent(txtPetSpecies.getText(), "Species");
String speciesValue = cbPetSpecies.getValue() != null ? cbPetSpecies.getValue().trim() : "";
if (speciesValue.isEmpty()) errorMsg += "Species is required\n";
String selectedStatus = cbPetStatus.getValue();
boolean needsPrice = !("Owned".equalsIgnoreCase(selectedStatus) || "Adopted".equalsIgnoreCase(selectedStatus));
if (needsPrice) {
errorMsg += Validator.isPresent(txtPetPrice.getText(), "Price");
}
if (cbPetStatus.getSelectionModel().getSelectedItem() == null){
errorMsg += "Status is required";
}
String selectedStatus = cbPetStatus.getValue();
if ("Owned".equalsIgnoreCase(selectedStatus) && cbCustomer.getValue() == null) {
errorMsg += "Customer is required for Owned status\n";
}
if (requiresStore(selectedStatus) && cbStore.getValue() == null) {
boolean storeRequired = requiresStore(selectedStatus) && !"Adopted".equalsIgnoreCase(selectedStatus);
if (storeRequired && cbStore.getValue() == null) {
errorMsg += "Store is required for " + selectedStatus + " status\n";
}
//Check validation (length size)
errorMsg += Validator.isLessThanVarChars(txtPetName.getText(), "Pet Name", 50);
errorMsg += Validator.isLessThanVarChars(txtPetSpecies.getText(), "Species", 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(txtPetAge.getText(), "Age", 11);
//Check validation (format)
if (needsPrice) {
errorMsg += Validator.isNonNegativeDouble(txtPetPrice.getText(), "Price");
}
errorMsg += Validator.isPositiveInteger(txtPetAge.getText(), "Age");
if(errorMsg.isEmpty()){
@@ -229,14 +244,18 @@ public class PetDialogController {
private PetRequest buildPetRequest() {
PetRequest request = new PetRequest();
request.setPetName(txtPetName.getText());
request.setPetSpecies(txtPetSpecies.getText());
request.setPetSpecies(cbPetSpecies.getValue() != null ? cbPetSpecies.getValue().trim() : "");
request.setPetBreed(txtPetBreed.getText());
request.setPetStatus(cbPetStatus.getValue());
String buildStatus = cbPetStatus.getValue();
boolean buildNeedsPrice = !("Owned".equalsIgnoreCase(buildStatus) || "Adopted".equalsIgnoreCase(buildStatus));
if (buildNeedsPrice && txtPetPrice.getText() != null && !txtPetPrice.getText().isBlank()) {
try {
request.setPetPrice(new BigDecimal(txtPetPrice.getText()));
} catch (NumberFormatException e) {
throw new IllegalArgumentException("Invalid price format");
}
}
int age;
try {
@@ -247,7 +266,7 @@ public class PetDialogController {
request.setPetAge(age);
String status = cbPetStatus.getValue();
if ("Owned".equalsIgnoreCase(status) && cbCustomer.getValue() != null) {
if (("Owned".equalsIgnoreCase(status) || "Adopted".equalsIgnoreCase(status)) && cbCustomer.getValue() != null) {
request.setCustomerId(cbCustomer.getValue().getId());
}
if (requiresStore(status) && cbStore.getValue() != null) {
@@ -257,6 +276,27 @@ public class PetDialogController {
return request;
}
private void loadSpecies() {
new Thread(() -> {
try {
List<DropdownOption> options = DropdownApi.getInstance().getPetSpecies();
List<String> species = options.stream()
.map(DropdownOption::getLabel)
.collect(java.util.stream.Collectors.toList());
Platform.runLater(() -> {
String current = cbPetSpecies.getValue();
cbPetSpecies.setItems(FXCollections.observableArrayList(species));
if (current != null && !current.isBlank()) {
cbPetSpecies.setValue(current);
}
});
} catch (Exception e) {
Platform.runLater(() -> ActivityLogger.getInstance().logException(
"PetDialogController.loadSpecies", e, "Loading species dropdown"));
}
}).start();
}
private void loadCustomers() {
new Thread(() -> {
try {
@@ -331,7 +371,7 @@ public class PetDialogController {
if (pet!=null){
lblPetId.setText("ID: " + pet.getPetId());
txtPetName.setText(pet.getPetName());
txtPetSpecies.setText(pet.getPetSpecies());
cbPetSpecies.setValue(pet.getPetSpecies());
txtPetBreed.setText(pet.getPetBreed());
txtPetAge.setText(pet.getPetAge() + "");
txtPetPrice.setText(pet.getPetPrice() + "");
@@ -432,16 +472,19 @@ public class PetDialogController {
}
private void updateStatusFieldVisibility(String status) {
boolean owned = "Owned".equalsIgnoreCase(status);
boolean needsCustomer = "Owned".equalsIgnoreCase(status) || "Adopted".equalsIgnoreCase(status);
boolean storeBased = requiresStore(status);
setFieldVisibility(vbCustomerField, owned);
boolean needsPrice = !needsCustomer;
setFieldVisibility(vbCustomerField, needsCustomer);
setFieldVisibility(vbStoreField, storeBased);
setFieldVisibility(vbPriceField, needsPrice);
}
private boolean requiresStore(String status) {
return "Available".equalsIgnoreCase(status)
|| "Pending".equalsIgnoreCase(status)
|| "Unadopted".equalsIgnoreCase(status);
|| "Unadopted".equalsIgnoreCase(status)
|| "Adopted".equalsIgnoreCase(status);
}
private void setFieldVisibility(VBox field, boolean visible) {

View File

@@ -91,11 +91,11 @@
<Font name="System Bold" size="16.0" />
</font>
</Label>
<TextField fx:id="txtPetSpecies" style="-fx-border-color: #E8EBED; -fx-border-width: 2; -fx-border-radius: 10; -fx-background-radius: 10;">
<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;">
<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.rowIndex="1">
@@ -139,7 +139,7 @@
</padding>
</ComboBox>
</children></VBox>
<VBox prefHeight="200.0" prefWidth="100.0" spacing="8.0" GridPane.columnIndex="1" GridPane.rowIndex="2">
<VBox fx:id="vbPriceField" prefHeight="200.0" prefWidth="100.0" spacing="8.0" GridPane.columnIndex="1" GridPane.rowIndex="2">
<children>
<Label text="Price:" textFill="#2c3e50">
<font>

View File

@@ -91,7 +91,7 @@
</padding>
</Label>
<Button fx:id="btnAnalytics" alignment="CENTER_LEFT" maxWidth="Infinity" mnemonicParsing="false" onAction="#btnAnalyticsClicked" style="-fx-background-color: transparent; -fx-background-radius: 8; -fx-cursor: hand; -fx-focus-color: transparent; -fx-faint-focus-color: transparent;" text="📊 Analytics" textFill="#cbd5e1">
<Button fx:id="btnAnalytics" alignment="CENTER_LEFT" maxWidth="Infinity" mnemonicParsing="false" onAction="#btnAnalyticsClicked" style="-fx-background-color: transparent; -fx-background-radius: 8; -fx-cursor: hand; -fx-focus-color: transparent; -fx-faint-focus-color: transparent;" text="Analytics" textFill="#cbd5e1">
<font>
<Font name="System" size="12.0" />
</font>
@@ -99,7 +99,7 @@
<Insets bottom="8.0" left="10.0" right="10.0" top="8.0" />
</padding>
</Button>
<Button fx:id="btnSalesHistory" alignment="CENTER_LEFT" maxWidth="Infinity" mnemonicParsing="false" onAction="#btnSalesHistoryClicked" style="-fx-background-color: transparent; -fx-background-radius: 8; -fx-cursor: hand; -fx-focus-color: transparent; -fx-faint-focus-color: transparent;" text="💰 Sales History" textFill="#cbd5e1">
<Button fx:id="btnSalesHistory" alignment="CENTER_LEFT" maxWidth="Infinity" mnemonicParsing="false" onAction="#btnSalesHistoryClicked" style="-fx-background-color: transparent; -fx-background-radius: 8; -fx-cursor: hand; -fx-focus-color: transparent; -fx-faint-focus-color: transparent;" text="Sales History" textFill="#cbd5e1">
<font>
<Font name="System" size="12.0" />
</font>
@@ -107,7 +107,7 @@
<Insets bottom="8.0" left="10.0" right="10.0" top="8.0" />
</padding>
</Button>
<Button fx:id="btnAppointments" alignment="CENTER_LEFT" maxWidth="Infinity" mnemonicParsing="false" onAction="#btnAppointmentsClicked" style="-fx-background-color: transparent; -fx-background-radius: 8; -fx-cursor: hand; -fx-focus-color: transparent; -fx-faint-focus-color: transparent;" text="📅 Appointments" textFill="#cbd5e1">
<Button fx:id="btnAppointments" alignment="CENTER_LEFT" maxWidth="Infinity" mnemonicParsing="false" onAction="#btnAppointmentsClicked" style="-fx-background-color: transparent; -fx-background-radius: 8; -fx-cursor: hand; -fx-focus-color: transparent; -fx-faint-focus-color: transparent;" text="Appointments" textFill="#cbd5e1">
<font>
<Font name="System" size="12.0" />
</font>
@@ -115,7 +115,7 @@
<Insets bottom="8.0" left="10.0" right="10.0" top="8.0" />
</padding>
</Button>
<Button fx:id="btnServices" alignment="CENTER_LEFT" maxWidth="Infinity" mnemonicParsing="false" onAction="#btnServicesClicked" style="-fx-background-color: transparent; -fx-background-radius: 8; -fx-cursor: hand; -fx-focus-color: transparent; -fx-faint-focus-color: transparent;" text="✂️ Services" textFill="#cbd5e1">
<Button fx:id="btnServices" alignment="CENTER_LEFT" maxWidth="Infinity" mnemonicParsing="false" onAction="#btnServicesClicked" style="-fx-background-color: transparent; -fx-background-radius: 8; -fx-cursor: hand; -fx-focus-color: transparent; -fx-faint-focus-color: transparent;" text="Services" textFill="#cbd5e1">
<font>
<Font name="System" size="12.0" />
</font>
@@ -123,7 +123,7 @@
<Insets bottom="8.0" left="10.0" right="10.0" top="8.0" />
</padding>
</Button>
<Button fx:id="btnChat" alignment="CENTER_LEFT" maxWidth="Infinity" mnemonicParsing="false" onAction="#btnChatClicked" style="-fx-background-color: transparent; -fx-background-radius: 8; -fx-cursor: hand; -fx-focus-color: transparent; -fx-faint-focus-color: transparent;" text="💬 Chat" textFill="#cbd5e1">
<Button fx:id="btnChat" alignment="CENTER_LEFT" maxWidth="Infinity" mnemonicParsing="false" onAction="#btnChatClicked" style="-fx-background-color: transparent; -fx-background-radius: 8; -fx-cursor: hand; -fx-focus-color: transparent; -fx-faint-focus-color: transparent;" text="Chat" textFill="#cbd5e1">
<font>
<Font name="System" size="12.0" />
</font>
@@ -143,7 +143,7 @@
</padding>
</Label>
<Button fx:id="btnPets" alignment="CENTER_LEFT" maxWidth="Infinity" mnemonicParsing="false" onAction="#btnPetsClicked" style="-fx-background-color: transparent; -fx-background-radius: 8; -fx-cursor: hand; -fx-focus-color: transparent; -fx-faint-focus-color: transparent;" text="🐾 Pets" textFill="#cbd5e1">
<Button fx:id="btnPets" alignment="CENTER_LEFT" maxWidth="Infinity" mnemonicParsing="false" onAction="#btnPetsClicked" style="-fx-background-color: transparent; -fx-background-radius: 8; -fx-cursor: hand; -fx-focus-color: transparent; -fx-faint-focus-color: transparent;" text="Pets" textFill="#cbd5e1">
<font>
<Font name="System" size="12.0" />
</font>
@@ -151,7 +151,7 @@
<Insets bottom="8.0" left="10.0" right="10.0" top="8.0" />
</padding>
</Button>
<Button fx:id="btnAdoptions" alignment="CENTER_LEFT" maxWidth="Infinity" mnemonicParsing="false" onAction="#btnAdoptionsClicked" style="-fx-background-color: transparent; -fx-background-radius: 8; -fx-cursor: hand; -fx-focus-color: transparent; -fx-faint-focus-color: transparent;" text="🏠 Adoptions" textFill="#cbd5e1">
<Button fx:id="btnAdoptions" alignment="CENTER_LEFT" maxWidth="Infinity" mnemonicParsing="false" onAction="#btnAdoptionsClicked" style="-fx-background-color: transparent; -fx-background-radius: 8; -fx-cursor: hand; -fx-focus-color: transparent; -fx-faint-focus-color: transparent;" text="Adoptions" textFill="#cbd5e1">
<font>
<Font name="System" size="12.0" />
</font>
@@ -159,7 +159,7 @@
<Insets bottom="8.0" left="10.0" right="10.0" top="8.0" />
</padding>
</Button>
<Button fx:id="btnProducts" alignment="CENTER_LEFT" maxWidth="Infinity" mnemonicParsing="false" onAction="#btnProductsClicked" style="-fx-background-color: transparent; -fx-background-radius: 8; -fx-cursor: hand; -fx-focus-color: transparent; -fx-faint-focus-color: transparent;" text="📦 Products" textFill="#cbd5e1">
<Button fx:id="btnProducts" alignment="CENTER_LEFT" maxWidth="Infinity" mnemonicParsing="false" onAction="#btnProductsClicked" style="-fx-background-color: transparent; -fx-background-radius: 8; -fx-cursor: hand; -fx-focus-color: transparent; -fx-faint-focus-color: transparent;" text="Products" textFill="#cbd5e1">
<font>
<Font name="System" size="12.0" />
</font>
@@ -179,7 +179,7 @@
</padding>
</Label>
<Button fx:id="btnInventory" alignment="CENTER_LEFT" maxWidth="Infinity" mnemonicParsing="false" onAction="#btnInventoryClicked" style="-fx-background-color: transparent; -fx-background-radius: 8; -fx-cursor: hand; -fx-focus-color: transparent; -fx-faint-focus-color: transparent;" text="📋 Inventory" textFill="#cbd5e1">
<Button fx:id="btnInventory" alignment="CENTER_LEFT" maxWidth="Infinity" mnemonicParsing="false" onAction="#btnInventoryClicked" style="-fx-background-color: transparent; -fx-background-radius: 8; -fx-cursor: hand; -fx-focus-color: transparent; -fx-faint-focus-color: transparent;" text="Inventory" textFill="#cbd5e1">
<font>
<Font name="System" size="12.0" />
</font>
@@ -187,7 +187,7 @@
<Insets bottom="8.0" left="10.0" right="10.0" top="8.0" />
</padding>
</Button>
<Button fx:id="btnSuppliers" alignment="CENTER_LEFT" maxWidth="Infinity" mnemonicParsing="false" onAction="#btnSuppliersClicked" style="-fx-background-color: transparent; -fx-background-radius: 8; -fx-cursor: hand; -fx-focus-color: transparent; -fx-faint-focus-color: transparent;" text="🚛 Suppliers" textFill="#cbd5e1">
<Button fx:id="btnSuppliers" alignment="CENTER_LEFT" maxWidth="Infinity" mnemonicParsing="false" onAction="#btnSuppliersClicked" style="-fx-background-color: transparent; -fx-background-radius: 8; -fx-cursor: hand; -fx-focus-color: transparent; -fx-faint-focus-color: transparent;" text="Suppliers" textFill="#cbd5e1">
<font>
<Font name="System" size="12.0" />
</font>
@@ -195,7 +195,7 @@
<Insets bottom="8.0" left="10.0" right="10.0" top="8.0" />
</padding>
</Button>
<Button fx:id="btnProductSuppliers" alignment="CENTER_LEFT" maxWidth="Infinity" mnemonicParsing="false" onAction="#btnProductSuppliersClicked" style="-fx-background-color: transparent; -fx-background-radius: 8; -fx-cursor: hand; -fx-focus-color: transparent; -fx-faint-focus-color: transparent;" text="🔗 Product-Suppliers" textFill="#cbd5e1">
<Button fx:id="btnProductSuppliers" alignment="CENTER_LEFT" maxWidth="Infinity" mnemonicParsing="false" onAction="#btnProductSuppliersClicked" style="-fx-background-color: transparent; -fx-background-radius: 8; -fx-cursor: hand; -fx-focus-color: transparent; -fx-faint-focus-color: transparent;" text="Product-Suppliers" textFill="#cbd5e1">
<font>
<Font name="System" size="12.0" />
</font>
@@ -203,7 +203,7 @@
<Insets bottom="8.0" left="10.0" right="10.0" top="8.0" />
</padding>
</Button>
<Button fx:id="btnPurchaseOrders" alignment="CENTER_LEFT" maxWidth="Infinity" mnemonicParsing="false" onAction="#btnPurchaseOrdersClicked" style="-fx-background-color: transparent; -fx-background-radius: 8; -fx-cursor: hand; -fx-focus-color: transparent; -fx-faint-focus-color: transparent;" text="📝 Purchase Orders" textFill="#cbd5e1">
<Button fx:id="btnPurchaseOrders" alignment="CENTER_LEFT" maxWidth="Infinity" mnemonicParsing="false" onAction="#btnPurchaseOrdersClicked" style="-fx-background-color: transparent; -fx-background-radius: 8; -fx-cursor: hand; -fx-focus-color: transparent; -fx-faint-focus-color: transparent;" text="Purchase Orders" textFill="#cbd5e1">
<font>
<Font name="System" size="12.0" />
</font>
@@ -211,7 +211,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="👥 Staff 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>
@@ -220,7 +220,7 @@
</padding>
</Button>
<Button fx:id="btnLogout" alignment="CENTER_LEFT" maxWidth="Infinity" mnemonicParsing="false" onAction="#btnLogoutClicked" style="-fx-background-color: rgba(255,255,255,0.08); -fx-background-radius: 8; -fx-cursor: hand; -fx-focus-color: transparent; -fx-faint-focus-color: transparent;" text="🚪 Logout" textFill="#e2e8f0">
<Button fx:id="btnLogout" alignment="CENTER_LEFT" maxWidth="Infinity" mnemonicParsing="false" onAction="#btnLogoutClicked" style="-fx-background-color: rgba(255,255,255,0.08); -fx-background-radius: 8; -fx-cursor: hand; -fx-focus-color: transparent; -fx-faint-focus-color: transparent;" text="Logout" textFill="#e2e8f0">
<font>
<Font name="System" size="12.0" />
</font>