Pet owner store #139

Merged
RecentRunner merged 3 commits from pet-owner-store into main 2026-04-06 16:12:09 -06:00
19 changed files with 671 additions and 44 deletions

View File

@@ -564,7 +564,7 @@
"name": "Get All Pets",
"request": {
"method": "GET",
"url": "{{baseUrl}}/api/v1/pets",
"url": "{{baseUrl}}/api/v1/pets?status=available&storeId=1",
"header": [
{
"key": "Content-Type",
@@ -585,7 +585,10 @@
"exec": [
"pm.test('Status code is 200', function () {",
" pm.response.to.have.status(200);",
"});"
"});",
"var json = pm.response.json();",
"pm.test('is page response', function () { pm.expect(json.content).to.be.an('array'); });",
"pm.test('all pets have storeName', function () { json.content.forEach(function(p) { pm.expect(p).to.have.property('storeName'); }); });"
]
}
}
@@ -616,7 +619,10 @@
"exec": [
"pm.test('Status code is 200', function () {",
" pm.response.to.have.status(200);",
"});"
"});",
"var json = pm.response.json();",
"pm.test('has petId', function () { pm.expect(json.petId).to.be.a('number'); });",
"pm.test('has owner fields', function () { pm.expect(json).to.have.all.keys('petId','petName','petSpecies','petBreed','petAge','petStatus','petPrice','imageUrl','createdAt','updatedAt','customerId','customerName','storeId','storeName'); });"
]
}
}
@@ -671,7 +677,7 @@
],
"body": {
"mode": "raw",
"raw": "{\n \"petName\": \"Postman Pet\",\n \"petSpecies\": \"Dog\",\n \"petBreed\": \"Mixed\",\n \"petAge\": 2,\n \"petStatus\": \"Available\",\n \"petPrice\": 350.00\n}",
"raw": "{\n \"petName\": \"Postman Pet\",\n \"petSpecies\": \"Dog\",\n \"petBreed\": \"Mixed\",\n \"petAge\": 2,\n \"petStatus\": \"Available\",\n \"petPrice\": 350.00,\n \"storeId\": 1\n}",
"options": {
"raw": {
"language": "json"
@@ -689,7 +695,11 @@
" pm.response.to.have.status(201);",
"});",
"var jsonData = pm.response.json();",
"if (jsonData.petId !== undefined) pm.collectionVariables.set('petId', jsonData.petId);"
"if (jsonData.petId !== undefined) pm.collectionVariables.set('petId', jsonData.petId);",
"pm.test('has petId', function () { pm.expect(jsonData.petId).to.be.a('number'); });",
"pm.test('has storeId', function () { pm.expect(jsonData.storeId).to.equal(1); });",
"pm.test('has storeName', function () { pm.expect(jsonData.storeName).to.be.a('string'); });",
"pm.test('customerId is null', function () { pm.expect(jsonData.customerId).to.be.null; });"
]
}
}
@@ -713,7 +723,7 @@
],
"body": {
"mode": "raw",
"raw": "{\n \"petName\": \"Postman Pet Updated\",\n \"petSpecies\": \"Dog\",\n \"petBreed\": \"Mixed\",\n \"petAge\": 3,\n \"petStatus\": \"Available\",\n \"petPrice\": 375.00\n}",
"raw": "{\n \"petName\": \"Postman Pet Updated\",\n \"petSpecies\": \"Dog\",\n \"petBreed\": \"Mixed\",\n \"petAge\": 3,\n \"petStatus\": \"Owned\",\n \"petPrice\": 375.00,\n \"customerId\": 1\n}",
"options": {
"raw": {
"language": "json"
@@ -729,7 +739,12 @@
"exec": [
"pm.test('Status code is 200', function () {",
" pm.response.to.have.status(200);",
"});"
"});",
"var json = pm.response.json();",
"pm.test('status is Owned', function () { pm.expect(json.petStatus).to.equal('Owned'); });",
"pm.test('has customerId', function () { pm.expect(json.customerId).to.be.a('number'); });",
"pm.test('has customerName', function () { pm.expect(json.customerName).to.be.a('string'); });",
"pm.test('storeId is null', function () { pm.expect(json.storeId).to.be.null; });"
]
}
}

View File

@@ -27,8 +27,9 @@ public class PetController {
@RequestParam(required = false) String q,
@RequestParam(required = false) String species,
@RequestParam(required = false) String status,
@RequestParam(required = false) Long storeId,
Pageable pageable) {
return ResponseEntity.ok(petService.getAllPets(q, species, status, pageable));
return ResponseEntity.ok(petService.getAllPets(q, species, status, storeId, pageable));
}
@GetMapping("/{id}")

View File

@@ -23,6 +23,10 @@ public class PetRequest {
private BigDecimal petPrice;
private Long customerId;
private Long storeId;
public String getPetName() {
return petName;
}
@@ -71,6 +75,22 @@ public class PetRequest {
this.petPrice = petPrice;
}
public Long getCustomerId() {
return customerId;
}
public void setCustomerId(Long customerId) {
this.customerId = customerId;
}
public Long getStoreId() {
return storeId;
}
public void setStoreId(Long storeId) {
this.storeId = storeId;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;

View File

@@ -15,11 +15,15 @@ public class PetResponse {
private String imageUrl;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
private Long customerId;
private String customerName;
private Long storeId;
private String storeName;
public PetResponse() {
}
public PetResponse(Long petId, String petName, String petSpecies, String petBreed, Integer petAge, String petStatus, BigDecimal petPrice, String imageUrl, LocalDateTime createdAt, LocalDateTime updatedAt) {
public PetResponse(Long petId, String petName, String petSpecies, String petBreed, Integer petAge, String petStatus, BigDecimal petPrice, String imageUrl, LocalDateTime createdAt, LocalDateTime updatedAt, Long customerId, String customerName, Long storeId, String storeName) {
this.petId = petId;
this.petName = petName;
this.petSpecies = petSpecies;
@@ -30,6 +34,10 @@ public class PetResponse {
this.imageUrl = imageUrl;
this.createdAt = createdAt;
this.updatedAt = updatedAt;
this.customerId = customerId;
this.customerName = customerName;
this.storeId = storeId;
this.storeName = storeName;
}
public Long getPetId() {
@@ -112,17 +120,49 @@ public class PetResponse {
this.updatedAt = updatedAt;
}
public Long getCustomerId() {
return customerId;
}
public void setCustomerId(Long customerId) {
this.customerId = customerId;
}
public String getCustomerName() {
return customerName;
}
public void setCustomerName(String customerName) {
this.customerName = customerName;
}
public Long getStoreId() {
return storeId;
}
public void setStoreId(Long storeId) {
this.storeId = storeId;
}
public String getStoreName() {
return storeName;
}
public void setStoreName(String storeName) {
this.storeName = storeName;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
PetResponse that = (PetResponse) o;
return Objects.equals(petId, that.petId) && Objects.equals(petName, that.petName) && Objects.equals(petSpecies, that.petSpecies) && Objects.equals(petBreed, that.petBreed) && Objects.equals(petAge, that.petAge) && Objects.equals(petStatus, that.petStatus) && Objects.equals(petPrice, that.petPrice) && Objects.equals(imageUrl, that.imageUrl) && Objects.equals(createdAt, that.createdAt) && Objects.equals(updatedAt, that.updatedAt);
return Objects.equals(petId, that.petId) && Objects.equals(petName, that.petName) && Objects.equals(petSpecies, that.petSpecies) && Objects.equals(petBreed, that.petBreed) && Objects.equals(petAge, that.petAge) && Objects.equals(petStatus, that.petStatus) && Objects.equals(petPrice, that.petPrice) && Objects.equals(imageUrl, that.imageUrl) && Objects.equals(createdAt, that.createdAt) && Objects.equals(updatedAt, that.updatedAt) && Objects.equals(customerId, that.customerId) && Objects.equals(customerName, that.customerName) && Objects.equals(storeId, that.storeId) && Objects.equals(storeName, that.storeName);
}
@Override
public int hashCode() {
return Objects.hash(petId, petName, petSpecies, petBreed, petAge, petStatus, petPrice, imageUrl, createdAt, updatedAt);
return Objects.hash(petId, petName, petSpecies, petBreed, petAge, petStatus, petPrice, imageUrl, createdAt, updatedAt, customerId, customerName, storeId, storeName);
}
@Override
@@ -138,6 +178,10 @@ public class PetResponse {
", imageUrl='" + imageUrl + '\'' +
", createdAt=" + createdAt +
", updatedAt=" + updatedAt +
", customerId=" + customerId +
", customerName='" + customerName + '\'' +
", storeId=" + storeId +
", storeName='" + storeName + '\'' +
'}';
}
}

View File

@@ -38,6 +38,14 @@ public class Pet {
@Column(length = 255)
private String imageUrl;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "customerId")
private Customer customer;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "storeId")
private StoreLocation store;
@CreationTimestamp
@Column(name = "created_at", updatable = false)
private LocalDateTime createdAt;
@@ -142,6 +150,22 @@ public class Pet {
this.updatedAt = updatedAt;
}
public Customer getCustomer() {
return customer;
}
public void setCustomer(Customer customer) {
this.customer = customer;
}
public StoreLocation getStore() {
return store;
}
public void setStore(StoreLocation store) {
this.store = store;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;

View File

@@ -18,16 +18,18 @@ public interface PetRepository extends JpaRepository<Pet, Long> {
@Query("SELECT p FROM Pet p WHERE " +
"(:q IS NULL OR LOWER(p.petName) LIKE LOWER(CONCAT('%', :q, '%')) OR LOWER(p.petSpecies) LIKE LOWER(CONCAT('%', :q, '%')) OR LOWER(p.petBreed) LIKE LOWER(CONCAT('%', :q, '%'))) AND " +
"(:species IS NULL OR LOWER(p.petSpecies) = LOWER(:species)) AND " +
"(:status IS NULL OR LOWER(p.petStatus) = LOWER(:status))")
Page<Pet> searchPets(@Param("q") String query, @Param("species") String species, @Param("status") String status, Pageable pageable);
"(:status IS NULL OR LOWER(p.petStatus) = LOWER(:status)) AND " +
"(:storeId IS NULL OR p.store.storeId = :storeId)")
Page<Pet> searchPets(@Param("q") String query, @Param("species") String species, @Param("status") String status, @Param("storeId") Long storeId, Pageable pageable);
@Query("SELECT p FROM Pet p WHERE LOWER(p.petStatus) = 'available' AND " +
"(:q IS NULL OR LOWER(p.petName) LIKE LOWER(CONCAT('%', :q, '%')) OR LOWER(p.petSpecies) LIKE LOWER(CONCAT('%', :q, '%')) OR LOWER(p.petBreed) LIKE LOWER(CONCAT('%', :q, '%'))) AND " +
"(:species IS NULL OR LOWER(p.petSpecies) = LOWER(:species))")
Page<Pet> searchPublicPets(@Param("q") String query, @Param("species") String species, Pageable pageable);
"(:species IS NULL OR LOWER(p.petSpecies) = LOWER(:species)) AND " +
"(:storeId IS NULL OR p.store.storeId = :storeId)")
Page<Pet> searchPublicPets(@Param("q") String query, @Param("species") String species, @Param("storeId") Long storeId, Pageable pageable);
@Query("SELECT DISTINCT p FROM Pet p LEFT JOIN Adoption a ON a.pet = p AND LOWER(a.adoptionStatus) = 'completed' WHERE " +
"(LOWER(p.petStatus) = 'available' OR a.customer.userId = :userId) AND " +
"(LOWER(p.petStatus) = 'available' OR a.customer.userId = :userId OR (LOWER(p.petStatus) = 'owned' AND p.customer.userId = :userId)) AND " +
"(:q IS NULL OR LOWER(p.petName) LIKE LOWER(CONCAT('%', :q, '%')) OR LOWER(p.petSpecies) LIKE LOWER(CONCAT('%', :q, '%')) OR LOWER(p.petBreed) LIKE LOWER(CONCAT('%', :q, '%'))) AND " +
"(:species IS NULL OR LOWER(p.petSpecies) = LOWER(:species)) AND " +
"(:status IS NULL OR LOWER(p.petStatus) = LOWER(:status))")

View File

@@ -4,12 +4,16 @@ import com.petshop.backend.dto.common.BulkDeleteRequest;
import com.petshop.backend.dto.pet.PetRequest;
import com.petshop.backend.dto.pet.PetResponse;
import com.petshop.backend.entity.Adoption;
import com.petshop.backend.entity.Customer;
import com.petshop.backend.entity.Pet;
import com.petshop.backend.entity.StoreLocation;
import com.petshop.backend.entity.User;
import com.petshop.backend.exception.ResourceNotFoundException;
import com.petshop.backend.security.AppPrincipal;
import com.petshop.backend.repository.AdoptionRepository;
import com.petshop.backend.repository.CustomerRepository;
import com.petshop.backend.repository.PetRepository;
import com.petshop.backend.repository.StoreRepository;
import org.springframework.core.io.Resource;
import org.springframework.http.MediaType;
import org.springframework.data.domain.Page;
@@ -29,15 +33,20 @@ public class PetService {
private final PetRepository petRepository;
private final AdoptionRepository adoptionRepository;
private final CustomerRepository customerRepository;
private final StoreRepository storeRepository;
private final CatalogImageStorageService catalogImageStorageService;
public PetService(PetRepository petRepository, AdoptionRepository adoptionRepository, CatalogImageStorageService catalogImageStorageService) {
public PetService(PetRepository petRepository, AdoptionRepository adoptionRepository, CustomerRepository customerRepository, StoreRepository storeRepository, CatalogImageStorageService catalogImageStorageService) {
this.petRepository = petRepository;
this.adoptionRepository = adoptionRepository;
this.customerRepository = customerRepository;
this.storeRepository = storeRepository;
this.catalogImageStorageService = catalogImageStorageService;
}
public Page<PetResponse> getAllPets(String query, String species, String status, Pageable pageable) {
@Transactional(readOnly = true)
public Page<PetResponse> getAllPets(String query, String species, String status, Long storeId, Pageable pageable) {
String normalizedQuery = normalizeFilter(query);
String normalizedSpecies = normalizeFilter(species);
String normalizedStatus = normalizeFilter(status);
@@ -48,22 +57,23 @@ public class PetService {
if (!isAllowedPublicStatus(normalizedStatus)) {
return new PageImpl<>(java.util.List.of(), pageable, 0);
}
pets = petRepository.searchPublicPets(normalizedQuery, normalizedSpecies, pageable);
pets = petRepository.searchPublicPets(normalizedQuery, normalizedSpecies, storeId, pageable);
} else if (viewer.role() == User.Role.STAFF || viewer.role() == User.Role.ADMIN) {
pets = petRepository.searchPets(normalizedQuery, normalizedSpecies, normalizedStatus, pageable);
pets = petRepository.searchPets(normalizedQuery, normalizedSpecies, normalizedStatus, storeId, pageable);
} else if (viewer.role() == User.Role.CUSTOMER) {
if (!isAllowedCustomerStatus(normalizedStatus)) {
return new PageImpl<>(java.util.List.of(), pageable, 0);
}
pets = petRepository.searchCustomerVisiblePets(viewer.userId(), normalizedQuery, normalizedSpecies, normalizedStatus, pageable);
} else {
pets = petRepository.searchPublicPets(normalizedQuery, normalizedSpecies, pageable);
pets = petRepository.searchPublicPets(normalizedQuery, normalizedSpecies, storeId, pageable);
}
return pets
.map(this::mapToResponse);
}
@Transactional(readOnly = true)
public PetResponse getPetById(Long id) {
Pet pet = petRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("Pet not found with id: " + id));
@@ -82,6 +92,7 @@ public class PetService {
pet.setPetAge(request.getPetAge());
pet.setPetStatus(request.getPetStatus());
pet.setPetPrice(request.getPetPrice());
applyOwnerAndStore(pet, request);
pet = petRepository.save(pet);
return mapToResponse(pet);
@@ -98,6 +109,7 @@ public class PetService {
pet.setPetAge(request.getPetAge());
pet.setPetStatus(request.getPetStatus());
pet.setPetPrice(request.getPetPrice());
applyOwnerAndStore(pet, request);
pet = petRepository.save(pet);
return mapToResponse(pet);
@@ -161,6 +173,9 @@ public class PetService {
if (viewer == null || viewer.userId() == null) {
return false;
}
if (isOwnedByUser(pet, viewer.userId())) {
return true;
}
return isAdoptedByUser(pet, viewer.userId());
}
@@ -230,7 +245,7 @@ public class PetService {
}
private boolean isAllowedCustomerStatus(String status) {
return status == null || "available".equalsIgnoreCase(status) || "adopted".equalsIgnoreCase(status);
return status == null || "available".equalsIgnoreCase(status) || "adopted".equalsIgnoreCase(status) || "owned".equalsIgnoreCase(status);
}
private String normalizeFilter(String value) {
@@ -242,6 +257,8 @@ public class PetService {
}
private PetResponse mapToResponse(Pet pet) {
Customer customer = pet.getCustomer();
StoreLocation store = pet.getStore();
return new PetResponse(
pet.getPetId(),
pet.getPetName(),
@@ -252,10 +269,59 @@ public class PetService {
pet.getPetPrice(),
pet.getImageUrl() != null && !pet.getImageUrl().isBlank() ? "/api/v1/pets/" + pet.getPetId() + "/image" : null,
pet.getCreatedAt(),
pet.getUpdatedAt()
pet.getUpdatedAt(),
customer != null ? customer.getCustomerId() : null,
customer != null ? customer.getFirstName() + " " + customer.getLastName() : null,
store != null ? store.getStoreId() : null,
store != null ? store.getStoreName() : null
);
}
private void applyOwnerAndStore(Pet pet, PetRequest request) {
if ("owned".equalsIgnoreCase(request.getPetStatus())) {
if (request.getCustomerId() != null) {
Customer customer = customerRepository.findById(request.getCustomerId())
.orElseThrow(() -> new ResourceNotFoundException("Customer not found with id: " + request.getCustomerId()));
pet.setCustomer(customer);
} else {
pet.setCustomer(null);
}
pet.setStore(null);
} else if ("available".equalsIgnoreCase(request.getPetStatus()) || "unadopted".equalsIgnoreCase(request.getPetStatus())) {
if (request.getStoreId() != null) {
StoreLocation store = storeRepository.findById(request.getStoreId())
.orElseThrow(() -> new ResourceNotFoundException("Store not found with id: " + request.getStoreId()));
pet.setStore(store);
} else {
pet.setStore(null);
}
pet.setCustomer(null);
} else {
if (request.getCustomerId() != null) {
Customer customer = customerRepository.findById(request.getCustomerId())
.orElseThrow(() -> new ResourceNotFoundException("Customer not found with id: " + request.getCustomerId()));
pet.setCustomer(customer);
} else {
pet.setCustomer(null);
}
if (request.getStoreId() != null) {
StoreLocation store = storeRepository.findById(request.getStoreId())
.orElseThrow(() -> new ResourceNotFoundException("Store not found with id: " + request.getStoreId()));
pet.setStore(store);
} else {
pet.setStore(null);
}
}
}
private boolean isOwnedByUser(Pet pet, Long userId) {
if (!"owned".equalsIgnoreCase(normalizeStatus(pet.getPetStatus()))) {
return false;
}
Customer customer = pet.getCustomer();
return customer != null && userId.equals(customer.getUserId());
}
public record ImagePayload(Resource resource, MediaType mediaType) {
}

View File

@@ -0,0 +1,23 @@
ALTER TABLE pet ADD COLUMN customerId BIGINT NULL;
ALTER TABLE pet ADD COLUMN storeId BIGINT NULL;
ALTER TABLE pet ADD CONSTRAINT fk_pet_customer
FOREIGN KEY (customerId) REFERENCES customer(customerId);
ALTER TABLE pet ADD CONSTRAINT fk_pet_store
FOREIGN KEY (storeId) REFERENCES storeLocation(storeId);
CREATE INDEX idx_pet_customerId ON pet(customerId);
CREATE INDEX idx_pet_storeId ON pet(storeId);
UPDATE pet
SET storeId = (SELECT storeId FROM storeLocation ORDER BY storeId ASC LIMIT 1)
WHERE LOWER(petStatus) IN ('available', 'unadopted');
UPDATE pet p
JOIN (
SELECT a.petId, a.customerId
FROM adoption a
WHERE LOWER(a.adoptionStatus) = 'completed'
) latest ON latest.petId = p.petId
SET p.customerId = latest.customerId
WHERE LOWER(p.petStatus) = 'adopted';

View File

@@ -0,0 +1,5 @@
INSERT INTO pet (petName, petSpecies, petBreed, petAge, petStatus, petPrice, customerId)
VALUES
('Pepper', 'Cat', 'Tabby', 3, 'Owned', 0.00, 1),
('Coco', 'Dog', 'Pomeranian', 2, 'Owned', 0.00, 4),
('Finn', 'Dog', 'Border Collie', 5, 'Owned', 0.00, 6);

View File

@@ -0,0 +1,144 @@
INSERT INTO customer (firstName, lastName, email) VALUES
('Noah', 'Parker', 'noah@gmail.com'),
('Mia', 'Evans', 'mia@gmail.com'),
('Ethan', 'Scott', 'ethan@gmail.com'),
('Chloe', 'Adams', 'chloe@gmail.com'),
('Lucas', 'Baker', 'lucas@gmail.com'),
('Lily', 'Hall', 'lily@gmail.com'),
('Mason', 'Rivera', 'mason@gmail.com'),
('Ella', 'Mitchell', 'ella@gmail.com'),
('James', 'Carter', 'jcarter@gmail.com'),
('Harper', 'Collins', 'harper@gmail.com');
INSERT INTO pet (petName, petSpecies, petBreed, petAge, petStatus, petPrice, storeId) VALUES
('Rocky', 'Dog', 'German Shepherd', 1, 'Available', 475.00, 1),
('Daisy', 'Dog', 'Poodle', 2, 'Available', 512.00, 1),
('Cooper', 'Dog', 'Bulldog', 3, 'Available', 560.00, 1),
('Ruby', 'Dog', 'Boxer', 4, 'Available', 575.00, 1),
('Tucker', 'Dog', 'Dachshund', 5, 'Available', 634.00, 1),
('Rosie', 'Dog', 'Shih Tzu', 1, 'Available', 660.00, 2),
('Bear', 'Dog', 'Rottweiler', 2, 'Available', 686.00, 2),
('Maggie', 'Dog', 'Corgi', 3, 'Available', 745.00, 2),
('Leo', 'Dog', 'Husky', 4, 'Available', 749.00, 2),
('Zoey', 'Cat', 'Ragdoll', 1, 'Available', 420.00, 1),
('Oliver', 'Cat', 'British Shorthair', 2, 'Available', 395.00, 1),
('Lola', 'Cat', 'Bengal', 3, 'Available', 465.00, 3),
('Buster', 'Dog', 'Beagle', 2, 'Available', 440.00, 3),
('Sadie', 'Dog', 'Golden Retriever', 1, 'Available', 535.00, 3),
('Toby', 'Dog', 'Labrador', 5, 'Available', 490.00, 1),
('Cleo', 'Cat', 'Abyssinian', 2, 'Available', 375.00, 2),
('Harley', 'Dog', 'Dalmatian', 3, 'Available', 520.00, 1),
('Mocha', 'Cat', 'Burmese', 1, 'Available', 345.00, 3),
('Rex', 'Dog', 'Doberman', 4, 'Available', 610.00, 1),
('Willow', 'Cat', 'Scottish Fold', 2, 'Available', 480.00, 2),
('Gizmo', 'Dog', 'Pomeranian', 1, 'Available', 530.00, 1),
('Nala', 'Cat', 'Siamese', 3, 'Available', 360.00, 2),
('Duke', 'Dog', 'Great Dane', 2, 'Available', 720.00, 3),
('Misty', 'Cat', 'Russian Blue', 4, 'Available', 410.00, 1),
('Ace', 'Dog', 'Australian Shepherd', 1, 'Available', 555.00, 1);
INSERT INTO pet (petName, petSpecies, petBreed, petAge, petStatus, petPrice, customerId) VALUES
('Shadow', 'Dog', 'Labrador', 3, 'Adopted', 500.00, 1),
('Kitty', 'Cat', 'Persian', 2, 'Adopted', 320.00, 2),
('Bruno', 'Dog', 'Rottweiler', 4, 'Adopted', 580.00, 3),
('Snowball', 'Cat', 'Turkish Angora', 1, 'Adopted', 390.00, 4),
('Zeus', 'Dog', 'Husky', 3, 'Adopted', 640.00, 5);
INSERT INTO pet (petName, petSpecies, petBreed, petAge, petStatus, petPrice, customerId) VALUES
('Biscuit', 'Dog', 'Beagle', 2, 'Owned', 0.00, 6),
('Patches', 'Cat', 'Calico', 5, 'Owned', 0.00, 7),
('Scout', 'Dog', 'Border Collie', 3, 'Owned', 0.00, 8),
('Mittens', 'Cat', 'Domestic Short', 4, 'Owned', 0.00, 9),
('Thor', 'Dog', 'German Shepherd', 2, 'Owned', 0.00, 10);
INSERT INTO adoption (petId, customerId, employeeId, adoptionDate, adoptionStatus)
SELECT p.petId, p.customerId,
(SELECT e.employeeId FROM employee e JOIN users u ON u.id = e.user_id
WHERE e.isActive = TRUE AND u.role = 'STAFF' ORDER BY e.employeeId LIMIT 1),
'2026-01-10', 'Completed'
FROM pet p WHERE p.petName = 'Shadow' AND p.petStatus = 'Adopted';
INSERT INTO adoption (petId, customerId, employeeId, adoptionDate, adoptionStatus)
SELECT p.petId, p.customerId,
(SELECT e.employeeId FROM employee e JOIN users u ON u.id = e.user_id
WHERE e.isActive = TRUE AND u.role = 'STAFF' ORDER BY e.employeeId LIMIT 1),
'2026-01-18', 'Completed'
FROM pet p WHERE p.petName = 'Kitty' AND p.petStatus = 'Adopted';
INSERT INTO adoption (petId, customerId, employeeId, adoptionDate, adoptionStatus)
SELECT p.petId, p.customerId,
(SELECT e.employeeId FROM employee e JOIN users u ON u.id = e.user_id
WHERE e.isActive = TRUE AND u.role = 'STAFF' ORDER BY e.employeeId LIMIT 1),
'2026-02-03', 'Completed'
FROM pet p WHERE p.petName = 'Bruno' AND p.petStatus = 'Adopted';
INSERT INTO adoption (petId, customerId, employeeId, adoptionDate, adoptionStatus)
SELECT p.petId, p.customerId,
(SELECT e.employeeId FROM employee e JOIN users u ON u.id = e.user_id
WHERE e.isActive = TRUE AND u.role = 'STAFF' ORDER BY e.employeeId LIMIT 1),
'2026-02-14', 'Completed'
FROM pet p WHERE p.petName = 'Snowball' AND p.petStatus = 'Adopted';
INSERT INTO adoption (petId, customerId, employeeId, adoptionDate, adoptionStatus)
SELECT p.petId, p.customerId,
(SELECT e.employeeId FROM employee e JOIN users u ON u.id = e.user_id
WHERE e.isActive = TRUE AND u.role = 'STAFF' ORDER BY e.employeeId LIMIT 1),
'2026-02-21', 'Completed'
FROM pet p WHERE p.petName = 'Zeus' AND p.petStatus = 'Adopted';
INSERT INTO customer_pet (customer_id, pet_name, species, breed) VALUES
(1, 'Rex', 'Dog', 'German Shepherd'),
(2, 'Whiskers', 'Cat', 'Tabby'),
(3, 'Goldie', 'Dog', 'Golden Retriever'),
(4, 'Midnight', 'Cat', 'Black'),
(5, 'Storm', 'Dog', 'Husky'),
(6, 'Peanut', 'Dog', 'Poodle'),
(7, 'Snowball', 'Cat', 'Persian'),
(8, 'Duke', 'Dog', 'Labrador'),
(9, 'Luna', 'Cat', 'Siamese'),
(10, 'Buster', 'Dog', 'Beagle'),
(11, 'Daisy', 'Dog', 'Corgi'),
(12, 'Cleo', 'Cat', 'Ragdoll');
INSERT INTO appointment (serviceId, customerId, appointmentDate, appointmentTime, appointmentStatus, storeId, employeeId) VALUES
(1, 1, '2026-01-10', '09:00:00', 'Completed', 1, 1),
(2, 2, '2026-01-10', '11:00:00', 'Completed', 1, 1),
(3, 3, '2026-01-17', '09:00:00', 'Missed', 1, 1),
(4, 4, '2026-01-17', '14:00:00', 'Completed', 1, 1),
(5, 5, '2026-01-24', '10:00:00', 'Completed', 1, 1),
(1, 6, '2026-01-24', '13:00:00', 'Missed', 1, 1),
(2, 7, '2026-02-07', '09:00:00', 'Completed', 1, 1),
(3, 8, '2026-02-07', '11:00:00', 'Completed', 1, 1),
(1, 9, '2026-01-11', '09:00:00', 'Completed', 1, 2),
(2, 10, '2026-01-11', '11:00:00', 'Missed', 1, 2),
(3, 11, '2026-01-18', '10:00:00', 'Completed', 1, 2),
(4, 12, '2026-01-18', '13:00:00', 'Completed', 1, 2),
(5, 1, '2026-02-01', '09:00:00', 'Completed', 1, 2),
(1, 2, '2026-02-01', '14:00:00', 'Missed', 1, 2),
(2, 3, '2026-02-08', '10:00:00', 'Completed', 1, 2),
(3, 4, '2026-02-08', '13:00:00', 'Completed', 1, 2),
(4, 5, '2026-01-12', '09:00:00', 'Completed', 1, 5),
(5, 6, '2026-01-12', '11:00:00', 'Completed', 1, 5),
(1, 7, '2026-01-19', '09:00:00', 'Missed', 1, 5),
(2, 8, '2026-01-19', '14:00:00', 'Completed', 1, 5),
(3, 9, '2026-02-09', '10:00:00', 'Completed', 1, 5),
(4, 10, '2026-02-09', '13:00:00', 'Completed', 1, 5),
(1, 11, '2026-01-13', '09:00:00', 'Completed', 2, 3),
(2, 12, '2026-01-13', '11:00:00', 'Completed', 2, 3),
(3, 1, '2026-02-10', '09:00:00', 'Missed', 2, 3),
(4, 2, '2026-02-10', '13:00:00', 'Completed', 2, 3),
(1, 3, '2026-01-14', '10:00:00', 'Completed', 3, 4),
(2, 4, '2026-01-14', '13:00:00', 'Completed', 3, 4),
(3, 5, '2026-02-11', '10:00:00', 'Missed', 3, 4),
(4, 6, '2026-02-11', '14:00:00', 'Completed', 3, 4),
(1, 7, '2026-04-15', '09:00:00', 'Booked', 1, 1),
(2, 8, '2026-04-15', '11:00:00', 'Booked', 1, 2),
(3, 9, '2026-04-16', '10:00:00', 'Booked', 1, 5),
(4, 10, '2026-04-17', '09:00:00', 'Booked', 2, 3),
(5, 11, '2026-04-18', '14:00:00', 'Booked', 3, 4);
INSERT INTO appointment_customer_pet (appointment_id, customer_pet_id)
SELECT a.appointmentId,
(((a.appointmentId - 6) % 12) + 1)
FROM appointment a
WHERE a.appointmentId BETWEEN 6 AND 40;

View File

@@ -6,7 +6,9 @@ import com.petshop.backend.entity.Pet;
import com.petshop.backend.entity.User;
import com.petshop.backend.exception.ResourceNotFoundException;
import com.petshop.backend.repository.AdoptionRepository;
import com.petshop.backend.repository.CustomerRepository;
import com.petshop.backend.repository.PetRepository;
import com.petshop.backend.repository.StoreRepository;
import com.petshop.backend.security.AppPrincipal;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
@@ -39,6 +41,12 @@ class PetServiceTest {
@Mock
private AdoptionRepository adoptionRepository;
@Mock
private CustomerRepository customerRepository;
@Mock
private StoreRepository storeRepository;
@Mock
private CatalogImageStorageService catalogImageStorageService;
@@ -54,24 +62,24 @@ class PetServiceTest {
void getAllPetsAnonymousReturnsOnlyPublicPets() {
Pageable pageable = PageRequest.of(0, 10);
Pet availablePet = pet(1L, "Buddy", "Available");
when(petRepository.searchPublicPets(null, null, pageable)).thenReturn(new PageImpl<>(List.of(availablePet), pageable, 1));
when(petRepository.searchPublicPets(null, null, null, pageable)).thenReturn(new PageImpl<>(List.of(availablePet), pageable, 1));
var result = petService.getAllPets(null, null, null, pageable);
var result = petService.getAllPets(null, null, null, null, pageable);
assertEquals(1, result.getTotalElements());
assertEquals("Buddy", result.getContent().get(0).getPetName());
verify(petRepository).searchPublicPets(null, null, pageable);
verify(petRepository, never()).searchPets(null, null, null, pageable);
verify(petRepository).searchPublicPets(null, null, null, pageable);
verify(petRepository, never()).searchPets(null, null, null, null, pageable);
}
@Test
void getAllPetsAnonymousWithAdoptedStatusReturnsEmptyPage() {
Pageable pageable = PageRequest.of(0, 10);
var result = petService.getAllPets(null, null, "Adopted", pageable);
var result = petService.getAllPets(null, null, "Adopted", null, pageable);
assertEquals(0, result.getTotalElements());
verify(petRepository, never()).searchPublicPets(null, null, pageable);
verify(petRepository, never()).searchPublicPets(null, null, null, pageable);
}
@Test
@@ -83,7 +91,7 @@ class PetServiceTest {
when(petRepository.searchCustomerVisiblePets(25L, null, null, null, pageable))
.thenReturn(new PageImpl<>(List.of(availablePet, adoptedPet), pageable, 2));
var result = petService.getAllPets(null, null, null, pageable);
var result = petService.getAllPets(null, null, null, null, pageable);
assertEquals(2, result.getTotalElements());
verify(petRepository).searchCustomerVisiblePets(25L, null, null, null, pageable);
@@ -95,13 +103,13 @@ class PetServiceTest {
setAuthentication(99L, User.Role.ADMIN);
Pet availablePet = pet(1L, "Buddy", "Available");
Pet adoptedPet = pet(2L, "Luna", "Adopted");
when(petRepository.searchPets(null, null, null, pageable))
when(petRepository.searchPets(null, null, null, null, pageable))
.thenReturn(new PageImpl<>(List.of(availablePet, adoptedPet), pageable, 2));
var result = petService.getAllPets(null, null, null, pageable);
var result = petService.getAllPets(null, null, null, null, pageable);
assertEquals(2, result.getTotalElements());
verify(petRepository).searchPets(null, null, null, pageable);
verify(petRepository).searchPets(null, null, null, null, pageable);
}
@Test

View File

@@ -9,6 +9,8 @@ public class PetRequest {
private Integer petAge;
private String petStatus;
private BigDecimal petPrice;
private Long customerId;
private Long storeId;
public PetRequest() {
}
@@ -60,4 +62,20 @@ public class PetRequest {
public void setPetPrice(BigDecimal petPrice) {
this.petPrice = petPrice;
}
public Long getCustomerId() {
return customerId;
}
public void setCustomerId(Long customerId) {
this.customerId = customerId;
}
public Long getStoreId() {
return storeId;
}
public void setStoreId(Long storeId) {
this.storeId = storeId;
}
}

View File

@@ -14,6 +14,10 @@ public class PetResponse {
private String imageUrl;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
private Long customerId;
private String customerName;
private Long storeId;
private String storeName;
public PetResponse() {
}
@@ -97,4 +101,36 @@ public class PetResponse {
public void setUpdatedAt(LocalDateTime updatedAt) {
this.updatedAt = updatedAt;
}
public Long getCustomerId() {
return customerId;
}
public void setCustomerId(Long customerId) {
this.customerId = customerId;
}
public String getCustomerName() {
return customerName;
}
public void setCustomerName(String customerName) {
this.customerName = customerName;
}
public Long getStoreId() {
return storeId;
}
public void setStoreId(Long storeId) {
this.storeId = storeId;
}
public String getStoreName() {
return storeName;
}
public void setStoreName(String storeName) {
this.storeName = storeName;
}
}

View File

@@ -24,7 +24,7 @@ public class PetApi {
return INSTANCE;
}
public List<PetResponse> listPets(String query, String species, String status) throws Exception {
public List<PetResponse> listPets(String query, String species, String status, Long storeId) throws Exception {
String path = "/api/v1/pets?page=0&size=1000";
if (query != null && !query.isEmpty()) {
path += "&q=" + URLEncoder.encode(query, StandardCharsets.UTF_8);
@@ -35,6 +35,9 @@ public class PetApi {
if (status != null && !status.isEmpty()) {
path += "&status=" + URLEncoder.encode(status, StandardCharsets.UTF_8);
}
if (storeId != null) {
path += "&storeId=" + storeId;
}
String response = apiClient.getRawResponse(path);
PageResponse<PetResponse> pageResponse = apiClient.getObjectMapper().readValue(
response,
@@ -46,8 +49,12 @@ public class PetApi {
return pageResponse.getContent();
}
public List<PetResponse> listPets(String query, String species, String status) throws Exception {
return listPets(query, species, status, null);
}
public List<PetResponse> listPets(String query) throws Exception {
return listPets(query, null, null);
return listPets(query, null, null, null);
}
public PetResponse createPet(PetRequest request) throws Exception {

View File

@@ -63,6 +63,12 @@ public class PetController {
@FXML
private TableColumn<Pet, String> colPetStatus;
@FXML
private TableColumn<Pet, String> colCustomerName;
@FXML
private TableColumn<Pet, String> colStoreName;
@FXML
private TableView<Pet> tvPets;
@@ -156,11 +162,13 @@ public class PetController {
colPetAge.setCellValueFactory(new PropertyValueFactory<Pet,Integer>("petAge"));
colPetStatus.setCellValueFactory(new PropertyValueFactory<Pet,String>("petStatus"));
colPetPrice.setCellValueFactory(new PropertyValueFactory<Pet,Double>("petPrice"));
colCustomerName.setCellValueFactory(new PropertyValueFactory<Pet,String>("customerName"));
colStoreName.setCellValueFactory(new PropertyValueFactory<Pet,String>("storeName"));
configureImageColumn(colPetImage);
loadSpeciesFilter();
cbStatusFilter.setItems(FXCollections.observableArrayList("All Statuses", "Available", "Adopted", "Pending"));
cbStatusFilter.setItems(FXCollections.observableArrayList("All Statuses", "Available", "Adopted", "Owned", "Pending"));
cbStatusFilter.getSelectionModel().selectFirst();
displayPets();
@@ -316,7 +324,7 @@ public class PetController {
}
private Pet mapToPet(PetResponse response) {
return new Pet(
Pet pet = new Pet(
response.getPetId().intValue(),
response.getPetName(),
response.getPetSpecies(),
@@ -326,6 +334,11 @@ public class PetController {
response.getPetPrice().doubleValue(),
response.getImageUrl()
);
pet.setCustomerName(response.getCustomerName());
pet.setStoreName(response.getStoreName());
pet.setCustomerId(response.getCustomerId() != null ? response.getCustomerId() : 0L);
pet.setStoreId(response.getStoreId() != null ? response.getStoreId() : 0L);
return pet;
}
private void configureImageColumn(TableColumn<Pet, String> column) {

View File

@@ -1,5 +1,6 @@
package org.example.petshopdesktop.controllers.dialogcontrollers;
import javafx.application.Platform;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.event.EventHandler;
@@ -10,8 +11,10 @@ import javafx.scene.image.ImageView;
import javafx.scene.input.MouseEvent;
import javafx.stage.Stage;
import org.example.petshopdesktop.Validator;
import org.example.petshopdesktop.api.dto.common.DropdownOption;
import org.example.petshopdesktop.api.dto.pet.PetRequest;
import org.example.petshopdesktop.api.dto.pet.PetResponse;
import org.example.petshopdesktop.api.endpoints.DropdownApi;
import org.example.petshopdesktop.api.endpoints.PetApi;
import org.example.petshopdesktop.models.Pet;
import org.example.petshopdesktop.util.ActivityLogger;
@@ -20,6 +23,7 @@ import org.example.petshopdesktop.util.FilePickerSupport;
import java.io.File;
import java.math.BigDecimal;
import java.util.List;
public class PetDialogController {
@@ -38,6 +42,12 @@ public class PetDialogController {
@FXML
private ComboBox<String> cbPetStatus;
@FXML
private ComboBox<DropdownOption> cbCustomer;
@FXML
private ComboBox<DropdownOption> cbStore;
@FXML
private Label lblMode;
@@ -70,16 +80,54 @@ public class PetDialogController {
private String currentImageUrl;
private boolean removeImageRequested;
private Long pendingCustomerId = null;
private Long pendingStoreId = null;
private ObservableList<String> statusList = FXCollections.observableArrayList(
"Available", "Adopted"
"Available", "Adopted", "Owned"
);
@FXML
void initialize() {
cbPetStatus.setItems(statusList); //set status combobox
cbPetStatus.setItems(statusList);
cbCustomer.setCellFactory(param -> new ListCell<>() {
@Override protected void updateItem(DropdownOption o, boolean empty) {
super.updateItem(o, empty);
setText(empty || o == null ? null : o.getLabel());
}
});
cbCustomer.setButtonCell(new ListCell<>() {
@Override protected void updateItem(DropdownOption o, boolean empty) {
super.updateItem(o, empty);
setText(empty || o == null ? null : o.getLabel());
}
});
cbStore.setCellFactory(param -> new ListCell<>() {
@Override protected void updateItem(DropdownOption o, boolean empty) {
super.updateItem(o, empty);
setText(empty || o == null ? null : o.getLabel());
}
});
cbStore.setButtonCell(new ListCell<>() {
@Override protected void updateItem(DropdownOption o, boolean empty) {
super.updateItem(o, empty);
setText(empty || o == null ? null : o.getLabel());
}
});
cbCustomer.setVisible(false);
cbStore.setVisible(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);
});
//Set up mouse handlers for buttons
btnSave.setOnMouseClicked(new EventHandler<MouseEvent>() {
@Override
public void handle(MouseEvent mouseEvent) {
@@ -97,6 +145,9 @@ public class PetDialogController {
btnChangeImage.setOnMouseClicked(mouseEvent -> handleChangeImage());
btnRemoveImage.setOnMouseClicked(mouseEvent -> handleRemoveImage());
refreshImagePreview();
loadCustomers();
loadStores();
}
private void buttonSaveClicked(MouseEvent mouseEvent) {
@@ -111,6 +162,10 @@ public class PetDialogController {
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";
}
//Check validation (length size)
errorMsg += Validator.isLessThanVarChars(txtPetName.getText(), "Pet Name", 50);
@@ -184,9 +239,81 @@ public class PetDialogController {
}
request.setPetAge(age);
String status = cbPetStatus.getValue();
if ("Owned".equalsIgnoreCase(status) && cbCustomer.getValue() != null) {
request.setCustomerId(cbCustomer.getValue().getId());
}
if (("Available".equalsIgnoreCase(status) || "Unadopted".equalsIgnoreCase(status)) && cbStore.getValue() != null) {
request.setStoreId(cbStore.getValue().getId());
}
return request;
}
private void loadCustomers() {
new Thread(() -> {
try {
List<DropdownOption> customers = DropdownApi.getInstance().getCustomers();
Platform.runLater(() -> {
cbCustomer.setItems(FXCollections.observableArrayList(customers));
applySelectedCustomer();
});
} catch (Exception e) {
Platform.runLater(() -> {
ActivityLogger.getInstance().logException(
"PetDialogController.loadCustomers", e, "Loading customers");
cbCustomer.setDisable(true);
cbCustomer.setPromptText("Unable to load customers");
});
}
}).start();
}
private void loadStores() {
new Thread(() -> {
try {
List<DropdownOption> stores = DropdownApi.getInstance().getStores();
Platform.runLater(() -> {
cbStore.setItems(FXCollections.observableArrayList(stores));
applySelectedStore();
});
} catch (Exception e) {
Platform.runLater(() -> {
ActivityLogger.getInstance().logException(
"PetDialogController.loadStores", e, "Loading stores");
cbStore.setDisable(true);
cbStore.setPromptText("Unable to load stores");
});
}
}).start();
}
private void applySelectedCustomer() {
if (pendingCustomerId == null) return;
DropdownOption selected = findOptionById(cbCustomer.getItems(), pendingCustomerId);
if (selected != null) {
cbCustomer.setValue(selected);
pendingCustomerId = null;
}
}
private void applySelectedStore() {
if (pendingStoreId == null) return;
DropdownOption selected = findOptionById(cbStore.getItems(), pendingStoreId);
if (selected != null) {
cbStore.setValue(selected);
pendingStoreId = null;
}
}
private DropdownOption findOptionById(List<DropdownOption> options, Long id) {
if (id == null || options == null) return null;
for (DropdownOption option : options) {
if (option.getId() != null && option.getId().equals(id)) return option;
}
return null;
}
private void closeStage(MouseEvent mouseEvent) {
Node node = (Node) mouseEvent.getSource();
Stage stage = (Stage) node.getScene().getWindow();
@@ -206,14 +333,14 @@ public class PetDialogController {
removeImageRequested = false;
refreshImagePreview();
//get the right combobox selection
pendingCustomerId = pet.getCustomerId() > 0 ? pet.getCustomerId() : null;
pendingStoreId = pet.getStoreId() > 0 ? pet.getStoreId() : null;
for (String status : cbPetStatus.getItems()) {
if(status.equals(pet.getPetStatus())){
cbPetStatus.getSelectionModel().select(status);
}
}
}
}

View File

@@ -13,6 +13,10 @@ public class Pet {
private SimpleStringProperty petStatus;
private SimpleDoubleProperty petPrice;
private SimpleStringProperty imageUrl;
private SimpleStringProperty customerName = new SimpleStringProperty("");
private SimpleStringProperty storeName = new SimpleStringProperty("");
private long customerId = 0L;
private long storeId = 0L;
public Pet(int petId, String petName, String petSpecies, String petBreed, int petAge, String petStatus, double petPrice, String imageUrl) {
this.petId = new SimpleIntegerProperty(petId);
@@ -120,4 +124,44 @@ public class Pet {
public SimpleStringProperty imageUrlProperty() {
return imageUrl;
}
public String getCustomerName() {
return customerName.get();
}
public void setCustomerName(String customerName) {
this.customerName.set(customerName != null ? customerName : "");
}
public SimpleStringProperty customerNameProperty() {
return customerName;
}
public String getStoreName() {
return storeName.get();
}
public void setStoreName(String storeName) {
this.storeName.set(storeName != null ? storeName : "");
}
public SimpleStringProperty storeNameProperty() {
return storeName;
}
public long getCustomerId() {
return customerId;
}
public void setCustomerId(long customerId) {
this.customerId = customerId;
}
public long getStoreId() {
return storeId;
}
public void setStoreId(long storeId) {
this.storeId = storeId;
}
}

View File

@@ -13,7 +13,7 @@
<?import javafx.scene.layout.VBox?>
<?import javafx.scene.text.Font?>
<VBox minHeight="-Infinity" minWidth="-Infinity" prefHeight="560.0" prefWidth="790.0" spacing="20.0" style="-fx-font-size: 14px;" xmlns="http://javafx.com/javafx/17" xmlns:fx="http://javafx.com/fxml/1" fx:controller="org.example.petshopdesktop.controllers.dialogcontrollers.PetDialogController">
<VBox minHeight="-Infinity" minWidth="-Infinity" prefHeight="660.0" prefWidth="790.0" spacing="20.0" style="-fx-font-size: 14px;" xmlns="http://javafx.com/javafx/17" xmlns:fx="http://javafx.com/fxml/1" fx:controller="org.example.petshopdesktop.controllers.dialogcontrollers.PetDialogController">
<children>
<HBox alignment="CENTER_LEFT" prefHeight="79.0" prefWidth="727.0" spacing="20.0" style="-fx-background-color: #2C3E50; -fx-background-radius: 14;">
<children>
@@ -153,6 +153,34 @@
</TextField>
</children>
</VBox>
<VBox prefHeight="200.0" prefWidth="100.0" spacing="8.0" GridPane.rowIndex="3">
<children>
<Label text="Customer:" textFill="#2c3e50">
<font>
<Font name="System Bold" size="16.0" />
</font>
</Label>
<ComboBox fx:id="cbCustomer" prefHeight="29.0" prefWidth="336.0" promptText="Select Customer" style="-fx-border-color: #E8EBED; -fx-border-width: 2; -fx-border-radius: 10; -fx-background-radius: 10; -fx-background-color: white;">
<padding>
<Insets bottom="3.0" left="10.0" right="10.0" top="3.0" />
</padding>
</ComboBox>
</children>
</VBox>
<VBox prefHeight="200.0" prefWidth="100.0" spacing="8.0" GridPane.columnIndex="1" GridPane.rowIndex="3">
<children>
<Label text="Store:" textFill="#2c3e50">
<font>
<Font name="System Bold" size="16.0" />
</font>
</Label>
<ComboBox fx:id="cbStore" prefHeight="29.0" prefWidth="336.0" promptText="Select Store" style="-fx-border-color: #E8EBED; -fx-border-width: 2; -fx-border-radius: 10; -fx-background-radius: 10; -fx-background-color: white;">
<padding>
<Insets bottom="3.0" left="10.0" right="10.0" top="3.0" />
</padding>
</ComboBox>
</children>
</VBox>
</children>
<VBox.margin>
<Insets bottom="15.0" left="15.0" right="15.0" top="15.0" />

View File

@@ -79,6 +79,8 @@
<TableColumn fx:id="colPetAge" prefWidth="60.0" text="Age" />
<TableColumn fx:id="colPetStatus" prefWidth="110.0" text="Status" />
<TableColumn fx:id="colPetPrice" prefWidth="80.0" text="Price" />
<TableColumn fx:id="colCustomerName" prefWidth="130.0" text="Owner" />
<TableColumn fx:id="colStoreName" prefWidth="130.0" text="Store" />
</columns>
</TableView>
</children>