Merge main branch
This commit is contained in:
@@ -50,7 +50,7 @@ public class AppointmentController {
|
|||||||
.orElse(null);
|
.orElse(null);
|
||||||
|
|
||||||
Long effectiveCustomerId = customerId;
|
Long effectiveCustomerId = customerId;
|
||||||
if (role != null && role.equals("CUSTOMER")) {
|
if ("CUSTOMER".equals(role)) {
|
||||||
User user = AuthenticationHelper.getAuthenticatedUser(userRepository);
|
User user = AuthenticationHelper.getAuthenticatedUser(userRepository);
|
||||||
effectiveCustomerId = user.getId();
|
effectiveCustomerId = user.getId();
|
||||||
}
|
}
|
||||||
@@ -88,7 +88,7 @@ public class AppointmentController {
|
|||||||
.map(authority -> authority.getAuthority().replace("ROLE_", ""))
|
.map(authority -> authority.getAuthority().replace("ROLE_", ""))
|
||||||
.orElse(null);
|
.orElse(null);
|
||||||
|
|
||||||
if (role != null && role.equals("CUSTOMER")) {
|
if ("CUSTOMER".equals(role)) {
|
||||||
User user = AuthenticationHelper.getAuthenticatedUser(userRepository);
|
User user = AuthenticationHelper.getAuthenticatedUser(userRepository);
|
||||||
if (!request.getCustomerId().equals(user.getId())) {
|
if (!request.getCustomerId().equals(user.getId())) {
|
||||||
throw new org.springframework.security.access.AccessDeniedException("You can only create appointments for yourself");
|
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')")
|
@PreAuthorize("hasAnyRole('STAFF', 'ADMIN')")
|
||||||
public ResponseEntity<List<DropdownOption>> getAdoptionPets() {
|
public ResponseEntity<List<DropdownOption>> getAdoptionPets() {
|
||||||
return ResponseEntity.ok(
|
return ResponseEntity.ok(
|
||||||
petRepository.findAllByPetStatusIgnoreCaseOrderByPetNameAsc("Available").stream()
|
petRepository.findAdoptablePetsOrderByPetNameAsc().stream()
|
||||||
.map(p -> new DropdownOption(p.getPetId(), p.getPetName()))
|
.map(p -> new DropdownOption(p.getPetId(), p.getPetName()))
|
||||||
.collect(Collectors.toList())
|
.collect(Collectors.toList())
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import jakarta.servlet.http.HttpServletRequest;
|
|||||||
import org.springframework.dao.DataIntegrityViolationException;
|
import org.springframework.dao.DataIntegrityViolationException;
|
||||||
import org.springframework.data.core.PropertyReferenceException;
|
import org.springframework.data.core.PropertyReferenceException;
|
||||||
import org.springframework.http.HttpStatus;
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.validation.FieldError;
|
import org.springframework.validation.FieldError;
|
||||||
import org.springframework.web.HttpRequestMethodNotSupportedException;
|
import org.springframework.web.HttpRequestMethodNotSupportedException;
|
||||||
@@ -118,7 +119,9 @@ public class GlobalExceptionHandler {
|
|||||||
request.getRequestURI(),
|
request.getRequestURI(),
|
||||||
LocalDateTime.now()
|
LocalDateTime.now()
|
||||||
);
|
);
|
||||||
return ResponseEntity.status(status).body(error);
|
return ResponseEntity.status(status)
|
||||||
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
|
.body(error);
|
||||||
}
|
}
|
||||||
|
|
||||||
private String buildDetails(Exception ex) {
|
private String buildDetails(Exception ex) {
|
||||||
|
|||||||
@@ -15,6 +15,14 @@ import java.util.Optional;
|
|||||||
public interface PetRepository extends JpaRepository<Pet, Long> {
|
public interface PetRepository extends JpaRepository<Pet, Long> {
|
||||||
|
|
||||||
List<Pet> findAllByPetStatusIgnoreCaseOrderByPetNameAsc(String petStatus);
|
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);
|
List<Pet> findAllByOwner_IdOrderByPetNameAsc(Long ownerId);
|
||||||
Optional<Pet> findByIdAndOwner_Id(Long id, 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()))
|
.orElseThrow(() -> new ResourceNotFoundException("Store not found with id: " + request.getSourceStoreId()))
|
||||||
: null;
|
: null;
|
||||||
String adoptionStatus = normalizeAdoptionStatus(request.getAdoptionStatus());
|
String adoptionStatus = normalizeAdoptionStatus(request.getAdoptionStatus());
|
||||||
validatePetAvailability(pet, null);
|
validatePetAvailability(pet, null, null);
|
||||||
|
|
||||||
Adoption adoption = new Adoption();
|
Adoption adoption = new Adoption();
|
||||||
adoption.setPet(pet);
|
adoption.setPet(pet);
|
||||||
@@ -111,7 +111,8 @@ public class AdoptionService {
|
|||||||
.orElseThrow(() -> new ResourceNotFoundException("Store not found with id: " + request.getSourceStoreId()))
|
.orElseThrow(() -> new ResourceNotFoundException("Store not found with id: " + request.getSourceStoreId()))
|
||||||
: null;
|
: null;
|
||||||
String adoptionStatus = normalizeAdoptionStatus(request.getAdoptionStatus());
|
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.setPet(pet);
|
||||||
adoption.setCustomer(customer);
|
adoption.setCustomer(customer);
|
||||||
@@ -201,7 +202,8 @@ public class AdoptionService {
|
|||||||
throw new IllegalArgumentException("Adoption status must be Pending, Completed, or Cancelled");
|
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
|
boolean adoptedElsewhere = adoptionId == null
|
||||||
? adoptionRepository.existsByPet_IdAndAdoptionStatusIgnoreCase(pet.getPetId(), ADOPTION_STATUS_COMPLETED)
|
? adoptionRepository.existsByPet_IdAndAdoptionStatusIgnoreCase(pet.getPetId(), ADOPTION_STATUS_COMPLETED)
|
||||||
: adoptionRepository.existsByPet_IdAndAdoptionStatusIgnoreCaseAndAdoptionIdNot(pet.getPetId(), ADOPTION_STATUS_COMPLETED, adoptionId);
|
: 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");
|
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");
|
throw new IllegalArgumentException("Selected pet is not available for adoption");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -247,7 +247,13 @@ public class PetService {
|
|||||||
if (principal instanceof AppPrincipal appPrincipal) {
|
if (principal instanceof AppPrincipal appPrincipal) {
|
||||||
return new CurrentViewer(appPrincipal.getUserId(), appPrincipal.getRole());
|
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) {
|
private Pet findPet(Long id) {
|
||||||
|
|||||||
@@ -245,9 +245,6 @@ public class SaleService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
String normalized = paymentMethod.trim();
|
String normalized = paymentMethod.trim();
|
||||||
if (normalized.equalsIgnoreCase("Debit")) {
|
|
||||||
return "Card";
|
|
||||||
}
|
|
||||||
if (normalized.equalsIgnoreCase("Cash")) {
|
if (normalized.equalsIgnoreCase("Cash")) {
|
||||||
return "Cash";
|
return "Cash";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -83,8 +83,9 @@ public class AdoptionController {
|
|||||||
|
|
||||||
tvAdoptions.getSelectionModel().selectedItemProperty().addListener(
|
tvAdoptions.getSelectionModel().selectedItemProperty().addListener(
|
||||||
(observable, oldValue, newValue) -> {
|
(observable, oldValue, newValue) -> {
|
||||||
btnEdit.setDisable(false);
|
boolean hasSelection = newValue != null;
|
||||||
btnDelete.setDisable(false);
|
btnEdit.setDisable(!hasSelection);
|
||||||
|
btnDelete.setDisable(!hasSelection);
|
||||||
});
|
});
|
||||||
|
|
||||||
txtSearch.textProperty().addListener((observable, oldValue, newValue) -> {
|
txtSearch.textProperty().addListener((observable, oldValue, newValue) -> {
|
||||||
|
|||||||
@@ -47,7 +47,8 @@ public class AppointmentController {
|
|||||||
|
|
||||||
@FXML
|
@FXML
|
||||||
public void initialize(){
|
public void initialize(){
|
||||||
|
btnEdit.setDisable(true);
|
||||||
|
btnDelete.setDisable(true);
|
||||||
tvAppointments.getSelectionModel().setSelectionMode(javafx.scene.control.SelectionMode.MULTIPLE);
|
tvAppointments.getSelectionModel().setSelectionMode(javafx.scene.control.SelectionMode.MULTIPLE);
|
||||||
|
|
||||||
colAppointmentId.setCellValueFactory(new PropertyValueFactory<>("appointmentId"));
|
colAppointmentId.setCellValueFactory(new PropertyValueFactory<>("appointmentId"));
|
||||||
@@ -66,6 +67,12 @@ public class AppointmentController {
|
|||||||
txtSearch.textProperty().addListener((obs, o, n) -> applyFilter(n));
|
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 -> {
|
tvAppointments.setOnKeyPressed(event -> {
|
||||||
if (event.getCode() == javafx.scene.input.KeyCode.DELETE) {
|
if (event.getCode() == javafx.scene.input.KeyCode.DELETE) {
|
||||||
if (tvAppointments.getSelectionModel().getSelectedItem() != null) {
|
if (tvAppointments.getSelectionModel().getSelectedItem() != null) {
|
||||||
|
|||||||
@@ -123,29 +123,20 @@ public class ChatController {
|
|||||||
|
|
||||||
@FXML
|
@FXML
|
||||||
void btnSendClicked() {
|
void btnSendClicked() {
|
||||||
try {
|
if (selectedConversation == null) {
|
||||||
if (selectedConversation == null) {
|
lblChatStatus.setText("Select a conversation");
|
||||||
lblChatStatus.setText("Select a conversation");
|
return;
|
||||||
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");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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() {
|
private void loadCustomers() {
|
||||||
@@ -211,19 +202,29 @@ public class ChatController {
|
|||||||
}).start();
|
}).start();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void sendMessageFallback(Long conversationId, String content) {
|
private void sendMessage(Long conversationId, String content) {
|
||||||
new Thread(() -> {
|
new Thread(() -> {
|
||||||
try {
|
try {
|
||||||
MessageResponse response = ChatApi.getInstance().sendMessage(conversationId, new MessageRequest(content));
|
MessageResponse response = ChatApi.getInstance().sendMessage(conversationId, new MessageRequest(content));
|
||||||
Platform.runLater(() -> {
|
Platform.runLater(() -> {
|
||||||
lblChatStatus.setText("Chat fallback active");
|
btnSend.setDisable(false);
|
||||||
appendMessageIfSelected(response);
|
if (!realtimeClient.isConnected()) {
|
||||||
|
appendMessageIfSelected(response);
|
||||||
|
}
|
||||||
|
if (selectedConversation != null && selectedConversation.getId().equals(conversationId)) {
|
||||||
|
lblChatStatus.setText("Message sent");
|
||||||
|
}
|
||||||
});
|
});
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
Platform.runLater(() -> ActivityLogger.getInstance().logException(
|
Platform.runLater(() -> {
|
||||||
"ChatController.sendMessageFallback",
|
txtMessage.setText(content);
|
||||||
e,
|
btnSend.setDisable(false);
|
||||||
"Sending chat message for conversation " + conversationId));
|
lblChatStatus.setText("Chat send failed");
|
||||||
|
ActivityLogger.getInstance().logException(
|
||||||
|
"ChatController.sendMessage",
|
||||||
|
e,
|
||||||
|
"Sending chat message for conversation " + conversationId);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}).start();
|
}).start();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -73,8 +73,9 @@ public class InventoryController {
|
|||||||
|
|
||||||
tvInventory.getSelectionModel().selectedItemProperty().addListener(
|
tvInventory.getSelectionModel().selectedItemProperty().addListener(
|
||||||
(observable, oldValue, newValue) -> {
|
(observable, oldValue, newValue) -> {
|
||||||
btnEdit.setDisable(false);
|
boolean hasSelection = newValue != null;
|
||||||
btnDelete.setDisable(false);
|
btnEdit.setDisable(!hasSelection);
|
||||||
|
btnDelete.setDisable(!hasSelection);
|
||||||
});
|
});
|
||||||
|
|
||||||
txtSearch.textProperty().addListener((observable, oldValue, newValue) -> {
|
txtSearch.textProperty().addListener((observable, oldValue, newValue) -> {
|
||||||
|
|||||||
@@ -175,8 +175,9 @@ public class PetController {
|
|||||||
|
|
||||||
tvPets.getSelectionModel().selectedItemProperty().addListener(
|
tvPets.getSelectionModel().selectedItemProperty().addListener(
|
||||||
(observable, oldValue, newValue) -> {
|
(observable, oldValue, newValue) -> {
|
||||||
btnEdit.setDisable(false);
|
boolean hasSelection = newValue != null;
|
||||||
btnDelete.setDisable(false);
|
btnEdit.setDisable(!hasSelection);
|
||||||
|
btnDelete.setDisable(!hasSelection);
|
||||||
});
|
});
|
||||||
|
|
||||||
txtSearch.textProperty().addListener((observable, oldValue, newValue) -> {
|
txtSearch.textProperty().addListener((observable, oldValue, newValue) -> {
|
||||||
|
|||||||
@@ -99,8 +99,9 @@ public class ProductController {
|
|||||||
//EventListener to Enable buttons when a row is selected
|
//EventListener to Enable buttons when a row is selected
|
||||||
tvProducts.getSelectionModel().selectedItemProperty().addListener(
|
tvProducts.getSelectionModel().selectedItemProperty().addListener(
|
||||||
(observable, oldValue, newValue) -> {
|
(observable, oldValue, newValue) -> {
|
||||||
btnEdit.setDisable(false);
|
boolean hasSelection = newValue != null;
|
||||||
btnDelete.setDisable(false);
|
btnEdit.setDisable(!hasSelection);
|
||||||
|
btnDelete.setDisable(!hasSelection);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -80,8 +80,9 @@ public class ProductSupplierController {
|
|||||||
//EventListener to Enable buttons when a row is selected
|
//EventListener to Enable buttons when a row is selected
|
||||||
tvProductSuppliers.getSelectionModel().selectedItemProperty().addListener(
|
tvProductSuppliers.getSelectionModel().selectedItemProperty().addListener(
|
||||||
(observable, oldValue, newValue) -> {
|
(observable, oldValue, newValue) -> {
|
||||||
btnEdit.setDisable(false);
|
boolean hasSelection = newValue != null;
|
||||||
btnDelete.setDisable(false);
|
btnEdit.setDisable(!hasSelection);
|
||||||
|
btnDelete.setDisable(!hasSelection);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -57,8 +57,9 @@ public class ServiceController {
|
|||||||
|
|
||||||
tvServices.getSelectionModel().selectedItemProperty().addListener(
|
tvServices.getSelectionModel().selectedItemProperty().addListener(
|
||||||
(observable, oldValue, newValue) -> {
|
(observable, oldValue, newValue) -> {
|
||||||
btnEdit.setDisable(false);
|
boolean hasSelection = newValue != null;
|
||||||
btnDelete.setDisable(false);
|
btnEdit.setDisable(!hasSelection);
|
||||||
|
btnDelete.setDisable(!hasSelection);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -230,4 +231,4 @@ public class ServiceController {
|
|||||||
response.getServicePrice().doubleValue()
|
response.getServicePrice().doubleValue()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -82,8 +82,9 @@ public class SupplierController {
|
|||||||
//EventListener to Enable buttons when a row is selected
|
//EventListener to Enable buttons when a row is selected
|
||||||
tvSuppliers.getSelectionModel().selectedItemProperty().addListener(
|
tvSuppliers.getSelectionModel().selectedItemProperty().addListener(
|
||||||
(observable, oldValue, newValue) -> {
|
(observable, oldValue, newValue) -> {
|
||||||
btnEdit.setDisable(false);
|
boolean hasSelection = newValue != null;
|
||||||
btnDelete.setDisable(false);
|
btnEdit.setDisable(!hasSelection);
|
||||||
|
btnDelete.setDisable(!hasSelection);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -285,19 +286,21 @@ public class SupplierController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private Supplier mapToSupplier(SupplierResponse response) {
|
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[] nameParts = contactPerson.split(" ", 2);
|
||||||
String firstName = nameParts.length > 0 ? nameParts[0] : "";
|
String mappedFirstName = nameParts.length > 0 ? nameParts[0] : "";
|
||||||
String lastName = nameParts.length > 1 ? nameParts[1] : "";
|
String mappedLastName = nameParts.length > 1 ? nameParts[1] : "";
|
||||||
|
|
||||||
return new Supplier(
|
return new Supplier(
|
||||||
response.getSupId().intValue(),
|
response.getSupId().intValue(),
|
||||||
response.getSupCompany(),
|
response.getSupCompany(),
|
||||||
firstName,
|
mappedFirstName,
|
||||||
lastName,
|
mappedLastName,
|
||||||
response.getSupEmail(),
|
response.getSupEmail(),
|
||||||
response.getSupPhone()
|
response.getSupPhone()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -74,6 +74,7 @@ public class AdoptionDialogController {
|
|||||||
Platform.runLater(() -> {
|
Platform.runLater(() -> {
|
||||||
if (pets != null) {
|
if (pets != null) {
|
||||||
ObservableList<DropdownOption> petsObs = FXCollections.observableArrayList(pets);
|
ObservableList<DropdownOption> petsObs = FXCollections.observableArrayList(pets);
|
||||||
|
ensureSelectedPetOption(petsObs);
|
||||||
cbPet.setItems(petsObs);
|
cbPet.setItems(petsObs);
|
||||||
applySelectedPet();
|
applySelectedPet();
|
||||||
}
|
}
|
||||||
@@ -318,4 +319,17 @@ public class AdoptionDialogController {
|
|||||||
}
|
}
|
||||||
return null;
|
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.control.*;
|
||||||
import javafx.scene.image.ImageView;
|
import javafx.scene.image.ImageView;
|
||||||
import javafx.scene.input.MouseEvent;
|
import javafx.scene.input.MouseEvent;
|
||||||
|
import javafx.scene.layout.VBox;
|
||||||
import javafx.stage.Stage;
|
import javafx.stage.Stage;
|
||||||
import org.example.petshopdesktop.Validator;
|
import org.example.petshopdesktop.Validator;
|
||||||
import org.example.petshopdesktop.api.dto.common.DropdownOption;
|
import org.example.petshopdesktop.api.dto.common.DropdownOption;
|
||||||
@@ -48,6 +49,12 @@ public class PetDialogController {
|
|||||||
@FXML
|
@FXML
|
||||||
private ComboBox<DropdownOption> cbStore;
|
private ComboBox<DropdownOption> cbStore;
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private VBox vbCustomerField;
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private VBox vbStoreField;
|
||||||
|
|
||||||
@FXML
|
@FXML
|
||||||
private Label lblMode;
|
private Label lblMode;
|
||||||
|
|
||||||
@@ -84,7 +91,7 @@ public class PetDialogController {
|
|||||||
private Long pendingStoreId = null;
|
private Long pendingStoreId = null;
|
||||||
|
|
||||||
private ObservableList<String> statusList = FXCollections.observableArrayList(
|
private ObservableList<String> statusList = FXCollections.observableArrayList(
|
||||||
"Available", "Adopted", "Owned"
|
"Available", "Adopted", "Owned", "Pending"
|
||||||
);
|
);
|
||||||
|
|
||||||
@FXML
|
@FXML
|
||||||
@@ -118,14 +125,11 @@ public class PetDialogController {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
cbCustomer.setVisible(false);
|
setFieldVisibility(vbCustomerField, false);
|
||||||
cbStore.setVisible(false);
|
setFieldVisibility(vbStoreField, false);
|
||||||
|
|
||||||
cbPetStatus.valueProperty().addListener((obs, oldVal, newVal) -> {
|
cbPetStatus.valueProperty().addListener((obs, oldVal, newVal) -> {
|
||||||
boolean isOwned = "Owned".equalsIgnoreCase(newVal);
|
updateStatusFieldVisibility(newVal);
|
||||||
boolean isAvailable = "Available".equalsIgnoreCase(newVal) || "Unadopted".equalsIgnoreCase(newVal);
|
|
||||||
cbCustomer.setVisible(isOwned);
|
|
||||||
cbStore.setVisible(isAvailable);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
btnSave.setOnMouseClicked(new EventHandler<MouseEvent>() {
|
btnSave.setOnMouseClicked(new EventHandler<MouseEvent>() {
|
||||||
@@ -166,6 +170,9 @@ public class PetDialogController {
|
|||||||
if ("Owned".equalsIgnoreCase(selectedStatus) && cbCustomer.getValue() == null) {
|
if ("Owned".equalsIgnoreCase(selectedStatus) && cbCustomer.getValue() == null) {
|
||||||
errorMsg += "Customer is required for Owned status\n";
|
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)
|
//Check validation (length size)
|
||||||
errorMsg += Validator.isLessThanVarChars(txtPetName.getText(), "Pet Name", 50);
|
errorMsg += Validator.isLessThanVarChars(txtPetName.getText(), "Pet Name", 50);
|
||||||
@@ -243,7 +250,7 @@ public class PetDialogController {
|
|||||||
if ("Owned".equalsIgnoreCase(status) && cbCustomer.getValue() != null) {
|
if ("Owned".equalsIgnoreCase(status) && cbCustomer.getValue() != null) {
|
||||||
request.setCustomerId(cbCustomer.getValue().getId());
|
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());
|
request.setStoreId(cbStore.getValue().getId());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -339,8 +346,10 @@ public class PetDialogController {
|
|||||||
for (String status : cbPetStatus.getItems()) {
|
for (String status : cbPetStatus.getItems()) {
|
||||||
if(status.equals(pet.getPetStatus())){
|
if(status.equals(pet.getPetStatus())){
|
||||||
cbPetStatus.getSelectionModel().select(status);
|
cbPetStatus.getSelectionModel().select(status);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
updateStatusFieldVisibility(cbPetStatus.getValue());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -358,6 +367,7 @@ public class PetDialogController {
|
|||||||
lblPetId.setVisible(true);
|
lblPetId.setVisible(true);
|
||||||
refreshImagePreview();
|
refreshImagePreview();
|
||||||
}
|
}
|
||||||
|
updateStatusFieldVisibility(cbPetStatus.getValue());
|
||||||
}
|
}
|
||||||
|
|
||||||
private void handleChangeImage() {
|
private void handleChangeImage() {
|
||||||
@@ -421,4 +431,25 @@ public class PetDialogController {
|
|||||||
btnRemoveImage.setDisable(true);
|
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 org.example.petshopdesktop.util.ActivityLogger;
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
public class ProductSupplierDialogController {
|
public class ProductSupplierDialogController {
|
||||||
|
|
||||||
@@ -46,6 +47,8 @@ public class ProductSupplierDialogController {
|
|||||||
private String mode = null;
|
private String mode = null;
|
||||||
private int selectedSupId = -1;
|
private int selectedSupId = -1;
|
||||||
private int selectedProdId = -1;
|
private int selectedProdId = -1;
|
||||||
|
private Long pendingSupplierId = null;
|
||||||
|
private Long pendingProductId = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* add event listeners to buttons and set up combobox
|
* add event listeners to buttons and set up combobox
|
||||||
@@ -120,9 +123,11 @@ public class ProductSupplierDialogController {
|
|||||||
Platform.runLater(() -> {
|
Platform.runLater(() -> {
|
||||||
if (suppliers != null) {
|
if (suppliers != null) {
|
||||||
cbSupplier.setItems(FXCollections.observableArrayList(suppliers));
|
cbSupplier.setItems(FXCollections.observableArrayList(suppliers));
|
||||||
|
applyPendingSupplierSelection();
|
||||||
}
|
}
|
||||||
if (products != null) {
|
if (products != null) {
|
||||||
cbProduct.setItems(FXCollections.observableArrayList(products));
|
cbProduct.setItems(FXCollections.observableArrayList(products));
|
||||||
|
applyPendingProductSelection();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
@@ -220,21 +225,14 @@ public class ProductSupplierDialogController {
|
|||||||
* @param productSupplier
|
* @param productSupplier
|
||||||
*/
|
*/
|
||||||
public void displayProductSupplierDetails(ProductSupplierDTO productSupplier){
|
public void displayProductSupplierDetails(ProductSupplierDTO productSupplier){
|
||||||
if(productSupplier != null){
|
if (productSupplier == null) {
|
||||||
txtCost.setText(productSupplier.getCost() + "");
|
return;
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
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) {
|
public void setMode(String mode) {
|
||||||
this.mode = mode;
|
this.mode = mode;
|
||||||
lblMode.setText(mode + " Product");
|
lblMode.setText(mode + " Product-Supplier");
|
||||||
lblProductSupplierId.setVisible(false);
|
lblProductSupplierId.setVisible(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -267,4 +265,38 @@ public class ProductSupplierDialogController {
|
|||||||
this.selectedProdId = prodId;
|
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
|
@FXML
|
||||||
public void initialize() {
|
public void initialize() {
|
||||||
setupTables();
|
setupTables();
|
||||||
cbPaymentMethod.setItems(FXCollections.observableArrayList("Cash", "Card", "Debit"));
|
cbPaymentMethod.setItems(FXCollections.observableArrayList("Cash", "Card"));
|
||||||
cbPaymentMethod.getSelectionModel().selectFirst();
|
cbPaymentMethod.getSelectionModel().selectFirst();
|
||||||
updateRefundTotal();
|
updateRefundTotal();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -153,7 +153,7 @@
|
|||||||
</TextField>
|
</TextField>
|
||||||
</children>
|
</children>
|
||||||
</VBox>
|
</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>
|
<children>
|
||||||
<Label text="Customer:" textFill="#2c3e50">
|
<Label text="Customer:" textFill="#2c3e50">
|
||||||
<font>
|
<font>
|
||||||
@@ -167,7 +167,7 @@
|
|||||||
</ComboBox>
|
</ComboBox>
|
||||||
</children>
|
</children>
|
||||||
</VBox>
|
</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>
|
<children>
|
||||||
<Label text="Store:" textFill="#2c3e50">
|
<Label text="Store:" textFill="#2c3e50">
|
||||||
<font>
|
<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() {
|
function AppointmentsPage() {
|
||||||
const { user, token, loading: authLoading } = useAuth();
|
const { user, token, loading: authLoading } = useAuth();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -224,7 +318,9 @@ function AppointmentsPage() {
|
|||||||
const [appointments, setAppointments] = useState([]);
|
const [appointments, setAppointments] = useState([]);
|
||||||
const [loadingAppointments, setLoadingAppointments] = useState(false);
|
const [loadingAppointments, setLoadingAppointments] = useState(false);
|
||||||
|
|
||||||
const canBookAppointments = user?.role === "CUSTOMER";
|
const [showAddPetModal, setShowAddPetModal] = useState(false);
|
||||||
|
|
||||||
|
const canBookAppointments = user?.role === "CUSTOMER" || user?.role === "ADMIN";
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!authLoading && !user) {
|
if (!authLoading && !user) {
|
||||||
@@ -234,6 +330,16 @@ function AppointmentsPage() {
|
|||||||
|
|
||||||
}, [authLoading, user, router, preselectedPetId]);
|
}, [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(() => {
|
useEffect(() => {
|
||||||
if (!token) {
|
if (!token) {
|
||||||
return;
|
return;
|
||||||
@@ -256,15 +362,8 @@ function AppointmentsPage() {
|
|||||||
.then((data) => setAllPets(data.content ?? []))
|
.then((data) => setAllPets(data.content ?? []))
|
||||||
.catch(() => {});
|
.catch(() => {});
|
||||||
|
|
||||||
if (canBookAppointments) {
|
loadCustomerPets();
|
||||||
fetch(`${API_BASE}/api/v1/my-pets`, {
|
}, [token, loadCustomerPets]);
|
||||||
headers: { Authorization: `Bearer ${token}` },
|
|
||||||
})
|
|
||||||
.then((r) => r.json())
|
|
||||||
.then((data) => setCustomerPets(Array.isArray(data) ? data : []))
|
|
||||||
.catch(() => {});
|
|
||||||
}
|
|
||||||
}, [token, canBookAppointments]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (didPreselectRef.current) {
|
if (didPreselectRef.current) {
|
||||||
@@ -392,7 +491,6 @@ function AppointmentsPage() {
|
|||||||
|
|
||||||
function getMinDate() {
|
function getMinDate() {
|
||||||
const d = new Date();
|
const d = new Date();
|
||||||
d.setDate(d.getDate() + 1);
|
|
||||||
|
|
||||||
return d.toISOString().split("T")[0];
|
return d.toISOString().split("T")[0];
|
||||||
}
|
}
|
||||||
@@ -411,12 +509,6 @@ function AppointmentsPage() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!user?.customerId) {
|
|
||||||
setError("Customer account not found. Please contact support.");
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (selectedPetIds.length === 0) {
|
if (selectedPetIds.length === 0) {
|
||||||
setError(isAdoptionService ? "Please select a pet to adopt." : "Please select at least one pet.");
|
setError(isAdoptionService ? "Please select a pet to adopt." : "Please select at least one pet.");
|
||||||
|
|
||||||
@@ -427,7 +519,7 @@ function AppointmentsPage() {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const body = {
|
const body = {
|
||||||
customerId: user.customerId,
|
customerId: user.customerId || user.id,
|
||||||
storeId: Number(storeId),
|
storeId: Number(storeId),
|
||||||
serviceId: Number(serviceId),
|
serviceId: Number(serviceId),
|
||||||
employeeId: employeeId ? Number(employeeId) : undefined,
|
employeeId: employeeId ? Number(employeeId) : undefined,
|
||||||
@@ -436,12 +528,8 @@ function AppointmentsPage() {
|
|||||||
appointmentStatus: "Booked",
|
appointmentStatus: "Booked",
|
||||||
};
|
};
|
||||||
|
|
||||||
if (isCustomerPetService) {
|
if (selectedPetIds.length > 0) {
|
||||||
body.customerPetIds = selectedPetIds;
|
body.petId = selectedPetIds[0];
|
||||||
}
|
|
||||||
|
|
||||||
else {
|
|
||||||
body.petIds = selectedPetIds;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const res = await fetch(`${API_BASE}/api/v1/appointments`, {
|
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 petSectionLabel = isAdoptionService ? "Select a Pet to Adopt" : "Select Pet(s)";
|
||||||
const noPetsMessage = isAdoptionService
|
const noPetsMessage = isAdoptionService
|
||||||
? "No pets are currently available for adoption."
|
? "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 (
|
return (
|
||||||
<main className="appt-page">
|
<main className="appt-page">
|
||||||
|
{showAddPetModal && (
|
||||||
|
<AddPetModal
|
||||||
|
token={token}
|
||||||
|
onClose={() => setShowAddPetModal(false)}
|
||||||
|
onAdded={loadCustomerPets}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
<section className="appt-hero">
|
<section className="appt-hero">
|
||||||
<h1 className="appt-hero-title">Schedule an Appointment</h1>
|
<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>
|
<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 && (
|
{serviceId && (
|
||||||
<div className="appt-label">
|
<div className="appt-label">
|
||||||
<span>{petSectionLabel}</span>
|
<span>{petSectionLabel}</span>
|
||||||
|
{isCustomerPetService && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="appt-add-pet-btn"
|
||||||
|
onClick={() => setShowAddPetModal(true)}
|
||||||
|
>
|
||||||
|
+ Add New Pet
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
{petsToShow.length === 0 ? (
|
{petsToShow.length === 0 ? (
|
||||||
<p className="appt-no-slots">{noPetsMessage}</p>
|
<p className="appt-no-slots">{noPetsMessage}</p>
|
||||||
) : isAdoptionService ? (
|
) : isAdoptionService ? (
|
||||||
|
|||||||
@@ -1351,6 +1351,44 @@ body {
|
|||||||
accent-color: orange;
|
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 {
|
.appt-link {
|
||||||
color: orange;
|
color: orange;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
|
|||||||
@@ -108,7 +108,7 @@ export default function ProfilePage() {
|
|||||||
}, [clearPetImageObjectUrls]);
|
}, [clearPetImageObjectUrls]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (user?.role === "CUSTOMER") {
|
if (user?.role === "CUSTOMER" || user?.role === "ADMIN") {
|
||||||
loadPets();
|
loadPets();
|
||||||
}
|
}
|
||||||
}, [user, loadPets]);
|
}, [user, loadPets]);
|
||||||
@@ -419,7 +419,7 @@ export default function ProfilePage() {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{user.role === "CUSTOMER" && (
|
{(user.role === "CUSTOMER" || user.role === "ADMIN") && (
|
||||||
<div className="profile-pets-section">
|
<div className="profile-pets-section">
|
||||||
<div className="profile-pets-header">
|
<div className="profile-pets-header">
|
||||||
<h2 className="profile-pets-title">My Pets</h2>
|
<h2 className="profile-pets-title">My Pets</h2>
|
||||||
|
|||||||
Reference in New Issue
Block a user