Merge branch 'main' into AttachmentsToChat

This commit is contained in:
Alex
2026-04-06 16:13:50 -06:00
19 changed files with 671 additions and 44 deletions

View File

@@ -564,7 +564,7 @@
"name": "Get All Pets", "name": "Get All Pets",
"request": { "request": {
"method": "GET", "method": "GET",
"url": "{{baseUrl}}/api/v1/pets", "url": "{{baseUrl}}/api/v1/pets?status=available&storeId=1",
"header": [ "header": [
{ {
"key": "Content-Type", "key": "Content-Type",
@@ -585,7 +585,10 @@
"exec": [ "exec": [
"pm.test('Status code is 200', function () {", "pm.test('Status code is 200', function () {",
" pm.response.to.have.status(200);", " 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": [ "exec": [
"pm.test('Status code is 200', function () {", "pm.test('Status code is 200', function () {",
" pm.response.to.have.status(200);", " 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": { "body": {
"mode": "raw", "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": { "options": {
"raw": { "raw": {
"language": "json" "language": "json"
@@ -689,7 +695,11 @@
" pm.response.to.have.status(201);", " pm.response.to.have.status(201);",
"});", "});",
"var jsonData = pm.response.json();", "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": { "body": {
"mode": "raw", "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": { "options": {
"raw": { "raw": {
"language": "json" "language": "json"
@@ -729,7 +739,12 @@
"exec": [ "exec": [
"pm.test('Status code is 200', function () {", "pm.test('Status code is 200', function () {",
" pm.response.to.have.status(200);", " 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 q,
@RequestParam(required = false) String species, @RequestParam(required = false) String species,
@RequestParam(required = false) String status, @RequestParam(required = false) String status,
@RequestParam(required = false) Long storeId,
Pageable pageable) { Pageable pageable) {
return ResponseEntity.ok(petService.getAllPets(q, species, status, pageable)); return ResponseEntity.ok(petService.getAllPets(q, species, status, storeId, pageable));
} }
@GetMapping("/{id}") @GetMapping("/{id}")

View File

@@ -23,6 +23,10 @@ public class PetRequest {
private BigDecimal petPrice; private BigDecimal petPrice;
private Long customerId;
private Long storeId;
public String getPetName() { public String getPetName() {
return petName; return petName;
} }
@@ -71,6 +75,22 @@ public class PetRequest {
this.petPrice = 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;
}
@Override @Override
public boolean equals(Object o) { public boolean equals(Object o) {
if (this == o) return true; if (this == o) return true;

View File

@@ -15,11 +15,15 @@ public class PetResponse {
private String imageUrl; private String imageUrl;
private LocalDateTime createdAt; private LocalDateTime createdAt;
private LocalDateTime updatedAt; private LocalDateTime updatedAt;
private Long customerId;
private String customerName;
private Long storeId;
private String storeName;
public PetResponse() { 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.petId = petId;
this.petName = petName; this.petName = petName;
this.petSpecies = petSpecies; this.petSpecies = petSpecies;
@@ -30,6 +34,10 @@ public class PetResponse {
this.imageUrl = imageUrl; this.imageUrl = imageUrl;
this.createdAt = createdAt; this.createdAt = createdAt;
this.updatedAt = updatedAt; this.updatedAt = updatedAt;
this.customerId = customerId;
this.customerName = customerName;
this.storeId = storeId;
this.storeName = storeName;
} }
public Long getPetId() { public Long getPetId() {
@@ -112,17 +120,49 @@ public class PetResponse {
this.updatedAt = 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;
}
@Override @Override
public boolean equals(Object o) { public boolean equals(Object o) {
if (this == o) return true; if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false; if (o == null || getClass() != o.getClass()) return false;
PetResponse that = (PetResponse) o; 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 @Override
public int hashCode() { 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 @Override
@@ -138,6 +178,10 @@ public class PetResponse {
", imageUrl='" + imageUrl + '\'' + ", imageUrl='" + imageUrl + '\'' +
", createdAt=" + createdAt + ", createdAt=" + createdAt +
", updatedAt=" + updatedAt + ", updatedAt=" + updatedAt +
", customerId=" + customerId +
", customerName='" + customerName + '\'' +
", storeId=" + storeId +
", storeName='" + storeName + '\'' +
'}'; '}';
} }
} }

View File

@@ -38,6 +38,14 @@ public class Pet {
@Column(length = 255) @Column(length = 255)
private String imageUrl; private String imageUrl;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "customerId")
private Customer customer;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "storeId")
private StoreLocation store;
@CreationTimestamp @CreationTimestamp
@Column(name = "created_at", updatable = false) @Column(name = "created_at", updatable = false)
private LocalDateTime createdAt; private LocalDateTime createdAt;
@@ -142,6 +150,22 @@ public class Pet {
this.updatedAt = updatedAt; 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 @Override
public boolean equals(Object o) { public boolean equals(Object o) {
if (this == o) return true; 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 " + @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 " + "(: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 " + "(:species IS NULL OR LOWER(p.petSpecies) = LOWER(:species)) AND " +
"(:status IS NULL OR LOWER(p.petStatus) = LOWER(:status))") "(:status IS NULL OR LOWER(p.petStatus) = LOWER(:status)) AND " +
Page<Pet> searchPets(@Param("q") String query, @Param("species") String species, @Param("status") String status, Pageable pageable); "(: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 " + @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 " + "(: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))") "(:species IS NULL OR LOWER(p.petSpecies) = LOWER(:species)) AND " +
Page<Pet> searchPublicPets(@Param("q") String query, @Param("species") String species, Pageable pageable); "(: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 " + @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 " + "(: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 " + "(:species IS NULL OR LOWER(p.petSpecies) = LOWER(:species)) AND " +
"(:status IS NULL OR LOWER(p.petStatus) = LOWER(:status))") "(: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.PetRequest;
import com.petshop.backend.dto.pet.PetResponse; import com.petshop.backend.dto.pet.PetResponse;
import com.petshop.backend.entity.Adoption; import com.petshop.backend.entity.Adoption;
import com.petshop.backend.entity.Customer;
import com.petshop.backend.entity.Pet; import com.petshop.backend.entity.Pet;
import com.petshop.backend.entity.StoreLocation;
import com.petshop.backend.entity.User; import com.petshop.backend.entity.User;
import com.petshop.backend.exception.ResourceNotFoundException; import com.petshop.backend.exception.ResourceNotFoundException;
import com.petshop.backend.security.AppPrincipal; import com.petshop.backend.security.AppPrincipal;
import com.petshop.backend.repository.AdoptionRepository; import com.petshop.backend.repository.AdoptionRepository;
import com.petshop.backend.repository.CustomerRepository;
import com.petshop.backend.repository.PetRepository; import com.petshop.backend.repository.PetRepository;
import com.petshop.backend.repository.StoreRepository;
import org.springframework.core.io.Resource; import org.springframework.core.io.Resource;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.data.domain.Page; import org.springframework.data.domain.Page;
@@ -29,15 +33,20 @@ public class PetService {
private final PetRepository petRepository; private final PetRepository petRepository;
private final AdoptionRepository adoptionRepository; private final AdoptionRepository adoptionRepository;
private final CustomerRepository customerRepository;
private final StoreRepository storeRepository;
private final CatalogImageStorageService catalogImageStorageService; 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.petRepository = petRepository;
this.adoptionRepository = adoptionRepository; this.adoptionRepository = adoptionRepository;
this.customerRepository = customerRepository;
this.storeRepository = storeRepository;
this.catalogImageStorageService = catalogImageStorageService; 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 normalizedQuery = normalizeFilter(query);
String normalizedSpecies = normalizeFilter(species); String normalizedSpecies = normalizeFilter(species);
String normalizedStatus = normalizeFilter(status); String normalizedStatus = normalizeFilter(status);
@@ -48,22 +57,23 @@ public class PetService {
if (!isAllowedPublicStatus(normalizedStatus)) { if (!isAllowedPublicStatus(normalizedStatus)) {
return new PageImpl<>(java.util.List.of(), pageable, 0); 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) { } 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) { } else if (viewer.role() == User.Role.CUSTOMER) {
if (!isAllowedCustomerStatus(normalizedStatus)) { if (!isAllowedCustomerStatus(normalizedStatus)) {
return new PageImpl<>(java.util.List.of(), pageable, 0); return new PageImpl<>(java.util.List.of(), pageable, 0);
} }
pets = petRepository.searchCustomerVisiblePets(viewer.userId(), normalizedQuery, normalizedSpecies, normalizedStatus, pageable); pets = petRepository.searchCustomerVisiblePets(viewer.userId(), normalizedQuery, normalizedSpecies, normalizedStatus, pageable);
} else { } else {
pets = petRepository.searchPublicPets(normalizedQuery, normalizedSpecies, pageable); pets = petRepository.searchPublicPets(normalizedQuery, normalizedSpecies, storeId, pageable);
} }
return pets return pets
.map(this::mapToResponse); .map(this::mapToResponse);
} }
@Transactional(readOnly = true)
public PetResponse getPetById(Long id) { public PetResponse getPetById(Long id) {
Pet pet = petRepository.findById(id) Pet pet = petRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("Pet not found with id: " + id)); .orElseThrow(() -> new ResourceNotFoundException("Pet not found with id: " + id));
@@ -82,6 +92,7 @@ public class PetService {
pet.setPetAge(request.getPetAge()); pet.setPetAge(request.getPetAge());
pet.setPetStatus(request.getPetStatus()); pet.setPetStatus(request.getPetStatus());
pet.setPetPrice(request.getPetPrice()); pet.setPetPrice(request.getPetPrice());
applyOwnerAndStore(pet, request);
pet = petRepository.save(pet); pet = petRepository.save(pet);
return mapToResponse(pet); return mapToResponse(pet);
@@ -98,6 +109,7 @@ public class PetService {
pet.setPetAge(request.getPetAge()); pet.setPetAge(request.getPetAge());
pet.setPetStatus(request.getPetStatus()); pet.setPetStatus(request.getPetStatus());
pet.setPetPrice(request.getPetPrice()); pet.setPetPrice(request.getPetPrice());
applyOwnerAndStore(pet, request);
pet = petRepository.save(pet); pet = petRepository.save(pet);
return mapToResponse(pet); return mapToResponse(pet);
@@ -161,6 +173,9 @@ public class PetService {
if (viewer == null || viewer.userId() == null) { if (viewer == null || viewer.userId() == null) {
return false; return false;
} }
if (isOwnedByUser(pet, viewer.userId())) {
return true;
}
return isAdoptedByUser(pet, viewer.userId()); return isAdoptedByUser(pet, viewer.userId());
} }
@@ -230,7 +245,7 @@ public class PetService {
} }
private boolean isAllowedCustomerStatus(String status) { 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) { private String normalizeFilter(String value) {
@@ -242,6 +257,8 @@ public class PetService {
} }
private PetResponse mapToResponse(Pet pet) { private PetResponse mapToResponse(Pet pet) {
Customer customer = pet.getCustomer();
StoreLocation store = pet.getStore();
return new PetResponse( return new PetResponse(
pet.getPetId(), pet.getPetId(),
pet.getPetName(), pet.getPetName(),
@@ -252,10 +269,59 @@ public class PetService {
pet.getPetPrice(), pet.getPetPrice(),
pet.getImageUrl() != null && !pet.getImageUrl().isBlank() ? "/api/v1/pets/" + pet.getPetId() + "/image" : null, pet.getImageUrl() != null && !pet.getImageUrl().isBlank() ? "/api/v1/pets/" + pet.getPetId() + "/image" : null,
pet.getCreatedAt(), 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) { 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.entity.User;
import com.petshop.backend.exception.ResourceNotFoundException; import com.petshop.backend.exception.ResourceNotFoundException;
import com.petshop.backend.repository.AdoptionRepository; import com.petshop.backend.repository.AdoptionRepository;
import com.petshop.backend.repository.CustomerRepository;
import com.petshop.backend.repository.PetRepository; import com.petshop.backend.repository.PetRepository;
import com.petshop.backend.repository.StoreRepository;
import com.petshop.backend.security.AppPrincipal; import com.petshop.backend.security.AppPrincipal;
import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
@@ -39,6 +41,12 @@ class PetServiceTest {
@Mock @Mock
private AdoptionRepository adoptionRepository; private AdoptionRepository adoptionRepository;
@Mock
private CustomerRepository customerRepository;
@Mock
private StoreRepository storeRepository;
@Mock @Mock
private CatalogImageStorageService catalogImageStorageService; private CatalogImageStorageService catalogImageStorageService;
@@ -54,24 +62,24 @@ class PetServiceTest {
void getAllPetsAnonymousReturnsOnlyPublicPets() { void getAllPetsAnonymousReturnsOnlyPublicPets() {
Pageable pageable = PageRequest.of(0, 10); Pageable pageable = PageRequest.of(0, 10);
Pet availablePet = pet(1L, "Buddy", "Available"); 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(1, result.getTotalElements());
assertEquals("Buddy", result.getContent().get(0).getPetName()); assertEquals("Buddy", result.getContent().get(0).getPetName());
verify(petRepository).searchPublicPets(null, null, pageable); verify(petRepository).searchPublicPets(null, null, null, pageable);
verify(petRepository, never()).searchPets(null, null, null, pageable); verify(petRepository, never()).searchPets(null, null, null, null, pageable);
} }
@Test @Test
void getAllPetsAnonymousWithAdoptedStatusReturnsEmptyPage() { void getAllPetsAnonymousWithAdoptedStatusReturnsEmptyPage() {
Pageable pageable = PageRequest.of(0, 10); 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()); assertEquals(0, result.getTotalElements());
verify(petRepository, never()).searchPublicPets(null, null, pageable); verify(petRepository, never()).searchPublicPets(null, null, null, pageable);
} }
@Test @Test
@@ -83,7 +91,7 @@ class PetServiceTest {
when(petRepository.searchCustomerVisiblePets(25L, null, null, null, pageable)) when(petRepository.searchCustomerVisiblePets(25L, null, null, null, pageable))
.thenReturn(new PageImpl<>(List.of(availablePet, adoptedPet), pageable, 2)); .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()); assertEquals(2, result.getTotalElements());
verify(petRepository).searchCustomerVisiblePets(25L, null, null, null, pageable); verify(petRepository).searchCustomerVisiblePets(25L, null, null, null, pageable);
@@ -95,13 +103,13 @@ class PetServiceTest {
setAuthentication(99L, User.Role.ADMIN); setAuthentication(99L, User.Role.ADMIN);
Pet availablePet = pet(1L, "Buddy", "Available"); Pet availablePet = pet(1L, "Buddy", "Available");
Pet adoptedPet = pet(2L, "Luna", "Adopted"); 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)); .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()); assertEquals(2, result.getTotalElements());
verify(petRepository).searchPets(null, null, null, pageable); verify(petRepository).searchPets(null, null, null, null, pageable);
} }
@Test @Test

View File

@@ -9,6 +9,8 @@ public class PetRequest {
private Integer petAge; private Integer petAge;
private String petStatus; private String petStatus;
private BigDecimal petPrice; private BigDecimal petPrice;
private Long customerId;
private Long storeId;
public PetRequest() { public PetRequest() {
} }
@@ -60,4 +62,20 @@ public class PetRequest {
public void setPetPrice(BigDecimal petPrice) { public void setPetPrice(BigDecimal petPrice) {
this.petPrice = 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 String imageUrl;
private LocalDateTime createdAt; private LocalDateTime createdAt;
private LocalDateTime updatedAt; private LocalDateTime updatedAt;
private Long customerId;
private String customerName;
private Long storeId;
private String storeName;
public PetResponse() { public PetResponse() {
} }
@@ -97,4 +101,36 @@ public class PetResponse {
public void setUpdatedAt(LocalDateTime updatedAt) { public void setUpdatedAt(LocalDateTime updatedAt) {
this.updatedAt = 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; 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"; String path = "/api/v1/pets?page=0&size=1000";
if (query != null && !query.isEmpty()) { if (query != null && !query.isEmpty()) {
path += "&q=" + URLEncoder.encode(query, StandardCharsets.UTF_8); path += "&q=" + URLEncoder.encode(query, StandardCharsets.UTF_8);
@@ -35,6 +35,9 @@ public class PetApi {
if (status != null && !status.isEmpty()) { if (status != null && !status.isEmpty()) {
path += "&status=" + URLEncoder.encode(status, StandardCharsets.UTF_8); path += "&status=" + URLEncoder.encode(status, StandardCharsets.UTF_8);
} }
if (storeId != null) {
path += "&storeId=" + storeId;
}
String response = apiClient.getRawResponse(path); String response = apiClient.getRawResponse(path);
PageResponse<PetResponse> pageResponse = apiClient.getObjectMapper().readValue( PageResponse<PetResponse> pageResponse = apiClient.getObjectMapper().readValue(
response, response,
@@ -46,8 +49,12 @@ public class PetApi {
return pageResponse.getContent(); 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 { 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 { public PetResponse createPet(PetRequest request) throws Exception {

View File

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

View File

@@ -1,5 +1,6 @@
package org.example.petshopdesktop.controllers.dialogcontrollers; package org.example.petshopdesktop.controllers.dialogcontrollers;
import javafx.application.Platform;
import javafx.collections.FXCollections; import javafx.collections.FXCollections;
import javafx.collections.ObservableList; import javafx.collections.ObservableList;
import javafx.event.EventHandler; import javafx.event.EventHandler;
@@ -10,8 +11,10 @@ import javafx.scene.image.ImageView;
import javafx.scene.input.MouseEvent; import javafx.scene.input.MouseEvent;
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.pet.PetRequest; import org.example.petshopdesktop.api.dto.pet.PetRequest;
import org.example.petshopdesktop.api.dto.pet.PetResponse; 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.api.endpoints.PetApi;
import org.example.petshopdesktop.models.Pet; import org.example.petshopdesktop.models.Pet;
import org.example.petshopdesktop.util.ActivityLogger; import org.example.petshopdesktop.util.ActivityLogger;
@@ -20,6 +23,7 @@ import org.example.petshopdesktop.util.FilePickerSupport;
import java.io.File; import java.io.File;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.util.List;
public class PetDialogController { public class PetDialogController {
@@ -38,6 +42,12 @@ public class PetDialogController {
@FXML @FXML
private ComboBox<String> cbPetStatus; private ComboBox<String> cbPetStatus;
@FXML
private ComboBox<DropdownOption> cbCustomer;
@FXML
private ComboBox<DropdownOption> cbStore;
@FXML @FXML
private Label lblMode; private Label lblMode;
@@ -70,16 +80,54 @@ public class PetDialogController {
private String currentImageUrl; private String currentImageUrl;
private boolean removeImageRequested; private boolean removeImageRequested;
private Long pendingCustomerId = null;
private Long pendingStoreId = null;
private ObservableList<String> statusList = FXCollections.observableArrayList( private ObservableList<String> statusList = FXCollections.observableArrayList(
"Available", "Adopted" "Available", "Adopted", "Owned"
); );
@FXML @FXML
void initialize() { 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>() { btnSave.setOnMouseClicked(new EventHandler<MouseEvent>() {
@Override @Override
public void handle(MouseEvent mouseEvent) { public void handle(MouseEvent mouseEvent) {
@@ -97,6 +145,9 @@ public class PetDialogController {
btnChangeImage.setOnMouseClicked(mouseEvent -> handleChangeImage()); btnChangeImage.setOnMouseClicked(mouseEvent -> handleChangeImage());
btnRemoveImage.setOnMouseClicked(mouseEvent -> handleRemoveImage()); btnRemoveImage.setOnMouseClicked(mouseEvent -> handleRemoveImage());
refreshImagePreview(); refreshImagePreview();
loadCustomers();
loadStores();
} }
private void buttonSaveClicked(MouseEvent mouseEvent) { private void buttonSaveClicked(MouseEvent mouseEvent) {
@@ -111,6 +162,10 @@ public class PetDialogController {
if (cbPetStatus.getSelectionModel().getSelectedItem() == null){ if (cbPetStatus.getSelectionModel().getSelectedItem() == null){
errorMsg += "Status is required"; 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) //Check validation (length size)
errorMsg += Validator.isLessThanVarChars(txtPetName.getText(), "Pet Name", 50); errorMsg += Validator.isLessThanVarChars(txtPetName.getText(), "Pet Name", 50);
@@ -184,9 +239,81 @@ public class PetDialogController {
} }
request.setPetAge(age); 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; 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) { private void closeStage(MouseEvent mouseEvent) {
Node node = (Node) mouseEvent.getSource(); Node node = (Node) mouseEvent.getSource();
Stage stage = (Stage) node.getScene().getWindow(); Stage stage = (Stage) node.getScene().getWindow();
@@ -206,14 +333,14 @@ public class PetDialogController {
removeImageRequested = false; removeImageRequested = false;
refreshImagePreview(); 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()) { for (String status : cbPetStatus.getItems()) {
if(status.equals(pet.getPetStatus())){ if(status.equals(pet.getPetStatus())){
cbPetStatus.getSelectionModel().select(status); cbPetStatus.getSelectionModel().select(status);
} }
} }
} }
} }

View File

@@ -13,6 +13,10 @@ public class Pet {
private SimpleStringProperty petStatus; private SimpleStringProperty petStatus;
private SimpleDoubleProperty petPrice; private SimpleDoubleProperty petPrice;
private SimpleStringProperty imageUrl; 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) { public Pet(int petId, String petName, String petSpecies, String petBreed, int petAge, String petStatus, double petPrice, String imageUrl) {
this.petId = new SimpleIntegerProperty(petId); this.petId = new SimpleIntegerProperty(petId);
@@ -120,4 +124,44 @@ public class Pet {
public SimpleStringProperty imageUrlProperty() { public SimpleStringProperty imageUrlProperty() {
return imageUrl; 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.layout.VBox?>
<?import javafx.scene.text.Font?> <?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> <children>
<HBox alignment="CENTER_LEFT" prefHeight="79.0" prefWidth="727.0" spacing="20.0" style="-fx-background-color: #2C3E50; -fx-background-radius: 14;"> <HBox alignment="CENTER_LEFT" prefHeight="79.0" prefWidth="727.0" spacing="20.0" style="-fx-background-color: #2C3E50; -fx-background-radius: 14;">
<children> <children>
@@ -153,6 +153,34 @@
</TextField> </TextField>
</children> </children>
</VBox> </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> </children>
<VBox.margin> <VBox.margin>
<Insets bottom="15.0" left="15.0" right="15.0" top="15.0" /> <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="colPetAge" prefWidth="60.0" text="Age" />
<TableColumn fx:id="colPetStatus" prefWidth="110.0" text="Status" /> <TableColumn fx:id="colPetStatus" prefWidth="110.0" text="Status" />
<TableColumn fx:id="colPetPrice" prefWidth="80.0" text="Price" /> <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> </columns>
</TableView> </TableView>
</children> </children>