Merge Attachments Branch #155
@@ -50,7 +50,7 @@ public class AppointmentController {
|
||||
.orElse(null);
|
||||
|
||||
Long effectiveCustomerId = customerId;
|
||||
if (role != null && role.equals("CUSTOMER")) {
|
||||
if ("CUSTOMER".equals(role)) {
|
||||
User user = AuthenticationHelper.getAuthenticatedUser(userRepository);
|
||||
effectiveCustomerId = user.getId();
|
||||
}
|
||||
@@ -88,7 +88,7 @@ public class AppointmentController {
|
||||
.map(authority -> authority.getAuthority().replace("ROLE_", ""))
|
||||
.orElse(null);
|
||||
|
||||
if (role != null && role.equals("CUSTOMER")) {
|
||||
if ("CUSTOMER".equals(role)) {
|
||||
User user = AuthenticationHelper.getAuthenticatedUser(userRepository);
|
||||
if (!request.getCustomerId().equals(user.getId())) {
|
||||
throw new org.springframework.security.access.AccessDeniedException("You can only create appointments for yourself");
|
||||
|
||||
@@ -52,7 +52,7 @@ public class DropdownController {
|
||||
@PreAuthorize("hasAnyRole('STAFF', 'ADMIN')")
|
||||
public ResponseEntity<List<DropdownOption>> getAdoptionPets() {
|
||||
return ResponseEntity.ok(
|
||||
petRepository.findAllByPetStatusIgnoreCaseOrderByPetNameAsc("Available").stream()
|
||||
petRepository.findAdoptablePetsOrderByPetNameAsc().stream()
|
||||
.map(p -> new DropdownOption(p.getPetId(), p.getPetName()))
|
||||
.collect(Collectors.toList())
|
||||
);
|
||||
|
||||
@@ -5,6 +5,7 @@ import jakarta.servlet.http.HttpServletRequest;
|
||||
import org.springframework.dao.DataIntegrityViolationException;
|
||||
import org.springframework.data.core.PropertyReferenceException;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.validation.FieldError;
|
||||
import org.springframework.web.HttpRequestMethodNotSupportedException;
|
||||
@@ -118,7 +119,9 @@ public class GlobalExceptionHandler {
|
||||
request.getRequestURI(),
|
||||
LocalDateTime.now()
|
||||
);
|
||||
return ResponseEntity.status(status).body(error);
|
||||
return ResponseEntity.status(status)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.body(error);
|
||||
}
|
||||
|
||||
private String buildDetails(Exception ex) {
|
||||
|
||||
@@ -15,6 +15,14 @@ import java.util.Optional;
|
||||
public interface PetRepository extends JpaRepository<Pet, Long> {
|
||||
|
||||
List<Pet> findAllByPetStatusIgnoreCaseOrderByPetNameAsc(String petStatus);
|
||||
@Query("SELECT p FROM Pet p " +
|
||||
"WHERE LOWER(p.petStatus) = 'available' " +
|
||||
"AND NOT EXISTS (" +
|
||||
" SELECT 1 FROM Adoption a " +
|
||||
" WHERE a.pet = p AND LOWER(a.adoptionStatus) = 'completed'" +
|
||||
") " +
|
||||
"ORDER BY p.petName ASC")
|
||||
List<Pet> findAdoptablePetsOrderByPetNameAsc();
|
||||
List<Pet> findAllByOwner_IdOrderByPetNameAsc(Long ownerId);
|
||||
Optional<Pet> findByIdAndOwner_Id(Long id, Long ownerId);
|
||||
|
||||
|
||||
@@ -80,7 +80,7 @@ public class AdoptionService {
|
||||
.orElseThrow(() -> new ResourceNotFoundException("Store not found with id: " + request.getSourceStoreId()))
|
||||
: null;
|
||||
String adoptionStatus = normalizeAdoptionStatus(request.getAdoptionStatus());
|
||||
validatePetAvailability(pet, null);
|
||||
validatePetAvailability(pet, null, null);
|
||||
|
||||
Adoption adoption = new Adoption();
|
||||
adoption.setPet(pet);
|
||||
@@ -111,7 +111,8 @@ public class AdoptionService {
|
||||
.orElseThrow(() -> new ResourceNotFoundException("Store not found with id: " + request.getSourceStoreId()))
|
||||
: null;
|
||||
String adoptionStatus = normalizeAdoptionStatus(request.getAdoptionStatus());
|
||||
validatePetAvailability(pet, adoption.getAdoptionId());
|
||||
Long currentPetId = adoption.getPet() != null ? adoption.getPet().getPetId() : null;
|
||||
validatePetAvailability(pet, adoption.getAdoptionId(), currentPetId);
|
||||
|
||||
adoption.setPet(pet);
|
||||
adoption.setCustomer(customer);
|
||||
@@ -201,7 +202,8 @@ public class AdoptionService {
|
||||
throw new IllegalArgumentException("Adoption status must be Pending, Completed, or Cancelled");
|
||||
}
|
||||
|
||||
private void validatePetAvailability(Pet pet, Long adoptionId) {
|
||||
private void validatePetAvailability(Pet pet, Long adoptionId, Long currentPetId) {
|
||||
boolean samePetAsCurrentAdoption = currentPetId != null && currentPetId.equals(pet.getPetId());
|
||||
boolean adoptedElsewhere = adoptionId == null
|
||||
? adoptionRepository.existsByPet_IdAndAdoptionStatusIgnoreCase(pet.getPetId(), ADOPTION_STATUS_COMPLETED)
|
||||
: adoptionRepository.existsByPet_IdAndAdoptionStatusIgnoreCaseAndAdoptionIdNot(pet.getPetId(), ADOPTION_STATUS_COMPLETED, adoptionId);
|
||||
@@ -209,7 +211,7 @@ public class AdoptionService {
|
||||
throw new IllegalArgumentException("Selected pet has already been adopted");
|
||||
}
|
||||
|
||||
if (!PET_STATUS_AVAILABLE.equalsIgnoreCase(pet.getPetStatus()) && adoptionId == null) {
|
||||
if (!samePetAsCurrentAdoption && !PET_STATUS_AVAILABLE.equalsIgnoreCase(pet.getPetStatus())) {
|
||||
throw new IllegalArgumentException("Selected pet is not available for adoption");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -247,7 +247,13 @@ public class PetService {
|
||||
if (principal instanceof AppPrincipal appPrincipal) {
|
||||
return new CurrentViewer(appPrincipal.getUserId(), appPrincipal.getRole());
|
||||
}
|
||||
return null;
|
||||
String username = authentication.getName();
|
||||
if (username == null || username.isBlank() || "anonymousUser".equalsIgnoreCase(username)) {
|
||||
return null;
|
||||
}
|
||||
return userRepository.findByUsername(username)
|
||||
.map(user -> new CurrentViewer(user.getId(), user.getRole()))
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
private Pet findPet(Long id) {
|
||||
|
||||
@@ -245,9 +245,6 @@ public class SaleService {
|
||||
}
|
||||
|
||||
String normalized = paymentMethod.trim();
|
||||
if (normalized.equalsIgnoreCase("Debit")) {
|
||||
return "Card";
|
||||
}
|
||||
if (normalized.equalsIgnoreCase("Cash")) {
|
||||
return "Cash";
|
||||
}
|
||||
|
||||
@@ -83,8 +83,9 @@ public class AdoptionController {
|
||||
|
||||
tvAdoptions.getSelectionModel().selectedItemProperty().addListener(
|
||||
(observable, oldValue, newValue) -> {
|
||||
btnEdit.setDisable(false);
|
||||
btnDelete.setDisable(false);
|
||||
boolean hasSelection = newValue != null;
|
||||
btnEdit.setDisable(!hasSelection);
|
||||
btnDelete.setDisable(!hasSelection);
|
||||
});
|
||||
|
||||
txtSearch.textProperty().addListener((observable, oldValue, newValue) -> {
|
||||
|
||||
@@ -47,7 +47,8 @@ public class AppointmentController {
|
||||
|
||||
@FXML
|
||||
public void initialize(){
|
||||
|
||||
btnEdit.setDisable(true);
|
||||
btnDelete.setDisable(true);
|
||||
tvAppointments.getSelectionModel().setSelectionMode(javafx.scene.control.SelectionMode.MULTIPLE);
|
||||
|
||||
colAppointmentId.setCellValueFactory(new PropertyValueFactory<>("appointmentId"));
|
||||
@@ -66,6 +67,12 @@ public class AppointmentController {
|
||||
txtSearch.textProperty().addListener((obs, o, n) -> applyFilter(n));
|
||||
}
|
||||
|
||||
tvAppointments.getSelectionModel().selectedItemProperty().addListener((obs, oldValue, newValue) -> {
|
||||
boolean hasSelection = newValue != null;
|
||||
btnEdit.setDisable(!hasSelection);
|
||||
btnDelete.setDisable(!hasSelection);
|
||||
});
|
||||
|
||||
tvAppointments.setOnKeyPressed(event -> {
|
||||
if (event.getCode() == javafx.scene.input.KeyCode.DELETE) {
|
||||
if (tvAppointments.getSelectionModel().getSelectedItem() != null) {
|
||||
|
||||
@@ -123,29 +123,20 @@ public class ChatController {
|
||||
|
||||
@FXML
|
||||
void btnSendClicked() {
|
||||
try {
|
||||
if (selectedConversation == null) {
|
||||
lblChatStatus.setText("Select a conversation");
|
||||
return;
|
||||
}
|
||||
|
||||
String content = txtMessage.getText() == null ? "" : txtMessage.getText().trim();
|
||||
if (content.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
txtMessage.clear();
|
||||
boolean sent = realtimeClient.sendMessage(selectedConversation.getId(), content);
|
||||
if (!sent) {
|
||||
sendMessageFallback(selectedConversation.getId(), content);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
ActivityLogger.getInstance().logException(
|
||||
"ChatController.btnSendClicked",
|
||||
e,
|
||||
"Sending chat message");
|
||||
lblChatStatus.setText("Chat send failed");
|
||||
if (selectedConversation == null) {
|
||||
lblChatStatus.setText("Select a conversation");
|
||||
return;
|
||||
}
|
||||
|
||||
String content = txtMessage.getText() == null ? "" : txtMessage.getText().trim();
|
||||
if (content.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
txtMessage.clear();
|
||||
btnSend.setDisable(true);
|
||||
lblChatStatus.setText("Sending message...");
|
||||
sendMessage(selectedConversation.getId(), content);
|
||||
}
|
||||
|
||||
private void loadCustomers() {
|
||||
@@ -211,19 +202,29 @@ public class ChatController {
|
||||
}).start();
|
||||
}
|
||||
|
||||
private void sendMessageFallback(Long conversationId, String content) {
|
||||
private void sendMessage(Long conversationId, String content) {
|
||||
new Thread(() -> {
|
||||
try {
|
||||
MessageResponse response = ChatApi.getInstance().sendMessage(conversationId, new MessageRequest(content));
|
||||
Platform.runLater(() -> {
|
||||
lblChatStatus.setText("Chat fallback active");
|
||||
appendMessageIfSelected(response);
|
||||
btnSend.setDisable(false);
|
||||
if (!realtimeClient.isConnected()) {
|
||||
appendMessageIfSelected(response);
|
||||
}
|
||||
if (selectedConversation != null && selectedConversation.getId().equals(conversationId)) {
|
||||
lblChatStatus.setText("Message sent");
|
||||
}
|
||||
});
|
||||
} catch (Exception e) {
|
||||
Platform.runLater(() -> ActivityLogger.getInstance().logException(
|
||||
"ChatController.sendMessageFallback",
|
||||
e,
|
||||
"Sending chat message for conversation " + conversationId));
|
||||
Platform.runLater(() -> {
|
||||
txtMessage.setText(content);
|
||||
btnSend.setDisable(false);
|
||||
lblChatStatus.setText("Chat send failed");
|
||||
ActivityLogger.getInstance().logException(
|
||||
"ChatController.sendMessage",
|
||||
e,
|
||||
"Sending chat message for conversation " + conversationId);
|
||||
});
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
|
||||
@@ -73,8 +73,9 @@ public class InventoryController {
|
||||
|
||||
tvInventory.getSelectionModel().selectedItemProperty().addListener(
|
||||
(observable, oldValue, newValue) -> {
|
||||
btnEdit.setDisable(false);
|
||||
btnDelete.setDisable(false);
|
||||
boolean hasSelection = newValue != null;
|
||||
btnEdit.setDisable(!hasSelection);
|
||||
btnDelete.setDisable(!hasSelection);
|
||||
});
|
||||
|
||||
txtSearch.textProperty().addListener((observable, oldValue, newValue) -> {
|
||||
|
||||
@@ -175,8 +175,9 @@ public class PetController {
|
||||
|
||||
tvPets.getSelectionModel().selectedItemProperty().addListener(
|
||||
(observable, oldValue, newValue) -> {
|
||||
btnEdit.setDisable(false);
|
||||
btnDelete.setDisable(false);
|
||||
boolean hasSelection = newValue != null;
|
||||
btnEdit.setDisable(!hasSelection);
|
||||
btnDelete.setDisable(!hasSelection);
|
||||
});
|
||||
|
||||
txtSearch.textProperty().addListener((observable, oldValue, newValue) -> {
|
||||
|
||||
@@ -99,8 +99,9 @@ public class ProductController {
|
||||
//EventListener to Enable buttons when a row is selected
|
||||
tvProducts.getSelectionModel().selectedItemProperty().addListener(
|
||||
(observable, oldValue, newValue) -> {
|
||||
btnEdit.setDisable(false);
|
||||
btnDelete.setDisable(false);
|
||||
boolean hasSelection = newValue != null;
|
||||
btnEdit.setDisable(!hasSelection);
|
||||
btnDelete.setDisable(!hasSelection);
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
@@ -80,8 +80,9 @@ public class ProductSupplierController {
|
||||
//EventListener to Enable buttons when a row is selected
|
||||
tvProductSuppliers.getSelectionModel().selectedItemProperty().addListener(
|
||||
(observable, oldValue, newValue) -> {
|
||||
btnEdit.setDisable(false);
|
||||
btnDelete.setDisable(false);
|
||||
boolean hasSelection = newValue != null;
|
||||
btnEdit.setDisable(!hasSelection);
|
||||
btnDelete.setDisable(!hasSelection);
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
@@ -57,8 +57,9 @@ public class ServiceController {
|
||||
|
||||
tvServices.getSelectionModel().selectedItemProperty().addListener(
|
||||
(observable, oldValue, newValue) -> {
|
||||
btnEdit.setDisable(false);
|
||||
btnDelete.setDisable(false);
|
||||
boolean hasSelection = newValue != null;
|
||||
btnEdit.setDisable(!hasSelection);
|
||||
btnDelete.setDisable(!hasSelection);
|
||||
}
|
||||
);
|
||||
|
||||
@@ -230,4 +231,4 @@ public class ServiceController {
|
||||
response.getServicePrice().doubleValue()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,8 +82,9 @@ public class SupplierController {
|
||||
//EventListener to Enable buttons when a row is selected
|
||||
tvSuppliers.getSelectionModel().selectedItemProperty().addListener(
|
||||
(observable, oldValue, newValue) -> {
|
||||
btnEdit.setDisable(false);
|
||||
btnDelete.setDisable(false);
|
||||
boolean hasSelection = newValue != null;
|
||||
btnEdit.setDisable(!hasSelection);
|
||||
btnDelete.setDisable(!hasSelection);
|
||||
}
|
||||
);
|
||||
|
||||
@@ -285,19 +286,21 @@ public class SupplierController {
|
||||
}
|
||||
|
||||
private Supplier mapToSupplier(SupplierResponse response) {
|
||||
String contactPerson = response.getSupContactFirstName() + " " + response.getSupContactLastName() != null ? response.getSupContactFirstName() + " " + response.getSupContactLastName() : "";
|
||||
String firstName = response.getSupContactFirstName() != null ? response.getSupContactFirstName().trim() : "";
|
||||
String lastName = response.getSupContactLastName() != null ? response.getSupContactLastName().trim() : "";
|
||||
String contactPerson = (firstName + " " + lastName).trim();
|
||||
String[] nameParts = contactPerson.split(" ", 2);
|
||||
String firstName = nameParts.length > 0 ? nameParts[0] : "";
|
||||
String lastName = nameParts.length > 1 ? nameParts[1] : "";
|
||||
String mappedFirstName = nameParts.length > 0 ? nameParts[0] : "";
|
||||
String mappedLastName = nameParts.length > 1 ? nameParts[1] : "";
|
||||
|
||||
return new Supplier(
|
||||
response.getSupId().intValue(),
|
||||
response.getSupCompany(),
|
||||
firstName,
|
||||
lastName,
|
||||
mappedFirstName,
|
||||
mappedLastName,
|
||||
response.getSupEmail(),
|
||||
response.getSupPhone()
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,6 +74,7 @@ public class AdoptionDialogController {
|
||||
Platform.runLater(() -> {
|
||||
if (pets != null) {
|
||||
ObservableList<DropdownOption> petsObs = FXCollections.observableArrayList(pets);
|
||||
ensureSelectedPetOption(petsObs);
|
||||
cbPet.setItems(petsObs);
|
||||
applySelectedPet();
|
||||
}
|
||||
@@ -318,4 +319,17 @@ public class AdoptionDialogController {
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private void ensureSelectedPetOption(ObservableList<DropdownOption> options) {
|
||||
if (selectedAdoption == null || selectedAdoption.getPetId() <= 0 || options == null) {
|
||||
return;
|
||||
}
|
||||
DropdownOption existing = findOptionById(options, (long) selectedAdoption.getPetId());
|
||||
if (existing == null) {
|
||||
DropdownOption option = new DropdownOption();
|
||||
option.setId((long) selectedAdoption.getPetId());
|
||||
option.setLabel(selectedAdoption.getPetName());
|
||||
options.add(0, option);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import javafx.scene.Node;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.input.MouseEvent;
|
||||
import javafx.scene.layout.VBox;
|
||||
import javafx.stage.Stage;
|
||||
import org.example.petshopdesktop.Validator;
|
||||
import org.example.petshopdesktop.api.dto.common.DropdownOption;
|
||||
@@ -48,6 +49,12 @@ public class PetDialogController {
|
||||
@FXML
|
||||
private ComboBox<DropdownOption> cbStore;
|
||||
|
||||
@FXML
|
||||
private VBox vbCustomerField;
|
||||
|
||||
@FXML
|
||||
private VBox vbStoreField;
|
||||
|
||||
@FXML
|
||||
private Label lblMode;
|
||||
|
||||
@@ -84,7 +91,7 @@ public class PetDialogController {
|
||||
private Long pendingStoreId = null;
|
||||
|
||||
private ObservableList<String> statusList = FXCollections.observableArrayList(
|
||||
"Available", "Adopted", "Owned"
|
||||
"Available", "Adopted", "Owned", "Pending"
|
||||
);
|
||||
|
||||
@FXML
|
||||
@@ -118,14 +125,11 @@ public class PetDialogController {
|
||||
}
|
||||
});
|
||||
|
||||
cbCustomer.setVisible(false);
|
||||
cbStore.setVisible(false);
|
||||
setFieldVisibility(vbCustomerField, false);
|
||||
setFieldVisibility(vbStoreField, false);
|
||||
|
||||
cbPetStatus.valueProperty().addListener((obs, oldVal, newVal) -> {
|
||||
boolean isOwned = "Owned".equalsIgnoreCase(newVal);
|
||||
boolean isAvailable = "Available".equalsIgnoreCase(newVal) || "Unadopted".equalsIgnoreCase(newVal);
|
||||
cbCustomer.setVisible(isOwned);
|
||||
cbStore.setVisible(isAvailable);
|
||||
updateStatusFieldVisibility(newVal);
|
||||
});
|
||||
|
||||
btnSave.setOnMouseClicked(new EventHandler<MouseEvent>() {
|
||||
@@ -166,6 +170,9 @@ public class PetDialogController {
|
||||
if ("Owned".equalsIgnoreCase(selectedStatus) && cbCustomer.getValue() == null) {
|
||||
errorMsg += "Customer is required for Owned status\n";
|
||||
}
|
||||
if (requiresStore(selectedStatus) && cbStore.getValue() == null) {
|
||||
errorMsg += "Store is required for " + selectedStatus + " status\n";
|
||||
}
|
||||
|
||||
//Check validation (length size)
|
||||
errorMsg += Validator.isLessThanVarChars(txtPetName.getText(), "Pet Name", 50);
|
||||
@@ -243,7 +250,7 @@ public class PetDialogController {
|
||||
if ("Owned".equalsIgnoreCase(status) && cbCustomer.getValue() != null) {
|
||||
request.setCustomerId(cbCustomer.getValue().getId());
|
||||
}
|
||||
if (("Available".equalsIgnoreCase(status) || "Unadopted".equalsIgnoreCase(status)) && cbStore.getValue() != null) {
|
||||
if (requiresStore(status) && cbStore.getValue() != null) {
|
||||
request.setStoreId(cbStore.getValue().getId());
|
||||
}
|
||||
|
||||
@@ -339,8 +346,10 @@ public class PetDialogController {
|
||||
for (String status : cbPetStatus.getItems()) {
|
||||
if(status.equals(pet.getPetStatus())){
|
||||
cbPetStatus.getSelectionModel().select(status);
|
||||
break;
|
||||
}
|
||||
}
|
||||
updateStatusFieldVisibility(cbPetStatus.getValue());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -358,6 +367,7 @@ public class PetDialogController {
|
||||
lblPetId.setVisible(true);
|
||||
refreshImagePreview();
|
||||
}
|
||||
updateStatusFieldVisibility(cbPetStatus.getValue());
|
||||
}
|
||||
|
||||
private void handleChangeImage() {
|
||||
@@ -421,4 +431,25 @@ public class PetDialogController {
|
||||
btnRemoveImage.setDisable(true);
|
||||
}
|
||||
|
||||
private void updateStatusFieldVisibility(String status) {
|
||||
boolean owned = "Owned".equalsIgnoreCase(status);
|
||||
boolean storeBased = requiresStore(status);
|
||||
setFieldVisibility(vbCustomerField, owned);
|
||||
setFieldVisibility(vbStoreField, storeBased);
|
||||
}
|
||||
|
||||
private boolean requiresStore(String status) {
|
||||
return "Available".equalsIgnoreCase(status)
|
||||
|| "Pending".equalsIgnoreCase(status)
|
||||
|| "Unadopted".equalsIgnoreCase(status);
|
||||
}
|
||||
|
||||
private void setFieldVisibility(VBox field, boolean visible) {
|
||||
if (field == null) {
|
||||
return;
|
||||
}
|
||||
field.setVisible(visible);
|
||||
field.setManaged(visible);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ import org.example.petshopdesktop.api.endpoints.ProductSupplierApi;
|
||||
import org.example.petshopdesktop.util.ActivityLogger;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
|
||||
public class ProductSupplierDialogController {
|
||||
|
||||
@@ -46,6 +47,8 @@ public class ProductSupplierDialogController {
|
||||
private String mode = null;
|
||||
private int selectedSupId = -1;
|
||||
private int selectedProdId = -1;
|
||||
private Long pendingSupplierId = null;
|
||||
private Long pendingProductId = null;
|
||||
|
||||
/**
|
||||
* add event listeners to buttons and set up combobox
|
||||
@@ -120,9 +123,11 @@ public class ProductSupplierDialogController {
|
||||
Platform.runLater(() -> {
|
||||
if (suppliers != null) {
|
||||
cbSupplier.setItems(FXCollections.observableArrayList(suppliers));
|
||||
applyPendingSupplierSelection();
|
||||
}
|
||||
if (products != null) {
|
||||
cbProduct.setItems(FXCollections.observableArrayList(products));
|
||||
applyPendingProductSelection();
|
||||
}
|
||||
});
|
||||
} catch (Exception e) {
|
||||
@@ -220,21 +225,14 @@ public class ProductSupplierDialogController {
|
||||
* @param productSupplier
|
||||
*/
|
||||
public void displayProductSupplierDetails(ProductSupplierDTO productSupplier){
|
||||
if(productSupplier != null){
|
||||
txtCost.setText(productSupplier.getCost() + "");
|
||||
}
|
||||
|
||||
for (DropdownOption product : cbProduct.getItems()) {
|
||||
if(product.getId() == productSupplier.getProdId()){
|
||||
cbProduct.getSelectionModel().select(product);
|
||||
}
|
||||
}
|
||||
|
||||
for (DropdownOption supplier : cbSupplier.getItems()) {
|
||||
if (supplier.getId() == productSupplier.getSupId()) {
|
||||
cbSupplier.getSelectionModel().select(supplier);
|
||||
}
|
||||
if (productSupplier == null) {
|
||||
return;
|
||||
}
|
||||
txtCost.setText(productSupplier.getCost() + "");
|
||||
pendingProductId = (long) productSupplier.getProdId();
|
||||
pendingSupplierId = (long) productSupplier.getSupId();
|
||||
applyPendingProductSelection();
|
||||
applyPendingSupplierSelection();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -253,7 +251,7 @@ public class ProductSupplierDialogController {
|
||||
*/
|
||||
public void setMode(String mode) {
|
||||
this.mode = mode;
|
||||
lblMode.setText(mode + " Product");
|
||||
lblMode.setText(mode + " Product-Supplier");
|
||||
lblProductSupplierId.setVisible(false);
|
||||
}
|
||||
|
||||
@@ -267,4 +265,38 @@ public class ProductSupplierDialogController {
|
||||
this.selectedProdId = prodId;
|
||||
}
|
||||
|
||||
private void applyPendingProductSelection() {
|
||||
if (pendingProductId == null) {
|
||||
return;
|
||||
}
|
||||
DropdownOption product = findOptionById(cbProduct.getItems(), pendingProductId);
|
||||
if (product != null) {
|
||||
cbProduct.getSelectionModel().select(product);
|
||||
pendingProductId = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void applyPendingSupplierSelection() {
|
||||
if (pendingSupplierId == null) {
|
||||
return;
|
||||
}
|
||||
DropdownOption supplier = findOptionById(cbSupplier.getItems(), pendingSupplierId);
|
||||
if (supplier != null) {
|
||||
cbSupplier.getSelectionModel().select(supplier);
|
||||
pendingSupplierId = null;
|
||||
}
|
||||
}
|
||||
|
||||
private DropdownOption findOptionById(List<DropdownOption> options, Long id) {
|
||||
if (options == null || id == null) {
|
||||
return null;
|
||||
}
|
||||
for (DropdownOption option : options) {
|
||||
if (option.getId() != null && option.getId().equals(id)) {
|
||||
return option;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -99,7 +99,7 @@ public class RefundDialogController {
|
||||
@FXML
|
||||
public void initialize() {
|
||||
setupTables();
|
||||
cbPaymentMethod.setItems(FXCollections.observableArrayList("Cash", "Card", "Debit"));
|
||||
cbPaymentMethod.setItems(FXCollections.observableArrayList("Cash", "Card"));
|
||||
cbPaymentMethod.getSelectionModel().selectFirst();
|
||||
updateRefundTotal();
|
||||
}
|
||||
|
||||
@@ -153,7 +153,7 @@
|
||||
</TextField>
|
||||
</children>
|
||||
</VBox>
|
||||
<VBox prefHeight="200.0" prefWidth="100.0" spacing="8.0" GridPane.rowIndex="3">
|
||||
<VBox fx:id="vbCustomerField" prefHeight="200.0" prefWidth="100.0" spacing="8.0" GridPane.rowIndex="3">
|
||||
<children>
|
||||
<Label text="Customer:" textFill="#2c3e50">
|
||||
<font>
|
||||
@@ -167,7 +167,7 @@
|
||||
</ComboBox>
|
||||
</children>
|
||||
</VBox>
|
||||
<VBox prefHeight="200.0" prefWidth="100.0" spacing="8.0" GridPane.columnIndex="1" GridPane.rowIndex="3">
|
||||
<VBox fx:id="vbStoreField" prefHeight="200.0" prefWidth="100.0" spacing="8.0" GridPane.columnIndex="1" GridPane.rowIndex="3">
|
||||
<children>
|
||||
<Label text="Store:" textFill="#2c3e50">
|
||||
<font>
|
||||
|
||||
@@ -195,6 +195,100 @@ function DatePicker({ value, minDate, onChange }) {
|
||||
);
|
||||
}
|
||||
|
||||
function AddPetModal({ token, onClose, onAdded }) {
|
||||
const [petName, setPetName] = useState("");
|
||||
const [species, setSpecies] = useState("");
|
||||
const [breed, setBreed] = useState("");
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const [petError, setPetError] = useState(null);
|
||||
|
||||
async function handleSubmit(e) {
|
||||
e.preventDefault();
|
||||
setPetError(null);
|
||||
setSubmitting(true);
|
||||
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/api/v1/my-pets`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
body: JSON.stringify({ petName, species, breed: breed || null }),
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
const data = await res.json().catch(() => null);
|
||||
throw new Error(data?.message || `Request failed (${res.status})`);
|
||||
}
|
||||
|
||||
onAdded();
|
||||
onClose();
|
||||
}
|
||||
|
||||
catch (err) {
|
||||
setPetError(err.message);
|
||||
}
|
||||
|
||||
finally {
|
||||
setSubmitting(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="appt-modal-overlay" onClick={onClose}>
|
||||
<div className="appt-modal" onClick={(e) => e.stopPropagation()}>
|
||||
<h3 className="profile-pet-form-title">Add a New Pet</h3>
|
||||
{petError && <div className="appt-error">{petError}</div>}
|
||||
<form onSubmit={handleSubmit}>
|
||||
<label className="appt-label">
|
||||
Name
|
||||
<input
|
||||
className="appt-input"
|
||||
type="text"
|
||||
value={petName}
|
||||
onChange={(e) => setPetName(e.target.value)}
|
||||
required
|
||||
maxLength={50}
|
||||
/>
|
||||
</label>
|
||||
<label className="appt-label">
|
||||
Species
|
||||
<input
|
||||
className="appt-input"
|
||||
type="text"
|
||||
value={species}
|
||||
onChange={(e) => setSpecies(e.target.value)}
|
||||
required
|
||||
maxLength={50}
|
||||
placeholder="e.g. Dog, Cat, Bird"
|
||||
/>
|
||||
</label>
|
||||
<label className="appt-label">
|
||||
Breed (optional)
|
||||
<input
|
||||
className="appt-input"
|
||||
type="text"
|
||||
value={breed}
|
||||
onChange={(e) => setBreed(e.target.value)}
|
||||
maxLength={50}
|
||||
placeholder="e.g. Golden Retriever"
|
||||
/>
|
||||
</label>
|
||||
<div className="profile-pet-form-actions">
|
||||
<button type="submit" className="appt-submit-btn" disabled={submitting}>
|
||||
{submitting ? "Saving..." : "Add Pet"}
|
||||
</button>
|
||||
<button type="button" className="profile-pet-cancel-btn" onClick={onClose}>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function AppointmentsPage() {
|
||||
const { user, token, loading: authLoading } = useAuth();
|
||||
const router = useRouter();
|
||||
@@ -224,7 +318,9 @@ function AppointmentsPage() {
|
||||
const [appointments, setAppointments] = useState([]);
|
||||
const [loadingAppointments, setLoadingAppointments] = useState(false);
|
||||
|
||||
const canBookAppointments = user?.role === "CUSTOMER";
|
||||
const [showAddPetModal, setShowAddPetModal] = useState(false);
|
||||
|
||||
const canBookAppointments = user?.role === "CUSTOMER" || user?.role === "ADMIN";
|
||||
|
||||
useEffect(() => {
|
||||
if (!authLoading && !user) {
|
||||
@@ -234,6 +330,16 @@ function AppointmentsPage() {
|
||||
|
||||
}, [authLoading, user, router, preselectedPetId]);
|
||||
|
||||
const loadCustomerPets = useCallback(() => {
|
||||
if (!token || !canBookAppointments) return;
|
||||
fetch(`${API_BASE}/api/v1/my-pets`, {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
})
|
||||
.then((r) => r.json())
|
||||
.then((data) => setCustomerPets(Array.isArray(data) ? data : []))
|
||||
.catch(() => {});
|
||||
}, [token, canBookAppointments]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!token) {
|
||||
return;
|
||||
@@ -256,15 +362,8 @@ function AppointmentsPage() {
|
||||
.then((data) => setAllPets(data.content ?? []))
|
||||
.catch(() => {});
|
||||
|
||||
if (canBookAppointments) {
|
||||
fetch(`${API_BASE}/api/v1/my-pets`, {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
})
|
||||
.then((r) => r.json())
|
||||
.then((data) => setCustomerPets(Array.isArray(data) ? data : []))
|
||||
.catch(() => {});
|
||||
}
|
||||
}, [token, canBookAppointments]);
|
||||
loadCustomerPets();
|
||||
}, [token, loadCustomerPets]);
|
||||
|
||||
useEffect(() => {
|
||||
if (didPreselectRef.current) {
|
||||
@@ -392,7 +491,6 @@ function AppointmentsPage() {
|
||||
|
||||
function getMinDate() {
|
||||
const d = new Date();
|
||||
d.setDate(d.getDate() + 1);
|
||||
|
||||
return d.toISOString().split("T")[0];
|
||||
}
|
||||
@@ -411,12 +509,6 @@ function AppointmentsPage() {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!user?.customerId) {
|
||||
setError("Customer account not found. Please contact support.");
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (selectedPetIds.length === 0) {
|
||||
setError(isAdoptionService ? "Please select a pet to adopt." : "Please select at least one pet.");
|
||||
|
||||
@@ -427,7 +519,7 @@ function AppointmentsPage() {
|
||||
|
||||
try {
|
||||
const body = {
|
||||
customerId: user.customerId,
|
||||
customerId: user.customerId || user.id,
|
||||
storeId: Number(storeId),
|
||||
serviceId: Number(serviceId),
|
||||
employeeId: employeeId ? Number(employeeId) : undefined,
|
||||
@@ -436,12 +528,8 @@ function AppointmentsPage() {
|
||||
appointmentStatus: "Booked",
|
||||
};
|
||||
|
||||
if (isCustomerPetService) {
|
||||
body.customerPetIds = selectedPetIds;
|
||||
}
|
||||
|
||||
else {
|
||||
body.petIds = selectedPetIds;
|
||||
if (selectedPetIds.length > 0) {
|
||||
body.petId = selectedPetIds[0];
|
||||
}
|
||||
|
||||
const res = await fetch(`${API_BASE}/api/v1/appointments`, {
|
||||
@@ -493,10 +581,18 @@ function AppointmentsPage() {
|
||||
const petSectionLabel = isAdoptionService ? "Select a Pet to Adopt" : "Select Pet(s)";
|
||||
const noPetsMessage = isAdoptionService
|
||||
? "No pets are currently available for adoption."
|
||||
: "No pets found. Please add your pets in your profile before booking.";
|
||||
: "No pets found on your profile.";
|
||||
|
||||
return (
|
||||
<main className="appt-page">
|
||||
{showAddPetModal && (
|
||||
<AddPetModal
|
||||
token={token}
|
||||
onClose={() => setShowAddPetModal(false)}
|
||||
onAdded={loadCustomerPets}
|
||||
/>
|
||||
)}
|
||||
|
||||
<section className="appt-hero">
|
||||
<h1 className="appt-hero-title">Schedule an Appointment</h1>
|
||||
<p className="appt-hero-subtitle">Book a service for your pet or schedule a pet adoption visit</p>
|
||||
@@ -600,6 +696,15 @@ function AppointmentsPage() {
|
||||
{serviceId && (
|
||||
<div className="appt-label">
|
||||
<span>{petSectionLabel}</span>
|
||||
{isCustomerPetService && (
|
||||
<button
|
||||
type="button"
|
||||
className="appt-add-pet-btn"
|
||||
onClick={() => setShowAddPetModal(true)}
|
||||
>
|
||||
+ Add New Pet
|
||||
</button>
|
||||
)}
|
||||
{petsToShow.length === 0 ? (
|
||||
<p className="appt-no-slots">{noPetsMessage}</p>
|
||||
) : isAdoptionService ? (
|
||||
|
||||
@@ -1351,6 +1351,44 @@ body {
|
||||
accent-color: orange;
|
||||
}
|
||||
|
||||
.appt-add-pet-btn {
|
||||
display: inline-block;
|
||||
margin-top: 0.5rem;
|
||||
padding: 0.4rem 0.85rem;
|
||||
background: none;
|
||||
border: 1.5px solid orange;
|
||||
border-radius: 6px;
|
||||
color: orange;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s, color 0.15s;
|
||||
}
|
||||
|
||||
.appt-add-pet-btn:hover {
|
||||
background: orange;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.appt-modal-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.45);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.appt-modal {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
padding: 2rem;
|
||||
width: 100%;
|
||||
max-width: 420px;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.18);
|
||||
}
|
||||
|
||||
.appt-link {
|
||||
color: orange;
|
||||
font-weight: 600;
|
||||
|
||||
@@ -108,7 +108,7 @@ export default function ProfilePage() {
|
||||
}, [clearPetImageObjectUrls]);
|
||||
|
||||
useEffect(() => {
|
||||
if (user?.role === "CUSTOMER") {
|
||||
if (user?.role === "CUSTOMER" || user?.role === "ADMIN") {
|
||||
loadPets();
|
||||
}
|
||||
}, [user, loadPets]);
|
||||
@@ -419,7 +419,7 @@ export default function ProfilePage() {
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{user.role === "CUSTOMER" && (
|
||||
{(user.role === "CUSTOMER" || user.role === "ADMIN") && (
|
||||
<div className="profile-pets-section">
|
||||
<div className="profile-pets-header">
|
||||
<h2 className="profile-pets-title">My Pets</h2>
|
||||
|
||||
Reference in New Issue
Block a user