Merge branch 'main' into AttachmentsToChat

This commit is contained in:
Alex
2026-04-06 15:39:38 -06:00
49 changed files with 2142 additions and 278 deletions

3
.gitignore vendored
View File

@@ -1,2 +1,5 @@
*.zip *.zip
.local/ .local/
commit-patches/
temp_photos/
uploads/

View File

@@ -8,16 +8,22 @@ public class AdoptionDTO {
private String petName; private String petName;
private Long customerId; private Long customerId;
private String customerName; private String customerName;
private Long employeeId;
private String employeeName;
private String adoptionDate; private String adoptionDate;
private String adoptionStatus; private String adoptionStatus;
private BigDecimal adoptionFee; private BigDecimal adoptionFee;
private String createdAt; private String createdAt;
private String updatedAt; private String updatedAt;
// Constructor for create/update requests
public AdoptionDTO(Long petId, Long customerId, String adoptionDate, String adoptionStatus) { public AdoptionDTO(Long petId, Long customerId, String adoptionDate, String adoptionStatus) {
this(petId, customerId, null, adoptionDate, adoptionStatus);
}
public AdoptionDTO(Long petId, Long customerId, Long employeeId, String adoptionDate, String adoptionStatus) {
this.petId = petId; this.petId = petId;
this.customerId = customerId; this.customerId = customerId;
this.employeeId = employeeId;
this.adoptionDate = adoptionDate; this.adoptionDate = adoptionDate;
this.adoptionStatus = adoptionStatus; this.adoptionStatus = adoptionStatus;
} }
@@ -42,6 +48,14 @@ public class AdoptionDTO {
return customerName; return customerName;
} }
public Long getEmployeeId() {
return employeeId;
}
public String getEmployeeName() {
return employeeName;
}
public String getAdoptionDate() { public String getAdoptionDate() {
return adoptionDate; return adoptionDate;
} }
@@ -65,4 +79,4 @@ public class AdoptionDTO {
public String getUpdatedAt() { public String getUpdatedAt() {
return updatedAt; return updatedAt;
} }
} }

View File

@@ -4,7 +4,7 @@ import java.math.BigDecimal;
import java.util.List; import java.util.List;
public class AppointmentDTO { public class AppointmentDTO {
// Response fields (from server)
private Long appointmentId; private Long appointmentId;
private Long customerId; private Long customerId;
private String customerName; private String customerName;
@@ -12,6 +12,8 @@ public class AppointmentDTO {
private String storeName; private String storeName;
private Long serviceId; private Long serviceId;
private String serviceName; private String serviceName;
private Long employeeId;
private String employeeName;
private String appointmentDate; private String appointmentDate;
private String appointmentTime; private String appointmentTime;
private String appointmentStatus; private String appointmentStatus;
@@ -20,21 +22,25 @@ public class AppointmentDTO {
private String createdAt; private String createdAt;
private String updatedAt; private String updatedAt;
// Constructor for CREATE/UPDATE request body
// Matches AppointmentRequest exactly
public AppointmentDTO(Long customerId, Long storeId, Long serviceId, public AppointmentDTO(Long customerId, Long storeId, Long serviceId,
String appointmentDate, String appointmentTime, String appointmentDate, String appointmentTime,
String appointmentStatus, List<Long> petIds) { String appointmentStatus, List<Long> petIds) {
this(customerId, storeId, serviceId, null, appointmentDate, appointmentTime, appointmentStatus, petIds);
}
public AppointmentDTO(Long customerId, Long storeId, Long serviceId, Long employeeId,
String appointmentDate, String appointmentTime,
String appointmentStatus, List<Long> petIds) {
this.customerId = customerId; this.customerId = customerId;
this.storeId = storeId; this.storeId = storeId;
this.serviceId = serviceId; this.serviceId = serviceId;
this.employeeId = employeeId;
this.appointmentDate = appointmentDate; this.appointmentDate = appointmentDate;
this.appointmentTime = appointmentTime; this.appointmentTime = appointmentTime;
this.appointmentStatus = appointmentStatus; this.appointmentStatus = appointmentStatus;
this.petIds = petIds; this.petIds = petIds;
} }
// Getters
public Long getAppointmentId() { public Long getAppointmentId() {
return appointmentId; return appointmentId;
} }
@@ -63,6 +69,14 @@ public class AppointmentDTO {
return serviceName; return serviceName;
} }
public Long getEmployeeId() {
return employeeId;
}
public String getEmployeeName() {
return employeeName;
}
public String getAppointmentDate() { public String getAppointmentDate() {
return appointmentDate; return appointmentDate;
} }
@@ -91,7 +105,6 @@ public class AppointmentDTO {
return updatedAt; return updatedAt;
} }
// Convenience getters for adapter/list display
public String getPetName() { public String getPetName() {
return (petNames != null && !petNames.isEmpty()) ? petNames.get(0) : ""; return (petNames != null && !petNames.isEmpty()) ? petNames.get(0) : "";
} }
@@ -104,7 +117,6 @@ public class AppointmentDTO {
return getPetID(); return getPetID();
} }
// Keep old name so adapter doesn't break
public String getServiceType() { public String getServiceType() {
return serviceName; return serviceName;
} }
@@ -113,7 +125,6 @@ public class AppointmentDTO {
return serviceId; return serviceId;
} }
// Status alias
public String getStatus() { public String getStatus() {
return appointmentStatus; return appointmentStatus;
} }

View File

