Match Postman API contract for desktop app

- Change bulk delete from POST /bulk-delete to DELETE with body
- Add search parameter support (q) to all list endpoints
- Add customer registration endpoint
- Add stores listing endpoint
- Add analytics dashboard endpoint (admin only)
- Update appointment availability to include storeId
- Add CUSTOMER role to User entity
- Implement search across all repositories
This commit is contained in:
2026-03-05 08:44:49 -07:00
parent bdfc592821
commit d7fb057e64
37 changed files with 650 additions and 46 deletions

View File

@@ -22,8 +22,10 @@ public class AdoptionController {
}
@GetMapping
public ResponseEntity<Page<AdoptionResponse>> getAllAdoptions(Pageable pageable) {
return ResponseEntity.ok(adoptionService.getAllAdoptions(pageable));
public ResponseEntity<Page<AdoptionResponse>> getAllAdoptions(
@RequestParam(required = false) String q,
Pageable pageable) {
return ResponseEntity.ok(adoptionService.getAllAdoptions(q, pageable));
}
@GetMapping("/{id}")
@@ -49,7 +51,7 @@ public class AdoptionController {
return ResponseEntity.noContent().build();
}
@PostMapping("/bulk-delete")
@DeleteMapping
public ResponseEntity<Void> bulkDeleteAdoptions(@Valid @RequestBody BulkDeleteRequest request) {
adoptionService.bulkDeleteAdoptions(request);
return ResponseEntity.noContent().build();

View File

@@ -0,0 +1,26 @@
package com.petshop.backend.controller;
import com.petshop.backend.dto.analytics.DashboardResponse;
import com.petshop.backend.service.AnalyticsService;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/v1/analytics")
@PreAuthorize("hasRole('ADMIN')")
public class AnalyticsController {
private final AnalyticsService analyticsService;
public AnalyticsController(AnalyticsService analyticsService) {
this.analyticsService = analyticsService;
}
@GetMapping("/dashboard")
public ResponseEntity<DashboardResponse> getDashboard(
@RequestParam(defaultValue = "30") int days,
@RequestParam(defaultValue = "10") int top) {
return ResponseEntity.ok(analyticsService.getDashboardData(days, top));
}
}

View File

@@ -11,6 +11,9 @@ import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.time.LocalDate;
import java.util.List;
@RestController
@RequestMapping("/api/v1/appointments")
public class AppointmentController {
@@ -22,8 +25,10 @@ public class AppointmentController {
}
@GetMapping
public ResponseEntity<Page<AppointmentResponse>> getAllAppointments(Pageable pageable) {
return ResponseEntity.ok(appointmentService.getAllAppointments(pageable));
public ResponseEntity<Page<AppointmentResponse>> getAllAppointments(
@RequestParam(required = false) String q,
Pageable pageable) {
return ResponseEntity.ok(appointmentService.getAllAppointments(q, pageable));
}
@GetMapping("/{id}")
@@ -49,9 +54,18 @@ public class AppointmentController {
return ResponseEntity.noContent().build();
}
@PostMapping("/bulk-delete")
@DeleteMapping
public ResponseEntity<Void> bulkDeleteAppointments(@Valid @RequestBody BulkDeleteRequest request) {
appointmentService.bulkDeleteAppointments(request);
return ResponseEntity.noContent().build();
}
@GetMapping("/availability")
public ResponseEntity<List<String>> checkAvailability(
@RequestParam Long storeId,
@RequestParam Long serviceId,
@RequestParam String date) {
LocalDate appointmentDate = LocalDate.parse(date);
return ResponseEntity.ok(appointmentService.checkAvailability(storeId, serviceId, appointmentDate));
}
}

View File

@@ -2,6 +2,7 @@ package com.petshop.backend.controller;
import com.petshop.backend.dto.auth.LoginRequest;
import com.petshop.backend.dto.auth.LoginResponse;
import com.petshop.backend.dto.auth.RegisterRequest;
import com.petshop.backend.dto.auth.UserInfoResponse;
import com.petshop.backend.entity.User;
import com.petshop.backend.repository.UserRepository;
@@ -16,6 +17,7 @@ import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
@@ -28,11 +30,47 @@ public class AuthController {
private final AuthenticationManager authenticationManager;
private final UserRepository userRepository;
private final JwtUtil jwtUtil;
private final PasswordEncoder passwordEncoder;
public AuthController(AuthenticationManager authenticationManager, UserRepository userRepository, JwtUtil jwtUtil) {
public AuthController(AuthenticationManager authenticationManager, UserRepository userRepository, JwtUtil jwtUtil, PasswordEncoder passwordEncoder) {
this.authenticationManager = authenticationManager;
this.userRepository = userRepository;
this.jwtUtil = jwtUtil;
this.passwordEncoder = passwordEncoder;
}
@PostMapping("/register")
public ResponseEntity<?> register(@Valid @RequestBody RegisterRequest request) {
if (userRepository.findByUsername(request.getEmail()).isPresent()) {
Map<String, String> error = new HashMap<>();
error.put("message", "Email already registered");
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(error);
}
User user = new User();
user.setUsername(request.getEmail());
user.setEmail(request.getEmail());
user.setPassword(passwordEncoder.encode(request.getPassword()));
user.setFullName(request.getFirstName() + " " + request.getLastName());
user.setRole(User.Role.CUSTOMER);
user.setActive(true);
user = userRepository.save(user);
UserDetails userDetails = new org.springframework.security.core.userdetails.User(
user.getUsername(),
user.getPassword(),
java.util.Collections.emptyList()
);
String token = jwtUtil.generateToken(userDetails);
return ResponseEntity.status(HttpStatus.CREATED).body(new LoginResponse(
token,
user.getUsername(),
user.getFullName(),
user.getRole().name()
));
}
@PostMapping("/login")

View File

@@ -51,7 +51,7 @@ public class CategoryController {
return ResponseEntity.noContent().build();
}
@PostMapping("/bulk-delete")
@DeleteMapping
public ResponseEntity<Void> bulkDeleteCategories(@Valid @RequestBody BulkDeleteRequest request) {
categoryService.bulkDeleteCategories(request);
return ResponseEntity.noContent().build();

View File

@@ -24,8 +24,10 @@ public class InventoryController {
}
@GetMapping
public ResponseEntity<Page<InventoryResponse>> getAllInventory(Pageable pageable) {
return ResponseEntity.ok(inventoryService.getAllInventory(pageable));
public ResponseEntity<Page<InventoryResponse>> getAllInventory(
@RequestParam(required = false) String q,
Pageable pageable) {
return ResponseEntity.ok(inventoryService.getAllInventory(q, pageable));
}
@GetMapping("/{id}")
@@ -51,7 +53,7 @@ public class InventoryController {
return ResponseEntity.noContent().build();
}
@PostMapping("/bulk-delete")
@DeleteMapping
public ResponseEntity<Void> bulkDeleteInventory(@Valid @RequestBody BulkDeleteRequest request) {
inventoryService.bulkDeleteInventory(request);
return ResponseEntity.noContent().build();

View File

@@ -51,7 +51,7 @@ public class PetController {
return ResponseEntity.noContent().build();
}
@PostMapping("/bulk-delete")
@DeleteMapping
public ResponseEntity<Void> bulkDeletePets(@Valid @RequestBody BulkDeleteRequest request) {
petService.bulkDeletePets(request);
return ResponseEntity.noContent().build();

View File

@@ -51,7 +51,7 @@ public class ProductController {
return ResponseEntity.noContent().build();
}
@PostMapping("/bulk-delete")
@DeleteMapping
public ResponseEntity<Void> bulkDeleteProducts(@Valid @RequestBody BulkDeleteRequest request) {
productService.bulkDeleteProducts(request);
return ResponseEntity.noContent().build();

View File

@@ -24,8 +24,10 @@ public class ProductSupplierController {
}
@GetMapping
public ResponseEntity<Page<ProductSupplierResponse>> getAllProductSuppliers(Pageable pageable) {
return ResponseEntity.ok(productSupplierService.getAllProductSuppliers(pageable));
public ResponseEntity<Page<ProductSupplierResponse>> getAllProductSuppliers(
@RequestParam(required = false) String q,
Pageable pageable) {
return ResponseEntity.ok(productSupplierService.getAllProductSuppliers(q, pageable));
}
@GetMapping("/{productId}/{supplierId}")
@@ -56,7 +58,7 @@ public class ProductSupplierController {
return ResponseEntity.noContent().build();
}
@PostMapping("/bulk-delete")
@DeleteMapping
public ResponseEntity<Void> bulkDeleteProductSuppliers(@Valid @RequestBody BulkDeleteProductSupplierRequest request) {
productSupplierService.bulkDeleteProductSuppliers(request);
return ResponseEntity.noContent().build();

View File

@@ -20,8 +20,10 @@ public class PurchaseOrderController {
}
@GetMapping
public ResponseEntity<Page<PurchaseOrderResponse>> getAllPurchaseOrders(Pageable pageable) {
return ResponseEntity.ok(purchaseOrderService.getAllPurchaseOrders(pageable));
public ResponseEntity<Page<PurchaseOrderResponse>> getAllPurchaseOrders(
@RequestParam(required = false) String q,
Pageable pageable) {
return ResponseEntity.ok(purchaseOrderService.getAllPurchaseOrders(q, pageable));
}
@GetMapping("/{id}")

View File

@@ -21,8 +21,10 @@ public class SaleController {
}
@GetMapping
public ResponseEntity<Page<SaleResponse>> getAllSales(Pageable pageable) {
return ResponseEntity.ok(saleService.getAllSales(pageable));
public ResponseEntity<Page<SaleResponse>> getAllSales(
@RequestParam(required = false) String q,
Pageable pageable) {
return ResponseEntity.ok(saleService.getAllSales(q, pageable));
}
@GetMapping("/{id}")

View File

@@ -51,7 +51,7 @@ public class ServiceController {
return ResponseEntity.noContent().build();
}
@PostMapping("/bulk-delete")
@DeleteMapping
public ResponseEntity<Void> bulkDeleteServices(@Valid @RequestBody BulkDeleteRequest request) {
serviceService.bulkDeleteServices(request);
return ResponseEntity.noContent().build();

View File

@@ -0,0 +1,26 @@
package com.petshop.backend.controller;
import com.petshop.backend.dto.store.StoreResponse;
import com.petshop.backend.service.StoreService;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/v1/stores")
public class StoreController {
private final StoreService storeService;
public StoreController(StoreService storeService) {
this.storeService = storeService;
}
@GetMapping
public ResponseEntity<Page<StoreResponse>> getAllStores(
@RequestParam(required = false) String q,
Pageable pageable) {
return ResponseEntity.ok(storeService.getAllStores(q, pageable));
}
}

View File

@@ -53,7 +53,7 @@ public class SupplierController {
return ResponseEntity.noContent().build();
}
@PostMapping("/bulk-delete")
@DeleteMapping
public ResponseEntity<Void> bulkDeleteSuppliers(@Valid @RequestBody BulkDeleteRequest request) {
supplierService.bulkDeleteSuppliers(request);
return ResponseEntity.noContent().build();

View File

@@ -24,8 +24,10 @@ public class UserController {
}
@GetMapping
public ResponseEntity<Page<UserResponse>> getAllUsers(Pageable pageable) {
return ResponseEntity.ok(userService.getAllUsers(pageable));
public ResponseEntity<Page<UserResponse>> getAllUsers(
@RequestParam(required = false) String q,
Pageable pageable) {
return ResponseEntity.ok(userService.getAllUsers(q, pageable));
}
@GetMapping("/{id}")
@@ -51,7 +53,7 @@ public class UserController {
return ResponseEntity.noContent().build();
}
@PostMapping("/bulk-delete")
@DeleteMapping
public ResponseEntity<Void> bulkDeleteUsers(@Valid @RequestBody BulkDeleteRequest request) {
userService.bulkDeleteUsers(request);
return ResponseEntity.noContent().build();

View File

@@ -74,9 +74,8 @@ public class DashboardResponse {
", dailySales=" + dailySales +
'}';
}
}
class SalesSummary {
public static class SalesSummary {
private BigDecimal totalRevenue;
private Long totalSales;
private BigDecimal totalRefunds;
@@ -148,7 +147,7 @@ class SalesSummary {
}
}
class InventorySummary {
public static class InventorySummary {
private Long totalProducts;
private Long lowStockProducts;
private Long outOfStockProducts;
@@ -209,7 +208,7 @@ class InventorySummary {
}
}
class TopProduct {
public static class TopProduct {
private Long productId;
private String productName;
private Long quantitySold;
@@ -281,7 +280,7 @@ class TopProduct {
}
}
class DailySales {
public static class DailySales {
private String date;
private BigDecimal revenue;
private Long salesCount;
@@ -341,3 +340,4 @@ class DailySales {
'}';
}
}
}

View File

@@ -0,0 +1,91 @@
package com.petshop.backend.dto.auth;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import java.util.Objects;
public class RegisterRequest {
@NotBlank(message = "First name is required")
private String firstName;
@NotBlank(message = "Last name is required")
private String lastName;
@NotBlank(message = "Email is required")
@Email(message = "Email must be valid")
private String email;
@NotBlank(message = "Phone is required")
private String phone;
@NotBlank(message = "Password is required")
private String password;
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;
}
public void setEmail(String email) {
this.email = email;
}
public String getPhone() {
return phone;
}
public void setPhone(String phone) {
this.phone = phone;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
RegisterRequest that = (RegisterRequest) o;
return Objects.equals(firstName, that.firstName) &&
Objects.equals(lastName, that.lastName) &&
Objects.equals(email, that.email) &&
Objects.equals(phone, that.phone) &&
Objects.equals(password, that.password);
}
@Override
public int hashCode() {
return Objects.hash(firstName, lastName, email, phone, password);
}
@Override
public String toString() {
return "RegisterRequest{" +
"firstName='" + firstName + '\'' +
", lastName='" + lastName + '\'' +
", email='" + email + '\'' +
", phone='" + phone + '\'' +
", password='[PROTECTED]'" +
'}';
}
}

View File

@@ -0,0 +1,76 @@
package com.petshop.backend.dto.store;
import java.time.LocalDateTime;
import java.util.Objects;
public class StoreResponse {
private Long id;
private String storeName;
private String storeLocation;
private LocalDateTime createdAt;
public StoreResponse() {
}
public StoreResponse(Long id, String storeName, String storeLocation, LocalDateTime createdAt) {
this.id = id;
this.storeName = storeName;
this.storeLocation = storeLocation;
this.createdAt = createdAt;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getStoreName() {
return storeName;
}
public void setStoreName(String storeName) {
this.storeName = storeName;
}
public String getStoreLocation() {
return storeLocation;
}
public void setStoreLocation(String storeLocation) {
this.storeLocation = storeLocation;
}
public LocalDateTime getCreatedAt() {
return createdAt;
}
public void setCreatedAt(LocalDateTime createdAt) {
this.createdAt = createdAt;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
StoreResponse that = (StoreResponse) o;
return Objects.equals(id, that.id) && Objects.equals(storeName, that.storeName) && Objects.equals(storeLocation, that.storeLocation) && Objects.equals(createdAt, that.createdAt);
}
@Override
public int hashCode() {
return Objects.hash(id, storeName, storeLocation, createdAt);
}
@Override
public String toString() {
return "StoreResponse{" +
"id=" + id +
", storeName='" + storeName + '\'' +
", storeLocation='" + storeLocation + '\'' +
", createdAt=" + createdAt +
'}';
}
}

View File

@@ -43,7 +43,7 @@ public class User {
private LocalDateTime updatedAt;
public enum Role {
STAFF, ADMIN
STAFF, ADMIN, CUSTOMER
}
public User() {

View File

@@ -1,9 +1,18 @@
package com.petshop.backend.repository;
import com.petshop.backend.entity.Adoption;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
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;
@Repository
public interface AdoptionRepository extends JpaRepository<Adoption, Long> {
@Query("SELECT a FROM Adoption a WHERE " +
"LOWER(a.customer.customerName) LIKE LOWER(CONCAT('%', :q, '%')) OR " +
"LOWER(a.pet.petName) LIKE LOWER(CONCAT('%', :q, '%'))")
Page<Adoption> searchAdoptions(@Param("q") String query, Pageable pageable);
}

View File

@@ -1,6 +1,8 @@
package com.petshop.backend.repository;
import com.petshop.backend.entity.Appointment;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
@@ -15,4 +17,13 @@ public interface AppointmentRepository extends JpaRepository<Appointment, Long>
@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);
@Query("SELECT a FROM Appointment a WHERE a.service.id = :serviceId AND a.appointmentDate = :date AND a.status != 'Cancelled'")
List<Appointment> findByServiceAndDate(@Param("serviceId") Long serviceId, @Param("date") LocalDate date);
@Query("SELECT DISTINCT a FROM Appointment a LEFT JOIN a.pets p WHERE " +
"LOWER(a.customer.customerName) LIKE LOWER(CONCAT('%', :q, '%')) OR " +
"LOWER(a.service.serviceName) LIKE LOWER(CONCAT('%', :q, '%')) OR " +
"LOWER(p.petName) LIKE LOWER(CONCAT('%', :q, '%'))")
Page<Appointment> searchAppointments(@Param("q") String query, Pageable pageable);
}

View File

@@ -1,6 +1,8 @@
package com.petshop.backend.repository;
import com.petshop.backend.entity.Inventory;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
@@ -13,4 +15,9 @@ public interface InventoryRepository extends JpaRepository<Inventory, Long> {
@Query("SELECT i FROM Inventory i WHERE i.product.id = :productId AND i.store.id = :storeId")
Optional<Inventory> findByProductIdAndStoreId(@Param("productId") Long productId, @Param("storeId") Long storeId);
@Query("SELECT i FROM Inventory i WHERE " +
"LOWER(i.product.productName) LIKE LOWER(CONCAT('%', :q, '%')) OR " +
"LOWER(i.store.storeName) LIKE LOWER(CONCAT('%', :q, '%'))")
Page<Inventory> searchInventory(@Param("q") String query, Pageable pageable);
}

View File

@@ -1,9 +1,18 @@
package com.petshop.backend.repository;
import com.petshop.backend.entity.ProductSupplier;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
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;
@Repository
public interface ProductSupplierRepository extends JpaRepository<ProductSupplier, ProductSupplier.ProductSupplierId> {
@Query("SELECT ps FROM ProductSupplier ps WHERE " +
"LOWER(ps.product.productName) LIKE LOWER(CONCAT('%', :q, '%')) OR " +
"LOWER(ps.supplier.supplierName) LIKE LOWER(CONCAT('%', :q, '%'))")
Page<ProductSupplier> searchProductSuppliers(@Param("q") String query, Pageable pageable);
}

View File

@@ -1,9 +1,18 @@
package com.petshop.backend.repository;
import com.petshop.backend.entity.PurchaseOrder;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
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;
@Repository
public interface PurchaseOrderRepository extends JpaRepository<PurchaseOrder, Long> {
@Query("SELECT po FROM PurchaseOrder po WHERE " +
"LOWER(po.supplier.supplierName) LIKE LOWER(CONCAT('%', :q, '%')) OR " +
"LOWER(po.notes) LIKE LOWER(CONCAT('%', :q, '%'))")
Page<PurchaseOrder> searchPurchaseOrders(@Param("q") String query, Pageable pageable);
}

View File

@@ -1,9 +1,19 @@
package com.petshop.backend.repository;
import com.petshop.backend.entity.Sale;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
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;
@Repository
public interface SaleRepository extends JpaRepository<Sale, Long> {
@Query("SELECT s FROM Sale s WHERE " +
"LOWER(s.customer.customerName) LIKE LOWER(CONCAT('%', :q, '%')) OR " +
"LOWER(s.employee.fullName) LIKE LOWER(CONCAT('%', :q, '%')) OR " +
"LOWER(s.store.storeName) LIKE LOWER(CONCAT('%', :q, '%'))")
Page<Sale> searchSales(@Param("q") String query, Pageable pageable);
}

View File

@@ -1,9 +1,18 @@
package com.petshop.backend.repository;
import com.petshop.backend.entity.Store;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
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;
@Repository
public interface StoreRepository extends JpaRepository<Store, Long> {
@Query("SELECT s FROM Store s WHERE " +
"LOWER(s.storeName) LIKE LOWER(CONCAT('%', :q, '%')) OR " +
"LOWER(s.storeLocation) LIKE LOWER(CONCAT('%', :q, '%'))")
Page<Store> searchStores(@Param("q") String query, Pageable pageable);
}

View File

@@ -1,7 +1,11 @@
package com.petshop.backend.repository;
import com.petshop.backend.entity.User;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
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 java.util.Optional;
@@ -10,4 +14,10 @@ import java.util.Optional;
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByUsername(String username);
boolean existsByUsername(String username);
@Query("SELECT u FROM User u WHERE " +
"LOWER(u.username) LIKE LOWER(CONCAT('%', :q, '%')) OR " +
"LOWER(u.fullName) LIKE LOWER(CONCAT('%', :q, '%')) OR " +
"LOWER(u.email) LIKE LOWER(CONCAT('%', :q, '%'))")
Page<User> searchUsers(@Param("q") String query, Pageable pageable);
}

View File

@@ -36,7 +36,7 @@ public class SecurityConfig {
http
.csrf(AbstractHttpConfigurer::disable)
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/v1/auth/login").permitAll()
.requestMatchers("/api/v1/auth/login", "/api/v1/auth/register").permitAll()
.requestMatchers("/swagger-ui/**", "/v3/api-docs/**", "/swagger-ui.html").permitAll()
.requestMatchers(HttpMethod.GET, "/api/v1/dropdowns/suppliers").hasRole("ADMIN")
.requestMatchers("/api/v1/inventory/**").hasRole("ADMIN")

View File

@@ -28,8 +28,14 @@ public class AdoptionService {
this.customerRepository = customerRepository;
}
public Page<AdoptionResponse> getAllAdoptions(Pageable pageable) {
return adoptionRepository.findAll(pageable).map(this::mapToResponse);
public Page<AdoptionResponse> getAllAdoptions(String query, Pageable pageable) {
Page<Adoption> adoptions;
if (query != null && !query.trim().isEmpty()) {
adoptions = adoptionRepository.searchAdoptions(query, pageable);
} else {
adoptions = adoptionRepository.findAll(pageable);
}
return adoptions.map(this::mapToResponse);
}
public AdoptionResponse getAdoptionById(Long id) {

View File

@@ -0,0 +1,141 @@
package com.petshop.backend.service;
import com.petshop.backend.dto.analytics.DashboardResponse;
import com.petshop.backend.entity.Inventory;
import com.petshop.backend.entity.Product;
import com.petshop.backend.entity.Refund;
import com.petshop.backend.entity.Sale;
import com.petshop.backend.repository.InventoryRepository;
import com.petshop.backend.repository.ProductRepository;
import com.petshop.backend.repository.RefundRepository;
import com.petshop.backend.repository.SaleRepository;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.stream.Collectors;
@Service
public class AnalyticsService {
private final SaleRepository saleRepository;
private final RefundRepository refundRepository;
private final InventoryRepository inventoryRepository;
private final ProductRepository productRepository;
public AnalyticsService(SaleRepository saleRepository, RefundRepository refundRepository,
InventoryRepository inventoryRepository, ProductRepository productRepository) {
this.saleRepository = saleRepository;
this.refundRepository = refundRepository;
this.inventoryRepository = inventoryRepository;
this.productRepository = productRepository;
}
public DashboardResponse getDashboardData(int days, int top) {
LocalDateTime startDate = LocalDateTime.now().minusDays(days);
List<Sale> sales = saleRepository.findAll().stream()
.filter(sale -> sale.getSaleDate().isAfter(startDate))
.collect(Collectors.toList());
List<Refund> refunds = refundRepository.findAll().stream()
.filter(refund -> refund.getRefundDate().isAfter(startDate))
.collect(Collectors.toList());
DashboardResponse.SalesSummary salesSummary = calculateSalesSummary(sales, refunds);
DashboardResponse.InventorySummary inventorySummary = calculateInventorySummary();
List<DashboardResponse.TopProduct> topProducts = calculateTopProducts(sales, top);
List<DashboardResponse.DailySales> dailySales = calculateDailySales(sales, days);
return new DashboardResponse(salesSummary, inventorySummary, topProducts, dailySales);
}
private DashboardResponse.SalesSummary calculateSalesSummary(List<Sale> sales, List<Refund> refunds) {
BigDecimal totalRevenue = sales.stream()
.map(Sale::getTotal)
.reduce(BigDecimal.ZERO, BigDecimal::add);
Long totalSales = (long) sales.size();
BigDecimal totalRefunds = refunds.stream()
.map(Refund::getRefundAmount)
.reduce(BigDecimal.ZERO, BigDecimal::add);
Long totalRefundCount = (long) refunds.size();
return new DashboardResponse.SalesSummary(totalRevenue, totalSales, totalRefunds, totalRefundCount);
}
private DashboardResponse.InventorySummary calculateInventorySummary() {
List<Inventory> allInventory = inventoryRepository.findAll();
Long totalProducts = productRepository.count();
Long lowStockProducts = allInventory.stream()
.filter(inv -> inv.getQuantity() > 0 && inv.getQuantity() <= inv.getReorderLevel())
.map(inv -> inv.getProduct().getId())
.distinct()
.count();
Long outOfStockProducts = allInventory.stream()
.filter(inv -> inv.getQuantity() == 0)
.map(inv -> inv.getProduct().getId())
.distinct()
.count();
return new DashboardResponse.InventorySummary(totalProducts, lowStockProducts, outOfStockProducts);
}
private List<DashboardResponse.TopProduct> calculateTopProducts(List<Sale> sales, int top) {
Map<Long, DashboardResponse.TopProduct> productSalesMap = new HashMap<>();
for (Sale sale : sales) {
for (var item : sale.getItems()) {
Long productId = item.getProduct().getId();
String productName = item.getProduct().getProductName();
Long quantitySold = Long.valueOf(item.getQuantity());
BigDecimal revenue = item.getSubtotal();
productSalesMap.compute(productId, (key, existing) -> {
if (existing == null) {
return new DashboardResponse.TopProduct(productId, productName, quantitySold, revenue);
} else {
existing.setQuantitySold(existing.getQuantitySold() + quantitySold);
existing.setRevenue(existing.getRevenue().add(revenue));
return existing;
}
});
}
}
return productSalesMap.values().stream()
.sorted((p1, p2) -> p2.getRevenue().compareTo(p1.getRevenue()))
.limit(top)
.collect(Collectors.toList());
}
private List<DashboardResponse.DailySales> calculateDailySales(List<Sale> sales, int days) {
Map<LocalDate, DashboardResponse.DailySales> dailySalesMap = new LinkedHashMap<>();
LocalDate startDate = LocalDate.now().minusDays(days - 1);
for (int i = 0; i < days; i++) {
LocalDate date = startDate.plusDays(i);
String dateStr = date.format(DateTimeFormatter.ISO_LOCAL_DATE);
dailySalesMap.put(date, new DashboardResponse.DailySales(dateStr, BigDecimal.ZERO, 0L));
}
for (Sale sale : sales) {
LocalDate saleDate = sale.getSaleDate().toLocalDate();
if (dailySalesMap.containsKey(saleDate)) {
DashboardResponse.DailySales dailySale = dailySalesMap.get(saleDate);
dailySale.setRevenue(dailySale.getRevenue().add(sale.getTotal()));
dailySale.setSalesCount(dailySale.getSalesCount() + 1);
}
}
return new ArrayList<>(dailySalesMap.values());
}
}

View File

@@ -16,6 +16,9 @@ import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDate;
import java.time.LocalTime;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
@@ -36,8 +39,14 @@ public class AppointmentService {
this.petRepository = petRepository;
}
public Page<AppointmentResponse> getAllAppointments(Pageable pageable) {
return appointmentRepository.findAll(pageable).map(this::mapToResponse);
public Page<AppointmentResponse> getAllAppointments(String query, Pageable pageable) {
Page<Appointment> appointments;
if (query != null && !query.trim().isEmpty()) {
appointments = appointmentRepository.searchAppointments(query, pageable);
} else {
appointments = appointmentRepository.findAll(pageable);
}
return appointments.map(this::mapToResponse);
}
public AppointmentResponse getAppointmentById(Long id) {
@@ -107,6 +116,30 @@ public class AppointmentService {
appointmentRepository.deleteAllById(request.getIds());
}
public List<String> checkAvailability(Long storeId, Long serviceId, LocalDate date) {
com.petshop.backend.entity.Service service = serviceRepository.findById(serviceId)
.orElseThrow(() -> new ResourceNotFoundException("Service not found with id: " + serviceId));
List<Appointment> existingAppointments = appointmentRepository.findByServiceAndDate(serviceId, date);
Set<LocalTime> bookedTimes = existingAppointments.stream()
.map(Appointment::getAppointmentTime)
.collect(Collectors.toSet());
List<String> availableSlots = new ArrayList<>();
LocalTime startTime = LocalTime.of(9, 0);
LocalTime endTime = LocalTime.of(17, 0);
LocalTime currentTime = startTime;
while (currentTime.isBefore(endTime)) {
if (!bookedTimes.contains(currentTime)) {
availableSlots.add(currentTime.toString());
}
currentTime = currentTime.plusMinutes(30);
}
return availableSlots;
}
private Set<Pet> fetchPets(List<Long> petIds) {
Set<Pet> pets = new HashSet<>();
for (Long petId : petIds) {

View File

@@ -30,8 +30,14 @@ public class InventoryService {
this.storeRepository = storeRepository;
}
public Page<InventoryResponse> getAllInventory(Pageable pageable) {
return inventoryRepository.findAll(pageable).map(this::mapToResponse);
public Page<InventoryResponse> getAllInventory(String query, Pageable pageable) {
Page<Inventory> inventory;
if (query != null && !query.trim().isEmpty()) {
inventory = inventoryRepository.searchInventory(query, pageable);
} else {
inventory = inventoryRepository.findAll(pageable);
}
return inventory.map(this::mapToResponse);
}
public InventoryResponse getInventoryById(Long id) {

View File

@@ -28,8 +28,14 @@ public class ProductSupplierService {
this.supplierRepository = supplierRepository;
}
public Page<ProductSupplierResponse> getAllProductSuppliers(Pageable pageable) {
return productSupplierRepository.findAll(pageable).map(this::mapToResponse);
public Page<ProductSupplierResponse> getAllProductSuppliers(String query, Pageable pageable) {
Page<ProductSupplier> productSuppliers;
if (query != null && !query.trim().isEmpty()) {
productSuppliers = productSupplierRepository.searchProductSuppliers(query, pageable);
} else {
productSuppliers = productSupplierRepository.findAll(pageable);
}
return productSuppliers.map(this::mapToResponse);
}
public ProductSupplierResponse getProductSupplierById(Long productId, Long supplierId) {

View File

@@ -22,8 +22,14 @@ public class PurchaseOrderService {
this.purchaseOrderRepository = purchaseOrderRepository;
}
public Page<PurchaseOrderResponse> getAllPurchaseOrders(Pageable pageable) {
return purchaseOrderRepository.findAll(pageable).map(this::mapToResponse);
public Page<PurchaseOrderResponse> getAllPurchaseOrders(String query, Pageable pageable) {
Page<PurchaseOrder> purchaseOrders;
if (query != null && !query.trim().isEmpty()) {
purchaseOrders = purchaseOrderRepository.searchPurchaseOrders(query, pageable);
} else {
purchaseOrders = purchaseOrderRepository.findAll(pageable);
}
return purchaseOrders.map(this::mapToResponse);
}
public PurchaseOrderResponse getPurchaseOrderById(Long id) {

View File

@@ -36,8 +36,14 @@ public class SaleService {
this.userRepository = userRepository;
}
public Page<SaleResponse> getAllSales(Pageable pageable) {
return saleRepository.findAll(pageable).map(this::mapToResponse);
public Page<SaleResponse> getAllSales(String query, Pageable pageable) {
Page<Sale> sales;
if (query != null && !query.trim().isEmpty()) {
sales = saleRepository.searchSales(query, pageable);
} else {
sales = saleRepository.findAll(pageable);
}
return sales.map(this::mapToResponse);
}
public SaleResponse getSaleById(Long id) {

View File

@@ -0,0 +1,37 @@
package com.petshop.backend.service;
import com.petshop.backend.dto.store.StoreResponse;
import com.petshop.backend.entity.Store;
import com.petshop.backend.repository.StoreRepository;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
@Service
public class StoreService {
private final StoreRepository storeRepository;
public StoreService(StoreRepository storeRepository) {
this.storeRepository = storeRepository;
}
public Page<StoreResponse> getAllStores(String query, Pageable pageable) {
Page<Store> stores;
if (query != null && !query.trim().isEmpty()) {
stores = storeRepository.searchStores(query, pageable);
} else {
stores = storeRepository.findAll(pageable);
}
return stores.map(this::mapToResponse);
}
private StoreResponse mapToResponse(Store store) {
return new StoreResponse(
store.getId(),
store.getStoreName(),
store.getStoreLocation(),
store.getCreatedAt()
);
}
}

View File

@@ -23,8 +23,14 @@ public class UserService {
this.passwordEncoder = passwordEncoder;
}
public Page<UserResponse> getAllUsers(Pageable pageable) {
return userRepository.findAll(pageable).map(this::mapToResponse);
public Page<UserResponse> getAllUsers(String query, Pageable pageable) {
Page<User> users;
if (query != null && !query.trim().isEmpty()) {
users = userRepository.searchUsers(query, pageable);
} else {
users = userRepository.findAll(pageable);
}
return users.map(this::mapToResponse);
}
public UserResponse getUserById(Long id) {