diff --git a/backend/src/main/java/com/petshop/backend/controller/AdoptionController.java b/backend/src/main/java/com/petshop/backend/controller/AdoptionController.java index 23e372bf..ee577f34 100644 --- a/backend/src/main/java/com/petshop/backend/controller/AdoptionController.java +++ b/backend/src/main/java/com/petshop/backend/controller/AdoptionController.java @@ -2,6 +2,7 @@ package com.petshop.backend.controller; import com.petshop.backend.dto.adoption.AdoptionRequest; import com.petshop.backend.dto.adoption.AdoptionResponse; +import com.petshop.backend.dto.adoption.CustomerAdoptionRequest; import com.petshop.backend.dto.common.BulkDeleteRequest; import com.petshop.backend.entity.User; import com.petshop.backend.repository.UserRepository; @@ -81,6 +82,33 @@ public class AdoptionController { return ResponseEntity.status(HttpStatus.CREATED).body(adoptionService.createAdoption(request)); } + @PostMapping("/request") + @PreAuthorize("hasAnyRole('CUSTOMER', 'ADMIN')") + public ResponseEntity requestAdoption(@Valid @RequestBody CustomerAdoptionRequest request) { + User user = AuthenticationHelper.getAuthenticatedUser(userRepository); + return ResponseEntity.status(HttpStatus.CREATED).body( + adoptionService.requestAdoption(user.getId(), request.getPetId(), request.getEmployeeId(), request.getSourceStoreId(), request.getAdoptionDate()) + ); + } + + @PatchMapping("/{id}/cancel") + @PreAuthorize("hasAnyRole('CUSTOMER', 'STAFF', 'ADMIN')") + public ResponseEntity cancelAdoption(@PathVariable Long id) { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + String role = authentication.getAuthorities().stream() + .findFirst() + .map(authority -> authority.getAuthority().replace("ROLE_", "")) + .orElse(null); + + Long customerId = null; + if ("CUSTOMER".equals(role)) { + User user = AuthenticationHelper.getAuthenticatedUser(userRepository); + customerId = user.getId(); + } + + return ResponseEntity.ok(adoptionService.cancelAdoption(id, customerId)); + } + @PutMapping("/{id}") @PreAuthorize("hasAnyRole('STAFF', 'ADMIN')") public ResponseEntity updateAdoption( diff --git a/backend/src/main/java/com/petshop/backend/controller/AppointmentController.java b/backend/src/main/java/com/petshop/backend/controller/AppointmentController.java index 0dc828a1..e0214fa7 100644 --- a/backend/src/main/java/com/petshop/backend/controller/AppointmentController.java +++ b/backend/src/main/java/com/petshop/backend/controller/AppointmentController.java @@ -98,6 +98,24 @@ public class AppointmentController { return ResponseEntity.status(HttpStatus.CREATED).body(appointmentService.createAppointment(request)); } + @PatchMapping("/{id}/cancel") + @PreAuthorize("hasAnyRole('CUSTOMER', 'STAFF', 'ADMIN')") + public ResponseEntity cancelAppointment(@PathVariable Long id) { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + String role = authentication.getAuthorities().stream() + .findFirst() + .map(authority -> authority.getAuthority().replace("ROLE_", "")) + .orElse(null); + + Long customerId = null; + if ("CUSTOMER".equals(role)) { + User user = AuthenticationHelper.getAuthenticatedUser(userRepository); + customerId = user.getId(); + } + + return ResponseEntity.ok(appointmentService.cancelAppointment(id, customerId)); + } + @PutMapping("/{id}") @PreAuthorize("hasAnyRole('STAFF', 'ADMIN')") public ResponseEntity updateAppointment( diff --git a/backend/src/main/java/com/petshop/backend/controller/AuthController.java b/backend/src/main/java/com/petshop/backend/controller/AuthController.java index a594f9e1..4e9939c8 100644 --- a/backend/src/main/java/com/petshop/backend/controller/AuthController.java +++ b/backend/src/main/java/com/petshop/backend/controller/AuthController.java @@ -73,7 +73,8 @@ public class AuthController { public ResponseEntity register(@Valid @RequestBody RegisterRequest request) { String username = trimToNull(request.getUsername()); String email = trimToNull(request.getEmail()); - NameParts nameParts = splitFullName(request.getFullName()); + String firstName = trimToNull(request.getFirstName()); + String lastName = trimToNull(request.getLastName()); String phone = normalizePhone(request.getPhone()); if (userRepository.findByUsername(username).isPresent()) { @@ -101,9 +102,9 @@ public class AuthController { user.setUsername(username); user.setPassword(passwordEncoder.encode(request.getPassword())); user.setEmail(email); - user.setFirstName(nameParts.firstName()); - user.setLastName(nameParts.lastName()); - user.setFullName(nameParts.fullName()); + user.setFirstName(firstName); + user.setLastName(lastName); + user.setFullName(joinFullName(firstName, lastName)); user.setPhone(phone); user.setRole(User.Role.CUSTOMER); user.setActive(true); @@ -208,11 +209,16 @@ public class AuthController { user.setEmail(email); } - if (request.getFullName() != null) { - NameParts nameParts = splitFullName(request.getFullName()); - user.setFirstName(nameParts.firstName()); - user.setLastName(nameParts.lastName()); - user.setFullName(nameParts.fullName()); + String firstName = trimToNull(request.getFirstName()); + if (firstName != null) { + user.setFirstName(firstName); + } + String lastName = trimToNull(request.getLastName()); + if (lastName != null) { + user.setLastName(lastName); + } + if (firstName != null || lastName != null) { + user.setFullName(joinFullName(user.getFirstName(), user.getLastName())); } if (request.getPhone() != null) { @@ -252,6 +258,8 @@ public class AuthController { return new UserInfoResponse( user.getId(), user.getUsername(), + user.getFirstName(), + user.getLastName(), user.getEmail(), fullName, user.getPhone(), diff --git a/backend/src/main/java/com/petshop/backend/controller/MyPetController.java b/backend/src/main/java/com/petshop/backend/controller/MyPetController.java index e43dbc42..18e8e914 100644 --- a/backend/src/main/java/com/petshop/backend/controller/MyPetController.java +++ b/backend/src/main/java/com/petshop/backend/controller/MyPetController.java @@ -38,8 +38,8 @@ public class MyPetController { } @GetMapping - public ResponseEntity> getMyPets() { - return ResponseEntity.ok(petService.getMyPets(currentUserId())); + public ResponseEntity> getMyPets(@RequestParam(required = false) String status) { + return ResponseEntity.ok(petService.getMyPets(currentUserId(), status)); } @PostMapping diff --git a/backend/src/main/java/com/petshop/backend/controller/StoreController.java b/backend/src/main/java/com/petshop/backend/controller/StoreController.java index 0c4a70c0..7dfb2b01 100644 --- a/backend/src/main/java/com/petshop/backend/controller/StoreController.java +++ b/backend/src/main/java/com/petshop/backend/controller/StoreController.java @@ -23,7 +23,6 @@ public class StoreController { } @GetMapping - @PreAuthorize("isAuthenticated()") public ResponseEntity> getAllStores( @RequestParam(required = false) String q, Pageable pageable) { @@ -31,7 +30,6 @@ public class StoreController { } @GetMapping("/{id}") - @PreAuthorize("isAuthenticated()") public ResponseEntity getStoreById(@PathVariable Long id) { return ResponseEntity.ok(storeService.getStoreById(id)); } diff --git a/backend/src/main/java/com/petshop/backend/dto/adoption/CustomerAdoptionRequest.java b/backend/src/main/java/com/petshop/backend/dto/adoption/CustomerAdoptionRequest.java new file mode 100644 index 00000000..287c1d6d --- /dev/null +++ b/backend/src/main/java/com/petshop/backend/dto/adoption/CustomerAdoptionRequest.java @@ -0,0 +1,49 @@ +package com.petshop.backend.dto.adoption; + +import jakarta.validation.constraints.NotNull; +import java.time.LocalDate; + +public class CustomerAdoptionRequest { + + @NotNull(message = "Pet ID is required") + private Long petId; + + private Long employeeId; + + private Long sourceStoreId; + + @NotNull(message = "Appointment date is required") + private LocalDate adoptionDate; + + public Long getPetId() { + return petId; + } + + public void setPetId(Long petId) { + this.petId = petId; + } + + public Long getEmployeeId() { + return employeeId; + } + + public void setEmployeeId(Long employeeId) { + this.employeeId = employeeId; + } + + public Long getSourceStoreId() { + return sourceStoreId; + } + + public void setSourceStoreId(Long sourceStoreId) { + this.sourceStoreId = sourceStoreId; + } + + public LocalDate getAdoptionDate() { + return adoptionDate; + } + + public void setAdoptionDate(LocalDate adoptionDate) { + this.adoptionDate = adoptionDate; + } +} diff --git a/backend/src/main/java/com/petshop/backend/dto/auth/ProfileUpdateRequest.java b/backend/src/main/java/com/petshop/backend/dto/auth/ProfileUpdateRequest.java index 58959678..dc1c98c0 100644 --- a/backend/src/main/java/com/petshop/backend/dto/auth/ProfileUpdateRequest.java +++ b/backend/src/main/java/com/petshop/backend/dto/auth/ProfileUpdateRequest.java @@ -11,8 +11,11 @@ public class ProfileUpdateRequest { @Email(message = "Email must be valid") private String email; - @Size(max = 100, message = "Full name must not exceed 100 characters") - private String fullName; + @Size(max = 50, message = "First name must not exceed 50 characters") + private String firstName; + + @Size(max = 50, message = "Last name must not exceed 50 characters") + private String lastName; @Size(max = 20, message = "Phone must not exceed 20 characters") private String phone; @@ -36,12 +39,20 @@ public class ProfileUpdateRequest { this.email = email; } - public String getFullName() { - return fullName; + public String getFirstName() { + return firstName; } - public void setFullName(String fullName) { - this.fullName = fullName; + public void setFirstName(String firstName) { + this.firstName = firstName; + } + + public String getLastName() { + return lastName; + } + + public void setLastName(String lastName) { + this.lastName = lastName; } public String getPhone() { @@ -67,14 +78,15 @@ public class ProfileUpdateRequest { ProfileUpdateRequest that = (ProfileUpdateRequest) o; return Objects.equals(username, that.username) && Objects.equals(email, that.email) && - Objects.equals(fullName, that.fullName) && + Objects.equals(firstName, that.firstName) && + Objects.equals(lastName, that.lastName) && Objects.equals(phone, that.phone) && Objects.equals(password, that.password); } @Override public int hashCode() { - return Objects.hash(username, email, fullName, phone, password); + return Objects.hash(username, email, firstName, lastName, phone, password); } @Override @@ -82,7 +94,8 @@ public class ProfileUpdateRequest { return "ProfileUpdateRequest{" + "username='" + username + '\'' + ", email='" + email + '\'' + - ", fullName='" + fullName + '\'' + + ", firstName='" + firstName + '\'' + + ", lastName='" + lastName + '\'' + ", phone='" + phone + '\'' + ", password='" + password + '\'' + '}'; diff --git a/backend/src/main/java/com/petshop/backend/dto/auth/RegisterRequest.java b/backend/src/main/java/com/petshop/backend/dto/auth/RegisterRequest.java index 2791746c..4758d923 100644 --- a/backend/src/main/java/com/petshop/backend/dto/auth/RegisterRequest.java +++ b/backend/src/main/java/com/petshop/backend/dto/auth/RegisterRequest.java @@ -18,9 +18,13 @@ public class RegisterRequest { @Email(message = "Email must be valid") private String email; - @NotBlank(message = "Full name is required") - @Size(max = 100, message = "Full name must not exceed 100 characters") - private String fullName; + @NotBlank(message = "First name is required") + @Size(max = 50, message = "First name must not exceed 50 characters") + private String firstName; + + @NotBlank(message = "Last name is required") + @Size(max = 50, message = "Last name must not exceed 50 characters") + private String lastName; @NotBlank(message = "Phone is required") @Size(max = 20, message = "Phone must not exceed 20 characters") @@ -50,12 +54,20 @@ public class RegisterRequest { this.email = email; } - public String getFullName() { - return fullName; + public String getFirstName() { + return firstName; } - public void setFullName(String fullName) { - this.fullName = fullName; + public void setFirstName(String firstName) { + this.firstName = firstName; + } + + public String getLastName() { + return lastName; + } + + public void setLastName(String lastName) { + this.lastName = lastName; } public String getPhone() { @@ -74,13 +86,14 @@ public class RegisterRequest { return Objects.equals(username, that.username) && Objects.equals(password, that.password) && Objects.equals(email, that.email) && - Objects.equals(fullName, that.fullName) && + Objects.equals(firstName, that.firstName) && + Objects.equals(lastName, that.lastName) && Objects.equals(phone, that.phone); } @Override public int hashCode() { - return Objects.hash(username, password, email, fullName, phone); + return Objects.hash(username, password, email, firstName, lastName, phone); } @Override @@ -89,7 +102,8 @@ public class RegisterRequest { "username='" + username + '\'' + ", password='" + password + '\'' + ", email='" + email + '\'' + - ", fullName='" + fullName + '\'' + + ", firstName='" + firstName + '\'' + + ", lastName='" + lastName + '\'' + ", phone='" + phone + '\'' + '}'; } diff --git a/backend/src/main/java/com/petshop/backend/dto/auth/UserInfoResponse.java b/backend/src/main/java/com/petshop/backend/dto/auth/UserInfoResponse.java index 84372638..e88c272d 100644 --- a/backend/src/main/java/com/petshop/backend/dto/auth/UserInfoResponse.java +++ b/backend/src/main/java/com/petshop/backend/dto/auth/UserInfoResponse.java @@ -5,6 +5,8 @@ import java.util.Objects; public class UserInfoResponse { private Long id; private String username; + private String firstName; + private String lastName; private String email; private String fullName; private String phone; @@ -17,9 +19,11 @@ public class UserInfoResponse { public UserInfoResponse() { } - public UserInfoResponse(Long id, String username, String email, String fullName, String phone, String avatarUrl, String role, Long customerId, Long storeId, String storeName) { + public UserInfoResponse(Long id, String username, String firstName, String lastName, String email, String fullName, String phone, String avatarUrl, String role, Long customerId, Long storeId, String storeName) { this.id = id; this.username = username; + this.firstName = firstName; + this.lastName = lastName; this.email = email; this.fullName = fullName; this.phone = phone; @@ -46,6 +50,22 @@ public class UserInfoResponse { this.username = username; } + public String getFirstName() { + return firstName; + } + + public void setFirstName(String firstName) { + this.firstName = firstName; + } + + public String getLastName() { + return lastName; + } + + public void setLastName(String lastName) { + this.lastName = lastName; + } + public String getEmail() { return email; } diff --git a/backend/src/main/java/com/petshop/backend/dto/pet/MyPetRequest.java b/backend/src/main/java/com/petshop/backend/dto/pet/MyPetRequest.java index 17d08e2f..37942a47 100644 --- a/backend/src/main/java/com/petshop/backend/dto/pet/MyPetRequest.java +++ b/backend/src/main/java/com/petshop/backend/dto/pet/MyPetRequest.java @@ -1,5 +1,7 @@ package com.petshop.backend.dto.pet; +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.Size; @@ -16,6 +18,10 @@ public class MyPetRequest { @Size(max = 50, message = "Breed must not exceed 50 characters") private String breed; + @Min(value = 0, message = "Age must be 0 or greater") + @Max(value = 100, message = "Age must not exceed 100") + private Integer petAge; + public String getPetName() { return petName; } @@ -39,4 +45,12 @@ public class MyPetRequest { public void setBreed(String breed) { this.breed = breed; } + + public Integer getPetAge() { + return petAge; + } + + public void setPetAge(Integer petAge) { + this.petAge = petAge; + } } diff --git a/backend/src/main/java/com/petshop/backend/dto/pet/MyPetResponse.java b/backend/src/main/java/com/petshop/backend/dto/pet/MyPetResponse.java index 7063b2f8..15117334 100644 --- a/backend/src/main/java/com/petshop/backend/dto/pet/MyPetResponse.java +++ b/backend/src/main/java/com/petshop/backend/dto/pet/MyPetResponse.java @@ -6,17 +6,21 @@ public class MyPetResponse { private String petName; private String species; private String breed; + private Integer petAge; private String imageUrl; + private String petStatus; public MyPetResponse() { } - public MyPetResponse(Long customerPetId, String petName, String species, String breed, String imageUrl) { + public MyPetResponse(Long customerPetId, String petName, String species, String breed, Integer petAge, String imageUrl, String petStatus) { this.customerPetId = customerPetId; this.petName = petName; this.species = species; this.breed = breed; + this.petAge = petAge; this.imageUrl = imageUrl; + this.petStatus = petStatus; } public Long getCustomerPetId() { @@ -51,6 +55,14 @@ public class MyPetResponse { this.breed = breed; } + public Integer getPetAge() { + return petAge; + } + + public void setPetAge(Integer petAge) { + this.petAge = petAge; + } + public String getImageUrl() { return imageUrl; } @@ -58,4 +70,12 @@ public class MyPetResponse { public void setImageUrl(String imageUrl) { this.imageUrl = imageUrl; } + + public String getPetStatus() { + return petStatus; + } + + public void setPetStatus(String petStatus) { + this.petStatus = petStatus; + } } diff --git a/backend/src/main/java/com/petshop/backend/repository/AppointmentRepository.java b/backend/src/main/java/com/petshop/backend/repository/AppointmentRepository.java index 616597eb..e18a007a 100644 --- a/backend/src/main/java/com/petshop/backend/repository/AppointmentRepository.java +++ b/backend/src/main/java/com/petshop/backend/repository/AppointmentRepository.java @@ -51,5 +51,7 @@ public interface AppointmentRepository extends JpaRepository @Query("SELECT a FROM Appointment a WHERE (a.appointmentDate < :currentDate OR (a.appointmentDate = :currentDate AND a.appointmentTime < :currentTime)) AND LOWER(a.appointmentStatus) = 'booked'") List findPastBookedAppointments(@Param("currentDate") LocalDate currentDate, @Param("currentTime") LocalTime currentTime); + List findByPet_Id(Long petId); + List findByAppointmentDateAndAppointmentStatusIgnoreCase(LocalDate date, String status); } diff --git a/backend/src/main/java/com/petshop/backend/security/SecurityConfig.java b/backend/src/main/java/com/petshop/backend/security/SecurityConfig.java index 12f784fb..fcc05bb4 100644 --- a/backend/src/main/java/com/petshop/backend/security/SecurityConfig.java +++ b/backend/src/main/java/com/petshop/backend/security/SecurityConfig.java @@ -66,6 +66,7 @@ public class SecurityConfig { .requestMatchers(HttpMethod.GET, "/api/v1/products/**").permitAll() .requestMatchers(HttpMethod.GET, "/api/v1/services/**").permitAll() .requestMatchers(HttpMethod.GET, "/api/v1/categories/**").permitAll() + .requestMatchers(HttpMethod.GET, "/api/v1/stores/**").permitAll() .requestMatchers(HttpMethod.GET, "/api/v1/dropdowns/pet-species").permitAll() .requestMatchers(HttpMethod.GET, "/api/v1/dropdowns/pet-breeds").permitAll() .requestMatchers(HttpMethod.GET, "/api/v1/dropdowns/stores").permitAll() @@ -101,7 +102,7 @@ public class SecurityConfig { public CorsConfigurationSource corsConfigurationSource() { CorsConfiguration config = new CorsConfiguration(); config.setAllowedOriginPatterns(List.of("http://localhost:*", "http://127.0.0.1:*")); - config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS")); + config.setAllowedMethods(List.of("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS")); config.setAllowedHeaders(List.of("*")); config.setAllowCredentials(true); UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); diff --git a/backend/src/main/java/com/petshop/backend/service/AdoptionService.java b/backend/src/main/java/com/petshop/backend/service/AdoptionService.java index 3e777060..734f8c63 100644 --- a/backend/src/main/java/com/petshop/backend/service/AdoptionService.java +++ b/backend/src/main/java/com/petshop/backend/service/AdoptionService.java @@ -158,6 +158,57 @@ public class AdoptionService { return mapToResponse(adoption); } + @Transactional + public AdoptionResponse requestAdoption(Long customerId, Long petId, Long employeeId, Long sourceStoreId, LocalDate adoptionDate) { + Pet pet = petRepository.findById(petId) + .orElseThrow(() -> new ResourceNotFoundException("Pet not found")); + + // Verify the pet is actually located at the claimed store + if (pet.getStore() == null || !pet.getStore().getStoreId().equals(sourceStoreId)) { + throw new IllegalArgumentException("The specified pet is not located at the selected store."); + } + + // Verify the pet is available for adoption + validatePetAvailability(pet, null, null); + + User customer = userRepository.findById(customerId) + .orElseThrow(() -> new ResourceNotFoundException("Customer not found")); + User employee = resolveAdoptionEmployee(employeeId); + StoreLocation sourceStore = storeRepository.findById(sourceStoreId) + .orElseThrow(() -> new ResourceNotFoundException("Store not found")); + + Adoption adoption = new Adoption(); + adoption.setPet(pet); + adoption.setCustomer(customer); + adoption.setEmployee(employee); + adoption.setSourceStore(sourceStore); + adoption.setAdoptionDate(adoptionDate); + adoption.setAdoptionStatus(ADOPTION_STATUS_PENDING); + + adoption = adoptionRepository.save(adoption); + syncPetStatus(pet, ADOPTION_STATUS_PENDING, adoption.getAdoptionId(), customer); + return mapToResponse(adoption); + } + + @Transactional + public AdoptionResponse cancelAdoption(Long adoptionId, Long requestingCustomerId) { + Adoption adoption = adoptionRepository.findById(adoptionId) + .orElseThrow(() -> new ResourceNotFoundException("Adoption not found with id: " + adoptionId)); + + if (requestingCustomerId != null && !adoption.getCustomer().getId().equals(requestingCustomerId)) { + throw new ResourceNotFoundException("Adoption not found"); + } + + if (!ADOPTION_STATUS_PENDING.equalsIgnoreCase(adoption.getAdoptionStatus())) { + throw new IllegalArgumentException("Only pending adoptions can be cancelled"); + } + + adoption.setAdoptionStatus(ADOPTION_STATUS_CANCELLED); + adoption = adoptionRepository.save(adoption); + syncPetStatus(adoption.getPet(), ADOPTION_STATUS_CANCELLED, adoption.getAdoptionId(), adoption.getCustomer()); + return mapToResponse(adoption); + } + @Transactional public void deleteAdoption(Long id) { Adoption adoption = adoptionRepository.findById(id) diff --git a/backend/src/main/java/com/petshop/backend/service/AppointmentService.java b/backend/src/main/java/com/petshop/backend/service/AppointmentService.java index b8b4ba60..bea0c682 100644 --- a/backend/src/main/java/com/petshop/backend/service/AppointmentService.java +++ b/backend/src/main/java/com/petshop/backend/service/AppointmentService.java @@ -109,6 +109,22 @@ public class AppointmentService { Pet pet = request.getPetId() != null ? fetchPet(request.getPetId()) : null; User employee = resolveAppointmentEmployee(request.getEmployeeId(), store.getStoreId()); + // Customers must supply a pet that is Adopted and owned by them + if (User.Role.CUSTOMER.equals(authenticatedUser.getRole())) { + if (pet == null) { + throw new IllegalArgumentException("A pet must be selected for your appointment"); + } + if (pet.getOwner() == null || !pet.getOwner().getId().equals(authenticatedUser.getId())) { + throw new IllegalArgumentException("The selected pet does not belong to your account"); + } + String petStatus = pet.getPetStatus(); + if (!"Owned".equalsIgnoreCase(petStatus) && !"Adopted".equalsIgnoreCase(petStatus)) { + throw new IllegalArgumentException("Only your own pets can be booked for appointments"); + } + } + + validateSpeciesServiceCompatibility(pet, service); + validateStoreAccess(store.getStoreId(), authenticatedUser); validatePetServiceCompatibility(pet, service); validateAvailability(employee, service, request.getAppointmentDate(), request.getAppointmentTime(), null); @@ -167,6 +183,23 @@ public class AppointmentService { return mapToResponse(appointment); } + @Transactional + public AppointmentResponse cancelAppointment(Long appointmentId, Long requestingCustomerId) { + Appointment appointment = appointmentRepository.findById(appointmentId) + .orElseThrow(() -> new ResourceNotFoundException("Appointment not found with id: " + appointmentId)); + + if (requestingCustomerId != null && !appointment.getCustomer().getId().equals(requestingCustomerId)) { + throw new ResourceNotFoundException("Appointment not found"); + } + + if (!"Booked".equalsIgnoreCase(appointment.getAppointmentStatus())) { + throw new IllegalArgumentException("Only booked appointments can be cancelled"); + } + + appointment.setAppointmentStatus("Cancelled"); + return mapToResponse(appointmentRepository.save(appointment)); + } + @Transactional public void deleteAppointment(Long id) { if (!appointmentRepository.existsById(id)) { @@ -350,6 +383,32 @@ public class AppointmentService { return true; } + private void validateSpeciesServiceCompatibility(Pet pet, com.petshop.backend.entity.Service service) { + if (pet == null || service == null) return; + String species = pet.getPetSpecies(); + if (species == null) return; + String serviceName = service.getServiceName().toLowerCase(); + + switch (species.toLowerCase()) { + case "bird": + if (!serviceName.contains("wing clipping") && !serviceName.contains("beak and nail")) { + throw new IllegalArgumentException( + "Service '" + service.getServiceName() + "' is not available for birds. " + + "Allowed services: Wing Clipping, Beak and Nail Care."); + } + break; + case "fish": + if (!serviceName.contains("aquarium health")) { + throw new IllegalArgumentException( + "Service '" + service.getServiceName() + "' is not available for fish. " + + "Allowed service: Aquarium Health Check."); + } + break; + default: + break; + } + } + private void validateStoreAccess(Long requestedStoreId, User user) { if (user.getRole() != User.Role.STAFF) { return; diff --git a/backend/src/main/java/com/petshop/backend/service/PetService.java b/backend/src/main/java/com/petshop/backend/service/PetService.java index ae414601..dd86eb4a 100644 --- a/backend/src/main/java/com/petshop/backend/service/PetService.java +++ b/backend/src/main/java/com/petshop/backend/service/PetService.java @@ -12,6 +12,7 @@ import com.petshop.backend.entity.User; import com.petshop.backend.exception.ResourceNotFoundException; import com.petshop.backend.security.AppPrincipal; import com.petshop.backend.repository.AdoptionRepository; +import com.petshop.backend.repository.AppointmentRepository; import com.petshop.backend.repository.PetRepository; import com.petshop.backend.repository.StoreRepository; import com.petshop.backend.repository.UserRepository; @@ -35,13 +36,15 @@ public class PetService { private final PetRepository petRepository; private final AdoptionRepository adoptionRepository; + private final AppointmentRepository appointmentRepository; private final UserRepository userRepository; private final StoreRepository storeRepository; private final CatalogImageStorageService catalogImageStorageService; - public PetService(PetRepository petRepository, AdoptionRepository adoptionRepository, UserRepository userRepository, StoreRepository storeRepository, CatalogImageStorageService catalogImageStorageService) { + public PetService(PetRepository petRepository, AdoptionRepository adoptionRepository, AppointmentRepository appointmentRepository, UserRepository userRepository, StoreRepository storeRepository, CatalogImageStorageService catalogImageStorageService) { this.petRepository = petRepository; this.adoptionRepository = adoptionRepository; + this.appointmentRepository = appointmentRepository; this.userRepository = userRepository; this.storeRepository = storeRepository; this.catalogImageStorageService = catalogImageStorageService; @@ -87,8 +90,9 @@ public class PetService { } @Transactional(readOnly = true) - public List getMyPets(Long ownerUserId) { + public List getMyPets(Long ownerUserId, String status) { return petRepository.findAllByOwner_IdOrderByPetNameAsc(ownerUserId).stream() + .filter(p -> status == null || status.isBlank() || status.equalsIgnoreCase(p.getPetStatus())) .map(this::mapToMyPetResponse) .toList(); } @@ -117,6 +121,18 @@ public class PetService { @Transactional public void deleteMyPet(Long ownerUserId, Long petId) { Pet pet = findOwnedPet(ownerUserId, petId); + List linkedAppointments = appointmentRepository.findByPet_Id(petId); + boolean hasBooked = linkedAppointments.stream() + .anyMatch(a -> "Booked".equalsIgnoreCase(a.getAppointmentStatus())); + if (hasBooked) { + throw new IllegalArgumentException( + "Your pet has a booked appointment. Please cancel the appointment before removing your pet from our database."); + } + // Nullify the pet reference on non-booked appointments to avoid FK constraint violations + for (com.petshop.backend.entity.Appointment appt : linkedAppointments) { + appt.setPet(null); + } + appointmentRepository.saveAll(linkedAppointments); deleteStoredImageIfPresent(pet.getImageUrl()); petRepository.delete(pet); } @@ -341,7 +357,9 @@ public class PetService { pet.getPetName(), pet.getPetSpecies(), pet.getPetBreed(), - pet.getImageUrl() != null && !pet.getImageUrl().isBlank() ? "/api/v1/pets/" + pet.getPetId() + "/image" : null + pet.getPetAge(), + pet.getImageUrl() != null && !pet.getImageUrl().isBlank() ? "/api/v1/pets/" + pet.getPetId() + "/image" : null, + pet.getPetStatus() ); } @@ -349,7 +367,7 @@ public class PetService { pet.setPetName(request.getPetName().trim()); pet.setPetSpecies(request.getSpecies().trim()); pet.setPetBreed(normalizeOptional(request.getBreed())); - pet.setPetAge(null); + pet.setPetAge(request.getPetAge()); pet.setPetPrice(null); } diff --git a/web/app/adopt/[id]/page.js b/web/app/adopt/[id]/page.js index 6c7b70e0..f2ade475 100644 --- a/web/app/adopt/[id]/page.js +++ b/web/app/adopt/[id]/page.js @@ -44,6 +44,8 @@ export default function PetDetailPage() { petStatus={pet.petStatus} petPrice={pet.petPrice} imageUrl={pet.imageUrl} + storeId={pet.storeId} + storeName={pet.storeName} /> )} diff --git a/web/app/appointments/page.js b/web/app/appointments/page.js index b4dd5cee..67170c59 100644 --- a/web/app/appointments/page.js +++ b/web/app/appointments/page.js @@ -7,6 +7,34 @@ import { useAuth } from "@/context/AuthContext"; const API_BASE = ""; +const SPECIES_BREEDS = { + Dog: ["Beagle", "Boxer", "Bulldog", "Chihuahua", "Dachshund", "German Shepherd", "Golden Retriever", "Labrador Retriever", "Poodle", "Rottweiler", "Shih Tzu", "Siberian Husky", "Yorkshire Terrier", "Mixed / Other"], + Cat: ["Abyssinian", "Bengal", "British Shorthair", "Maine Coon", "Persian", "Ragdoll", "Scottish Fold", "Siamese", "Sphynx", "Mixed / Other"], + Bird: ["Canary", "Cockatiel", "Cockatoo", "Finch", "Lovebird", "Macaw", "Parakeet", "Parrot", "Other"], + Rabbit: ["Dutch", "Flemish Giant", "Holland Lop", "Lionhead", "Mini Rex", "Other"], + Hamster: ["Dwarf", "Roborovski", "Syrian", "Other"], + "Guinea Pig": ["Abyssinian", "American", "Peruvian", "Teddy", "Other"], + Reptile: ["Ball Python", "Bearded Dragon", "Blue-tongued Skink", "Corn Snake", "Leopard Gecko", "Other"], + Fish: ["Angelfish", "Betta", "Cichlid", "Clownfish", "Goldfish", "Guppy", "Tetra", "Other"], + Other: ["Other"], +}; + +// Explicit allowlists for species with restricted service availability. +// Species not listed here may use all services. +const SPECIES_SERVICE_ALLOWLIST = { + Bird: ["wing clipping", "beak and nail"], + Fish: ["aquarium health"], +}; + +function getAvailableServices(services, species) { + if (!species) return services; + const allowlist = SPECIES_SERVICE_ALLOWLIST[species]; + if (!allowlist) return services; + return services.filter((s) => + allowlist.some((kw) => s.serviceName.toLowerCase().includes(kw)) + ); +} + const DAYS = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; const MONTHS = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December", ]; @@ -254,26 +282,32 @@ function AddPetModal({ token, onClose, onAdded }) {
- {storeId && serviceId && appointmentDate && ( + {!adoptionMode && storeId && serviceId && appointmentDate && (
Available Time Slots {loadingSlots ? ( @@ -693,83 +930,62 @@ const canBookAppointments = user?.role === "CUSTOMER" || user?.role === "ADMIN";
)} - {serviceId && ( -
- {petSectionLabel} - {isCustomerPetService && ( - - )} - {petsToShow.length === 0 ? ( -

{noPetsMessage}

- ) : isAdoptionService ? ( -
- {petsToShow.map((p) => ( - - ))} -
- ) : ( -
- {petsToShow.map((p) => ( - - ))} -
- )} -
- )} - + + {success &&
{success}
} + + )} + + )} ) : null}
-

{canBookAppointments ? "Your Appointments" : "Appointments"}

- {loadingAppointments ? ( +

+ {adoptionMode ? "Your Adoptions" : canBookAppointments ? "Your Appointments" : "Appointments"} +

+ {adoptionMode ? ( + loadingAdoptions ? ( +

Loading adoptions...

+ ) : adoptions.length === 0 ? ( +

No adoption appointments yet.

+ ) : ( +
+ {adoptions.map((a) => ( +
+
+ {a.petName} + + {a.adoptionStatus} + +
+
+ {a.sourceStoreName} + {a.adoptionDate} +
+ {a.adoptionStatus?.toLowerCase() === "pending" && ( +
+ +
+ )} +
+ ))} +
+ ) + ) : loadingAppointments ? (

Loading appointments...

) : appointments.length === 0 ? (

No appointments yet.

@@ -797,6 +1013,18 @@ const canBookAppointments = user?.role === "CUSTOMER" || user?.role === "ADMIN"; Pets: {a.customerPetNames.join(", ")}
)} + {a.appointmentStatus?.toLowerCase() === "booked" && ( +
+ +
+ )} ))} diff --git a/web/app/contact/page.js b/web/app/contact/page.js index 85fba175..19295ebd 100644 --- a/web/app/contact/page.js +++ b/web/app/contact/page.js @@ -1,31 +1,24 @@ -const LOCATIONS = [ - { - name: "Downtown Branch", - address: "123 Main St", - phone: "(123) 456-7890", - email: "downtown@petshop.com", - }, - { - name: "North Branch", - address: "456 North Ave", - phone: "(987) 654-3210", - email: "north@petshop.com", - }, - { - name: "West Side Store", - address: "789 West Blvd", - phone: "(555) 123-4567", - email: "westside@petshop.com", - }, -]; +"use client"; -const PERSONNEL = [ - { name: "John Doe", role: "Store Manager" }, - { name: "Sara Smith", role: "Staff" }, - { name: "Michael Johnson", role: "Grooming Team" }, -]; +import { useState, useEffect } from "react"; export default function ContactPage() { + const [locations, setLocations] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + const params = new URLSearchParams({ page: "0", size: "100", sort: "storeName,asc" }); + fetch(`/api/v1/stores?${params}`) + .then((res) => { + if (!res.ok) throw new Error(`HTTP ${res.status}`); + return res.json(); + }) + .then((data) => setLocations(data.content ?? [])) + .catch((err) => setError(err.message)) + .finally(() => setLoading(false)); + }, []); + return (
@@ -44,28 +37,40 @@ export default function ContactPage() {

Store Locations

-
- {LOCATIONS.map((location) => ( -
-

{location.name}

-

{location.address}

-

{location.phone}

-

{location.email}

-
- ))} -
-
-
-

Store Personnel

-
- {PERSONNEL.map((person) => ( -
-

{person.name}

-

{person.role}

-
- ))} -
+ {loading &&

Loading locations...

} + + {error &&

Failed to load locations: {error}

} + + {!loading && !error && locations.length === 0 && ( +

No store locations found.

+ )} + + {!loading && !error && locations.length > 0 && ( +
+ {locations.map((location) => ( +
+
+ {location.storeName} { + e.currentTarget.onerror = null; + e.currentTarget.src = "/images/pet-placeholder.png"; + }} + /> +
+
+

{location.storeName}

+

{location.address}

+

{location.phone}

+

{location.email}

+
+
+ ))} +
+ )}
diff --git a/web/app/favicon.ico b/web/app/favicon.ico index 718d6fea..5e11bd61 100644 Binary files a/web/app/favicon.ico and b/web/app/favicon.ico differ diff --git a/web/app/globals.css b/web/app/globals.css index dd607b01..2dbbe14a 100644 --- a/web/app/globals.css +++ b/web/app/globals.css @@ -38,7 +38,7 @@ body { align-items: center; flex-wrap: wrap; min-height: 70px; - border-radius: 0px 0px 10px 10px; + /* border-radius: 0px 0px 10px 10px; */ } /* Add padding to body to account for fixed header */ @@ -62,11 +62,9 @@ body { .nav-links { display: flex; align-items: center; - gap: 2rem; - position: absolute; - left: 50%; - top: 50%; - transform: translate(-50%, -50%); + gap: 1.25rem; + flex: 1; + justify-content: center; } /* Indivdual Link Styles */ @@ -758,6 +756,39 @@ body { margin-bottom: 0.5rem; } +.location-card { + padding: 0; + overflow: hidden; +} + +.location-card-image-wrapper { + width: 100%; + aspect-ratio: 16 / 9; + overflow: hidden; + background: #eee; +} + +.location-card-image { + width: 100%; + height: 100%; + object-fit: cover; +} + +.location-card-body { + padding: 1rem; +} + +.location-card-body h3 { + margin-top: 0; + margin-bottom: 0.5rem; +} + +.location-card-body p { + margin: 0.25rem 0; + font-size: 0.9rem; + color: #555; +} + .products-hero { text-align: center; padding: 4rem 2rem 3rem; @@ -1356,6 +1387,17 @@ body { box-shadow: 0 0 0 3px rgba(255, 165, 0, 0.2); } +.appt-locked-field { + padding: 0.6rem 0.85rem; + border: 1px solid #e0e0e0; + border-radius: 8px; + font-size: 1rem; + background: #f5f5f5; + color: #555; + font-weight: 600; + cursor: not-allowed; +} + .appt-service-info { background: #fff8f0; border: 1px solid #ffd180; @@ -1616,6 +1658,11 @@ body { color: #c62828; } +.appt-card-status--pending { + background: #fff8e1; + color: #f57f17; +} + .appt-card-details { display: flex; justify-content: space-between; @@ -1629,6 +1676,34 @@ body { margin-top: 0.35rem; } +.appt-card-actions { + display: flex; + justify-content: flex-end; + margin-top: 0.6rem; +} + +.appt-cancel-btn { + font-size: 0.8rem; + font-weight: 600; + padding: 0.25rem 0.85rem; + border-radius: 6px; + border: 1px solid #e53935; + background: transparent; + color: #e53935; + cursor: pointer; + transition: background 0.15s, color 0.15s; +} + +.appt-cancel-btn:hover:not(:disabled) { + background: #e53935; + color: #fff; +} + +.appt-cancel-btn:disabled { + opacity: 0.5; + cursor: default; +} + /* Adoption Pet Selection */ .appt-adopt-grid { @@ -2573,7 +2648,24 @@ body { /* Mobile / Responsive */ -@media (max-width: 768px) { +/* Compact nav at mid-range widths before collapsing to hamburger */ +@media (min-width: 1101px) and (max-width: 1350px) { + .nav-links { + gap: 0.25rem; + } + + .nav-link { + font-size: 0.9rem; + padding: 0.4rem 0.5rem; + } + + .nav-auth { + gap: 0.35rem; + padding-left: 0.5rem; + } +} + +@media (max-width: 1100px) { .navbar { padding: 0.5rem 1rem; } @@ -2629,7 +2721,7 @@ body { display: none; } -@media (max-width: 768px) { +@media (max-width: 1100px) { /* Show hamburger bar, hide desktop nav */ .nav-mobile-bar { display: flex; diff --git a/web/app/profile/page.js b/web/app/profile/page.js index cf2f0d2d..ddb23d62 100644 --- a/web/app/profile/page.js +++ b/web/app/profile/page.js @@ -6,6 +6,18 @@ import { useAuth } from "@/context/AuthContext"; const API_BASE = ""; +const SPECIES_BREEDS = { + Dog: ["Beagle", "Boxer", "Bulldog", "Chihuahua", "Dachshund", "German Shepherd", "Golden Retriever", "Labrador Retriever", "Poodle", "Rottweiler", "Shih Tzu", "Siberian Husky", "Yorkshire Terrier", "Mixed / Other"], + Cat: ["Abyssinian", "Bengal", "British Shorthair", "Maine Coon", "Persian", "Ragdoll", "Scottish Fold", "Siamese", "Sphynx", "Mixed / Other"], + Bird: ["Canary", "Cockatiel", "Cockatoo", "Finch", "Lovebird", "Macaw", "Parakeet", "Parrot", "Other"], + Rabbit: ["Dutch", "Flemish Giant", "Holland Lop", "Lionhead", "Mini Rex", "Other"], + Hamster: ["Dwarf", "Roborovski", "Syrian", "Other"], + "Guinea Pig": ["Abyssinian", "American", "Peruvian", "Teddy", "Other"], + Reptile: ["Ball Python", "Bearded Dragon", "Blue-tongued Skink", "Corn Snake", "Leopard Gecko", "Other"], + Fish: ["Angelfish", "Betta", "Cichlid", "Clownfish", "Goldfish", "Guppy", "Tetra", "Other"], + Other: ["Other"], +}; + export default function ProfilePage() { const {user, token, loading, logout, refreshUser} = useAuth(); const router = useRouter(); @@ -18,9 +30,11 @@ export default function ProfilePage() { const [petName, setPetName] = useState(""); const [species, setSpecies] = useState(""); const [breed, setBreed] = useState(""); + const [petAge, setPetAge] = useState("1"); const [submitting, setSubmitting] = useState(false); const [petError, setPetError] = useState(null); - const [profileForm, setProfileForm] = useState({ fullName: "", email: "", phone: "" }); + const [avatarObjectUrl, setAvatarObjectUrl] = useState(null); + const [profileForm, setProfileForm] = useState({ firstName: "", lastName: "", email: "", phone: "", password: "", confirmPassword: "" }); const [profileSubmitting, setProfileSubmitting] = useState(false); const [profileError, setProfileError] = useState(null); const [profileSuccess, setProfileSuccess] = useState(null); @@ -42,9 +56,12 @@ export default function ProfilePage() { useEffect(() => { setProfileForm({ - fullName: user?.fullName || "", + firstName: user?.firstName || "", + lastName: user?.lastName || "", email: user?.email || "", phone: user?.phone || "", + password: "", + confirmPassword: "", }); }, [user]); @@ -53,7 +70,7 @@ export default function ProfilePage() { setLoadingPets(true); try { - const response = await fetch(`${API_BASE}/api/v1/my-pets`, { + const response = await fetch(`${API_BASE}/api/v1/my-pets?status=Owned`, { headers: { Authorization: `Bearer ${token}` }, }); @@ -64,25 +81,21 @@ export default function ProfilePage() { const petData = await response.json(); clearPetImageObjectUrls(); - const petsWithResolvedImages = await Promise.all( - (Array.isArray(petData) ? petData : []).map(async (pet) => { - if (!pet.imageUrl) { - return pet; - } + const ownedPets = Array.isArray(petData) ? petData : []; + const petsWithResolvedImages = await Promise.all( + ownedPets.map(async (pet) => { + if (!pet.imageUrl) return pet; try { const imageResponse = await fetch(`${API_BASE}${pet.imageUrl}`, { headers: { Authorization: `Bearer ${token}` }, }); - if (!imageResponse.ok) { - return { ...pet, imageUrl: null }; - } + if (!imageResponse.ok) return { ...pet, imageUrl: null }; const blob = await imageResponse.blob(); const objectUrl = URL.createObjectURL(blob); petImageObjectUrlsRef.current.push(objectUrl); - return { ...pet, imageUrl: objectUrl }; } catch { return { ...pet, imageUrl: null }; @@ -108,11 +121,37 @@ export default function ProfilePage() { }, [clearPetImageObjectUrls]); useEffect(() => { -if (user?.role === "CUSTOMER" || user?.role === "ADMIN") { + if (user?.role === "CUSTOMER" || user?.role === "ADMIN") { loadPets(); } }, [user, loadPets]); + useEffect(() => { + let objectUrl = null; + + if (user?.avatarUrl && token) { + fetch(`${API_BASE}${user.avatarUrl}`, { + headers: { Authorization: `Bearer ${token}` }, + }) + .then((res) => (res.ok ? res.blob() : null)) + .then((blob) => { + if (blob) { + objectUrl = URL.createObjectURL(blob); + setAvatarObjectUrl(objectUrl); + } else { + setAvatarObjectUrl(null); + } + }) + .catch(() => setAvatarObjectUrl(null)); + } else { + setAvatarObjectUrl(null); + } + + return () => { + if (objectUrl) URL.revokeObjectURL(objectUrl); + }; + }, [user?.avatarUrl, token]); + function handleLogout() { logout(); router.push("/"); @@ -120,10 +159,26 @@ if (user?.role === "CUSTOMER" || user?.role === "ADMIN") { async function handleProfileSubmit(e) { e.preventDefault(); - setProfileSubmitting(true); setProfileError(null); + + if (profileForm.password && profileForm.password !== profileForm.confirmPassword) { + setProfileError("Passwords do not match."); + return; + } + + setProfileSubmitting(true); setProfileSuccess(null); + const payload = { + firstName: profileForm.firstName, + lastName: profileForm.lastName, + email: profileForm.email, + phone: profileForm.phone, + }; + if (profileForm.password) { + payload.password = profileForm.password; + } + try { const res = await fetch(`${API_BASE}/api/v1/auth/me`, { method: "PUT", @@ -131,7 +186,7 @@ if (user?.role === "CUSTOMER" || user?.role === "ADMIN") { "Content-Type": "application/json", Authorization: `Bearer ${token}`, }, - body: JSON.stringify(profileForm), + body: JSON.stringify(payload), }); const data = await res.json().catch(() => null); @@ -140,6 +195,7 @@ if (user?.role === "CUSTOMER" || user?.role === "ADMIN") { } await refreshUser(); + setProfileForm((prev) => ({ ...prev, password: "", confirmPassword: "" })); setProfileSuccess("Profile updated successfully."); } @@ -222,6 +278,7 @@ if (user?.role === "CUSTOMER" || user?.role === "ADMIN") { setPetName(""); setSpecies(""); setBreed(""); + setPetAge("1"); setPetError(null); setShowForm(true); } @@ -231,6 +288,7 @@ if (user?.role === "CUSTOMER" || user?.role === "ADMIN") { setPetName(pet.petName); setSpecies(pet.species); setBreed(pet.breed || ""); + setPetAge(pet.petAge != null ? String(pet.petAge) : "1"); setPetError(null); setShowForm(true); } @@ -257,7 +315,7 @@ if (user?.role === "CUSTOMER" || user?.role === "ADMIN") { "Content-Type": "application/json", Authorization: `Bearer ${token}`, }, - body: JSON.stringify({ petName, species, breed: breed || null }), + body: JSON.stringify({ petName, species, breed: breed || null, petAge: Number(petAge) }), }); if (!res.ok) { @@ -284,14 +342,19 @@ if (user?.role === "CUSTOMER" || user?.role === "ADMIN") { } try { - await fetch(`${API_BASE}/api/v1/my-pets/${id}`, { + const res = await fetch(`${API_BASE}/api/v1/my-pets/${id}`, { method: "DELETE", headers: { Authorization: `Bearer ${token}` }, }); + if (!res.ok) { + const data = await res.json().catch(() => null); + throw new Error(data?.message || `Failed to remove pet (${res.status})`); + } loadPets(); } - - catch { + + catch (err) { + alert(err.message); } } @@ -324,12 +387,14 @@ if (user?.role === "CUSTOMER" || user?.role === "ADMIN") { return

Loading…

; } + const displayName = [user.firstName, user.lastName].filter(Boolean).join(" ") || user.username; + const fields = [ - {label: "Full Name", value: user.fullName}, + {label: "First Name", value: user.firstName || "N/A"}, + {label: "Last Name", value: user.lastName || "N/A"}, {label: "Username", value: user.username}, {label: "Email", value: user.email}, - {label: "Phone", value: user.phone || "—"}, - {label: "Role", value: user.role}, + {label: "Phone", value: user.phone || "N/A"}, ...(user.storeName ? [{ label: "Store", value: user.storeName }] : []), ]; @@ -337,14 +402,14 @@ if (user?.role === "CUSTOMER" || user?.role === "ADMIN") {
- {user.avatarUrl ? ( - {user.fullName + {avatarObjectUrl ? ( + {displayName} ) : ( - (user.fullName || user.username).charAt(0).toUpperCase() + displayName.charAt(0).toUpperCase() )}
-

{user.fullName || user.username}

+

{displayName}

{user.role}
@@ -361,13 +426,23 @@ if (user?.role === "CUSTOMER" || user?.role === "ADMIN") { {profileError &&
{profileError}
} {profileSuccess &&
{profileSuccess}
} + + +
- ))} + + +
+ + ))} )} diff --git a/web/app/register/page.js b/web/app/register/page.js index 17f49672..3b3ba59b 100644 --- a/web/app/register/page.js +++ b/web/app/register/page.js @@ -22,12 +22,14 @@ function RegisterPage() { const searchParams = useSearchParams(); const [form, setForm] = useState({ - fullName: "", + firstName: "", + lastName: "", username: "", email: "", phone: "", password: "", - confirmPassword: "",}); + confirmPassword: "", + }); const [error, setError] = useState(""); const [loading, setLoading] = useState(false); @@ -47,7 +49,9 @@ function RegisterPage() { setLoading(true); try { - await register({fullName: form.fullName, + await register({ + firstName: form.firstName, + lastName: form.lastName, username: form.username, email: form.email, phone: form.phone, @@ -74,12 +78,24 @@ function RegisterPage() {
+ +