@@ -2069,6 +2069,37 @@
{ {
"name": "Appointments", "name": "Appointments",
"item": [ "item": [
{
"name": "Get Appointment Customers Dropdown",
"request": {
"method": "GET",
"url": "{{baseUrl}}/api/v1/dropdowns/appointment-customers",
"header": [
{
"key": "Content-Type",
"value": "application/json"
},
{
"key": "Authorization",
"value": "Bearer {{staffToken}}",
"type": "text"
}
]
},
"event": [
{
"listen": "test",
"script": {
"type": "text/javascript",
"exec": [
"pm.test('Status code is 200', function () {",
" pm.response.to.have.status(200);",
"});"
]
}
}
]
},
{ {
"name": "Check Appointment Availability", "name": "Check Appointment Availability",
"request": { "request": {
@@ -2180,7 +2211,7 @@
], ],
"body": { "body": {
"mode": "raw", "mode": "raw",
"raw": "{\n \"customerId\": 1,\n \"storeId\": 1,\n \"serviceId\": 1,\n \"appointmentDate\": \"2026-12-20\",\n \"appointmentTime\": \"10:00:00\",\n \"appointmentStatus\": \"Booked\",\n \"petIds\": [1]\n}", "raw": "{\n \"customerId\": 1,\n \"storeId\": 1,\n \"serviceId\": 1,\n \"appointmentDate\": \"2026-12-20\",\n \"appointmentTime\": \"10:00:00\",\n \"appointmentStatus\": \"Booked\",\n \"petIds\": [\n 1\n ],\n \"employeeId\": 1\n}",
"options": { "options": {
"raw": { "raw": {
"language": "json" "language": "json"
@@ -2222,7 +2253,7 @@
], ],
"body": { "body": {
"mode": "raw", "mode": "raw",
"raw": "{\n \"customerId\": 1,\n \"storeId\": 1,\n \"serviceId\": 1,\n \"appointmentDate\": \"2026-12-20\",\n \"appointmentTime\": \"11:00:00\",\n \"appointmentStatus\": \"Booked\",\n \"petIds\": [1]\n}", "raw": "{\n \"customerId\": 1,\n \"storeId\": 1,\n \"serviceId\": 1,\n \"appointmentDate\": \"2026-12-20\",\n \"appointmentTime\": \"11:00:00\",\n \"appointmentStatus\": \"Booked\",\n \"petIds\": [\n 1\n ],\n \"employeeId\": 1\n}",
"options": { "options": {
"raw": { "raw": {
"language": "json" "language": "json"
@@ -2315,6 +2346,37 @@
{ {
"name": "Adoptions", "name": "Adoptions",
"item": [ "item": [
{
"name": "Get Adoption Pets Dropdown",
"request": {
"method": "GET",
"url": "{{baseUrl}}/api/v1/dropdowns/adoption-pets",
"header": [
{
"key": "Content-Type",
"value": "application/json"
},
{
"key": "Authorization",
"value": "Bearer {{staffToken}}",
"type": "text"
}
]
},
"event": [
{
"listen": "test",
"script": {
"type": "text/javascript",
"exec": [
"pm.test('Status code is 200', function () {",
" pm.response.to.have.status(200);",
"});"
]
}
}
]
},
{ {
"name": "List Adoptions", "name": "List Adoptions",
"request": { "request": {
@@ -2395,7 +2457,7 @@
], ],
"body": { "body": {
"mode": "raw", "mode": "raw",
"raw": "{\n \"petId\": 3,\n \"customerId\": 1,\n \"adoptionDate\": \"2026-12-21\",\n \"adoptionStatus\": \"Pending\"\n}", "raw": "{\n \"petId\": 3,\n \"customerId\": 1,\n \"adoptionDate\": \"2026-12-21\",\n \"adoptionStatus\": \"Pending\",\n \"employeeId\": 1\n}",
"options": { "options": {
"raw": { "raw": {
"language": "json" "language": "json"
@@ -2437,7 +2499,7 @@
], ],
"body": { "body": {
"mode": "raw", "mode": "raw",
"raw": "{\n \"petId\": 3,\n \"customerId\": 1,\n \"adoptionDate\": \"2026-12-22\",\n \"adoptionStatus\": \"Completed\"\n}", "raw": "{\n \"petId\": 3,\n \"customerId\": 1,\n \"adoptionDate\": \"2026-12-22\",\n \"adoptionStatus\": \"Completed\",\n \"employeeId\": 1\n}",
"options": { "options": {
"raw": { "raw": {
"language": "json" "language": "json"
@@ -3719,6 +3781,68 @@
} }
] ]
}, },
{
"name": "Get Store Employees Dropdown",
"request": {
"method": "GET",
"url": "{{baseUrl}}/api/v1/dropdowns/stores/{{storeId}}/employees",
"header": [
{
"key": "Content-Type",
"value": "application/json"
},
{
"key": "Authorization",
"value": "Bearer {{staffToken}}",
"type": "text"
}
]
},
"event": [
{
"listen": "test",
"script": {
"type": "text/javascript",
"exec": [
"pm.test('Status code is 200', function () {",
" pm.response.to.have.status(200);",
"});"
]
}
}
]
},
{
"name": "Get All Employees Dropdown",
"request": {
"method": "GET",
"url": "{{baseUrl}}/api/v1/dropdowns/employees",
"header": [
{
"key": "Content-Type",
"value": "application/json"
},
{
"key": "Authorization",
"value": "Bearer {{staffToken}}",
"type": "text"
}
]
},
"event": [
{
"listen": "test",
"script": {
"type": "text/javascript",
"exec": [
"pm.test('Status code is 200', function () {",
" pm.response.to.have.status(200);",
"});"
]
}
}
]
},
{ {
"name": "List Stores", "name": "List Stores",
"request": { "request": {

View File

@@ -35,13 +35,14 @@ public class FlywayContextInitializer implements ApplicationContextInitializer<C
RuntimeException lastFailure = null; RuntimeException lastFailure = null;
for (int attempt = 1; attempt <= MAX_RETRIES; attempt++) { for (int attempt = 1; attempt <= MAX_RETRIES; attempt++) {
try { try {
Flyway.configure() Flyway flyway = Flyway.configure()
.dataSource(url, username, password) .dataSource(url, username, password)
.locations(locations) .locations(locations)
.baselineOnMigrate(environment.getProperty("spring.flyway.baseline-on-migrate", Boolean.class, false)) .baselineOnMigrate(environment.getProperty("spring.flyway.baseline-on-migrate", Boolean.class, false))
.baselineVersion(MigrationVersion.fromVersion(environment.getProperty("spring.flyway.baseline-version", "1"))) .baselineVersion(MigrationVersion.fromVersion(environment.getProperty("spring.flyway.baseline-version", "1")))
.load() .load();
.migrate(); flyway.repair();
flyway.migrate();
return; return;
} catch (RuntimeException ex) { } catch (RuntimeException ex) {
lastFailure = ex; lastFailure = ex;

View File

@@ -0,0 +1,34 @@
package com.petshop.backend.config;
import com.petshop.backend.repository.CustomerPetRepository;
import org.springframework.boot.CommandLineRunner;
import org.springframework.context.annotation.Profile;
import org.springframework.core.io.ClassPathResource;
import org.springframework.jdbc.datasource.init.ResourceDatabasePopulator;
import org.springframework.stereotype.Component;
import javax.sql.DataSource;
@Component
@Profile("local")
public class LocalAppointmentCustomerSeedInitializer implements CommandLineRunner {
private final DataSource dataSource;
private final CustomerPetRepository customerPetRepository;
public LocalAppointmentCustomerSeedInitializer(DataSource dataSource, CustomerPetRepository customerPetRepository) {
this.dataSource = dataSource;
this.customerPetRepository = customerPetRepository;
}
@Override
public void run(String... args) {
if (customerPetRepository.count() > 0) {
return;
}
ResourceDatabasePopulator populator = new ResourceDatabasePopulator(false, false, "UTF-8",
new ClassPathResource("dev/seed_demo_customer_pets.sql"));
populator.execute(dataSource);
}
}

View File

@@ -71,21 +71,8 @@ public class AdoptionController {
} }
@PostMapping @PostMapping
@PreAuthorize("hasAnyRole('CUSTOMER', 'STAFF', 'ADMIN')") @PreAuthorize("hasAnyRole('STAFF', 'ADMIN')")
public ResponseEntity<AdoptionResponse> createAdoption(@Valid @RequestBody AdoptionRequest request) { public ResponseEntity<AdoptionResponse> createAdoption(@Valid @RequestBody AdoptionRequest request) {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
String role = authentication.getAuthorities().stream()
.findFirst()
.map(authority -> authority.getAuthority().replace("ROLE_", ""))
.orElse(null);
if (role != null && role.equals("CUSTOMER")) {
Customer customer = AuthenticationHelper.getAuthenticatedCustomer(userRepository, customerRepository);
if (!request.getCustomerId().equals(customer.getCustomerId())) {
throw new org.springframework.security.access.AccessDeniedException("You can only create adoptions for yourself");
}
}
return ResponseEntity.status(HttpStatus.CREATED).body(adoptionService.createAdoption(request)); return ResponseEntity.status(HttpStatus.CREATED).body(adoptionService.createAdoption(request));
} }

View File

@@ -1,10 +1,14 @@
package com.petshop.backend.controller; package com.petshop.backend.controller;
import com.petshop.backend.dto.common.DropdownOption; import com.petshop.backend.dto.common.DropdownOption;
import com.petshop.backend.entity.CustomerPet;
import com.petshop.backend.entity.EmployeeStore;
import com.petshop.backend.entity.User;
import com.petshop.backend.repository.*; import com.petshop.backend.repository.*;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
@@ -17,23 +21,31 @@ public class DropdownController {
private final PetRepository petRepository; private final PetRepository petRepository;
private final CustomerRepository customerRepository; private final CustomerRepository customerRepository;
private final CustomerPetRepository customerPetRepository;
private final ServiceRepository serviceRepository; private final ServiceRepository serviceRepository;
private final ProductRepository productRepository; private final ProductRepository productRepository;
private final CategoryRepository categoryRepository; private final CategoryRepository categoryRepository;
private final StoreRepository storeRepository; private final StoreRepository storeRepository;
private final SupplierRepository supplierRepository; private final SupplierRepository supplierRepository;
private final EmployeeStoreRepository employeeStoreRepository;
private final UserRepository userRepository;
public DropdownController(PetRepository petRepository, CustomerRepository customerRepository, public DropdownController(PetRepository petRepository, CustomerRepository customerRepository,
ServiceRepository serviceRepository, ProductRepository productRepository, CustomerPetRepository customerPetRepository,
CategoryRepository categoryRepository, StoreRepository storeRepository, ServiceRepository serviceRepository, ProductRepository productRepository,
SupplierRepository supplierRepository) { CategoryRepository categoryRepository, StoreRepository storeRepository,
SupplierRepository supplierRepository, EmployeeStoreRepository employeeStoreRepository,
UserRepository userRepository) {
this.petRepository = petRepository; this.petRepository = petRepository;
this.customerRepository = customerRepository; this.customerRepository = customerRepository;
this.customerPetRepository = customerPetRepository;
this.serviceRepository = serviceRepository; this.serviceRepository = serviceRepository;
this.productRepository = productRepository; this.productRepository = productRepository;
this.categoryRepository = categoryRepository; this.categoryRepository = categoryRepository;
this.storeRepository = storeRepository; this.storeRepository = storeRepository;
this.supplierRepository = supplierRepository; this.supplierRepository = supplierRepository;
this.employeeStoreRepository = employeeStoreRepository;
this.userRepository = userRepository;
} }
@GetMapping("/pets") @GetMapping("/pets")
@@ -45,6 +57,16 @@ public class DropdownController {
); );
} }
@GetMapping("/adoption-pets")
@PreAuthorize("hasAnyRole('STAFF', 'ADMIN')")
public ResponseEntity<List<DropdownOption>> getAdoptionPets() {
return ResponseEntity.ok(
petRepository.findAllByPetStatusIgnoreCaseOrderByPetNameAsc("Available").stream()
.map(p -> new DropdownOption(p.getPetId(), p.getPetName()))
.collect(Collectors.toList())
);
}
@GetMapping("/customers") @GetMapping("/customers")
@PreAuthorize("hasAnyRole('STAFF', 'ADMIN')") @PreAuthorize("hasAnyRole('STAFF', 'ADMIN')")
public ResponseEntity<List<DropdownOption>> getCustomers() { public ResponseEntity<List<DropdownOption>> getCustomers() {
@@ -55,6 +77,26 @@ public class DropdownController {
); );
} }
@GetMapping("/appointment-customers")
@PreAuthorize("hasAnyRole('STAFF', 'ADMIN')")
public ResponseEntity<List<DropdownOption>> getAppointmentCustomers() {
return ResponseEntity.ok(
customerRepository.findAllWithPets().stream()
.map(c -> new DropdownOption(c.getCustomerId(), c.getFirstName() + " " + c.getLastName()))
.collect(Collectors.toList())
);
}
@GetMapping("/customers/{customerId}/pets")
@PreAuthorize("hasAnyRole('STAFF', 'ADMIN')")
public ResponseEntity<List<DropdownOption>> getCustomerPets(@PathVariable Long customerId) {
return ResponseEntity.ok(
customerPetRepository.findByCustomerCustomerIdOrderByPetNameAsc(customerId).stream()
.map(this::toCustomerPetOption)
.collect(Collectors.toList())
);
}
@GetMapping("/services") @GetMapping("/services")
public ResponseEntity<List<DropdownOption>> getServices() { public ResponseEntity<List<DropdownOption>> getServices() {
return ResponseEntity.ok( return ResponseEntity.ok(
@@ -114,6 +156,24 @@ public class DropdownController {
); );
} }
@GetMapping({"/stores/{storeId}/employees", "/employees"})
@PreAuthorize("hasAnyRole('CUSTOMER', 'STAFF', 'ADMIN')")
public ResponseEntity<List<DropdownOption>> getStoreEmployees(@PathVariable(required = false) Long storeId) {
List<EmployeeStore> employees;
if (storeId == null || storeId == 0) {
employees = employeeStoreRepository.findActiveAllOrderByEmployeeEmployeeIdAsc();
} else {
employees = employeeStoreRepository.findActiveByStoreStoreIdOrderByEmployeeEmployeeIdAsc(storeId);
}
return ResponseEntity.ok(
employees.stream()
.filter(this::isAssignableEmployee)
.map(this::toEmployeeOption)
.distinct()
.collect(Collectors.toList())
);
}
@GetMapping("/suppliers") @GetMapping("/suppliers")
@PreAuthorize("hasRole('ADMIN')") @PreAuthorize("hasRole('ADMIN')")
public ResponseEntity<List<DropdownOption>> getSuppliers() { public ResponseEntity<List<DropdownOption>> getSuppliers() {
@@ -123,4 +183,26 @@ public class DropdownController {
.collect(Collectors.toList()) .collect(Collectors.toList())
); );
} }
private DropdownOption toCustomerPetOption(CustomerPet pet) {
String species = pet.getSpecies() == null || pet.getSpecies().isBlank() ? "Pet" : pet.getSpecies();
String breed = pet.getBreed() == null || pet.getBreed().isBlank() ? "" : " · " + pet.getBreed();
return new DropdownOption(pet.getCustomerPetId(), pet.getPetName() + " (" + species + breed + ")");
}
private DropdownOption toEmployeeOption(EmployeeStore employeeStore) {
var employee = employeeStore.getEmployee();
return new DropdownOption(employee.getEmployeeId(), employee.getFirstName() + " " + employee.getLastName());
}
private boolean isAssignableEmployee(EmployeeStore employeeStore) {
Long userId = employeeStore.getEmployee().getUserId();
if (userId == null) {
return false;
}
return userRepository.findById(userId)
.filter(user -> user.getRole() == User.Role.STAFF)
.filter(user -> Boolean.TRUE.equals(user.getActive()))
.isPresent();
}
} }

View File

@@ -18,6 +18,8 @@ public class AdoptionRequest {
@NotBlank(message = "Adoption status is required") @NotBlank(message = "Adoption status is required")
private String adoptionStatus; private String adoptionStatus;
private Long employeeId;
public Long getPetId() { public Long getPetId() {
return petId; return petId;
} }
@@ -50,6 +52,14 @@ public class AdoptionRequest {
this.adoptionStatus = adoptionStatus; this.adoptionStatus = adoptionStatus;
} }
public Long getEmployeeId() {
return employeeId;
}
public void setEmployeeId(Long employeeId) {
this.employeeId = employeeId;
}
@Override @Override
public boolean equals(Object o) { public boolean equals(Object o) {
if (this == o) return true; if (this == o) return true;
@@ -58,12 +68,13 @@ public class AdoptionRequest {
return Objects.equals(petId, that.petId) && return Objects.equals(petId, that.petId) &&
Objects.equals(customerId, that.customerId) && Objects.equals(customerId, that.customerId) &&
Objects.equals(adoptionDate, that.adoptionDate) && Objects.equals(adoptionDate, that.adoptionDate) &&
Objects.equals(adoptionStatus, that.adoptionStatus); Objects.equals(adoptionStatus, that.adoptionStatus) &&
Objects.equals(employeeId, that.employeeId);
} }
@Override @Override
public int hashCode() { public int hashCode() {
return Objects.hash(petId, customerId, adoptionDate, adoptionStatus); return Objects.hash(petId, customerId, adoptionDate, adoptionStatus, employeeId);
} }
@Override @Override
@@ -73,6 +84,7 @@ public class AdoptionRequest {
", customerId=" + customerId + ", customerId=" + customerId +
", adoptionDate=" + adoptionDate + ", adoptionDate=" + adoptionDate +
", adoptionStatus='" + adoptionStatus + '\'' + ", adoptionStatus='" + adoptionStatus + '\'' +
", employeeId=" + employeeId +
'}'; '}';
} }
} }

View File

@@ -11,6 +11,8 @@ public class AdoptionResponse {
private String petName; private String petName;
private Long customerId; private Long customerId;
private String customerName; private String customerName;
private Long employeeId;
private String employeeName;
private LocalDate adoptionDate; private LocalDate adoptionDate;
private String adoptionStatus; private String adoptionStatus;
private BigDecimal adoptionFee; private BigDecimal adoptionFee;
@@ -20,12 +22,14 @@ public class AdoptionResponse {
public AdoptionResponse() { public AdoptionResponse() {
} }
public AdoptionResponse(Long adoptionId, Long petId, String petName, Long customerId, String customerName, LocalDate adoptionDate, String adoptionStatus, BigDecimal adoptionFee, LocalDateTime createdAt, LocalDateTime updatedAt) { public AdoptionResponse(Long adoptionId, Long petId, String petName, Long customerId, String customerName, Long employeeId, String employeeName, LocalDate adoptionDate, String adoptionStatus, BigDecimal adoptionFee, LocalDateTime createdAt, LocalDateTime updatedAt) {
this.adoptionId = adoptionId; this.adoptionId = adoptionId;
this.petId = petId; this.petId = petId;
this.petName = petName; this.petName = petName;
this.customerId = customerId; this.customerId = customerId;
this.customerName = customerName; this.customerName = customerName;
this.employeeId = employeeId;
this.employeeName = employeeName;
this.adoptionDate = adoptionDate; this.adoptionDate = adoptionDate;
this.adoptionStatus = adoptionStatus; this.adoptionStatus = adoptionStatus;
this.adoptionFee = adoptionFee; this.adoptionFee = adoptionFee;
@@ -73,6 +77,22 @@ public class AdoptionResponse {
this.customerName = customerName; this.customerName = customerName;
} }
public Long getEmployeeId() {
return employeeId;
}
public void setEmployeeId(Long employeeId) {
this.employeeId = employeeId;
}
public String getEmployeeName() {
return employeeName;
}
public void setEmployeeName(String employeeName) {
this.employeeName = employeeName;
}
public LocalDate getAdoptionDate() { public LocalDate getAdoptionDate() {
return adoptionDate; return adoptionDate;
} }

View File

@@ -29,6 +29,8 @@ public class AppointmentRequest {
private List<Long> customerPetIds; private List<Long> customerPetIds;
private Long employeeId;
public Long getCustomerId() { public Long getCustomerId() {
return customerId; return customerId;
} }
@@ -93,6 +95,14 @@ public class AppointmentRequest {
this.customerPetIds = customerPetIds; this.customerPetIds = customerPetIds;
} }
public Long getEmployeeId() {
return employeeId;
}
public void setEmployeeId(Long employeeId) {
this.employeeId = employeeId;
}
@Override @Override
public boolean equals(Object o) { public boolean equals(Object o) {
if (this == o) return true; if (this == o) return true;
@@ -105,12 +115,13 @@ public class AppointmentRequest {
Objects.equals(appointmentTime, that.appointmentTime) && Objects.equals(appointmentTime, that.appointmentTime) &&
Objects.equals(appointmentStatus, that.appointmentStatus) && Objects.equals(appointmentStatus, that.appointmentStatus) &&
Objects.equals(petIds, that.petIds) && Objects.equals(petIds, that.petIds) &&
Objects.equals(customerPetIds, that.customerPetIds); Objects.equals(customerPetIds, that.customerPetIds) &&
Objects.equals(employeeId, that.employeeId);
} }
@Override @Override
public int hashCode() { public int hashCode() {
return Objects.hash(customerId, storeId, serviceId, appointmentDate, appointmentTime, appointmentStatus, petIds, customerPetIds); return Objects.hash(customerId, storeId, serviceId, appointmentDate, appointmentTime, appointmentStatus, petIds, customerPetIds, employeeId);
} }
@Override @Override
@@ -124,6 +135,7 @@ public class AppointmentRequest {
", appointmentStatus='" + appointmentStatus + '\'' + ", appointmentStatus='" + appointmentStatus + '\'' +
", petIds=" + petIds + ", petIds=" + petIds +
", customerPetIds=" + customerPetIds + ", customerPetIds=" + customerPetIds +
", employeeId=" + employeeId +
'}'; '}';
} }
} }

View File

@@ -17,6 +17,8 @@ public class AppointmentResponse {
private LocalDate appointmentDate; private LocalDate appointmentDate;
private LocalTime appointmentTime; private LocalTime appointmentTime;
private String appointmentStatus; private String appointmentStatus;
private Long employeeId;
private String employeeName;
private List<String> petNames; private List<String> petNames;
private List<Long> petIds; private List<Long> petIds;
private List<String> customerPetNames; private List<String> customerPetNames;
@@ -124,6 +126,22 @@ public class AppointmentResponse {
this.appointmentStatus = appointmentStatus; this.appointmentStatus = appointmentStatus;
} }
public Long getEmployeeId() {
return employeeId;
}
public void setEmployeeId(Long employeeId) {
this.employeeId = employeeId;
}
public String getEmployeeName() {
return employeeName;
}
public void setEmployeeName(String employeeName) {
this.employeeName = employeeName;
}
public List<String> getPetNames() { public List<String> getPetNames() {
return petNames; return petNames;
} }

View File

@@ -25,6 +25,10 @@ public class Adoption {
@JoinColumn(name = "customerId", nullable = false) @JoinColumn(name = "customerId", nullable = false)
private Customer customer; private Customer customer;
@ManyToOne
@JoinColumn(name = "employeeId", nullable = false)
private Employee employee;
@Column(nullable = false) @Column(nullable = false)
private LocalDate adoptionDate; private LocalDate adoptionDate;
@@ -42,10 +46,11 @@ public class Adoption {
public Adoption() { public Adoption() {
} }
public Adoption(Long adoptionId, Pet pet, Customer customer, LocalDate adoptionDate, String adoptionStatus, LocalDateTime createdAt, LocalDateTime updatedAt) { public Adoption(Long adoptionId, Pet pet, Customer customer, Employee employee, LocalDate adoptionDate, String adoptionStatus, LocalDateTime createdAt, LocalDateTime updatedAt) {
this.adoptionId = adoptionId; this.adoptionId = adoptionId;
this.pet = pet; this.pet = pet;
this.customer = customer; this.customer = customer;
this.employee = employee;
this.adoptionDate = adoptionDate; this.adoptionDate = adoptionDate;
this.adoptionStatus = adoptionStatus; this.adoptionStatus = adoptionStatus;
this.createdAt = createdAt; this.createdAt = createdAt;
@@ -76,6 +81,14 @@ public class Adoption {
this.customer = customer; this.customer = customer;
} }
public Employee getEmployee() {
return employee;
}
public void setEmployee(Employee employee) {
this.employee = employee;
}
public LocalDate getAdoptionDate() { public LocalDate getAdoptionDate() {
return adoptionDate; return adoptionDate;
} }
@@ -127,6 +140,7 @@ public class Adoption {
"adoptionId=" + adoptionId + "adoptionId=" + adoptionId +
", pet=" + pet + ", pet=" + pet +
", customer=" + customer + ", customer=" + customer +
", employee=" + employee +
", adoptionDate=" + adoptionDate + ", adoptionDate=" + adoptionDate +
", adoptionStatus='" + adoptionStatus + '\'' + ", adoptionStatus='" + adoptionStatus + '\'' +
", createdAt=" + createdAt + ", createdAt=" + createdAt +

View File

@@ -31,6 +31,10 @@ public class Appointment {
@JoinColumn(name = "serviceId", nullable = false) @JoinColumn(name = "serviceId", nullable = false)
private Service service; private Service service;
@ManyToOne
@JoinColumn(name = "employeeId", nullable = false)
private Employee employee;
@Column(nullable = false) @Column(nullable = false)
private LocalDate appointmentDate; private LocalDate appointmentDate;
@@ -67,11 +71,12 @@ public class Appointment {
public Appointment() { public Appointment() {
} }
public Appointment(Long appointmentId, Customer customer, StoreLocation store, Service service, LocalDate appointmentDate, LocalTime appointmentTime, String appointmentStatus, Set<Pet> pets, LocalDateTime createdAt, LocalDateTime updatedAt) { public Appointment(Long appointmentId, Customer customer, StoreLocation store, Service service, Employee employee, LocalDate appointmentDate, LocalTime appointmentTime, String appointmentStatus, Set<Pet> pets, LocalDateTime createdAt, LocalDateTime updatedAt) {
this.appointmentId = appointmentId; this.appointmentId = appointmentId;
this.customer = customer; this.customer = customer;
this.store = store; this.store = store;
this.service = service; this.service = service;
this.employee = employee;
this.appointmentDate = appointmentDate; this.appointmentDate = appointmentDate;
this.appointmentTime = appointmentTime; this.appointmentTime = appointmentTime;
this.appointmentStatus = appointmentStatus; this.appointmentStatus = appointmentStatus;
@@ -112,6 +117,14 @@ public class Appointment {
this.service = service; this.service = service;
} }
public Employee getEmployee() {
return employee;
}
public void setEmployee(Employee employee) {
this.employee = employee;
}
public LocalDate getAppointmentDate() { public LocalDate getAppointmentDate() {
return appointmentDate; return appointmentDate;
} }
@@ -189,6 +202,7 @@ public class Appointment {
", customer=" + customer + ", customer=" + customer +
", store=" + store + ", store=" + store +
", service=" + service + ", service=" + service +
", employee=" + employee +
", appointmentDate=" + appointmentDate + ", appointmentDate=" + appointmentDate +
", appointmentTime=" + appointmentTime + ", appointmentTime=" + appointmentTime +
", appointmentStatus='" + appointmentStatus + '\'' + ", appointmentStatus='" + appointmentStatus + '\'' +

View File

@@ -28,4 +28,8 @@ public interface AdoptionRepository extends JpaRepository<Adoption, Long> {
Page<Adoption> searchAdoptionsByCustomer(@Param("customerId") Long customerId, @Param("q") String query, Pageable pageable); Page<Adoption> searchAdoptionsByCustomer(@Param("customerId") Long customerId, @Param("q") String query, Pageable pageable);
Optional<Adoption> findFirstByPet_IdAndAdoptionStatusOrderByAdoptionDateDesc(Long petId, String adoptionStatus); Optional<Adoption> findFirstByPet_IdAndAdoptionStatusOrderByAdoptionDateDesc(Long petId, String adoptionStatus);
boolean existsByPetPetIdAndAdoptionStatusIgnoreCaseAndAdoptionIdNot(Long petId, String adoptionStatus, Long adoptionId);
boolean existsByPetPetIdAndAdoptionStatusIgnoreCase(Long petId, String adoptionStatus);
} }

View File

@@ -18,7 +18,7 @@ public interface AppointmentRepository extends JpaRepository<Appointment, Long>
@Query("SELECT a FROM Appointment a WHERE a.appointmentDate = :date AND a.appointmentTime = :time") @Query("SELECT a FROM Appointment a WHERE a.appointmentDate = :date AND a.appointmentTime = :time")
List<Appointment> findByDateAndTime(@Param("date") LocalDate date, @Param("time") LocalTime time); List<Appointment> findByDateAndTime(@Param("date") LocalDate date, @Param("time") LocalTime time);
@Query("SELECT a FROM Appointment a JOIN FETCH a.service WHERE a.store.storeId = :storeId AND a.appointmentDate = :date AND LOWER(a.appointmentStatus) <> 'cancelled'") @Query("SELECT a FROM Appointment a JOIN FETCH a.service WHERE a.store.storeId = :storeId AND a.appointmentDate = :date AND LOWER(a.appointmentStatus) NOT IN ('cancelled', 'missed')")
List<Appointment> findByStoreAndDate(@Param("storeId") Long storeId, @Param("date") LocalDate date); List<Appointment> findByStoreAndDate(@Param("storeId") Long storeId, @Param("date") LocalDate date);
@Query("SELECT DISTINCT a FROM Appointment a LEFT JOIN a.pets p WHERE " + @Query("SELECT DISTINCT a FROM Appointment a LEFT JOIN a.pets p WHERE " +
@@ -36,4 +36,10 @@ public interface AppointmentRepository extends JpaRepository<Appointment, Long>
"LOWER(a.service.serviceName) LIKE LOWER(CONCAT('%', :q, '%')) OR " + "LOWER(a.service.serviceName) LIKE LOWER(CONCAT('%', :q, '%')) OR " +
"LOWER(p.petName) LIKE LOWER(CONCAT('%', :q, '%')))") "LOWER(p.petName) LIKE LOWER(CONCAT('%', :q, '%')))")
Page<Appointment> searchAppointmentsByCustomer(@Param("customerId") Long customerId, @Param("q") String query, Pageable pageable); Page<Appointment> searchAppointmentsByCustomer(@Param("customerId") Long customerId, @Param("q") String query, Pageable pageable);
@Query("SELECT a FROM Appointment a JOIN FETCH a.service WHERE a.employee.employeeId = :employeeId AND a.appointmentDate = :date AND LOWER(a.appointmentStatus) NOT IN ('cancelled', 'missed')")
List<Appointment> findByEmployeeEmployeeIdAndAppointmentDate(@Param("employeeId") Long employeeId, @Param("date") LocalDate date);
@Query("SELECT a FROM Appointment a JOIN FETCH a.service WHERE a.employee.employeeId IN :employeeIds AND a.appointmentDate = :date AND LOWER(a.appointmentStatus) NOT IN ('cancelled', 'missed')")
List<Appointment> findByEmployeeEmployeeIdInAndAppointmentDate(@Param("employeeIds") List<Long> employeeIds, @Param("date") LocalDate date);
} }

View File

@@ -12,5 +12,7 @@ public interface CustomerPetRepository extends JpaRepository<CustomerPet, Long>
List<CustomerPet> findByCustomerCustomerIdOrderByCreatedAtDesc(Long customerId); List<CustomerPet> findByCustomerCustomerIdOrderByCreatedAtDesc(Long customerId);
List<CustomerPet> findByCustomerCustomerIdOrderByPetNameAsc(Long customerId);
Optional<CustomerPet> findByCustomerPetIdAndCustomerCustomerId(Long customerPetId, Long customerId); Optional<CustomerPet> findByCustomerPetIdAndCustomerCustomerId(Long customerPetId, Long customerId);
} }

View File

@@ -16,6 +16,9 @@ public interface CustomerRepository extends JpaRepository<Customer, Long> {
Optional<Customer> findByUserId(Long userId); Optional<Customer> findByUserId(Long userId);
List<Customer> findAllByEmail(String email); List<Customer> findAllByEmail(String email);
@Query("SELECT DISTINCT c FROM Customer c WHERE EXISTS (SELECT cp FROM CustomerPet cp WHERE cp.customer = c) ORDER BY c.firstName ASC, c.lastName ASC")
List<Customer> findAllWithPets();
@Query("SELECT c FROM Customer c WHERE " + @Query("SELECT c FROM Customer c WHERE " +
"LOWER(c.firstName) LIKE LOWER(CONCAT('%', :q, '%')) OR " + "LOWER(c.firstName) LIKE LOWER(CONCAT('%', :q, '%')) OR " +

View File

@@ -15,6 +15,8 @@ import java.util.Optional;
public interface EmployeeRepository extends JpaRepository<Employee, Long> { public interface EmployeeRepository extends JpaRepository<Employee, Long> {
Optional<Employee> findByUserId(Long userId); Optional<Employee> findByUserId(Long userId);
List<Employee> findAllByEmail(String email); List<Employee> findAllByEmail(String email);
Optional<Employee> findFirstByIsActiveTrueOrderByEmployeeIdAsc();
List<Employee> findAllByIsActiveTrueOrderByEmployeeIdAsc();
@Query("SELECT e FROM Employee e WHERE " + @Query("SELECT e FROM Employee e WHERE " +
"LOWER(e.firstName) LIKE LOWER(CONCAT('%', :q, '%')) OR " + "LOWER(e.firstName) LIKE LOWER(CONCAT('%', :q, '%')) OR " +

View File

@@ -2,11 +2,20 @@ package com.petshop.backend.repository;
import com.petshop.backend.entity.EmployeeStore; import com.petshop.backend.entity.EmployeeStore;
import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository; import org.springframework.stereotype.Repository;
import java.util.List;
import java.util.Optional; import java.util.Optional;
@Repository @Repository
public interface EmployeeStoreRepository extends JpaRepository<EmployeeStore, EmployeeStore.EmployeeStoreId> { public interface EmployeeStoreRepository extends JpaRepository<EmployeeStore, EmployeeStore.EmployeeStoreId> {
Optional<EmployeeStore> findByEmployeeEmployeeId(Long employeeId); Optional<EmployeeStore> findByEmployeeEmployeeId(Long employeeId);
@Query("SELECT es FROM EmployeeStore es WHERE es.store.storeId = :storeId AND es.employee.isActive = true ORDER BY es.employee.employeeId ASC")
List<EmployeeStore> findActiveByStoreStoreIdOrderByEmployeeEmployeeIdAsc(@Param("storeId") Long storeId);
@Query("SELECT es FROM EmployeeStore es WHERE es.employee.isActive = true ORDER BY es.employee.employeeId ASC")
List<EmployeeStore> findActiveAllOrderByEmployeeEmployeeIdAsc();
} }

View File

@@ -8,12 +8,28 @@ import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param; import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository; import org.springframework.stereotype.Repository;
import java.util.List;
@Repository @Repository
public interface PetRepository extends JpaRepository<Pet, Long> { public interface PetRepository extends JpaRepository<Pet, Long> {
List<Pet> findAllByPetStatusIgnoreCaseOrderByPetNameAsc(String petStatus);
@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))")
Page<Pet> searchPets(@Param("q") String query, @Param("species") String species, @Param("status") String status, Pageable pageable); Page<Pet> searchPets(@Param("q") String query, @Param("species") String species, @Param("status") String status, 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);
@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 " +
"(: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> searchCustomerVisiblePets(@Param("userId") Long userId, @Param("q") String query, @Param("species") String species, @Param("status") String status, Pageable pageable);
} }

View File

@@ -5,11 +5,15 @@ import com.petshop.backend.dto.adoption.AdoptionResponse;
import com.petshop.backend.dto.common.BulkDeleteRequest; import com.petshop.backend.dto.common.BulkDeleteRequest;
import com.petshop.backend.entity.Adoption; import com.petshop.backend.entity.Adoption;
import com.petshop.backend.entity.Customer; import com.petshop.backend.entity.Customer;
import com.petshop.backend.entity.Employee;
import com.petshop.backend.entity.Pet; import com.petshop.backend.entity.Pet;
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.CustomerRepository;
import com.petshop.backend.repository.EmployeeRepository;
import com.petshop.backend.repository.PetRepository; import com.petshop.backend.repository.PetRepository;
import com.petshop.backend.repository.UserRepository;
import org.springframework.data.domain.Page; import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
@@ -18,14 +22,24 @@ import org.springframework.transaction.annotation.Transactional;
@Service @Service
public class AdoptionService { public class AdoptionService {
private static final String ADOPTION_STATUS_PENDING = "Pending";
private static final String ADOPTION_STATUS_COMPLETED = "Completed";
private static final String ADOPTION_STATUS_CANCELLED = "Cancelled";
private static final String PET_STATUS_AVAILABLE = "Available";
private static final String PET_STATUS_ADOPTED = "Adopted";
private final AdoptionRepository adoptionRepository; private final AdoptionRepository adoptionRepository;
private final PetRepository petRepository; private final PetRepository petRepository;
private final CustomerRepository customerRepository; private final CustomerRepository customerRepository;
private final EmployeeRepository employeeRepository;
private final UserRepository userRepository;
public AdoptionService(AdoptionRepository adoptionRepository, PetRepository petRepository, CustomerRepository customerRepository) { public AdoptionService(AdoptionRepository adoptionRepository, PetRepository petRepository, CustomerRepository customerRepository, EmployeeRepository employeeRepository, UserRepository userRepository) {
this.adoptionRepository = adoptionRepository; this.adoptionRepository = adoptionRepository;
this.petRepository = petRepository; this.petRepository = petRepository;
this.customerRepository = customerRepository; this.customerRepository = customerRepository;
this.employeeRepository = employeeRepository;
this.userRepository = userRepository;
} }
public Page<AdoptionResponse> getAllAdoptions(String query, Pageable pageable, Long customerId) { public Page<AdoptionResponse> getAllAdoptions(String query, Pageable pageable, Long customerId) {
@@ -66,14 +80,19 @@ public class AdoptionService {
Customer customer = customerRepository.findById(request.getCustomerId()) Customer customer = customerRepository.findById(request.getCustomerId())
.orElseThrow(() -> new ResourceNotFoundException("Customer not found with id: " + request.getCustomerId())); .orElseThrow(() -> new ResourceNotFoundException("Customer not found with id: " + request.getCustomerId()));
Employee employee = resolveAdoptionEmployee(request.getEmployeeId());
String adoptionStatus = normalizeAdoptionStatus(request.getAdoptionStatus());
validatePetAvailability(pet, null);
Adoption adoption = new Adoption(); Adoption adoption = new Adoption();
adoption.setPet(pet); adoption.setPet(pet);
adoption.setCustomer(customer); adoption.setCustomer(customer);
adoption.setEmployee(employee);
adoption.setAdoptionDate(request.getAdoptionDate()); adoption.setAdoptionDate(request.getAdoptionDate());
adoption.setAdoptionStatus(request.getAdoptionStatus()); adoption.setAdoptionStatus(adoptionStatus);
adoption = adoptionRepository.save(adoption); adoption = adoptionRepository.save(adoption);
syncPetStatus(pet, adoptionStatus, adoption.getAdoptionId());
return mapToResponse(adoption); return mapToResponse(adoption);
} }
@@ -87,13 +106,18 @@ public class AdoptionService {
Customer customer = customerRepository.findById(request.getCustomerId()) Customer customer = customerRepository.findById(request.getCustomerId())
.orElseThrow(() -> new ResourceNotFoundException("Customer not found with id: " + request.getCustomerId())); .orElseThrow(() -> new ResourceNotFoundException("Customer not found with id: " + request.getCustomerId()));
Employee employee = resolveAdoptionEmployee(request.getEmployeeId());
String adoptionStatus = normalizeAdoptionStatus(request.getAdoptionStatus());
validatePetAvailability(pet, adoption.getAdoptionId());
adoption.setPet(pet); adoption.setPet(pet);
adoption.setCustomer(customer); adoption.setCustomer(customer);
adoption.setEmployee(employee);
adoption.setAdoptionDate(request.getAdoptionDate()); adoption.setAdoptionDate(request.getAdoptionDate());
adoption.setAdoptionStatus(request.getAdoptionStatus()); adoption.setAdoptionStatus(adoptionStatus);
adoption = adoptionRepository.save(adoption); adoption = adoptionRepository.save(adoption);
syncPetStatus(pet, adoptionStatus, adoption.getAdoptionId());
return mapToResponse(adoption); return mapToResponse(adoption);
} }
@@ -117,6 +141,8 @@ public class AdoptionService {
adoption.getPet().getPetName(), adoption.getPet().getPetName(),
adoption.getCustomer().getCustomerId(), adoption.getCustomer().getCustomerId(),
adoption.getCustomer().getFirstName() + " " + adoption.getCustomer().getLastName(), adoption.getCustomer().getFirstName() + " " + adoption.getCustomer().getLastName(),
adoption.getEmployee().getEmployeeId(),
adoption.getEmployee().getFirstName() + " " + adoption.getEmployee().getLastName(),
adoption.getAdoptionDate(), adoption.getAdoptionDate(),
adoption.getAdoptionStatus(), adoption.getAdoptionStatus(),
adoption.getPet().getPetPrice(), adoption.getPet().getPetPrice(),
@@ -124,4 +150,72 @@ public class AdoptionService {
adoption.getUpdatedAt() adoption.getUpdatedAt()
); );
} }
private Employee resolveAdoptionEmployee(Long requestedEmployeeId) {
if (requestedEmployeeId != null) {
Employee employee = employeeRepository.findById(requestedEmployeeId)
.orElseThrow(() -> new ResourceNotFoundException("Employee not found with id: " + requestedEmployeeId));
if (!isAssignableEmployee(employee)) {
throw new IllegalArgumentException("Selected employee is not assignable for adoption work");
}
return employee;
}
return employeeRepository.findAllByIsActiveTrueOrderByEmployeeIdAsc().stream()
.filter(this::isAssignableEmployee)
.findFirst()
.orElseThrow(() -> new IllegalArgumentException("No assignable staff member is available for adoption assignment"));
}
private boolean isAssignableEmployee(Employee employee) {
Long userId = employee.getUserId();
if (userId == null || !Boolean.TRUE.equals(employee.getIsActive())) {
return false;
}
return userRepository.findById(userId)
.filter(user -> user.getRole() == User.Role.STAFF)
.filter(user -> Boolean.TRUE.equals(user.getActive()))
.isPresent();
}
private String normalizeAdoptionStatus(String adoptionStatus) {
if (adoptionStatus == null) {
throw new IllegalArgumentException("Adoption status is required");
}
String trimmedStatus = adoptionStatus.trim();
if (ADOPTION_STATUS_PENDING.equalsIgnoreCase(trimmedStatus)) {
return ADOPTION_STATUS_PENDING;
}
if (ADOPTION_STATUS_COMPLETED.equalsIgnoreCase(trimmedStatus)) {
return ADOPTION_STATUS_COMPLETED;
}
if (ADOPTION_STATUS_CANCELLED.equalsIgnoreCase(trimmedStatus)) {
return ADOPTION_STATUS_CANCELLED;
}
throw new IllegalArgumentException("Adoption status must be Pending, Completed, or Cancelled");
}
private void validatePetAvailability(Pet pet, Long adoptionId) {
boolean adoptedElsewhere = adoptionId == null
? adoptionRepository.existsByPetPetIdAndAdoptionStatusIgnoreCase(pet.getPetId(), ADOPTION_STATUS_COMPLETED)
: adoptionRepository.existsByPetPetIdAndAdoptionStatusIgnoreCaseAndAdoptionIdNot(pet.getPetId(), ADOPTION_STATUS_COMPLETED, adoptionId);
if (adoptedElsewhere) {
throw new IllegalArgumentException("Selected pet has already been adopted");
}
if (!PET_STATUS_AVAILABLE.equalsIgnoreCase(pet.getPetStatus()) && adoptionId == null) {
throw new IllegalArgumentException("Selected pet is not available for adoption");
}
}
private void syncPetStatus(Pet pet, String adoptionStatus, Long adoptionId) {
boolean completedElsewhere = adoptionId != null
&& adoptionRepository.existsByPetPetIdAndAdoptionStatusIgnoreCaseAndAdoptionIdNot(pet.getPetId(), ADOPTION_STATUS_COMPLETED, adoptionId);
if (ADOPTION_STATUS_COMPLETED.equalsIgnoreCase(adoptionStatus) || completedElsewhere) {
pet.setPetStatus(PET_STATUS_ADOPTED);
} else {
pet.setPetStatus(PET_STATUS_AVAILABLE);
}
petRepository.save(pet);
}
} }

View File

@@ -99,6 +99,8 @@ public class AppointmentService {
public AppointmentResponse createAppointment(AppointmentRequest request) { public AppointmentResponse createAppointment(AppointmentRequest request) {
validateAppointmentRequest(request); validateAppointmentRequest(request);
User authenticatedUser = AuthenticationHelper.getAuthenticatedUser(userRepository);
Customer customer = customerRepository.findById(request.getCustomerId()) Customer customer = customerRepository.findById(request.getCustomerId())
.orElseThrow(() -> new ResourceNotFoundException("Customer not found with id: " + request.getCustomerId())); .orElseThrow(() -> new ResourceNotFoundException("Customer not found with id: " + request.getCustomerId()));
@@ -108,19 +110,19 @@ public class AppointmentService {
com.petshop.backend.entity.Service service = serviceRepository.findById(request.getServiceId()) com.petshop.backend.entity.Service service = serviceRepository.findById(request.getServiceId())
.orElseThrow(() -> new ResourceNotFoundException("Service not found with id: " + request.getServiceId())); .orElseThrow(() -> new ResourceNotFoundException("Service not found with id: " + request.getServiceId()));
validateStoreAccess(store.getStoreId());
validateAvailability(store, service, request.getAppointmentDate(), request.getAppointmentTime(), null);
boolean hasPetIds = request.getPetIds() != null && !request.getPetIds().isEmpty(); boolean hasPetIds = request.getPetIds() != null && !request.getPetIds().isEmpty();
boolean hasCustomerPetIds = request.getCustomerPetIds() != null && !request.getCustomerPetIds().isEmpty(); boolean hasCustomerPetIds = request.getCustomerPetIds() != null && !request.getCustomerPetIds().isEmpty();
if (!hasPetIds && !hasCustomerPetIds) { if (!hasPetIds && !hasCustomerPetIds) {
throw new IllegalArgumentException("Please specify at least one pet."); throw new IllegalArgumentException("Please specify at least one pet.");
} }
Set<Pet> pets = hasPetIds ? fetchPets(request.getPetIds()) : new HashSet<>(); Set<Pet> pets = hasPetIds ? fetchPets(request.getPetIds()) : new HashSet<>();
Set<CustomerPet> customerPets = hasCustomerPetIds ? fetchCustomerPets(request.getCustomerPetIds()) : new HashSet<>(); Set<CustomerPet> customerPets = hasCustomerPetIds ? fetchCustomerPets(request.getCustomerPetIds(), customer.getCustomerId()) : new HashSet<>();
Employee employee = resolveAppointmentEmployee(request.getEmployeeId(), store.getStoreId());
validateStoreAccess(store.getStoreId(), authenticatedUser);
validateAvailability(employee, service, request.getAppointmentDate(), request.getAppointmentTime(), null);
Appointment appointment = new Appointment(); Appointment appointment = new Appointment();
appointment.setCustomer(customer); appointment.setCustomer(customer);
@@ -131,6 +133,7 @@ public class AppointmentService {
appointment.setAppointmentStatus(request.getAppointmentStatus()); appointment.setAppointmentStatus(request.getAppointmentStatus());
appointment.setPets(pets); appointment.setPets(pets);
appointment.setCustomerPets(customerPets); appointment.setCustomerPets(customerPets);
appointment.setEmployee(employee);
appointment = appointmentRepository.save(appointment); appointment = appointmentRepository.save(appointment);
return mapToResponse(appointment); return mapToResponse(appointment);
@@ -140,6 +143,8 @@ public class AppointmentService {
public AppointmentResponse updateAppointment(Long id, AppointmentRequest request) { public AppointmentResponse updateAppointment(Long id, AppointmentRequest request) {
validateAppointmentRequest(request); validateAppointmentRequest(request);
User authenticatedUser = AuthenticationHelper.getAuthenticatedUser(userRepository);
Appointment appointment = appointmentRepository.findById(id) Appointment appointment = appointmentRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("Appointment not found with id: " + id)); .orElseThrow(() -> new ResourceNotFoundException("Appointment not found with id: " + id));
@@ -152,19 +157,19 @@ public class AppointmentService {
com.petshop.backend.entity.Service service = serviceRepository.findById(request.getServiceId()) com.petshop.backend.entity.Service service = serviceRepository.findById(request.getServiceId())
.orElseThrow(() -> new ResourceNotFoundException("Service not found with id: " + request.getServiceId())); .orElseThrow(() -> new ResourceNotFoundException("Service not found with id: " + request.getServiceId()));
validateStoreAccess(store.getStoreId());
validateAvailability(store, service, request.getAppointmentDate(), request.getAppointmentTime(), id);
boolean hasPetIds = request.getPetIds() != null && !request.getPetIds().isEmpty(); boolean hasPetIds = request.getPetIds() != null && !request.getPetIds().isEmpty();
boolean hasCustomerPetIds = request.getCustomerPetIds() != null && !request.getCustomerPetIds().isEmpty(); boolean hasCustomerPetIds = request.getCustomerPetIds() != null && !request.getCustomerPetIds().isEmpty();
if (!hasPetIds && !hasCustomerPetIds) { if (!hasPetIds && !hasCustomerPetIds) {
throw new IllegalArgumentException("Please specify at least one pet."); throw new IllegalArgumentException("Please specify at least one pet.");
} }
Set<Pet> pets = hasPetIds ? fetchPets(request.getPetIds()) : new HashSet<>(); Set<Pet> pets = hasPetIds ? fetchPets(request.getPetIds()) : new HashSet<>();
Set<CustomerPet> customerPets = hasCustomerPetIds ? fetchCustomerPets(request.getCustomerPetIds()) : new HashSet<>(); Set<CustomerPet> customerPets = hasCustomerPetIds ? fetchCustomerPets(request.getCustomerPetIds(), customer.getCustomerId()) : new HashSet<>();
Employee employee = resolveAppointmentEmployee(request.getEmployeeId(), store.getStoreId());
validateStoreAccess(store.getStoreId(), authenticatedUser);
validateAvailability(employee, service, request.getAppointmentDate(), request.getAppointmentTime(), id);
appointment.setCustomer(customer); appointment.setCustomer(customer);
appointment.setStore(store); appointment.setStore(store);
@@ -174,6 +179,7 @@ public class AppointmentService {
appointment.setAppointmentStatus(request.getAppointmentStatus()); appointment.setAppointmentStatus(request.getAppointmentStatus());
appointment.setPets(pets); appointment.setPets(pets);
appointment.setCustomerPets(customerPets); appointment.setCustomerPets(customerPets);
appointment.setEmployee(employee);
appointment = appointmentRepository.save(appointment); appointment = appointmentRepository.save(appointment);
return mapToResponse(appointment); return mapToResponse(appointment);
@@ -193,7 +199,6 @@ public class AppointmentService {
} }
@Transactional(readOnly = true) @Transactional(readOnly = true)
public List<String> checkAvailability(Long storeId, Long serviceId, LocalDate date) { public List<String> checkAvailability(Long storeId, Long serviceId, LocalDate date) {
storeRepository.findById(storeId) storeRepository.findById(storeId)
.orElseThrow(() -> new ResourceNotFoundException("Store not found with id: " + storeId)); .orElseThrow(() -> new ResourceNotFoundException("Store not found with id: " + storeId));
@@ -201,16 +206,20 @@ public class AppointmentService {
com.petshop.backend.entity.Service service = serviceRepository.findById(serviceId) com.petshop.backend.entity.Service service = serviceRepository.findById(serviceId)
.orElseThrow(() -> new ResourceNotFoundException("Service not found with id: " + serviceId)); .orElseThrow(() -> new ResourceNotFoundException("Service not found with id: " + serviceId));
List<Employee> assignableEmployees = employeeStoreRepository.findActiveByStoreStoreIdOrderByEmployeeEmployeeIdAsc(storeId).stream()
//-------------------------------------------------------------------------- .filter(es -> isAssignableEmployee(es.getEmployee()))
// CHANGED: filter by serviceId too .map(EmployeeStore::getEmployee)
List<Appointment> existingAppointments = appointmentRepository
.findByStoreAndDate(storeId, date)
.stream()
.filter(a -> a.getService().getServiceId().equals(serviceId))
.collect(Collectors.toList()); .collect(Collectors.toList());
// -------------------------------------------------------
if (assignableEmployees.isEmpty()) {
return List.of();
}
List<Long> employeeIds = assignableEmployees.stream().map(Employee::getEmployeeId).collect(Collectors.toList());
List<Appointment> allAppointments = appointmentRepository.findByEmployeeEmployeeIdInAndAppointmentDate(employeeIds, date);
java.util.Map<Long, List<Appointment>> appointmentsByEmployee = allAppointments.stream()
.collect(Collectors.groupingBy(a -> a.getEmployee().getEmployeeId()));
List<String> availableSlots = new ArrayList<>(); List<String> availableSlots = new ArrayList<>();
LocalTime startTime = LocalTime.of(9, 0); LocalTime startTime = LocalTime.of(9, 0);
@@ -219,7 +228,13 @@ public class AppointmentService {
LocalTime currentTime = startTime; LocalTime currentTime = startTime;
while (!currentTime.isAfter(latestStart)) { while (!currentTime.isAfter(latestStart)) {
if (isSlotAvailable(existingAppointments, service, currentTime, null)) { final LocalTime slotTime = currentTime;
boolean anyEmployeeAvailable = assignableEmployees.stream().anyMatch(emp -> {
List<Appointment> empAppointments = appointmentsByEmployee.getOrDefault(emp.getEmployeeId(), List.of());
return isSlotAvailable(empAppointments, service, slotTime, null);
});
if (anyEmployeeAvailable) {
availableSlots.add(currentTime.toString()); availableSlots.add(currentTime.toString());
} }
currentTime = currentTime.plusMinutes(30); currentTime = currentTime.plusMinutes(30);
@@ -247,11 +262,14 @@ public class AppointmentService {
return pets; return pets;
} }
private Set<CustomerPet> fetchCustomerPets(List<Long> customerPetIds) { private Set<CustomerPet> fetchCustomerPets(List<Long> customerPetIds, Long customerId) {
Set<CustomerPet> customerPets = new HashSet<>(); Set<CustomerPet> customerPets = new HashSet<>();
for (Long customerPetId : customerPetIds) { for (Long customerPetId : customerPetIds) {
CustomerPet customerPet = customerPetRepository.findById(customerPetId) CustomerPet customerPet = customerPetRepository.findById(customerPetId)
.orElseThrow(() -> new ResourceNotFoundException("Customer pet not found with id: " + customerPetId)); .orElseThrow(() -> new ResourceNotFoundException("Customer pet not found with id: " + customerPetId));
if (!customerPet.getCustomer().getCustomerId().equals(customerId)) {
throw new IllegalArgumentException("Selected pet does not belong to the selected customer");
}
customerPets.add(customerPet); customerPets.add(customerPet);
} }
@@ -286,6 +304,8 @@ public class AppointmentService {
response.setAppointmentDate(appointment.getAppointmentDate()); response.setAppointmentDate(appointment.getAppointmentDate());
response.setAppointmentTime(appointment.getAppointmentTime()); response.setAppointmentTime(appointment.getAppointmentTime());
response.setAppointmentStatus(appointment.getAppointmentStatus()); response.setAppointmentStatus(appointment.getAppointmentStatus());
response.setEmployeeId(appointment.getEmployee().getEmployeeId());
response.setEmployeeName(appointment.getEmployee().getFirstName() + " " + appointment.getEmployee().getLastName());
response.setPetNames(petNames); response.setPetNames(petNames);
response.setPetIds(petIds); response.setPetIds(petIds);
response.setCustomerPetNames(customerPetNames); response.setCustomerPetNames(customerPetNames);
@@ -296,20 +316,46 @@ public class AppointmentService {
return response; return response;
} }
//------------------------------------ private Employee resolveAppointmentEmployee(Long requestedEmployeeId, Long storeId) {
private void validateAvailability(StoreLocation store, com.petshop.backend.entity.Service service, LocalDate date, LocalTime time, Long appointmentIdToIgnore) { List<EmployeeStore> assignableEmployees = employeeStoreRepository.findActiveByStoreStoreIdOrderByEmployeeEmployeeIdAsc(storeId).stream()
// Filter by same service only - different services can run at same time .filter(es -> isAssignableEmployee(es.getEmployee()))
List<Appointment> existingAppointments = appointmentRepository
.findByStoreAndDate(store.getStoreId(), date)
.stream()
.filter(a -> a.getService().getServiceId().equals(service.getServiceId()))
.collect(Collectors.toList()); .collect(Collectors.toList());
if (!isSlotAvailable(existingAppointments, service, time, appointmentIdToIgnore)) {
throw new IllegalArgumentException("Appointment time is not available for the selected store and service"); if (requestedEmployeeId != null) {
Employee employee = employeeRepository.findById(requestedEmployeeId)
.orElseThrow(() -> new ResourceNotFoundException("Employee not found with id: " + requestedEmployeeId));
boolean assignedToStore = assignableEmployees.stream()
.anyMatch(es -> es.getEmployee().getEmployeeId().equals(requestedEmployeeId));
if (!assignedToStore) {
throw new IllegalArgumentException("Selected employee is not assignable for the selected store");
}
return employee;
} }
return assignableEmployees.stream()
.map(EmployeeStore::getEmployee)
.findFirst()
.orElseThrow(() -> new IllegalArgumentException("No assignable staff member is assigned to the selected store"));
} }
//------------------------------------------------ private boolean isAssignableEmployee(Employee employee) {
Long userId = employee.getUserId();
if (userId == null || !Boolean.TRUE.equals(employee.getIsActive())) {
return false;
}
return userRepository.findById(userId)
.filter(user -> user.getRole() == User.Role.STAFF)
.filter(user -> Boolean.TRUE.equals(user.getActive()))
.isPresent();
}
private void validateAvailability(Employee employee, com.petshop.backend.entity.Service service, LocalDate date, LocalTime time, Long appointmentIdToIgnore) {
List<Appointment> existingAppointments = appointmentRepository
.findByEmployeeEmployeeIdAndAppointmentDate(employee.getEmployeeId(), date);
if (!isSlotAvailable(existingAppointments, service, time, appointmentIdToIgnore)) {
throw new IllegalArgumentException("The selected employee is already booked for this time slot");
}
}
private boolean isSlotAvailable(List<Appointment> existingAppointments, com.petshop.backend.entity.Service requestedService, LocalTime requestedStart, Long appointmentIdToIgnore) { private boolean isSlotAvailable(List<Appointment> existingAppointments, com.petshop.backend.entity.Service requestedService, LocalTime requestedStart, Long appointmentIdToIgnore) {
LocalTime requestedEnd = requestedStart.plusMinutes(requestedService.getServiceDuration()); LocalTime requestedEnd = requestedStart.plusMinutes(requestedService.getServiceDuration());
@@ -326,8 +372,7 @@ public class AppointmentService {
return true; return true;
} }
private void validateStoreAccess(Long requestedStoreId) { private void validateStoreAccess(Long requestedStoreId, User user) {
User user = AuthenticationHelper.getAuthenticatedUser(userRepository);
if (user.getRole() != User.Role.STAFF) { if (user.getRole() != User.Role.STAFF) {
return; return;
} }

View File

@@ -7,12 +7,16 @@ import com.petshop.backend.entity.Adoption;
import com.petshop.backend.entity.Pet; 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.security.AppPrincipal;
import com.petshop.backend.repository.AdoptionRepository; import com.petshop.backend.repository.AdoptionRepository;
import com.petshop.backend.repository.PetRepository; import com.petshop.backend.repository.PetRepository;
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;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Pageable;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
@@ -34,13 +38,38 @@ public class PetService {
} }
public Page<PetResponse> getAllPets(String query, String species, String status, Pageable pageable) { public Page<PetResponse> getAllPets(String query, String species, String status, Pageable pageable) {
return petRepository.searchPets(normalizeFilter(query), normalizeFilter(species), normalizeFilter(status), pageable) String normalizedQuery = normalizeFilter(query);
String normalizedSpecies = normalizeFilter(species);
String normalizedStatus = normalizeFilter(status);
CurrentViewer viewer = getCurrentViewer();
Page<Pet> pets;
if (viewer == null) {
if (!isAllowedPublicStatus(normalizedStatus)) {
return new PageImpl<>(java.util.List.of(), pageable, 0);
}
pets = petRepository.searchPublicPets(normalizedQuery, normalizedSpecies, pageable);
} else if (viewer.role() == User.Role.STAFF || viewer.role() == User.Role.ADMIN) {
pets = petRepository.searchPets(normalizedQuery, normalizedSpecies, normalizedStatus, 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);
}
return pets
.map(this::mapToResponse); .map(this::mapToResponse);
} }
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));
if (!canViewPet(pet, getCurrentViewer())) {
throw new ResourceNotFoundException("Pet not found with id: " + id);
}
return mapToResponse(pet); return mapToResponse(pet);
} }
@@ -110,7 +139,7 @@ public class PetService {
if (pet.getImageUrl() == null || pet.getImageUrl().isBlank()) { if (pet.getImageUrl() == null || pet.getImageUrl().isBlank()) {
throw new ResourceNotFoundException("Pet image not found for id: " + id); throw new ResourceNotFoundException("Pet image not found for id: " + id);
} }
if (!canViewPetImage(pet, requesterUserId, requesterRole)) { if (!canViewPet(pet, new CurrentViewer(requesterUserId, requesterRole))) {
throw new ForbiddenImageAccessException(); throw new ForbiddenImageAccessException();
} }
Resource resource = catalogImageStorageService.loadPetImage(pet.getImageUrl()); Resource resource = catalogImageStorageService.loadPetImage(pet.getImageUrl());
@@ -122,14 +151,21 @@ public class PetService {
return "available".equalsIgnoreCase(normalizeStatus(pet.getPetStatus())); return "available".equalsIgnoreCase(normalizeStatus(pet.getPetStatus()));
} }
private boolean canViewPetImage(Pet pet, Long requesterUserId, User.Role requesterRole) { private boolean canViewPet(Pet pet, CurrentViewer viewer) {
if (isPubliclyVisible(pet)) { if (isPubliclyVisible(pet)) {
return true; return true;
} }
if (requesterRole == User.Role.STAFF || requesterRole == User.Role.ADMIN) { if (viewer != null && (viewer.role() == User.Role.STAFF || viewer.role() == User.Role.ADMIN)) {
return true; return true;
} }
if (requesterUserId == null) { if (viewer == null || viewer.userId() == null) {
return false;
}
return isAdoptedByUser(pet, viewer.userId());
}
private boolean isAdoptedByUser(Pet pet, Long userId) {
if (userId == null) {
return false; return false;
} }
if (!"adopted".equalsIgnoreCase(normalizeStatus(pet.getPetStatus()))) { if (!"adopted".equalsIgnoreCase(normalizeStatus(pet.getPetStatus()))) {
@@ -137,10 +173,22 @@ public class PetService {
} }
return adoptionRepository.findFirstByPet_IdAndAdoptionStatusOrderByAdoptionDateDesc(pet.getPetId(), "Completed") return adoptionRepository.findFirstByPet_IdAndAdoptionStatusOrderByAdoptionDateDesc(pet.getPetId(), "Completed")
.map(Adoption::getCustomer) .map(Adoption::getCustomer)
.map(customer -> requesterUserId.equals(customer.getUserId())) .map(customer -> userId.equals(customer.getUserId()))
.orElse(false); .orElse(false);
} }
private CurrentViewer getCurrentViewer() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication == null || !authentication.isAuthenticated()) {
return null;
}
Object principal = authentication.getPrincipal();
if (principal instanceof AppPrincipal appPrincipal) {
return new CurrentViewer(appPrincipal.getUserId(), appPrincipal.getRole());
}
return null;
}
private Pet findPet(Long id) { private Pet findPet(Long id) {
return petRepository.findById(id) return petRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("Pet not found with id: " + id)); .orElseThrow(() -> new ResourceNotFoundException("Pet not found with id: " + id));
@@ -177,6 +225,14 @@ public class PetService {
return status == null ? "" : status.trim(); return status == null ? "" : status.trim();
} }
private boolean isAllowedPublicStatus(String status) {
return status == null || "available".equalsIgnoreCase(status);
}
private boolean isAllowedCustomerStatus(String status) {
return status == null || "available".equalsIgnoreCase(status) || "adopted".equalsIgnoreCase(status);
}
private String normalizeFilter(String value) { private String normalizeFilter(String value) {
if (value == null) { if (value == null) {
return null; return null;
@@ -203,6 +259,9 @@ public class PetService {
public record ImagePayload(Resource resource, MediaType mediaType) { public record ImagePayload(Resource resource, MediaType mediaType) {
} }
private record CurrentViewer(Long userId, User.Role role) {
}
public static class ForbiddenImageAccessException extends RuntimeException { public static class ForbiddenImageAccessException extends RuntimeException {
} }
} }

View File

@@ -0,0 +1,61 @@
ALTER TABLE appointment
ADD COLUMN employeeId BIGINT NULL;
UPDATE appointment a
SET a.employeeId = (
SELECT es.employeeId
FROM employeeStore es
JOIN employee e ON e.employeeId = es.employeeId
JOIN users u ON u.id = e.user_id
WHERE es.storeId = a.storeId
AND e.isActive = TRUE
AND u.role = 'STAFF'
ORDER BY es.employeeId ASC
LIMIT 1
)
WHERE a.employeeId IS NULL;
UPDATE appointment a
SET a.employeeId = (
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 ASC
LIMIT 1
)
WHERE a.employeeId IS NULL;
ALTER TABLE appointment
ADD CONSTRAINT fk_appointment_employee
FOREIGN KEY (employeeId) REFERENCES employee(employeeId);
CREATE INDEX idx_appointment_employeeId ON appointment(employeeId);
ALTER TABLE appointment
MODIFY employeeId BIGINT NOT NULL;
ALTER TABLE adoption
ADD COLUMN employeeId BIGINT NULL;
UPDATE adoption a
SET a.employeeId = (
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 ASC
LIMIT 1
)
WHERE a.employeeId IS NULL;
ALTER TABLE adoption
ADD CONSTRAINT fk_adoption_employee
FOREIGN KEY (employeeId) REFERENCES employee(employeeId);
CREATE INDEX idx_adoption_employeeId ON adoption(employeeId);
ALTER TABLE adoption
MODIFY employeeId BIGINT NOT NULL;

View File

@@ -0,0 +1,4 @@
UPDATE users u
SET u.active = TRUE
WHERE u.role IN ('STAFF', 'ADMIN')
AND EXISTS (SELECT 1 FROM employee e WHERE e.user_id = u.id);

View File

@@ -0,0 +1,22 @@
INSERT INTO customer_pet (customer_id, pet_name, species, breed)
SELECT DISTINCT a.customerId, p.petName, p.petSpecies, p.petBreed
FROM appointmentPet ap
JOIN appointment a ON a.appointmentId = ap.appointmentId
JOIN pet p ON p.petId = ap.petId
WHERE NOT EXISTS (
SELECT 1 FROM customer_pet cp
WHERE cp.customer_id = a.customerId AND cp.pet_name = p.petName
);
INSERT INTO appointment_customer_pet (appointment_id, customer_pet_id)
SELECT ap.appointmentId, cp.customer_pet_id
FROM appointmentPet ap
JOIN appointment a ON a.appointmentId = ap.appointmentId
JOIN pet p ON p.petId = ap.petId
JOIN customer_pet cp ON cp.customer_id = a.customerId AND cp.pet_name = p.petName
WHERE NOT EXISTS (
SELECT 1 FROM appointment_customer_pet acp
WHERE acp.appointment_id = ap.appointmentId AND acp.customer_pet_id = cp.customer_pet_id
);
DELETE FROM appointmentPet;

View File

@@ -0,0 +1,40 @@
UPDATE appointment
SET appointmentStatus = 'Missed'
WHERE LOWER(appointmentStatus) = 'booked'
AND (
appointmentDate < CURRENT_DATE
OR (appointmentDate = CURRENT_DATE AND appointmentTime < CURRENT_TIME)
);
UPDATE appointment a1
JOIN (
SELECT a3.appointmentId
FROM appointment a3
INNER JOIN appointment a4
ON a4.employeeId = a3.employeeId
AND a4.appointmentDate = a3.appointmentDate
AND a4.appointmentTime = a3.appointmentTime
AND a4.appointmentId < a3.appointmentId
WHERE LOWER(a3.appointmentStatus) NOT IN ('cancelled', 'missed')
) conflicting ON conflicting.appointmentId = a1.appointmentId
SET a1.employeeId = (
SELECT es.employeeId
FROM employeeStore es
JOIN employee e ON e.employeeId = es.employeeId
JOIN users u ON u.id = e.user_id
WHERE es.storeId = a1.storeId
AND e.isActive = TRUE
AND u.role = 'STAFF'
AND NOT EXISTS (
SELECT 1 FROM (
SELECT employeeId, appointmentDate, appointmentTime, appointmentId
FROM appointment
) snap
WHERE snap.employeeId = es.employeeId
AND snap.appointmentDate = a1.appointmentDate
AND snap.appointmentTime = a1.appointmentTime
AND snap.appointmentId <> a1.appointmentId
)
ORDER BY es.employeeId ASC
LIMIT 1
);

View File

@@ -0,0 +1,53 @@
INSERT INTO customer_pet (customer_id, pet_name, species, breed, image_url)
SELECT c.customerId, 'Rocky', 'dog', 'Labrador', NULL
FROM customer c
WHERE c.email = 'alex@gmail.com'
AND NOT EXISTS (
SELECT 1 FROM customer_pet cp
WHERE cp.customer_id = c.customerId AND cp.pet_name = 'Rocky'
);
INSERT INTO customer_pet (customer_id, pet_name, species, breed, image_url)
SELECT c.customerId, 'Whiskers', 'cat', 'Persian', NULL
FROM customer c
WHERE c.email = 'emily@gmail.com'
AND NOT EXISTS (
SELECT 1 FROM customer_pet cp
WHERE cp.customer_id = c.customerId AND cp.pet_name = 'Whiskers'
);
INSERT INTO customer_pet (customer_id, pet_name, species, breed, image_url)
SELECT c.customerId, 'Daisy', 'dog', 'Beagle', NULL
FROM customer c
WHERE c.email = 'james@gmail.com'
AND NOT EXISTS (
SELECT 1 FROM customer_pet cp
WHERE cp.customer_id = c.customerId AND cp.pet_name = 'Daisy'
);
INSERT INTO customer_pet (customer_id, pet_name, species, breed, image_url)
SELECT c.customerId, 'Pepper', 'cat', 'Domestic Shorthair', NULL
FROM customer c
WHERE c.email = 'olivia@gmail.com'
AND NOT EXISTS (
SELECT 1 FROM customer_pet cp
WHERE cp.customer_id = c.customerId AND cp.pet_name = 'Pepper'
);
INSERT INTO customer_pet (customer_id, pet_name, species, breed, image_url)
SELECT c.customerId, 'Cooper', 'dog', 'Golden Retriever', NULL
FROM customer c
WHERE c.email = 'william@gmail.com'
AND NOT EXISTS (
SELECT 1 FROM customer_pet cp
WHERE cp.customer_id = c.customerId AND cp.pet_name = 'Cooper'
);
INSERT INTO customer_pet (customer_id, pet_name, species, breed, image_url)
SELECT c.customerId, 'Mittens', 'cat', 'Siamese', NULL
FROM customer c
WHERE c.email = 'sophia@gmail.com'
AND NOT EXISTS (
SELECT 1 FROM customer_pet cp
WHERE cp.customer_id = c.customerId AND cp.pet_name = 'Mittens'
);

View File

@@ -0,0 +1,201 @@
package com.petshop.backend.controller;
import com.petshop.backend.entity.Employee;
import com.petshop.backend.entity.EmployeeStore;
import com.petshop.backend.entity.Customer;
import com.petshop.backend.entity.StoreLocation;
import com.petshop.backend.entity.User;
import com.petshop.backend.repository.CategoryRepository;
import com.petshop.backend.repository.CustomerPetRepository;
import com.petshop.backend.repository.CustomerRepository;
import com.petshop.backend.repository.EmployeeStoreRepository;
import com.petshop.backend.repository.PetRepository;
import com.petshop.backend.repository.ProductRepository;
import com.petshop.backend.repository.ServiceRepository;
import com.petshop.backend.repository.StoreRepository;
import com.petshop.backend.repository.SupplierRepository;
import com.petshop.backend.repository.UserRepository;
import com.petshop.backend.security.AppPrincipal;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import java.util.List;
import java.util.Optional;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
class DropdownControllerTest {
private PetRepository petRepository;
private CustomerRepository customerRepository;
private CustomerPetRepository customerPetRepository;
private ServiceRepository serviceRepository;
private ProductRepository productRepository;
private CategoryRepository categoryRepository;
private StoreRepository storeRepository;
private SupplierRepository supplierRepository;
private EmployeeStoreRepository employeeStoreRepository;
private UserRepository userRepository;
private DropdownController controller;
@BeforeEach
void setUp() {
petRepository = mock(PetRepository.class);
customerRepository = mock(CustomerRepository.class);
customerPetRepository = mock(CustomerPetRepository.class);
serviceRepository = mock(ServiceRepository.class);
productRepository = mock(ProductRepository.class);
categoryRepository = mock(CategoryRepository.class);
storeRepository = mock(StoreRepository.class);
supplierRepository = mock(SupplierRepository.class);
employeeStoreRepository = mock(EmployeeStoreRepository.class);
userRepository = mock(UserRepository.class);
controller = new DropdownController(
petRepository,
customerRepository,
customerPetRepository,
serviceRepository,
productRepository,
categoryRepository,
storeRepository,
supplierRepository,
employeeStoreRepository,
userRepository
);
}
@AfterEach
void tearDown() {
SecurityContextHolder.clearContext();
}
private void setAuthentication(Long userId, User.Role role) {
SecurityContextHolder.getContext().setAuthentication(
new UsernamePasswordAuthenticationToken(
new AppPrincipal(userId, "user", role, 0),
null,
List.of()
)
);
}
@Test
void getStoreEmployeesReturnsOnlyStaffLinkedEmployees() {
StoreLocation store = new StoreLocation();
store.setStoreId(1L);
Employee staffEmployee = new Employee();
staffEmployee.setEmployeeId(7L);
staffEmployee.setUserId(7L);
staffEmployee.setFirstName("Alex");
staffEmployee.setLastName("Jones");
staffEmployee.setIsActive(true);
Employee adminEmployee = new Employee();
adminEmployee.setEmployeeId(8L);
adminEmployee.setUserId(8L);
adminEmployee.setFirstName("Admin");
adminEmployee.setLastName("Helper");
adminEmployee.setIsActive(true);
User staffUser = new User();
staffUser.setId(7L);
staffUser.setRole(User.Role.STAFF);
staffUser.setActive(true);
User adminUser = new User();
adminUser.setId(8L);
adminUser.setRole(User.Role.ADMIN);
adminUser.setActive(true);
when(employeeStoreRepository.findActiveByStoreStoreIdOrderByEmployeeEmployeeIdAsc(1L))
.thenReturn(List.of(new EmployeeStore(staffEmployee, store), new EmployeeStore(adminEmployee, store)));
when(userRepository.findById(7L)).thenReturn(Optional.of(staffUser));
when(userRepository.findById(8L)).thenReturn(Optional.of(adminUser));
var response = controller.getStoreEmployees(1L);
assertEquals(1, response.getBody().size());
assertEquals(Long.valueOf(7L), response.getBody().get(0).getId());
}
@Test
void getStoreEmployeesReturnsAllStaffWhenStoreIdIsNull() {
StoreLocation store = new StoreLocation();
store.setStoreId(1L);
Employee staffEmployee = new Employee();
staffEmployee.setEmployeeId(7L);
staffEmployee.setUserId(7L);
staffEmployee.setFirstName("Alex");
staffEmployee.setLastName("Jones");
staffEmployee.setIsActive(true);
User staffUser = new User();
staffUser.setId(7L);
staffUser.setRole(User.Role.STAFF);
staffUser.setActive(true);
when(employeeStoreRepository.findActiveAllOrderByEmployeeEmployeeIdAsc())
.thenReturn(List.of(new EmployeeStore(staffEmployee, store)));
when(userRepository.findById(7L)).thenReturn(Optional.of(staffUser));
var response = controller.getStoreEmployees(null);
assertEquals(1, response.getBody().size());
assertEquals(Long.valueOf(7L), response.getBody().get(0).getId());
}
@Test
void getStoreEmployeesExcludesInactiveStaffUsers() {
StoreLocation store = new StoreLocation();
store.setStoreId(1L);
Employee inactiveStaffEmployee = new Employee();
inactiveStaffEmployee.setEmployeeId(7L);
inactiveStaffEmployee.setUserId(7L);
inactiveStaffEmployee.setFirstName("Alex");
inactiveStaffEmployee.setLastName("Jones");
inactiveStaffEmployee.setIsActive(true);
User inactiveStaffUser = new User();
inactiveStaffUser.setId(7L);
inactiveStaffUser.setRole(User.Role.STAFF);
inactiveStaffUser.setActive(false);
when(employeeStoreRepository.findActiveByStoreStoreIdOrderByEmployeeEmployeeIdAsc(1L))
.thenReturn(List.of(new EmployeeStore(inactiveStaffEmployee, store)));
when(userRepository.findById(7L)).thenReturn(Optional.of(inactiveStaffUser));
var response = controller.getStoreEmployees(1L);
assertEquals(0, response.getBody().size());
}
@Test
void getAppointmentCustomersReturnsOnlyCustomersWithPetsForStaff() {
User staffUser = new User();
staffUser.setId(99L);
staffUser.setRole(User.Role.STAFF);
when(userRepository.findById(99L)).thenReturn(Optional.of(staffUser));
setAuthentication(99L, User.Role.STAFF);
Customer one = new Customer();
one.setCustomerId(1L);
one.setFirstName("Alex");
one.setLastName("Brown");
when(customerRepository.findAllWithPets()).thenReturn(List.of(one));
var response = controller.getAppointmentCustomers();
assertEquals(1, response.getBody().size());
assertEquals(Long.valueOf(1L), response.getBody().get(0).getId());
}
}

View File

@@ -0,0 +1,150 @@
package com.petshop.backend.service;
import com.petshop.backend.dto.adoption.AdoptionRequest;
import com.petshop.backend.entity.Adoption;
import com.petshop.backend.entity.Customer;
import com.petshop.backend.entity.Employee;
import com.petshop.backend.entity.Pet;
import com.petshop.backend.entity.User;
import com.petshop.backend.repository.AdoptionRepository;
import com.petshop.backend.repository.CustomerRepository;
import com.petshop.backend.repository.EmployeeRepository;
import com.petshop.backend.repository.PetRepository;
import com.petshop.backend.repository.UserRepository;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoSettings;
import org.mockito.junit.jupiter.MockitoExtension;
import org.mockito.quality.Strictness;
import java.time.LocalDate;
import java.util.List;
import java.util.Optional;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
@MockitoSettings(strictness = Strictness.LENIENT)
class AdoptionServiceTest {
@Mock private AdoptionRepository adoptionRepository;
@Mock private PetRepository petRepository;
@Mock private CustomerRepository customerRepository;
@Mock private EmployeeRepository employeeRepository;
@Mock private UserRepository userRepository;
@InjectMocks
private AdoptionService adoptionService;
private Pet pet;
private Customer customer;
private Employee staffEmployee;
private Employee adminEmployee;
@BeforeEach
void setUp() {
pet = new Pet();
pet.setPetId(1L);
pet.setPetName("Buddy");
pet.setPetStatus("Available");
customer = new Customer();
customer.setCustomerId(1L);
customer.setFirstName("Pat");
customer.setLastName("Owner");
staffEmployee = new Employee();
staffEmployee.setEmployeeId(7L);
staffEmployee.setUserId(7L);
staffEmployee.setFirstName("Alex");
staffEmployee.setLastName("Jones");
staffEmployee.setIsActive(true);
adminEmployee = new Employee();
adminEmployee.setEmployeeId(8L);
adminEmployee.setUserId(8L);
adminEmployee.setFirstName("Admin");
adminEmployee.setLastName("Helper");
adminEmployee.setIsActive(true);
User staffUser = new User();
staffUser.setId(7L);
staffUser.setRole(User.Role.STAFF);
staffUser.setActive(true);
when(userRepository.findById(7L)).thenReturn(Optional.of(staffUser));
User adminUser = new User();
adminUser.setId(8L);
adminUser.setRole(User.Role.ADMIN);
adminUser.setActive(true);
when(userRepository.findById(8L)).thenReturn(Optional.of(adminUser));
}
@Test
void createAdoptionAutoAssignsFirstStaffEmployee() {
when(petRepository.findById(1L)).thenReturn(Optional.of(pet));
when(customerRepository.findById(1L)).thenReturn(Optional.of(customer));
when(employeeRepository.findAllByIsActiveTrueOrderByEmployeeIdAsc()).thenReturn(List.of(adminEmployee, staffEmployee));
when(adoptionRepository.save(any(Adoption.class))).thenAnswer(invocation -> {
Adoption adoption = invocation.getArgument(0);
adoption.setAdoptionId(10L);
return adoption;
});
AdoptionRequest request = new AdoptionRequest();
request.setPetId(1L);
request.setCustomerId(1L);
request.setAdoptionDate(LocalDate.now());
request.setAdoptionStatus("Pending");
var response = adoptionService.createAdoption(request);
assertEquals(7L, response.getEmployeeId());
assertEquals("Alex Jones", response.getEmployeeName());
}
@Test
void createAdoptionRejectsAdminEmployeeSelection() {
when(petRepository.findById(1L)).thenReturn(Optional.of(pet));
when(customerRepository.findById(1L)).thenReturn(Optional.of(customer));
when(employeeRepository.findById(8L)).thenReturn(Optional.of(adminEmployee));
AdoptionRequest request = new AdoptionRequest();
request.setPetId(1L);
request.setCustomerId(1L);
request.setEmployeeId(8L);
request.setAdoptionDate(LocalDate.now());
request.setAdoptionStatus("Pending");
assertThrows(IllegalArgumentException.class, () -> adoptionService.createAdoption(request));
}
@Test
void createAdoptionRejectsInactiveStaffUserSelection() {
User inactiveStaffUser = new User();
inactiveStaffUser.setId(7L);
inactiveStaffUser.setRole(User.Role.STAFF);
inactiveStaffUser.setActive(false);
when(userRepository.findById(7L)).thenReturn(Optional.of(inactiveStaffUser));
when(petRepository.findById(1L)).thenReturn(Optional.of(pet));
when(customerRepository.findById(1L)).thenReturn(Optional.of(customer));
when(employeeRepository.findById(7L)).thenReturn(Optional.of(staffEmployee));
AdoptionRequest request = new AdoptionRequest();
request.setPetId(1L);
request.setCustomerId(1L);
request.setEmployeeId(7L);
request.setAdoptionDate(LocalDate.now());
request.setAdoptionStatus("Pending");
assertThrows(IllegalArgumentException.class, () -> adoptionService.createAdoption(request));
}
}

View File

@@ -2,23 +2,32 @@ package com.petshop.backend.service;
import com.petshop.backend.entity.Appointment; import com.petshop.backend.entity.Appointment;
import com.petshop.backend.entity.Customer; import com.petshop.backend.entity.Customer;
import com.petshop.backend.entity.CustomerPet;
import com.petshop.backend.entity.Employee;
import com.petshop.backend.entity.EmployeeStore;
import com.petshop.backend.entity.Pet; import com.petshop.backend.entity.Pet;
import com.petshop.backend.entity.Service; import com.petshop.backend.entity.Service;
import com.petshop.backend.entity.StoreLocation; import com.petshop.backend.entity.StoreLocation;
import com.petshop.backend.entity.User; import com.petshop.backend.entity.User;
import com.petshop.backend.repository.AppointmentRepository; import com.petshop.backend.repository.AppointmentRepository;
import com.petshop.backend.repository.CustomerPetRepository;
import com.petshop.backend.repository.CustomerRepository; import com.petshop.backend.repository.CustomerRepository;
import com.petshop.backend.repository.EmployeeRepository;
import com.petshop.backend.repository.EmployeeStoreRepository;
import com.petshop.backend.repository.PetRepository; import com.petshop.backend.repository.PetRepository;
import com.petshop.backend.repository.ServiceRepository; import com.petshop.backend.repository.ServiceRepository;
import com.petshop.backend.repository.StoreRepository; import com.petshop.backend.repository.StoreRepository;
import com.petshop.backend.repository.UserRepository; import com.petshop.backend.repository.UserRepository;
import com.petshop.backend.security.AppPrincipal;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks; import org.mockito.InjectMocks;
import org.mockito.Mock; import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoSettings;
import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.junit.jupiter.MockitoExtension;
import org.mockito.quality.Strictness;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.context.SecurityContextHolder;
@@ -29,32 +38,26 @@ import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.Set; import java.util.Set;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.any; import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class) @ExtendWith(MockitoExtension.class)
@MockitoSettings(strictness = Strictness.LENIENT)
class AppointmentServiceTest { class AppointmentServiceTest {
@Mock @Mock private AppointmentRepository appointmentRepository;
private AppointmentRepository appointmentRepository; @Mock private CustomerRepository customerRepository;
@Mock private CustomerPetRepository customerPetRepository;
@Mock @Mock private ServiceRepository serviceRepository;
private CustomerRepository customerRepository; @Mock private PetRepository petRepository;
@Mock private StoreRepository storeRepository;
@Mock @Mock private UserRepository userRepository;
private PetRepository petRepository; @Mock private EmployeeRepository employeeRepository;
@Mock private EmployeeStoreRepository employeeStoreRepository;
@Mock
private ServiceRepository serviceRepository;
@Mock
private StoreRepository storeRepository;
@Mock
private UserRepository userRepository;
@InjectMocks @InjectMocks
private AppointmentService appointmentService; private AppointmentService appointmentService;
@@ -64,10 +67,19 @@ class AppointmentServiceTest {
private Service grooming; private Service grooming;
private Service nailTrim; private Service nailTrim;
private Pet pet; private Pet pet;
private CustomerPet customerPet;
private Employee employee;
private LocalDate date; private LocalDate date;
@BeforeEach @BeforeEach
void setUp() { void setUp() {
setAuthentication(99L, User.Role.ADMIN);
User adminUser = new User();
adminUser.setId(99L);
adminUser.setRole(User.Role.ADMIN);
adminUser.setActive(true);
when(userRepository.findById(99L)).thenReturn(Optional.of(adminUser));
customer = new Customer(); customer = new Customer();
customer.setCustomerId(1L); customer.setCustomerId(1L);
customer.setFirstName("Pat"); customer.setFirstName("Pat");
@@ -91,8 +103,25 @@ class AppointmentServiceTest {
pet.setPetId(1L); pet.setPetId(1L);
pet.setPetName("Milo"); pet.setPetName("Milo");
date = LocalDate.now().plusDays(1); customerPet = new CustomerPet();
customerPet.setCustomerPetId(11L);
customerPet.setPetName("Milo Jr");
customerPet.setCustomer(customer);
employee = new Employee();
employee.setEmployeeId(7L);
employee.setUserId(7L);
employee.setFirstName("Alex");
employee.setLastName("Jones");
employee.setIsActive(true);
User staffUser = new User();
staffUser.setId(7L);
staffUser.setRole(User.Role.STAFF);
staffUser.setActive(true);
when(userRepository.findById(7L)).thenReturn(Optional.of(staffUser));
date = LocalDate.now().plusDays(1);
} }
@AfterEach @AfterEach
@@ -101,11 +130,28 @@ class AppointmentServiceTest {
} }
@Test @Test
void checkAvailabilityAllowsDifferentServicesAtSameTime() { void checkAvailabilityAllowsConcurrentAppointmentsIfAnotherEmployeeFree() {
Employee employee2 = new Employee();
employee2.setEmployeeId(8L);
employee2.setUserId(8L);
employee2.setFirstName("Bob");
employee2.setIsActive(true);
User staffUser2 = new User();
staffUser2.setId(8L);
staffUser2.setRole(User.Role.STAFF);
staffUser2.setActive(true);
when(userRepository.findById(8L)).thenReturn(Optional.of(staffUser2));
Appointment existing = appointment(1L, date, LocalTime.of(10, 0), grooming, store); Appointment existing = appointment(1L, date, LocalTime.of(10, 0), grooming, store);
when(storeRepository.findById(1L)).thenReturn(Optional.of(store)); when(storeRepository.findById(1L)).thenReturn(Optional.of(store));
when(serviceRepository.findById(2L)).thenReturn(Optional.of(nailTrim)); when(serviceRepository.findById(2L)).thenReturn(Optional.of(nailTrim));
when(appointmentRepository.findByStoreAndDate(1L, date)).thenReturn(List.of(existing));
when(employeeStoreRepository.findActiveByStoreStoreIdOrderByEmployeeEmployeeIdAsc(1L))
.thenReturn(List.of(new EmployeeStore(employee, store), new EmployeeStore(employee2, store)));
when(appointmentRepository.findByEmployeeEmployeeIdAndAppointmentDate(7L, date)).thenReturn(List.of(existing));
when(appointmentRepository.findByEmployeeEmployeeIdAndAppointmentDate(8L, date)).thenReturn(List.of());
List<String> slots = appointmentService.checkAvailability(1L, 2L, date); List<String> slots = appointmentService.checkAvailability(1L, 2L, date);
@@ -113,53 +159,26 @@ class AppointmentServiceTest {
} }
@Test @Test
void checkAvailabilityBlocksSameServiceAtSameTime() { void createAppointmentRejectsCustomerPetOwnedByDifferentCustomerForStaff() {
Appointment existing = appointment(1L, date, LocalTime.of(10, 0), grooming, store); setAuthentication(7L, User.Role.STAFF);
when(storeRepository.findById(1L)).thenReturn(Optional.of(store)); when(employeeRepository.findByUserId(7L)).thenReturn(Optional.of(employee));
when(serviceRepository.findById(1L)).thenReturn(Optional.of(grooming)); when(employeeStoreRepository.findByEmployeeEmployeeId(7L)).thenReturn(Optional.of(new EmployeeStore(employee, store)));
when(appointmentRepository.findByStoreAndDate(1L, date)).thenReturn(List.of(existing));
List<String> slots = appointmentService.checkAvailability(1L, 1L, date); Customer otherCustomer = new Customer();
otherCustomer.setCustomerId(2L);
assertFalse(slots.contains("10:00")); CustomerPet otherCustomerPet = new CustomerPet();
} otherCustomerPet.setCustomerPetId(22L);
otherCustomerPet.setCustomer(otherCustomer);
otherCustomerPet.setPetName("Not Yours");
@Test
void cancelledAppointmentsDoNotBlockAvailability() {
when(storeRepository.findById(1L)).thenReturn(Optional.of(store));
when(serviceRepository.findById(1L)).thenReturn(Optional.of(grooming));
when(appointmentRepository.findByStoreAndDate(1L, date)).thenReturn(List.of());
List<String> slots = appointmentService.checkAvailability(1L, 1L, date);
assertTrue(slots.contains("10:00"));
}
@Test
void updateAppointmentDoesNotConflictWithItself() {
Appointment existing = appointment(1L, date, LocalTime.of(10, 0), grooming, store);
User user = new User();
user.setId(10L);
user.setUsername("pat");
user.setRole(User.Role.CUSTOMER);
user.setTokenVersion(0);
when(userRepository.findById(10L)).thenReturn(Optional.of(user));
SecurityContextHolder.getContext().setAuthentication(
new UsernamePasswordAuthenticationToken(
new com.petshop.backend.security.AppPrincipal(10L, "pat", User.Role.CUSTOMER, 0),
"n/a",
List.of(new SimpleGrantedAuthority("ROLE_CUSTOMER"))
)
);
when(appointmentRepository.findById(1L)).thenReturn(Optional.of(existing));
when(customerRepository.findById(1L)).thenReturn(Optional.of(customer)); when(customerRepository.findById(1L)).thenReturn(Optional.of(customer));
when(storeRepository.findById(1L)).thenReturn(Optional.of(store)); when(storeRepository.findById(1L)).thenReturn(Optional.of(store));
when(serviceRepository.findById(1L)).thenReturn(Optional.of(grooming)); when(serviceRepository.findById(1L)).thenReturn(Optional.of(grooming));
when(petRepository.findById(1L)).thenReturn(Optional.of(pet)); when(employeeStoreRepository.findActiveByStoreStoreIdOrderByEmployeeEmployeeIdAsc(1L))
when(appointmentRepository.findByStoreAndDate(1L, date)).thenReturn(List.of(existing)); .thenReturn(List.of(new EmployeeStore(employee, store)));
when(appointmentRepository.save(any(Appointment.class))).thenAnswer(invocation -> invocation.getArgument(0)); when(appointmentRepository.findByEmployeeEmployeeIdAndAppointmentDate(7L, date)).thenReturn(List.of());
when(customerPetRepository.findById(22L)).thenReturn(Optional.of(otherCustomerPet));
var request = new com.petshop.backend.dto.appointment.AppointmentRequest(); var request = new com.petshop.backend.dto.appointment.AppointmentRequest();
request.setCustomerId(1L); request.setCustomerId(1L);
@@ -168,12 +187,122 @@ class AppointmentServiceTest {
request.setAppointmentDate(date); request.setAppointmentDate(date);
request.setAppointmentTime(LocalTime.of(10, 0)); request.setAppointmentTime(LocalTime.of(10, 0));
request.setAppointmentStatus("Booked"); request.setAppointmentStatus("Booked");
request.setPetIds(List.of(1L)); request.setCustomerPetIds(List.of(22L));
var response = appointmentService.updateAppointment(1L, request); assertThrows(IllegalArgumentException.class, () -> appointmentService.createAppointment(request));
}
assertEquals(1L, response.getAppointmentId()); @Test
assertEquals("Booked", response.getAppointmentStatus()); void createAppointmentRejectsCustomerPetOwnedByDifferentCustomer() {
setAuthentication(99L, User.Role.ADMIN);
Customer otherCustomer = new Customer();
otherCustomer.setCustomerId(2L);
CustomerPet otherCustomerPet = new CustomerPet();
otherCustomerPet.setCustomerPetId(22L);
otherCustomerPet.setCustomer(otherCustomer);
otherCustomerPet.setPetName("Not Yours");
when(customerRepository.findById(1L)).thenReturn(Optional.of(customer));
when(storeRepository.findById(1L)).thenReturn(Optional.of(store));
when(serviceRepository.findById(1L)).thenReturn(Optional.of(grooming));
when(employeeStoreRepository.findActiveByStoreStoreIdOrderByEmployeeEmployeeIdAsc(1L))
.thenReturn(List.of(new EmployeeStore(employee, store)));
when(appointmentRepository.findByEmployeeEmployeeIdAndAppointmentDate(7L, date)).thenReturn(List.of());
when(customerPetRepository.findById(22L)).thenReturn(Optional.of(otherCustomerPet));
var request = new com.petshop.backend.dto.appointment.AppointmentRequest();
request.setCustomerId(1L);
request.setStoreId(1L);
request.setServiceId(1L);
request.setAppointmentDate(date);
request.setAppointmentTime(LocalTime.of(10, 0));
request.setAppointmentStatus("Booked");
request.setCustomerPetIds(List.of(22L));
assertThrows(IllegalArgumentException.class, () -> appointmentService.createAppointment(request));
}
@Test
void createAppointmentRejectsAdminEmployeeSelection() {
setAuthentication(99L, User.Role.ADMIN);
User adminUser = new User();
adminUser.setId(99L);
adminUser.setRole(User.Role.ADMIN);
adminUser.setActive(true);
when(userRepository.findById(99L)).thenReturn(Optional.of(adminUser));
Employee adminEmployee = new Employee();
adminEmployee.setEmployeeId(8L);
adminEmployee.setUserId(8L);
adminEmployee.setFirstName("Admin");
adminEmployee.setLastName("Helper");
adminEmployee.setIsActive(true);
User adminLinkedUser = new User();
adminLinkedUser.setId(8L);
adminLinkedUser.setRole(User.Role.ADMIN);
adminLinkedUser.setActive(true);
when(userRepository.findById(8L)).thenReturn(Optional.of(adminLinkedUser));
when(customerRepository.findById(1L)).thenReturn(Optional.of(customer));
when(storeRepository.findById(1L)).thenReturn(Optional.of(store));
when(serviceRepository.findById(1L)).thenReturn(Optional.of(grooming));
when(employeeRepository.findById(8L)).thenReturn(Optional.of(adminEmployee));
when(appointmentRepository.findByEmployeeEmployeeIdAndAppointmentDate(7L, date)).thenReturn(List.of());
when(customerPetRepository.findById(11L)).thenReturn(Optional.of(customerPet));
when(employeeStoreRepository.findActiveByStoreStoreIdOrderByEmployeeEmployeeIdAsc(1L))
.thenReturn(List.of(new EmployeeStore(adminEmployee, store), new EmployeeStore(employee, store)));
var request = new com.petshop.backend.dto.appointment.AppointmentRequest();
request.setCustomerId(1L);
request.setStoreId(1L);
request.setServiceId(1L);
request.setEmployeeId(8L);
request.setAppointmentDate(date);
request.setAppointmentTime(LocalTime.of(10, 0));
request.setAppointmentStatus("Booked");
request.setCustomerPetIds(List.of(11L));
assertThrows(IllegalArgumentException.class, () -> appointmentService.createAppointment(request));
}
@Test
void createAppointmentRejectsInactiveStaffUserSelection() {
setAuthentication(99L, User.Role.ADMIN);
User adminUser = new User();
adminUser.setId(99L);
adminUser.setRole(User.Role.ADMIN);
adminUser.setActive(true);
when(userRepository.findById(99L)).thenReturn(Optional.of(adminUser));
User inactiveStaffUser = new User();
inactiveStaffUser.setId(7L);
inactiveStaffUser.setRole(User.Role.STAFF);
inactiveStaffUser.setActive(false);
when(userRepository.findById(7L)).thenReturn(Optional.of(inactiveStaffUser));
when(customerRepository.findById(1L)).thenReturn(Optional.of(customer));
when(storeRepository.findById(1L)).thenReturn(Optional.of(store));
when(serviceRepository.findById(1L)).thenReturn(Optional.of(grooming));
when(employeeRepository.findById(7L)).thenReturn(Optional.of(employee));
when(appointmentRepository.findByEmployeeEmployeeIdAndAppointmentDate(7L, date)).thenReturn(List.of());
when(customerPetRepository.findById(11L)).thenReturn(Optional.of(customerPet));
when(employeeStoreRepository.findActiveByStoreStoreIdOrderByEmployeeEmployeeIdAsc(1L))
.thenReturn(List.of(new EmployeeStore(employee, store)));
var request = new com.petshop.backend.dto.appointment.AppointmentRequest();
request.setCustomerId(1L);
request.setStoreId(1L);
request.setServiceId(1L);
request.setEmployeeId(7L);
request.setAppointmentDate(date);
request.setAppointmentTime(LocalTime.of(10, 0));
request.setAppointmentStatus("Booked");
request.setCustomerPetIds(List.of(11L));
assertThrows(IllegalArgumentException.class, () -> appointmentService.createAppointment(request));
} }
private Appointment appointment(Long id, LocalDate date, LocalTime time, Service service, StoreLocation storeLocation) { private Appointment appointment(Long id, LocalDate date, LocalTime time, Service service, StoreLocation storeLocation) {
@@ -184,8 +313,20 @@ class AppointmentServiceTest {
appointment.setAppointmentStatus("Booked"); appointment.setAppointmentStatus("Booked");
appointment.setService(service); appointment.setService(service);
appointment.setStore(storeLocation); appointment.setStore(storeLocation);
appointment.setEmployee(employee);
appointment.setCustomer(customer); appointment.setCustomer(customer);
appointment.setPets(Set.of()); appointment.setPets(Set.of());
appointment.setCustomerPets(Set.of());
return appointment; return appointment;
} }
private void setAuthentication(Long userId, User.Role role) {
SecurityContextHolder.getContext().setAuthentication(
new UsernamePasswordAuthenticationToken(
new AppPrincipal(userId, "user", role, 0),
"n/a",
List.of(new SimpleGrantedAuthority("ROLE_" + role.name()))
)
);
}
} }

View File

@@ -0,0 +1,165 @@
package com.petshop.backend.service;
import com.petshop.backend.entity.Adoption;
import com.petshop.backend.entity.Customer;
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.PetRepository;
import com.petshop.backend.security.AppPrincipal;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import java.util.List;
import java.util.Optional;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class PetServiceTest {
@Mock
private PetRepository petRepository;
@Mock
private AdoptionRepository adoptionRepository;
@Mock
private CatalogImageStorageService catalogImageStorageService;
@InjectMocks
private PetService petService;
@AfterEach
void tearDown() {
SecurityContextHolder.clearContext();
}
@Test
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));
var result = petService.getAllPets(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);
}
@Test
void getAllPetsAnonymousWithAdoptedStatusReturnsEmptyPage() {
Pageable pageable = PageRequest.of(0, 10);
var result = petService.getAllPets(null, null, "Adopted", pageable);
assertEquals(0, result.getTotalElements());
verify(petRepository, never()).searchPublicPets(null, null, pageable);
}
@Test
void getAllPetsCustomerReturnsVisiblePetsOnly() {
Pageable pageable = PageRequest.of(0, 10);
setAuthentication(25L, User.Role.CUSTOMER);
Pet availablePet = pet(1L, "Buddy", "Available");
Pet adoptedPet = pet(2L, "Luna", "Adopted");
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);
assertEquals(2, result.getTotalElements());
verify(petRepository).searchCustomerVisiblePets(25L, null, null, null, pageable);
}
@Test
void getAllPetsAdminReturnsAllPets() {
Pageable pageable = PageRequest.of(0, 10);
setAuthentication(99L, User.Role.ADMIN);
Pet availablePet = pet(1L, "Buddy", "Available");
Pet adoptedPet = pet(2L, "Luna", "Adopted");
when(petRepository.searchPets(null, null, null, pageable))
.thenReturn(new PageImpl<>(List.of(availablePet, adoptedPet), pageable, 2));
var result = petService.getAllPets(null, null, null, pageable);
assertEquals(2, result.getTotalElements());
verify(petRepository).searchPets(null, null, null, pageable);
}
@Test
void getPetByIdHidesAdoptedPetFromUnrelatedCustomer() {
setAuthentication(50L, User.Role.CUSTOMER);
Pet adoptedPet = pet(2L, "Luna", "Adopted");
when(petRepository.findById(2L)).thenReturn(Optional.of(adoptedPet));
when(adoptionRepository.findFirstByPet_IdAndAdoptionStatusOrderByAdoptionDateDesc(2L, "Completed"))
.thenReturn(Optional.of(adoption(2L, 25L)));
assertThrows(ResourceNotFoundException.class, () -> petService.getPetById(2L));
}
@Test
void getPetByIdAllowsOwnerToSeeAdoptedPet() {
setAuthentication(25L, User.Role.CUSTOMER);
Pet adoptedPet = pet(2L, "Luna", "Adopted");
when(petRepository.findById(2L)).thenReturn(Optional.of(adoptedPet));
when(adoptionRepository.findFirstByPet_IdAndAdoptionStatusOrderByAdoptionDateDesc(2L, "Completed"))
.thenReturn(Optional.of(adoption(2L, 25L)));
var result = petService.getPetById(2L);
assertEquals(2L, result.getPetId());
}
private void setAuthentication(Long userId, User.Role role) {
SecurityContextHolder.getContext().setAuthentication(
new UsernamePasswordAuthenticationToken(
new AppPrincipal(userId, "user", role, 0),
"n/a",
List.of(new SimpleGrantedAuthority("ROLE_" + role.name()))
)
);
}
private Pet pet(Long id, String name, String status) {
Pet pet = new Pet();
pet.setPetId(id);
pet.setPetName(name);
pet.setPetSpecies("Cat");
pet.setPetBreed("Mixed");
pet.setPetAge(2);
pet.setPetStatus(status);
pet.setPetPrice(java.math.BigDecimal.TEN);
return pet;
}
private Adoption adoption(Long petId, Long userId) {
Adoption adoption = new Adoption();
Pet pet = new Pet();
pet.setPetId(petId);
adoption.setPet(pet);
Customer customer = new Customer();
customer.setCustomerId(1L);
customer.setUserId(userId);
adoption.setCustomer(customer);
adoption.setAdoptionStatus("Completed");
return adoption;
}
}

View File

@@ -15,16 +15,19 @@ public class AppointmentDTO {
private SimpleIntegerProperty serviceId; private SimpleIntegerProperty serviceId;
private SimpleStringProperty serviceName; private SimpleStringProperty serviceName;
private SimpleIntegerProperty employeeId;
private SimpleStringProperty employeeName;
private SimpleStringProperty appointmentDate; private SimpleStringProperty appointmentDate;
private SimpleStringProperty appointmentTime; private SimpleStringProperty appointmentTime;
private SimpleStringProperty appointmentStatus; private SimpleStringProperty appointmentStatus;
// Constructor
public AppointmentDTO(int appointmentId, public AppointmentDTO(int appointmentId,
int customerId, String customerName, int customerId, String customerName,
int petId, String petName, int petId, String petName,
int serviceId, String serviceName, int serviceId, String serviceName,
int employeeId,
String employeeName,
String appointmentDate, String appointmentDate,
String appointmentTime, String appointmentTime,
String appointmentStatus) { String appointmentStatus) {
@@ -36,12 +39,13 @@ public class AppointmentDTO {
this.petName = new SimpleStringProperty(petName); this.petName = new SimpleStringProperty(petName);
this.serviceId = new SimpleIntegerProperty(serviceId); this.serviceId = new SimpleIntegerProperty(serviceId);
this.serviceName = new SimpleStringProperty(serviceName); this.serviceName = new SimpleStringProperty(serviceName);
this.employeeId = new SimpleIntegerProperty(employeeId);
this.employeeName = new SimpleStringProperty(employeeName);
this.appointmentDate = new SimpleStringProperty(appointmentDate); this.appointmentDate = new SimpleStringProperty(appointmentDate);
this.appointmentTime = new SimpleStringProperty(appointmentTime); this.appointmentTime = new SimpleStringProperty(appointmentTime);
this.appointmentStatus = new SimpleStringProperty(appointmentStatus); this.appointmentStatus = new SimpleStringProperty(appointmentStatus);
} }
// Getters
public int getAppointmentId() { return appointmentId.get(); } public int getAppointmentId() { return appointmentId.get(); }
public int getCustomerId() { return customerId.get(); } public int getCustomerId() { return customerId.get(); }
@@ -52,8 +56,10 @@ public class AppointmentDTO {
public int getServiceId() { return serviceId.get(); } public int getServiceId() { return serviceId.get(); }
public String getServiceName() { return serviceName.get(); } public String getServiceName() { return serviceName.get(); }
public int getEmployeeId() { return employeeId.get(); }
public String getEmployeeName() { return employeeName.get(); }
public String getAppointmentDate() { return appointmentDate.get(); } public String getAppointmentDate() { return appointmentDate.get(); }
public String getAppointmentTime() { return appointmentTime.get(); } public String getAppointmentTime() { return appointmentTime.get(); }
public String getAppointmentStatus() { return appointmentStatus.get(); } public String getAppointmentStatus() { return appointmentStatus.get(); }
} }

View File

@@ -5,6 +5,7 @@ import java.time.LocalDate;
public class AdoptionRequest { public class AdoptionRequest {
private Long petId; private Long petId;
private Long customerId; private Long customerId;
private Long employeeId;
private LocalDate adoptionDate; private LocalDate adoptionDate;
private String adoptionStatus; private String adoptionStatus;
@@ -27,6 +28,14 @@ public class AdoptionRequest {
this.customerId = customerId; this.customerId = customerId;
} }
public Long getEmployeeId() {
return employeeId;
}
public void setEmployeeId(Long employeeId) {
this.employeeId = employeeId;
}
public LocalDate getAdoptionDate() { public LocalDate getAdoptionDate() {
return adoptionDate; return adoptionDate;
} }

View File

@@ -6,8 +6,10 @@ public class AdoptionResponse {
private Long adoptionId; private Long adoptionId;
private Long petId; private Long petId;
private Long customerId; private Long customerId;
private Long employeeId;
private String petName; private String petName;
private String customerName; private String customerName;
private String employeeName;
private LocalDate adoptionDate; private LocalDate adoptionDate;
private java.math.BigDecimal adoptionFee; private java.math.BigDecimal adoptionFee;
private String adoptionStatus; private String adoptionStatus;
@@ -39,6 +41,14 @@ public class AdoptionResponse {
this.customerId = customerId; this.customerId = customerId;
} }
public Long getEmployeeId() {
return employeeId;
}
public void setEmployeeId(Long employeeId) {
this.employeeId = employeeId;
}
public String getPetName() { public String getPetName() {
return petName; return petName;
} }
@@ -55,6 +65,14 @@ public class AdoptionResponse {
this.customerName = customerName; this.customerName = customerName;
} }
public String getEmployeeName() {
return employeeName;
}
public void setEmployeeName(String employeeName) {
this.employeeName = employeeName;
}
public LocalDate getAdoptionDate() { public LocalDate getAdoptionDate() {
return adoptionDate; return adoptionDate;
} }

View File

@@ -6,9 +6,11 @@ import java.util.List;
public class AppointmentRequest { public class AppointmentRequest {
private List<Long> petIds; private List<Long> petIds;
private List<Long> customerPetIds;
private Long customerId; private Long customerId;
private Long storeId; private Long storeId;
private Long serviceId; private Long serviceId;
private Long employeeId;
private LocalDate appointmentDate; private LocalDate appointmentDate;
private LocalTime appointmentTime; private LocalTime appointmentTime;
private String appointmentStatus; private String appointmentStatus;
@@ -24,6 +26,14 @@ public class AppointmentRequest {
this.petIds = petIds; this.petIds = petIds;
} }
public List<Long> getCustomerPetIds() {
return customerPetIds;
}
public void setCustomerPetIds(List<Long> customerPetIds) {
this.customerPetIds = customerPetIds;
}
public Long getCustomerId() { public Long getCustomerId() {
return customerId; return customerId;
} }
@@ -48,6 +58,14 @@ public class AppointmentRequest {
this.serviceId = serviceId; this.serviceId = serviceId;
} }
public Long getEmployeeId() {
return employeeId;
}
public void setEmployeeId(Long employeeId) {
this.employeeId = employeeId;
}
public LocalDate getAppointmentDate() { public LocalDate getAppointmentDate() {
return appointmentDate; return appointmentDate;
} }

View File

@@ -12,7 +12,11 @@ public class AppointmentResponse {
private Long serviceId; private Long serviceId;
private java.util.List<String> petNames; private java.util.List<String> petNames;
private java.util.List<Long> petIds; private java.util.List<Long> petIds;
private java.util.List<String> customerPetNames;
private java.util.List<Long> customerPetIds;
private String serviceName; private String serviceName;
private Long employeeId;
private String employeeName;
private LocalDate appointmentDate; private LocalDate appointmentDate;
private LocalTime appointmentTime; private LocalTime appointmentTime;
private String appointmentStatus; private String appointmentStatus;
@@ -84,6 +88,22 @@ public class AppointmentResponse {
this.petIds = petIds; this.petIds = petIds;
} }
public java.util.List<String> getCustomerPetNames() {
return customerPetNames;
}
public void setCustomerPetNames(java.util.List<String> customerPetNames) {
this.customerPetNames = customerPetNames;
}
public java.util.List<Long> getCustomerPetIds() {
return customerPetIds;
}
public void setCustomerPetIds(java.util.List<Long> customerPetIds) {
this.customerPetIds = customerPetIds;
}
public String getServiceName() { public String getServiceName() {
return serviceName; return serviceName;
} }
@@ -92,6 +112,22 @@ public class AppointmentResponse {
this.serviceName = serviceName; this.serviceName = serviceName;
} }
public Long getEmployeeId() {
return employeeId;
}
public void setEmployeeId(Long employeeId) {
this.employeeId = employeeId;
}
public String getEmployeeName() {
return employeeName;
}
public void setEmployeeName(String employeeName) {
this.employeeName = employeeName;
}
public LocalDate getAppointmentDate() { public LocalDate getAppointmentDate() {
return appointmentDate; return appointmentDate;
} }

View File

@@ -74,6 +74,14 @@ public class DropdownApi {
return apiClient.getObjectMapper().readValue(response, new TypeReference<List<DropdownOption>>() {}); return apiClient.getObjectMapper().readValue(response, new TypeReference<List<DropdownOption>>() {});
} }
public List<DropdownOption> getAppointmentCustomers() throws Exception {
String response = apiClient.getRawResponse("/api/v1/dropdowns/appointment-customers");
if (response == null || response.isEmpty()) {
throw new IllegalStateException("Empty response from appointment customers endpoint");
}
return apiClient.getObjectMapper().readValue(response, new TypeReference<List<DropdownOption>>() {});
}
public List<DropdownOption> getPets() throws Exception { public List<DropdownOption> getPets() throws Exception {
String response = apiClient.getRawResponse("/api/v1/dropdowns/pets"); String response = apiClient.getRawResponse("/api/v1/dropdowns/pets");
if (response == null || response.isEmpty()) { if (response == null || response.isEmpty()) {
@@ -82,6 +90,22 @@ public class DropdownApi {
return apiClient.getObjectMapper().readValue(response, new TypeReference<List<DropdownOption>>() {}); return apiClient.getObjectMapper().readValue(response, new TypeReference<List<DropdownOption>>() {});
} }
public List<DropdownOption> getAdoptionPets() throws Exception {
String response = apiClient.getRawResponse("/api/v1/dropdowns/adoption-pets");
if (response == null || response.isEmpty()) {
throw new IllegalStateException("Empty response from adoption pets endpoint");
}
return apiClient.getObjectMapper().readValue(response, new TypeReference<List<DropdownOption>>() {});
}
public List<DropdownOption> getCustomerPets(Long customerId) throws Exception {
String response = apiClient.getRawResponse("/api/v1/dropdowns/customers/" + customerId + "/pets");
if (response == null || response.isEmpty()) {
throw new IllegalStateException("Empty response from customer pets endpoint");
}
return apiClient.getObjectMapper().readValue(response, new TypeReference<List<DropdownOption>>() {});
}
public List<DropdownOption> getStores() throws Exception { public List<DropdownOption> getStores() throws Exception {
String response = apiClient.getRawResponse("/api/v1/dropdowns/stores"); String response = apiClient.getRawResponse("/api/v1/dropdowns/stores");
if (response == null || response.isEmpty()) { if (response == null || response.isEmpty()) {
@@ -89,4 +113,20 @@ public class DropdownApi {
} }
return apiClient.getObjectMapper().readValue(response, new TypeReference<List<DropdownOption>>() {}); return apiClient.getObjectMapper().readValue(response, new TypeReference<List<DropdownOption>>() {});
} }
public List<DropdownOption> getStoreEmployees(Long storeId) throws Exception {
String response = apiClient.getRawResponse("/api/v1/dropdowns/stores/" + storeId + "/employees");
if (response == null || response.isEmpty()) {
throw new IllegalStateException("Empty response from store employees endpoint");
}
return apiClient.getObjectMapper().readValue(response, new TypeReference<List<DropdownOption>>() {});
}
public List<DropdownOption> getEmployees() throws Exception {
String response = apiClient.getRawResponse("/api/v1/dropdowns/employees");
if (response == null || response.isEmpty()) {
throw new IllegalStateException("Empty response from all employees endpoint");
}
return apiClient.getObjectMapper().readValue(response, new TypeReference<List<DropdownOption>>() {});
}
} }

View File

@@ -43,6 +43,9 @@ public class AdoptionController {
@FXML @FXML
private TableColumn<Adoption, String> colCustomerName; private TableColumn<Adoption, String> colCustomerName;
@FXML
private TableColumn<Adoption, String> colEmployeeName;
@FXML @FXML
private TableColumn<Adoption, String> colAdoptionDate; private TableColumn<Adoption, String> colAdoptionDate;
@@ -65,12 +68,13 @@ public class AdoptionController {
void initialize() { void initialize() {
btnEdit.setDisable(true); btnEdit.setDisable(true);
btnDelete.setDisable(true); btnDelete.setDisable(true);
//Enable multiple selection
tvAdoptions.getSelectionModel().setSelectionMode(javafx.scene.control.SelectionMode.MULTIPLE); tvAdoptions.getSelectionModel().setSelectionMode(javafx.scene.control.SelectionMode.MULTIPLE);
colAdoptionId.setCellValueFactory(new PropertyValueFactory<>("adoptionId")); colAdoptionId.setCellValueFactory(new PropertyValueFactory<>("adoptionId"));
colPetId.setCellValueFactory(new PropertyValueFactory<>("petName")); colPetId.setCellValueFactory(new PropertyValueFactory<>("petName"));
colCustomerName.setCellValueFactory(new PropertyValueFactory<>("customerName")); colCustomerName.setCellValueFactory(new PropertyValueFactory<>("customerName"));
colEmployeeName.setCellValueFactory(new PropertyValueFactory<>("employeeName"));
colAdoptionDate.setCellValueFactory(new PropertyValueFactory<>("adoptionDate")); colAdoptionDate.setCellValueFactory(new PropertyValueFactory<>("adoptionDate"));
colAdoptionFee.setCellValueFactory(new PropertyValueFactory<>("adoptionFee")); colAdoptionFee.setCellValueFactory(new PropertyValueFactory<>("adoptionFee"));
colAdoptionStatus.setCellValueFactory(new PropertyValueFactory<>("adoptionStatus")); colAdoptionStatus.setCellValueFactory(new PropertyValueFactory<>("adoptionStatus"));
@@ -87,7 +91,6 @@ public class AdoptionController {
displayFilteredAdoptions(newValue); displayFilteredAdoptions(newValue);
}); });
//EventListener for DELETE key
tvAdoptions.setOnKeyPressed(event -> { tvAdoptions.setOnKeyPressed(event -> {
if (event.getCode() == javafx.scene.input.KeyCode.DELETE) { if (event.getCode() == javafx.scene.input.KeyCode.DELETE) {
if (tvAdoptions.getSelectionModel().getSelectedItem() != null) { if (tvAdoptions.getSelectionModel().getSelectedItem() != null) {
@@ -105,11 +108,10 @@ public class AdoptionController {
@FXML @FXML
void btnDeleteClicked(ActionEvent event) { void btnDeleteClicked(ActionEvent event) {
//get selected adoptions
var selectedAdoptions = tvAdoptions.getSelectionModel().getSelectedItems(); var selectedAdoptions = tvAdoptions.getSelectionModel().getSelectedItems();
if (selectedAdoptions.isEmpty()) return; if (selectedAdoptions.isEmpty()) return;
//ask user to confirm
Alert question = new Alert(Alert.AlertType.CONFIRMATION); Alert question = new Alert(Alert.AlertType.CONFIRMATION);
question.setHeaderText("Please confirm delete"); question.setHeaderText("Please confirm delete");
String message = selectedAdoptions.size() == 1 String message = selectedAdoptions.size() == 1
@@ -119,7 +121,6 @@ public class AdoptionController {
question.getDialogPane().lookupButton(ButtonType.OK).requestFocus(); question.getDialogPane().lookupButton(ButtonType.OK).requestFocus();
Optional<ButtonType> result = question.showAndWait(); Optional<ButtonType> result = question.showAndWait();
//if confirmed, start deletion
if (result.isPresent() && result.get() == ButtonType.OK) { if (result.isPresent() && result.get() == ButtonType.OK) {
List<Long> ids = selectedAdoptions.stream() List<Long> ids = selectedAdoptions.stream()
.map(a -> (long) a.getAdoptionId()) .map(a -> (long) a.getAdoptionId())
@@ -142,7 +143,6 @@ public class AdoptionController {
alert.showAndWait(); alert.showAndWait();
} }
//refresh display and reset inputs
displayAdoptions(); displayAdoptions();
btnDelete.setDisable(true); btnDelete.setDisable(true);
btnEdit.setDisable(true); btnEdit.setDisable(true);
@@ -252,8 +252,10 @@ public class AdoptionController {
response.getAdoptionId().intValue(), response.getAdoptionId().intValue(),
response.getPetId() != null ? response.getPetId().intValue() : 0, response.getPetId() != null ? response.getPetId().intValue() : 0,
response.getCustomerId() != null ? response.getCustomerId().intValue() : 0, response.getCustomerId() != null ? response.getCustomerId().intValue() : 0,
response.getEmployeeId() != null ? response.getEmployeeId().intValue() : 0,
response.getPetName(), response.getPetName(),
response.getCustomerName(), response.getCustomerName(),
response.getEmployeeName(),
response.getAdoptionDate() != null ? response.getAdoptionDate().toString() : "", response.getAdoptionDate() != null ? response.getAdoptionDate().toString() : "",
response.getAdoptionFee() != null ? response.getAdoptionFee().doubleValue() : 0.0, response.getAdoptionFee() != null ? response.getAdoptionFee().doubleValue() : 0.0,
response.getAdoptionStatus() response.getAdoptionStatus()

View File

@@ -33,6 +33,7 @@ public class AppointmentController {
@FXML private TableColumn<AppointmentDTO,String> colAppointmentDate; @FXML private TableColumn<AppointmentDTO,String> colAppointmentDate;
@FXML private TableColumn<AppointmentDTO,String> colAppointmentTime; @FXML private TableColumn<AppointmentDTO,String> colAppointmentTime;
@FXML private TableColumn<AppointmentDTO,String> colCustomerName; @FXML private TableColumn<AppointmentDTO,String> colCustomerName;
@FXML private TableColumn<AppointmentDTO,String> colEmployeeName;
@FXML private TableColumn<AppointmentDTO,String> colAppointmentStatus; @FXML private TableColumn<AppointmentDTO,String> colAppointmentStatus;
@FXML private Button btnAdd; @FXML private Button btnAdd;
@@ -46,7 +47,7 @@ public class AppointmentController {
@FXML @FXML
public void initialize(){ public void initialize(){
//Enable multiple selection
tvAppointments.getSelectionModel().setSelectionMode(javafx.scene.control.SelectionMode.MULTIPLE); tvAppointments.getSelectionModel().setSelectionMode(javafx.scene.control.SelectionMode.MULTIPLE);
colAppointmentId.setCellValueFactory(new PropertyValueFactory<>("appointmentId")); colAppointmentId.setCellValueFactory(new PropertyValueFactory<>("appointmentId"));
@@ -55,6 +56,7 @@ public class AppointmentController {
colAppointmentDate.setCellValueFactory(new PropertyValueFactory<>("appointmentDate")); colAppointmentDate.setCellValueFactory(new PropertyValueFactory<>("appointmentDate"));
colAppointmentTime.setCellValueFactory(new PropertyValueFactory<>("appointmentTime")); colAppointmentTime.setCellValueFactory(new PropertyValueFactory<>("appointmentTime"));
colCustomerName.setCellValueFactory(new PropertyValueFactory<>("customerName")); colCustomerName.setCellValueFactory(new PropertyValueFactory<>("customerName"));
colEmployeeName.setCellValueFactory(new PropertyValueFactory<>("employeeName"));
colAppointmentStatus.setCellValueFactory(new PropertyValueFactory<>("appointmentStatus")); colAppointmentStatus.setCellValueFactory(new PropertyValueFactory<>("appointmentStatus"));
filtered = new FilteredList<>(appointments, a -> true); filtered = new FilteredList<>(appointments, a -> true);
@@ -64,7 +66,6 @@ public class AppointmentController {
txtSearch.textProperty().addListener((obs, o, n) -> applyFilter(n)); txtSearch.textProperty().addListener((obs, o, n) -> applyFilter(n));
} }
//EventListener for DELETE key
tvAppointments.setOnKeyPressed(event -> { tvAppointments.setOnKeyPressed(event -> {
if (event.getCode() == javafx.scene.input.KeyCode.DELETE) { if (event.getCode() == javafx.scene.input.KeyCode.DELETE) {
if (tvAppointments.getSelectionModel().getSelectedItem() != null) { if (tvAppointments.getSelectionModel().getSelectedItem() != null) {
@@ -146,11 +147,10 @@ public class AppointmentController {
@FXML @FXML
void btnDeleteClicked(ActionEvent event){ void btnDeleteClicked(ActionEvent event){
//get selected appointments
var selectedAppointments = tvAppointments.getSelectionModel().getSelectedItems(); var selectedAppointments = tvAppointments.getSelectionModel().getSelectedItems();
if (selectedAppointments.isEmpty()) return; if (selectedAppointments.isEmpty()) return;
//ask user to confirm
Alert question = new Alert(Alert.AlertType.CONFIRMATION); Alert question = new Alert(Alert.AlertType.CONFIRMATION);
question.setHeaderText("Please confirm delete"); question.setHeaderText("Please confirm delete");
String message = selectedAppointments.size() == 1 String message = selectedAppointments.size() == 1
@@ -160,7 +160,6 @@ public class AppointmentController {
question.getDialogPane().lookupButton(ButtonType.OK).requestFocus(); question.getDialogPane().lookupButton(ButtonType.OK).requestFocus();
java.util.Optional<ButtonType> result = question.showAndWait(); java.util.Optional<ButtonType> result = question.showAndWait();
//if confirmed, start deletion
if (result.isPresent() && result.get() == ButtonType.OK) { if (result.isPresent() && result.get() == ButtonType.OK) {
List<Long> ids = selectedAppointments.stream() List<Long> ids = selectedAppointments.stream()
.map(a -> (long) a.getAppointmentId()) .map(a -> (long) a.getAppointmentId())
@@ -183,7 +182,6 @@ public class AppointmentController {
alert.showAndWait(); alert.showAndWait();
} }
//refresh display
loadAppointments(); loadAppointments();
} }
} }
@@ -233,15 +231,22 @@ public class AppointmentController {
} }
private AppointmentDTO mapToAppointmentDTO(AppointmentResponse response) { private AppointmentDTO mapToAppointmentDTO(AppointmentResponse response) {
Long petId = response.getPetIds() != null && !response.getPetIds().isEmpty() ? response.getPetIds().get(0) : null; Long petId = response.getCustomerPetIds() != null && !response.getCustomerPetIds().isEmpty()
? response.getCustomerPetIds().get(0)
: response.getPetIds() != null && !response.getPetIds().isEmpty() ? response.getPetIds().get(0) : null;
String petName = response.getCustomerPetNames() != null && !response.getCustomerPetNames().isEmpty()
? String.join(", ", response.getCustomerPetNames())
: String.join(", ", response.getPetNames());
return new AppointmentDTO( return new AppointmentDTO(
response.getAppointmentId().intValue(), response.getAppointmentId().intValue(),
response.getCustomerId() != null ? response.getCustomerId().intValue() : 0, response.getCustomerId() != null ? response.getCustomerId().intValue() : 0,
response.getCustomerName(), response.getCustomerName(),
petId != null ? petId.intValue() : 0, petId != null ? petId.intValue() : 0,
String.join(", ", response.getPetNames()), petName,
response.getServiceId() != null ? response.getServiceId().intValue() : 0, response.getServiceId() != null ? response.getServiceId().intValue() : 0,
response.getServiceName(), response.getServiceName(),
response.getEmployeeId() != null ? response.getEmployeeId().intValue() : 0,
response.getEmployeeName(),
response.getAppointmentDate().toString(), response.getAppointmentDate().toString(),
response.getAppointmentTime().toString(), response.getAppointmentTime().toString(),
response.getAppointmentStatus() response.getAppointmentStatus()

View File

@@ -11,21 +11,23 @@ import javafx.scene.control.Button;
import javafx.scene.control.ComboBox; import javafx.scene.control.ComboBox;
import javafx.scene.control.DatePicker; import javafx.scene.control.DatePicker;
import javafx.scene.control.Label; import javafx.scene.control.Label;
import javafx.scene.control.ListCell;
import javafx.scene.input.MouseEvent; import javafx.scene.input.MouseEvent;
import javafx.stage.Stage; import javafx.stage.Stage;
import org.example.petshopdesktop.api.dto.adoption.AdoptionRequest; import org.example.petshopdesktop.api.dto.adoption.AdoptionRequest;
import org.example.petshopdesktop.api.dto.common.DropdownOption; import org.example.petshopdesktop.api.dto.common.DropdownOption;
import org.example.petshopdesktop.api.endpoints.AdoptionApi; import org.example.petshopdesktop.api.endpoints.AdoptionApi;
import org.example.petshopdesktop.api.endpoints.DropdownApi; import org.example.petshopdesktop.api.endpoints.DropdownApi;
import org.example.petshopdesktop.auth.UserSession;
import org.example.petshopdesktop.models.Adoption; import org.example.petshopdesktop.models.Adoption;
import org.example.petshopdesktop.util.ActivityLogger; import org.example.petshopdesktop.util.ActivityLogger;
import java.time.LocalDate; import java.time.LocalDate;
import java.util.List; import java.util.List;
import java.util.Objects;
public class AdoptionDialogController { public class AdoptionDialogController {
//FXML elements
@FXML @FXML
private Button btnCancel; private Button btnCancel;
@@ -38,6 +40,9 @@ public class AdoptionDialogController {
@FXML @FXML
private ComboBox<DropdownOption> cbCustomer; private ComboBox<DropdownOption> cbCustomer;
@FXML
private ComboBox<DropdownOption> cbEmployee;
@FXML @FXML
private ComboBox<DropdownOption> cbPet; private ComboBox<DropdownOption> cbPet;
@@ -50,10 +55,9 @@ public class AdoptionDialogController {
@FXML @FXML
private Label lblMode; private Label lblMode;
//Stores if the dialog view is in add/edit mode
private String mode = null; private String mode = null;
private Adoption selectedAdoption = null;
//Adoption statuses
private ObservableList<String> statusList = FXCollections.observableArrayList( private ObservableList<String> statusList = FXCollections.observableArrayList(
"Pending", "Completed", "Cancelled" "Pending", "Completed", "Cancelled"
); );
@@ -62,14 +66,16 @@ public class AdoptionDialogController {
void initialize() { void initialize() {
cbAdoptionStatus.setItems(statusList); cbAdoptionStatus.setItems(statusList);
cbEmployee.setPromptText("Select an employee");
new Thread(() -> { new Thread(() -> {
try { try {
List<DropdownOption> pets = DropdownApi.getInstance().getPets(); List<DropdownOption> pets = DropdownApi.getInstance().getAdoptionPets();
Platform.runLater(() -> { Platform.runLater(() -> {
if (pets != null) { if (pets != null) {
ObservableList<DropdownOption> petsObs = FXCollections.observableArrayList(pets); ObservableList<DropdownOption> petsObs = FXCollections.observableArrayList(pets);
cbPet.setItems(petsObs); cbPet.setItems(petsObs);
applySelectedPet();
} }
}); });
} catch (Exception e) { } catch (Exception e) {
@@ -83,6 +89,46 @@ public class AdoptionDialogController {
} }
}).start(); }).start();
new Thread(() -> {
try {
Long storeId = UserSession.getInstance().getStoreId();
List<DropdownOption> employees;
if (storeId != null && storeId > 0) {
employees = DropdownApi.getInstance().getStoreEmployees(storeId);
} else {
employees = DropdownApi.getInstance().getEmployees();
}
Platform.runLater(() -> {
cbEmployee.setItems(FXCollections.observableArrayList(employees));
applySelectedEmployee();
});
} catch (Exception e) {
Platform.runLater(() -> {
ActivityLogger.getInstance().logException(
"AdoptionDialogController.initialize",
e,
"Loading employees for combo box");
cbEmployee.setDisable(true);
cbEmployee.setPromptText("Unable to load employees");
});
}
}).start();
cbEmployee.setCellFactory(param -> new ListCell<>() {
@Override
protected void updateItem(DropdownOption option, boolean empty) {
super.updateItem(option, empty);
setText(empty || option == null ? null : option.getLabel());
}
});
cbEmployee.setButtonCell(new ListCell<>() {
@Override
protected void updateItem(DropdownOption option, boolean empty) {
super.updateItem(option, empty);
setText(empty || option == null ? null : option.getLabel());
}
});
new Thread(() -> { new Thread(() -> {
try { try {
List<DropdownOption> customers = DropdownApi.getInstance().getCustomers(); List<DropdownOption> customers = DropdownApi.getInstance().getCustomers();
@@ -90,6 +136,7 @@ public class AdoptionDialogController {
if (customers != null) { if (customers != null) {
ObservableList<DropdownOption> customersObs = FXCollections.observableArrayList(customers); ObservableList<DropdownOption> customersObs = FXCollections.observableArrayList(customers);
cbCustomer.setItems(customersObs); cbCustomer.setItems(customersObs);
applySelectedCustomer();
} }
}); });
} catch (Exception e) { } catch (Exception e) {
@@ -129,6 +176,10 @@ public class AdoptionDialogController {
errorMsg += "Customer is required.\n"; errorMsg += "Customer is required.\n";
} }
if (cbEmployee.getSelectionModel().getSelectedItem() == null) {
errorMsg += "Employee is required.\n";
}
if (dpAdoptionDate.getValue() == null) { if (dpAdoptionDate.getValue() == null) {
errorMsg += "Adoption Date is required.\n"; errorMsg += "Adoption Date is required.\n";
} }
@@ -142,6 +193,7 @@ public class AdoptionDialogController {
AdoptionRequest request = new AdoptionRequest(); AdoptionRequest request = new AdoptionRequest();
request.setPetId(cbPet.getSelectionModel().getSelectedItem().getId()); request.setPetId(cbPet.getSelectionModel().getSelectedItem().getId());
request.setCustomerId(cbCustomer.getSelectionModel().getSelectedItem().getId()); request.setCustomerId(cbCustomer.getSelectionModel().getSelectedItem().getId());
request.setEmployeeId(cbEmployee.getSelectionModel().getSelectedItem().getId());
request.setAdoptionDate(dpAdoptionDate.getValue()); request.setAdoptionDate(dpAdoptionDate.getValue());
request.setAdoptionStatus(cbAdoptionStatus.getValue()); request.setAdoptionStatus(cbAdoptionStatus.getValue());
@@ -179,7 +231,6 @@ public class AdoptionDialogController {
} }
} }
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();
@@ -188,21 +239,11 @@ public class AdoptionDialogController {
public void displayAdoptionDetails(Adoption adoption) { public void displayAdoptionDetails(Adoption adoption) {
if (adoption != null) { if (adoption != null) {
selectedAdoption = adoption;
lblAdoptionId.setText("ID: " + adoption.getAdoptionId()); lblAdoptionId.setText("ID: " + adoption.getAdoptionId());
applySelectedPet();
for (DropdownOption pet : cbPet.getItems()) { applySelectedCustomer();
if (pet.getLabel().equals(adoption.getPetName())) { applySelectedEmployee();
cbPet.getSelectionModel().select(pet);
break;
}
}
for (DropdownOption customer : cbCustomer.getItems()) {
if (customer.getLabel().equals(adoption.getCustomerName())) {
cbCustomer.getSelectionModel().select(customer);
break;
}
}
if (adoption.getAdoptionDate() != null && !adoption.getAdoptionDate().isEmpty()) { if (adoption.getAdoptionDate() != null && !adoption.getAdoptionDate().isEmpty()) {
try { try {
@@ -229,4 +270,46 @@ public class AdoptionDialogController {
lblMode.setText(mode + " Adoption"); lblMode.setText(mode + " Adoption");
lblAdoptionId.setVisible(mode.equals("Edit")); lblAdoptionId.setVisible(mode.equals("Edit"));
} }
private void applySelectedPet() {
if (selectedAdoption == null || selectedAdoption.getPetId() <= 0) {
return;
}
DropdownOption selected = findOptionById(cbPet.getItems(), (long) selectedAdoption.getPetId());
if (selected != null && !Objects.equals(cbPet.getValue(), selected)) {
cbPet.setValue(selected);
}
}
private void applySelectedCustomer() {
if (selectedAdoption == null || selectedAdoption.getCustomerId() <= 0) {
return;
}
DropdownOption selected = findOptionById(cbCustomer.getItems(), (long) selectedAdoption.getCustomerId());
if (selected != null && !Objects.equals(cbCustomer.getValue(), selected)) {
cbCustomer.setValue(selected);
}
}
private void applySelectedEmployee() {
if (selectedAdoption == null || selectedAdoption.getEmployeeId() <= 0) {
return;
}
DropdownOption selected = findOptionById(cbEmployee.getItems(), (long) selectedAdoption.getEmployeeId());
if (selected != null && !Objects.equals(cbEmployee.getValue(), selected)) {
cbEmployee.setValue(selected);
}
}
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;
}
} }

View File

@@ -19,21 +19,20 @@ import org.example.petshopdesktop.auth.UserSession;
import org.example.petshopdesktop.util.ActivityLogger; import org.example.petshopdesktop.util.ActivityLogger;
import java.time.LocalTime; import java.time.LocalTime;
import java.util.Collections; import java.time.LocalDate;
import java.util.List; import java.util.List;
import java.util.Collections;
import java.util.Objects;
public class AppointmentDialogController { public class AppointmentDialogController {
// ============================
// FXML
// ============================
@FXML private Button btnCancel; @FXML private Button btnCancel;
@FXML private Button btnSave; @FXML private Button btnSave;
@FXML private ComboBox<DropdownOption> cbService; @FXML private ComboBox<DropdownOption> cbService;
@FXML private ComboBox<DropdownOption> cbCustomer; @FXML private ComboBox<DropdownOption> cbCustomer;
@FXML private ComboBox<DropdownOption> cbPet; @FXML private ComboBox<DropdownOption> cbPet;
@FXML private ComboBox<DropdownOption> cbEmployee;
@FXML private ComboBox<Integer> cbHour; @FXML private ComboBox<Integer> cbHour;
@FXML private ComboBox<Integer> cbMinute; @FXML private ComboBox<Integer> cbMinute;
@@ -44,74 +43,38 @@ public class AppointmentDialogController {
@FXML private Label lblAppointmentId; @FXML private Label lblAppointmentId;
@FXML private Label lblMode; @FXML private Label lblMode;
// ============================ private String mode = null;
// DATA
// ============================
private String mode = null; // Add | Edit
private AppointmentDTO selectedAppointment = null; private AppointmentDTO selectedAppointment = null;
private Long pendingPetSelectionId = null;
private ObservableList<String> statusList = private ObservableList<String> statusList =
FXCollections.observableArrayList( FXCollections.observableArrayList(
"Booked", "Completed", "Cancelled" "Booked", "Completed", "Cancelled", "Missed"
); );
//
// MODE
//
public void setMode(String mode) { public void setMode(String mode) {
this.mode = mode; this.mode = mode;
lblMode.setText(mode + " Appointment"); lblMode.setText(mode + " Appointment");
lblAppointmentId.setVisible(!mode.equals("Add")); lblAppointmentId.setVisible(!mode.equals("Add"));
} }
//
// INITIALIZE
//
@FXML @FXML
public void initialize() { public void initialize() {
new Thread(() -> {
try {
List<DropdownOption> services = DropdownApi.getInstance().getServices();
List<DropdownOption> customers = DropdownApi.getInstance().getCustomers();
List<DropdownOption> pets = DropdownApi.getInstance().getPets();
Platform.runLater(() -> {
if (services != null) {
cbService.setItems(FXCollections.observableArrayList(services));
}
if (customers != null) {
cbCustomer.setItems(FXCollections.observableArrayList(customers));
}
if (pets != null) {
cbPet.setItems(FXCollections.observableArrayList(pets));
}
syncSelectedAppointment();
});
} catch (Exception e) {
Platform.runLater(() -> {
ActivityLogger.getInstance().logException(
"AppointmentDialogController.initialize",
e,
"Loading combo box data for services, customers, and pets");
e.printStackTrace();
});
}
}).start();
cbAppointmentStatus.setItems(statusList); cbAppointmentStatus.setItems(statusList);
cbPet.setDisable(true);
cbEmployee.setPromptText("Select an employee");
cbPet.setPromptText("Select a customer first");
cbCustomer.setPromptText("Select a customer");
cbService.setPromptText("Select a service");
dpAppointmentDate.setValue(LocalDate.now().plusDays(1));
cbAppointmentStatus.setValue("Booked");
// Hours 9 AM - 5 PM
for (int i = 9; i <= 17; i++) { for (int i = 9; i <= 17; i++) {
cbHour.getItems().add(i); cbHour.getItems().add(i);
} }
cbMinute.getItems().addAll(0, 15, 30, 45); cbMinute.getItems().addAll(0, 15, 30, 45);
// Show dropdown labels
cbService.setCellFactory(param -> new ListCell<>() { cbService.setCellFactory(param -> new ListCell<>() {
@Override @Override
protected void updateItem(DropdownOption option, boolean empty) { protected void updateItem(DropdownOption option, boolean empty) {
@@ -157,18 +120,48 @@ public class AppointmentDialogController {
} }
}); });
cbEmployee.setCellFactory(param -> new ListCell<>() {
@Override
protected void updateItem(DropdownOption option, boolean empty) {
super.updateItem(option, empty);
setText(empty || option == null ? null : option.getLabel());
}
});
cbEmployee.setButtonCell(new ListCell<>() {
@Override
protected void updateItem(DropdownOption option, boolean empty) {
super.updateItem(option, empty);
setText(empty || option == null ? null : option.getLabel());
}
});
cbCustomer.valueProperty().addListener((obs, oldValue, newValue) -> {
Long customerId = newValue != null ? newValue.getId() : null;
cbPet.setValue(null);
cbPet.setItems(FXCollections.observableArrayList());
cbPet.setDisable(customerId == null);
if (customerId != null) {
cbPet.setPromptText("Loading customer pets...");
loadCustomerPets(customerId);
} else {
cbPet.setPromptText("Select a customer first");
pendingPetSelectionId = null;
}
});
btnSave.setOnMouseClicked(this::buttonSaveClicked); btnSave.setOnMouseClicked(this::buttonSaveClicked);
btnCancel.setOnMouseClicked(this::closeStage); btnCancel.setOnMouseClicked(this::closeStage);
}
// loadServices();
// DISPLAY FOR EDIT loadAppointmentCustomers();
// loadEmployees();
}
public void displayAppointmentDetails(AppointmentDTO appt) { public void displayAppointmentDetails(AppointmentDTO appt) {
selectedAppointment = appt; selectedAppointment = appt;
lblAppointmentId.setText("ID: " + appt.getAppointmentId()); lblAppointmentId.setText("ID: " + appt.getAppointmentId());
pendingPetSelectionId = appt.getPetId() > 0 ? (long) appt.getPetId() : null;
try { try {
dpAppointmentDate.setValue( dpAppointmentDate.setValue(
@@ -194,28 +187,17 @@ public class AppointmentDialogController {
"Parsing appointment time"); "Parsing appointment time");
} }
cbService.getItems().forEach(s -> { applySelectedService();
if (s.getId() != null && s.getId().longValue() == appt.getServiceId()) cbService.setValue(s); applySelectedCustomer();
}); applySelectedEmployee();
cbCustomer.getItems().forEach(c -> {
if (c.getId() != null && c.getId().longValue() == appt.getCustomerId()) cbCustomer.setValue(c);
});
cbPet.getItems().forEach(p -> {
if (p.getId() != null && p.getId().longValue() == appt.getPetId()) cbPet.setValue(p);
});
} }
//
// SAVE
//
private void buttonSaveClicked(MouseEvent e) { private void buttonSaveClicked(MouseEvent e) {
if (cbService.getValue() == null || if (cbService.getValue() == null ||
cbCustomer.getValue() == null || cbCustomer.getValue() == null ||
cbPet.getValue() == null || cbPet.getValue() == null ||
cbEmployee.getValue() == null ||
dpAppointmentDate.getValue() == null || dpAppointmentDate.getValue() == null ||
cbHour.getValue() == null || cbHour.getValue() == null ||
cbMinute.getValue() == null || cbMinute.getValue() == null ||
@@ -233,10 +215,11 @@ public class AppointmentDialogController {
} }
AppointmentRequest request = new AppointmentRequest(); AppointmentRequest request = new AppointmentRequest();
request.setPetIds(Collections.singletonList(cbPet.getValue().getId())); request.setCustomerPetIds(Collections.singletonList(cbPet.getValue().getId()));
request.setCustomerId(cbCustomer.getValue().getId()); request.setCustomerId(cbCustomer.getValue().getId());
request.setStoreId(storeId); request.setStoreId(storeId);
request.setServiceId(cbService.getValue().getId()); request.setServiceId(cbService.getValue().getId());
request.setEmployeeId(cbEmployee.getValue().getId());
request.setAppointmentDate(dpAppointmentDate.getValue()); request.setAppointmentDate(dpAppointmentDate.getValue());
request.setAppointmentTime(appointmentTime); request.setAppointmentTime(appointmentTime);
request.setAppointmentStatus(cbAppointmentStatus.getValue()); request.setAppointmentStatus(cbAppointmentStatus.getValue());
@@ -267,10 +250,6 @@ public class AppointmentDialogController {
}).start(); }).start();
} }
//
// UTIL
//
private void closeStage(MouseEvent e) { private void closeStage(MouseEvent e) {
Stage stage = (Stage) ((Node) e.getSource()).getScene().getWindow(); Stage stage = (Stage) ((Node) e.getSource()).getScene().getWindow();
stage.close(); stage.close();
@@ -288,4 +267,156 @@ public class AppointmentDialogController {
displayAppointmentDetails(selectedAppointment); displayAppointmentDetails(selectedAppointment);
} }
} }
private void applySelectedService() {
if (selectedAppointment == null || selectedAppointment.getServiceId() <= 0) {
return;
}
DropdownOption selected = findOptionById(cbService.getItems(), (long) selectedAppointment.getServiceId());
if (selected != null && !Objects.equals(cbService.getValue(), selected)) {
cbService.setValue(selected);
}
}
private void applySelectedCustomer() {
if (selectedAppointment == null || selectedAppointment.getCustomerId() <= 0) {
return;
}
DropdownOption selected = findOptionById(cbCustomer.getItems(), (long) selectedAppointment.getCustomerId());
if (selected != null && !Objects.equals(cbCustomer.getValue(), selected)) {
cbCustomer.setValue(selected);
}
}
private void applySelectedEmployee() {
if (selectedAppointment == null || selectedAppointment.getEmployeeId() <= 0) {
return;
}
DropdownOption selected = findOptionById(cbEmployee.getItems(), (long) selectedAppointment.getEmployeeId());
if (selected != null && !Objects.equals(cbEmployee.getValue(), selected)) {
cbEmployee.setValue(selected);
}
}
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 loadCustomerPets(Long customerId) {
new Thread(() -> {
try {
List<DropdownOption> pets = DropdownApi.getInstance().getCustomerPets(customerId);
Platform.runLater(() -> {
cbPet.setItems(FXCollections.observableArrayList(pets));
cbPet.setDisable(pets == null || pets.isEmpty());
cbPet.setPromptText(pets == null || pets.isEmpty() ? "No pets for selected customer" : "Select a pet");
if (pendingPetSelectionId != null) {
for (DropdownOption pet : cbPet.getItems()) {
if (pet.getId() != null && pet.getId().equals(pendingPetSelectionId)) {
cbPet.setValue(pet);
break;
}
}
pendingPetSelectionId = null;
}
});
} catch (Exception ex) {
Platform.runLater(() -> {
ActivityLogger.getInstance().logException(
"AppointmentDialogController.loadCustomerPets",
ex,
"Loading customer pets for appointment dialog");
cbPet.setDisable(true);
cbPet.setPromptText("Unable to load pets");
showError("Error loading pets for selected customer");
});
}
}).start();
}
private void loadServices() {
new Thread(() -> {
try {
List<DropdownOption> services = DropdownApi.getInstance().getServices();
Platform.runLater(() -> {
cbService.setItems(FXCollections.observableArrayList(services));
applySelectedService();
});
} catch (Exception e) {
Platform.runLater(() -> {
ActivityLogger.getInstance().logException(
"AppointmentDialogController.loadServices",
e,
"Loading services for appointment dialog");
cbService.setDisable(true);
cbService.setPromptText("Unable to load services");
});
}
}).start();
}
private void loadAppointmentCustomers() {
new Thread(() -> {
try {
List<DropdownOption> customers = DropdownApi.getInstance().getAppointmentCustomers();
Platform.runLater(() -> {
cbCustomer.setItems(FXCollections.observableArrayList(customers));
boolean hasCustomers = customers != null && !customers.isEmpty();
cbCustomer.setDisable(!hasCustomers);
cbPet.setDisable(true);
cbPet.setItems(FXCollections.observableArrayList());
cbCustomer.setPromptText(hasCustomers ? "Select a customer" : "No customers with pets yet");
cbPet.setPromptText(hasCustomers ? "Select a customer first" : "No customer pets available");
applySelectedCustomer();
});
} catch (Exception e) {
Platform.runLater(() -> {
ActivityLogger.getInstance().logException(
"AppointmentDialogController.loadAppointmentCustomers",
e,
"Loading appointment customers for appointment dialog");
cbCustomer.setDisable(true);
cbPet.setDisable(true);
cbCustomer.setPromptText("Unable to load customers");
cbPet.setPromptText("Unable to load pets");
});
}
}).start();
}
private void loadEmployees() {
new Thread(() -> {
try {
Long storeId = UserSession.getInstance().getStoreId();
List<DropdownOption> employees;
if (storeId != null && storeId > 0) {
employees = DropdownApi.getInstance().getStoreEmployees(storeId);
} else {
employees = DropdownApi.getInstance().getEmployees();
}
Platform.runLater(() -> {
cbEmployee.setItems(FXCollections.observableArrayList(employees));
applySelectedEmployee();
});
} catch (Exception e) {
Platform.runLater(() -> {
ActivityLogger.getInstance().logException(
"AppointmentDialogController.loadEmployees",
e,
"Loading employees for appointment dialog");
cbEmployee.setDisable(true);
cbEmployee.setPromptText("Unable to load employees");
});
}
}).start();
}
} }

View File

@@ -8,18 +8,22 @@ public class Adoption {
private SimpleIntegerProperty adoptionId; private SimpleIntegerProperty adoptionId;
private SimpleIntegerProperty petId; private SimpleIntegerProperty petId;
private SimpleIntegerProperty customerId; private SimpleIntegerProperty customerId;
private SimpleIntegerProperty employeeId;
private SimpleStringProperty petName; private SimpleStringProperty petName;
private SimpleStringProperty customerName; private SimpleStringProperty customerName;
private SimpleStringProperty employeeName;
private SimpleStringProperty adoptionDate; private SimpleStringProperty adoptionDate;
private SimpleDoubleProperty adoptionFee; private SimpleDoubleProperty adoptionFee;
private SimpleStringProperty adoptionStatus; private SimpleStringProperty adoptionStatus;
public Adoption(int adoptionId, int petId, int customerId, String petName, String customerName, String adoptionDate, double adoptionFee, String adoptionStatus) { public Adoption(int adoptionId, int petId, int customerId, int employeeId, String petName, String customerName, String employeeName, String adoptionDate, double adoptionFee, String adoptionStatus) {
this.adoptionId = new SimpleIntegerProperty(adoptionId); this.adoptionId = new SimpleIntegerProperty(adoptionId);
this.petId = new SimpleIntegerProperty(petId); this.petId = new SimpleIntegerProperty(petId);
this.customerId = new SimpleIntegerProperty(customerId); this.customerId = new SimpleIntegerProperty(customerId);
this.employeeId = new SimpleIntegerProperty(employeeId);
this.petName = new SimpleStringProperty(petName); this.petName = new SimpleStringProperty(petName);
this.customerName = new SimpleStringProperty(customerName); this.customerName = new SimpleStringProperty(customerName);
this.employeeName = new SimpleStringProperty(employeeName);
this.adoptionDate = new SimpleStringProperty(adoptionDate); this.adoptionDate = new SimpleStringProperty(adoptionDate);
this.adoptionFee = new SimpleDoubleProperty(adoptionFee); this.adoptionFee = new SimpleDoubleProperty(adoptionFee);
this.adoptionStatus = new SimpleStringProperty(adoptionStatus); this.adoptionStatus = new SimpleStringProperty(adoptionStatus);
@@ -43,6 +47,12 @@ public class Adoption {
public SimpleIntegerProperty customerIdProperty() { return customerId; } public SimpleIntegerProperty customerIdProperty() { return customerId; }
public int getEmployeeId() { return employeeId.get(); }
public void setEmployeeId(int employeeId) { this.employeeId.set(employeeId); }
public SimpleIntegerProperty employeeIdProperty() { return employeeId; }
public String getPetName() { return petName.get(); } public String getPetName() { return petName.get(); }
public void setPetName(String petName) { this.petName.set(petName); } public void setPetName(String petName) { this.petName.set(petName); }
@@ -55,6 +65,12 @@ public class Adoption {
public SimpleStringProperty customerNameProperty() { return customerName; } public SimpleStringProperty customerNameProperty() { return customerName; }
public String getEmployeeName() { return employeeName.get(); }
public void setEmployeeName(String employeeName) { this.employeeName.set(employeeName); }
public SimpleStringProperty employeeNameProperty() { return employeeName; }
public String getAdoptionDate() { return adoptionDate.get(); } public String getAdoptionDate() { return adoptionDate.get(); }
public void setAdoptionDate(String adoptionDate) { this.adoptionDate.set(adoptionDate); } public void setAdoptionDate(String adoptionDate) { this.adoptionDate.set(adoptionDate); }

View File

@@ -73,6 +73,7 @@
<RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="SOMETIMES" /> <RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="SOMETIMES" />
<RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="SOMETIMES" /> <RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="SOMETIMES" />
<RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="SOMETIMES" /> <RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="SOMETIMES" />
<RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="SOMETIMES" />
</rowConstraints> </rowConstraints>
<children> <children>
<VBox prefHeight="200.0" prefWidth="100.0" spacing="8.0"> <VBox prefHeight="200.0" prefWidth="100.0" spacing="8.0">
@@ -131,6 +132,20 @@
</ComboBox> </ComboBox>
</children> </children>
</VBox> </VBox>
<VBox prefHeight="200.0" prefWidth="100.0" spacing="8.0" GridPane.rowIndex="2">
<children>
<Label text="Employee:" textFill="#2c3e50">
<font>
<Font name="System Bold" size="16.0" />
</font>
</Label>
<ComboBox fx:id="cbEmployee" prefHeight="29.0" prefWidth="336.0" promptText="Select Employee" 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

@@ -83,6 +83,7 @@
<RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="SOMETIMES" /> <RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="SOMETIMES" />
<RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="SOMETIMES" /> <RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="SOMETIMES" />
<RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="SOMETIMES" /> <RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="SOMETIMES" />
<RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="SOMETIMES" />
</rowConstraints> </rowConstraints>
<children> <children>
<VBox prefHeight="200.0" prefWidth="100.0" spacing="8.0"> <VBox prefHeight="200.0" prefWidth="100.0" spacing="8.0">
@@ -190,9 +191,9 @@
</children> </children>
</VBox> </VBox>
<VBox prefHeight="200.0" prefWidth="100.0" spacing="8.0" GridPane.rowIndex="2"> <VBox prefHeight="200.0" prefWidth="100.0" spacing="8.0" GridPane.rowIndex="2">
<children> <children>
<Label text="Pet:" textFill="#2c3e50"> <Label text="Pet:" textFill="#2c3e50">
<font> <font>
<Font name="System Bold" size="16.0" /> <Font name="System Bold" size="16.0" />
</font> </font>
@@ -201,9 +202,23 @@
<padding> <padding>
<Insets bottom="3.0" left="10.0" right="10.0" top="3.0" /> <Insets bottom="3.0" left="10.0" right="10.0" top="3.0" />
</padding> </padding>
</ComboBox> </ComboBox>
</children> </children>
</VBox> </VBox>
<VBox prefHeight="200.0" prefWidth="100.0" spacing="8.0" GridPane.columnIndex="1" GridPane.rowIndex="3">
<children>
<Label text="Employee:" textFill="#2c3e50">
<font>
<Font name="System Bold" size="16.0" />
</font>
</Label>
<ComboBox fx:id="cbEmployee" prefHeight="29.0" prefWidth="336.0" promptText="Select Employee" 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

@@ -68,11 +68,12 @@
<TableView fx:id="tvAdoptions" prefHeight="362.0" prefWidth="752.0" style="-fx-background-color: white; -fx-background-radius: 12;" VBox.vgrow="ALWAYS"> <TableView fx:id="tvAdoptions" prefHeight="362.0" prefWidth="752.0" style="-fx-background-color: white; -fx-background-radius: 12;" VBox.vgrow="ALWAYS">
<columns> <columns>
<TableColumn fx:id="colAdoptionId" prefWidth="60.0" text="ID" /> <TableColumn fx:id="colAdoptionId" prefWidth="60.0" text="ID" />
<TableColumn fx:id="colPetId" prefWidth="66.2857666015625" text="Pet ID" /> <TableColumn fx:id="colPetId" prefWidth="66.2857666015625" text="Pet ID" />
<TableColumn fx:id="colCustomerName" prefWidth="200.57147216796875" text="Customer Name" /> <TableColumn fx:id="colCustomerName" prefWidth="200.57147216796875" text="Customer Name" />
<TableColumn fx:id="colAdoptionDate" prefWidth="190.85711669921875" text="Adoption Date" /> <TableColumn fx:id="colEmployeeName" prefWidth="160.0" text="Employee" />
<TableColumn fx:id="colAdoptionFee" prefWidth="91.4285888671875" text="Fee" /> <TableColumn fx:id="colAdoptionDate" prefWidth="190.85711669921875" text="Adoption Date" />
<TableColumn fx:id="colAdoptionStatus" prefWidth="142.28570556640625" text="Status" /> <TableColumn fx:id="colAdoptionFee" prefWidth="91.4285888671875" text="Fee" />
<TableColumn fx:id="colAdoptionStatus" prefWidth="120.0" text="Status" />
</columns> </columns>
</TableView> </TableView>
</children> </children>

View File

@@ -68,12 +68,13 @@
<TableView fx:id="tvAppointments" prefHeight="362.0" prefWidth="752.0" style="-fx-background-color: white; -fx-background-radius: 12;" VBox.vgrow="ALWAYS"> <TableView fx:id="tvAppointments" prefHeight="362.0" prefWidth="752.0" style="-fx-background-color: white; -fx-background-radius: 12;" VBox.vgrow="ALWAYS">
<columns> <columns>
<TableColumn fx:id="colAppointmentId" prefWidth="53.14288330078125" text="ID" /> <TableColumn fx:id="colAppointmentId" prefWidth="53.14288330078125" text="ID" />
<TableColumn fx:id="colPetName" prefWidth="108.00003051757812" text="Pet Name" /> <TableColumn fx:id="colPetName" prefWidth="108.00003051757812" text="Pet Name" />
<TableColumn fx:id="colServiceName" prefWidth="132.0" text="Service" /> <TableColumn fx:id="colServiceName" prefWidth="132.0" text="Service" />
<TableColumn fx:id="colAppointmentDate" prefWidth="101.14288330078125" text="Date" /> <TableColumn fx:id="colEmployeeName" prefWidth="132.0" text="Employee" />
<TableColumn fx:id="colAppointmentTime" prefWidth="89.7142333984375" text="Time" /> <TableColumn fx:id="colAppointmentDate" prefWidth="101.14288330078125" text="Date" />
<TableColumn fx:id="colCustomerName" prefWidth="168.57147216796875" text="Customer" /> <TableColumn fx:id="colAppointmentTime" prefWidth="89.7142333984375" text="Time" />
<TableColumn fx:id="colAppointmentStatus" prefWidth="98.28570556640625" text="Status" /> <TableColumn fx:id="colCustomerName" prefWidth="140.0" text="Customer" />
<TableColumn fx:id="colAppointmentStatus" prefWidth="98.28570556640625" text="Status" />
</columns> </columns>
</TableView> </TableView>
</children> </children>

View File

@@ -203,6 +203,7 @@ function AppointmentsPage() {
const didPreselectRef = useRef(false); const didPreselectRef = useRef(false);
const [stores, setStores] = useState([]); const [stores, setStores] = useState([]);
const [employees, setEmployees] = useState([]);
const [services, setServices] = useState([]); const [services, setServices] = useState([]);
const [allPets, setAllPets] = useState([]); const [allPets, setAllPets] = useState([]);
const [customerPets, setCustomerPets] = useState([]); const [customerPets, setCustomerPets] = useState([]);
@@ -210,6 +211,7 @@ function AppointmentsPage() {
const [storeId, setStoreId] = useState(""); const [storeId, setStoreId] = useState("");
const [serviceId, setServiceId] = useState(""); const [serviceId, setServiceId] = useState("");
const [employeeId, setEmployeeId] = useState("");
const [appointmentDate, setAppointmentDate] = useState(""); const [appointmentDate, setAppointmentDate] = useState("");
const [appointmentTime, setAppointmentTime] = useState(""); const [appointmentTime, setAppointmentTime] = useState("");
const [selectedPetIds, setSelectedPetIds] = useState([]); const [selectedPetIds, setSelectedPetIds] = useState([]);
@@ -302,6 +304,33 @@ function AppointmentsPage() {
loadAppointments(); loadAppointments();
}, [loadAppointments]); }, [loadAppointments]);
useEffect(() => {
if (!token || !storeId) {
setEmployees([]);
setEmployeeId("");
return;
}
fetch(`${API_BASE}/api/v1/dropdowns/stores/${storeId}/employees`, {
headers: { Authorization: `Bearer ${token}` },
})
.then((r) => r.json())
.then((data) => setEmployees(Array.isArray(data) ? data : []))
.catch(() => setEmployees([]));
}, [token, storeId]);
useEffect(() => {
if (!employees.length) {
setEmployeeId("");
return;
}
const currentExists = employees.some((employee) => String(employee.id) === String(employeeId));
if (!currentExists) {
setEmployeeId(String(employees[0].id));
}
}, [employees, employeeId]);
useEffect(() => { useEffect(() => {
if (!storeId || !serviceId || !appointmentDate) { if (!storeId || !serviceId || !appointmentDate) {
setAvailableSlots([]); setAvailableSlots([]);
@@ -401,6 +430,7 @@ function AppointmentsPage() {
customerId: user.customerId, customerId: user.customerId,
storeId: Number(storeId), storeId: Number(storeId),
serviceId: Number(serviceId), serviceId: Number(serviceId),
employeeId: employeeId ? Number(employeeId) : undefined,
appointmentDate, appointmentDate,
appointmentTime: appointmentTime + ":00", appointmentTime: appointmentTime + ":00",
appointmentStatus: "Booked", appointmentStatus: "Booked",
@@ -513,6 +543,21 @@ function AppointmentsPage() {
</select> </select>
</label> </label>
{employees.length > 0 && (
<label className="appt-label">
Employee
<select
className="appt-select"
value={employeeId}
onChange={(e) => setEmployeeId(e.target.value)}
>
{employees.map((employee) => (
<option key={employee.id} value={employee.id}>{employee.label}</option>
))}
</select>
</label>
)}
{selectedService && ( {selectedService && (
<div className="appt-service-info"> <div className="appt-service-info">
<p>{selectedService.serviceDesc}</p> <p>{selectedService.serviceDesc}</p>