mirror of
https://gitea.gofwd.group/Forward_Group/ballistic-builder-spring.git
synced 2026-01-20 16:51:03 -05:00
new admin endpoints and cleaned up product dto for faster response
This commit is contained in:
@@ -70,7 +70,7 @@ public class SecurityConfig {
|
|||||||
public CorsConfigurationSource corsConfigurationSource() {
|
public CorsConfigurationSource corsConfigurationSource() {
|
||||||
CorsConfiguration cfg = new CorsConfiguration();
|
CorsConfiguration cfg = new CorsConfiguration();
|
||||||
cfg.setAllowedOrigins(List.of("http://localhost:3000"));
|
cfg.setAllowedOrigins(List.of("http://localhost:3000"));
|
||||||
cfg.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS"));
|
cfg.setAllowedMethods(List.of("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"));
|
||||||
cfg.setAllowedHeaders(List.of("Authorization", "Content-Type"));
|
cfg.setAllowedHeaders(List.of("Authorization", "Content-Type"));
|
||||||
cfg.setExposedHeaders(List.of("Authorization"));
|
cfg.setExposedHeaders(List.of("Authorization"));
|
||||||
cfg.setAllowCredentials(true);
|
cfg.setAllowCredentials(true);
|
||||||
|
|||||||
@@ -6,6 +6,9 @@ import group.goforward.battlbuilder.web.dto.ProductSummaryDto;
|
|||||||
import org.springframework.cache.annotation.Cacheable;
|
import org.springframework.cache.annotation.Cacheable;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
import org.springframework.data.domain.Page;
|
||||||
|
import org.springframework.data.domain.Pageable;
|
||||||
|
import org.springframework.data.web.PageableDefault;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@@ -23,13 +26,14 @@ public class ProductV1Controller {
|
|||||||
@GetMapping
|
@GetMapping
|
||||||
@Cacheable(
|
@Cacheable(
|
||||||
value = "gunbuilderProductsV1",
|
value = "gunbuilderProductsV1",
|
||||||
key = "#platform + '::' + (#partRoles == null ? 'ALL' : #partRoles.toString())"
|
key = "#platform + '::' + (#partRoles == null ? 'ALL' : #partRoles.toString()) + '::' + #pageable.pageNumber + '::' + #pageable.pageSize"
|
||||||
)
|
)
|
||||||
public List<ProductSummaryDto> getProducts(
|
public Page<ProductSummaryDto> getProducts(
|
||||||
@RequestParam(defaultValue = "AR-15") String platform,
|
@RequestParam(defaultValue = "AR-15") String platform,
|
||||||
@RequestParam(required = false, name = "partRoles") List<String> partRoles
|
@RequestParam(required = false, name = "partRoles") List<String> partRoles,
|
||||||
|
@PageableDefault(size = 50) Pageable pageable
|
||||||
) {
|
) {
|
||||||
return productQueryService.getProducts(platform, partRoles);
|
return productQueryService.getProductsPage(platform, partRoles, pageable);
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/{id}/offers")
|
@GetMapping("/{id}/offers")
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import java.util.Comparator;
|
|||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import group.goforward.battlbuilder.model.PartRoleSource;
|
|
||||||
|
|
||||||
@Entity
|
@Entity
|
||||||
@Table(name = "products")
|
@Table(name = "products")
|
||||||
@@ -130,6 +129,24 @@ public class Product {
|
|||||||
public String getCaliberGroup() { return caliberGroup; }
|
public String getCaliberGroup() { return caliberGroup; }
|
||||||
public void setCaliberGroup(String caliberGroup) { this.caliberGroup = caliberGroup; }
|
public void setCaliberGroup(String caliberGroup) { this.caliberGroup = caliberGroup; }
|
||||||
|
|
||||||
|
/* Admin Management */
|
||||||
|
@Enumerated(EnumType.STRING)
|
||||||
|
@Column(name = "visibility", nullable = false)
|
||||||
|
private ProductVisibility visibility = ProductVisibility.PUBLIC;
|
||||||
|
|
||||||
|
@Column(name = "builder_eligible", nullable = false)
|
||||||
|
private Boolean builderEligible = true;
|
||||||
|
|
||||||
|
@Column(name = "admin_locked", nullable = false)
|
||||||
|
private Boolean adminLocked = false;
|
||||||
|
|
||||||
|
@Enumerated(EnumType.STRING)
|
||||||
|
@Column(name = "status", nullable = false)
|
||||||
|
private ProductStatus status = ProductStatus.ACTIVE;
|
||||||
|
|
||||||
|
@Column(name = "admin_note")
|
||||||
|
private String adminNote;
|
||||||
|
|
||||||
// --- lifecycle hooks ---
|
// --- lifecycle hooks ---
|
||||||
@PrePersist
|
@PrePersist
|
||||||
public void prePersist() {
|
public void prePersist() {
|
||||||
@@ -237,6 +254,22 @@ public class Product {
|
|||||||
public Boolean getPartRoleLocked() { return partRoleLocked; }
|
public Boolean getPartRoleLocked() { return partRoleLocked; }
|
||||||
public void setPartRoleLocked(Boolean partRoleLocked) { this.partRoleLocked = partRoleLocked; }
|
public void setPartRoleLocked(Boolean partRoleLocked) { this.partRoleLocked = partRoleLocked; }
|
||||||
|
|
||||||
|
|
||||||
|
// --- Admin Getters/setters
|
||||||
|
public ProductVisibility getVisibility() { return visibility; }
|
||||||
|
public void setVisibility(ProductVisibility visibility) { this.visibility = visibility; }
|
||||||
|
|
||||||
|
public Boolean getBuilderEligible() { return builderEligible; }
|
||||||
|
public void setBuilderEligible(Boolean builderEligible) { this.builderEligible = builderEligible; }
|
||||||
|
|
||||||
|
public Boolean getAdminLocked() { return adminLocked; }
|
||||||
|
public void setAdminLocked(Boolean adminLocked) { this.adminLocked = adminLocked; }
|
||||||
|
|
||||||
|
public ProductStatus getStatus() { return status; }
|
||||||
|
public void setStatus(ProductStatus status) { this.status = status; }
|
||||||
|
|
||||||
|
public String getAdminNote() { return adminNote; }
|
||||||
|
public void setAdminNote(String adminNote) { this.adminNote = adminNote; }
|
||||||
// --- computed helpers ---
|
// --- computed helpers ---
|
||||||
|
|
||||||
public BigDecimal getBestOfferPrice() {
|
public BigDecimal getBestOfferPrice() {
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package group.goforward.battlbuilder.model;
|
||||||
|
|
||||||
|
public enum ProductStatus {
|
||||||
|
ACTIVE,
|
||||||
|
DISABLED,
|
||||||
|
ARCHIVED
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
package group.goforward.battlbuilder.model;
|
||||||
|
|
||||||
|
public enum ProductVisibility {
|
||||||
|
PUBLIC,
|
||||||
|
HIDDEN
|
||||||
|
}
|
||||||
@@ -10,12 +10,16 @@ import org.springframework.data.jpa.repository.JpaRepository;
|
|||||||
import org.springframework.data.jpa.repository.Modifying;
|
import org.springframework.data.jpa.repository.Modifying;
|
||||||
import org.springframework.data.jpa.repository.Query;
|
import org.springframework.data.jpa.repository.Query;
|
||||||
import org.springframework.data.repository.query.Param;
|
import org.springframework.data.repository.query.Param;
|
||||||
|
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
|
||||||
|
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
public interface ProductRepository extends JpaRepository<Product, Integer> {
|
public interface ProductRepository
|
||||||
|
extends JpaRepository<Product, Integer>,
|
||||||
|
JpaSpecificationExecutor<Product> {
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -238,6 +242,20 @@ where po.product_id = p.id
|
|||||||
List<Product> findByMerchantIdAndRawCategoryKey(@Param("merchantId") Integer merchantId,
|
List<Product> findByMerchantIdAndRawCategoryKey(@Param("merchantId") Integer merchantId,
|
||||||
@Param("rawCategoryKey") String rawCategoryKey);
|
@Param("rawCategoryKey") String rawCategoryKey);
|
||||||
|
|
||||||
|
|
||||||
|
@Query("SELECT p FROM Product p JOIN FETCH p.brand b WHERE p.deletedAt IS NULL")
|
||||||
|
Page<Product> findAllWithBrand(Pageable pageable);
|
||||||
|
|
||||||
|
@Query("SELECT p FROM Product p JOIN FETCH p.brand b WHERE p.platform = :platform AND p.deletedAt IS NULL")
|
||||||
|
Page<Product> findByPlatformWithBrand(String platform, Pageable pageable);
|
||||||
|
|
||||||
|
@Query("SELECT p FROM Product p JOIN FETCH p.brand b WHERE p.partRole IN :roles AND p.deletedAt IS NULL")
|
||||||
|
Page<Product> findByPartRoleInWithBrand(List<String> roles, Pageable pageable);
|
||||||
|
|
||||||
|
@Query("SELECT p FROM Product p JOIN FETCH p.brand b WHERE p.platform = :platform AND p.partRole IN :roles AND p.deletedAt IS NULL")
|
||||||
|
Page<Product> findByPlatformAndPartRoleInWithBrand(String platform, List<String> roles, Pageable pageable);
|
||||||
|
|
||||||
|
|
||||||
// -------------------------------------------------
|
// -------------------------------------------------
|
||||||
// Admin import-status dashboard (summary)
|
// Admin import-status dashboard (summary)
|
||||||
// -------------------------------------------------
|
// -------------------------------------------------
|
||||||
|
|||||||
@@ -5,6 +5,9 @@ import group.goforward.battlbuilder.web.dto.ProductSummaryDto;
|
|||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.springframework.data.domain.Page;
|
||||||
|
import org.springframework.data.domain.Pageable;
|
||||||
|
|
||||||
public interface ProductQueryService {
|
public interface ProductQueryService {
|
||||||
|
|
||||||
List<ProductSummaryDto> getProducts(String platform, List<String> partRoles);
|
List<ProductSummaryDto> getProducts(String platform, List<String> partRoles);
|
||||||
@@ -12,4 +15,6 @@ public interface ProductQueryService {
|
|||||||
List<ProductOfferDto> getOffersForProduct(Integer productId);
|
List<ProductOfferDto> getOffersForProduct(Integer productId);
|
||||||
|
|
||||||
ProductSummaryDto getProductById(Integer productId);
|
ProductSummaryDto getProductById(Integer productId);
|
||||||
|
|
||||||
|
Page<ProductSummaryDto> getProductsPage(String platform, List<String> partRoles, Pageable pageable);
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package group.goforward.battlbuilder.services.admin;
|
||||||
|
|
||||||
|
import group.goforward.battlbuilder.web.dto.admin.AdminProductSearchRequest;
|
||||||
|
import group.goforward.battlbuilder.web.dto.admin.ProductAdminRowDto;
|
||||||
|
import group.goforward.battlbuilder.web.dto.admin.ProductBulkUpdateRequest;
|
||||||
|
|
||||||
|
import org.springframework.data.domain.Page;
|
||||||
|
import org.springframework.data.domain.Pageable;
|
||||||
|
|
||||||
|
public interface AdminProductService {
|
||||||
|
|
||||||
|
Page<ProductAdminRowDto> search(
|
||||||
|
AdminProductSearchRequest request,
|
||||||
|
Pageable pageable
|
||||||
|
);
|
||||||
|
|
||||||
|
int bulkUpdate(ProductBulkUpdateRequest request);
|
||||||
|
}
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
package group.goforward.battlbuilder.services.admin.impl;
|
||||||
|
|
||||||
|
import group.goforward.battlbuilder.model.Product;
|
||||||
|
import group.goforward.battlbuilder.repos.ProductRepository;
|
||||||
|
import group.goforward.battlbuilder.services.admin.AdminProductService;
|
||||||
|
import group.goforward.battlbuilder.specs.ProductSpecifications;
|
||||||
|
import group.goforward.battlbuilder.web.dto.admin.AdminProductSearchRequest;
|
||||||
|
import group.goforward.battlbuilder.web.dto.admin.ProductAdminRowDto;
|
||||||
|
import group.goforward.battlbuilder.web.dto.admin.ProductBulkUpdateRequest;
|
||||||
|
|
||||||
|
import org.springframework.data.domain.Page;
|
||||||
|
import org.springframework.data.domain.Pageable;
|
||||||
|
import org.springframework.data.jpa.domain.Specification;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@Transactional
|
||||||
|
public class AdminProductServiceImpl implements AdminProductService {
|
||||||
|
|
||||||
|
private final ProductRepository productRepository;
|
||||||
|
|
||||||
|
public AdminProductServiceImpl(ProductRepository productRepository) {
|
||||||
|
this.productRepository = productRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Page<ProductAdminRowDto> search(
|
||||||
|
AdminProductSearchRequest request,
|
||||||
|
Pageable pageable
|
||||||
|
) {
|
||||||
|
Specification<Product> spec =
|
||||||
|
ProductSpecifications.adminSearch(request);
|
||||||
|
|
||||||
|
return productRepository
|
||||||
|
.findAll(spec, pageable)
|
||||||
|
.map(ProductAdminRowDto::fromEntity);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int bulkUpdate(ProductBulkUpdateRequest request) {
|
||||||
|
var products = productRepository.findAllById(request.getProductIds());
|
||||||
|
|
||||||
|
products.forEach(p -> {
|
||||||
|
if (request.getVisibility() != null) {
|
||||||
|
p.setVisibility(request.getVisibility());
|
||||||
|
}
|
||||||
|
if (request.getStatus() != null) {
|
||||||
|
p.setStatus(request.getStatus());
|
||||||
|
}
|
||||||
|
if (request.getBuilderEligible() != null) {
|
||||||
|
p.setBuilderEligible(request.getBuilderEligible());
|
||||||
|
}
|
||||||
|
if (request.getAdminLocked() != null) {
|
||||||
|
p.setAdminLocked(request.getAdminLocked());
|
||||||
|
}
|
||||||
|
if (request.getAdminNote() != null) {
|
||||||
|
p.setAdminNote(request.getAdminNote());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
productRepository.saveAll(products);
|
||||||
|
return products.size();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,6 +13,15 @@ import org.springframework.stereotype.Service;
|
|||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
import org.springframework.data.domain.Page;
|
||||||
|
import org.springframework.data.domain.PageImpl;
|
||||||
|
import org.springframework.data.domain.Pageable;
|
||||||
|
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
public class ProductQueryServiceImpl implements ProductQueryService {
|
public class ProductQueryServiceImpl implements ProductQueryService {
|
||||||
@@ -69,6 +78,57 @@ public class ProductQueryServiceImpl implements ProductQueryService {
|
|||||||
.toList();
|
.toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Page<ProductSummaryDto> getProductsPage(String platform, List<String> partRoles, Pageable pageable) {
|
||||||
|
final boolean allPlatforms = platform != null && platform.equalsIgnoreCase("ALL");
|
||||||
|
|
||||||
|
Page<Product> productPage;
|
||||||
|
|
||||||
|
if (partRoles == null || partRoles.isEmpty()) {
|
||||||
|
productPage = allPlatforms
|
||||||
|
? productRepository.findAllWithBrand(pageable)
|
||||||
|
: productRepository.findByPlatformWithBrand(platform, pageable);
|
||||||
|
} else {
|
||||||
|
productPage = allPlatforms
|
||||||
|
? productRepository.findByPartRoleInWithBrand(partRoles, pageable)
|
||||||
|
: productRepository.findByPlatformAndPartRoleInWithBrand(platform, partRoles, pageable);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Product> products = productPage.getContent();
|
||||||
|
if (products.isEmpty()) {
|
||||||
|
return Page.empty(pageable);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Integer> productIds = products.stream().map(Product::getId).toList();
|
||||||
|
|
||||||
|
// Only fetch offers for THIS PAGE of products
|
||||||
|
List<ProductOffer> allOffers = productOfferRepository.findByProduct_IdIn(productIds);
|
||||||
|
|
||||||
|
Map<Integer, List<ProductOffer>> offersByProductId = allOffers.stream()
|
||||||
|
.filter(o -> o.getProduct() != null && o.getProduct().getId() != null)
|
||||||
|
.collect(Collectors.groupingBy(o -> o.getProduct().getId()));
|
||||||
|
|
||||||
|
List<ProductSummaryDto> dtos = products.stream()
|
||||||
|
.map(p -> {
|
||||||
|
List<ProductOffer> offersForProduct =
|
||||||
|
offersByProductId.getOrDefault(p.getId(), Collections.emptyList());
|
||||||
|
|
||||||
|
ProductOffer bestOffer = pickBestOffer(offersForProduct);
|
||||||
|
|
||||||
|
BigDecimal price = bestOffer != null ? bestOffer.getEffectivePrice() : null;
|
||||||
|
String buyUrl = bestOffer != null ? bestOffer.getBuyUrl() : null;
|
||||||
|
|
||||||
|
return ProductMapper.toSummary(p, price, buyUrl);
|
||||||
|
})
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
return new PageImpl<>(dtos, pageable, productPage.getTotalElements());
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Product Offers
|
||||||
|
//
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<ProductOfferDto> getOffersForProduct(Integer productId) {
|
public List<ProductOfferDto> getOffersForProduct(Integer productId) {
|
||||||
// ✅ canonical repo method
|
// ✅ canonical repo method
|
||||||
|
|||||||
@@ -0,0 +1,113 @@
|
|||||||
|
package group.goforward.battlbuilder.specs;
|
||||||
|
|
||||||
|
import group.goforward.battlbuilder.model.ImportStatus;
|
||||||
|
import group.goforward.battlbuilder.model.Product;
|
||||||
|
import group.goforward.battlbuilder.model.ProductStatus;
|
||||||
|
import group.goforward.battlbuilder.model.ProductVisibility;
|
||||||
|
import group.goforward.battlbuilder.web.dto.admin.AdminProductSearchRequest;
|
||||||
|
|
||||||
|
import org.springframework.data.jpa.domain.Specification;
|
||||||
|
|
||||||
|
import jakarta.persistence.criteria.Predicate;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public final class ProductSpecifications {
|
||||||
|
|
||||||
|
private ProductSpecifications() {}
|
||||||
|
|
||||||
|
public static Specification<Product> adminSearch(AdminProductSearchRequest req) {
|
||||||
|
return (root, query, cb) -> {
|
||||||
|
List<Predicate> predicates = new ArrayList<>();
|
||||||
|
|
||||||
|
// Always exclude soft-deleted
|
||||||
|
predicates.add(cb.isNull(root.get("deletedAt")));
|
||||||
|
|
||||||
|
if (req == null) {
|
||||||
|
return cb.and(predicates.toArray(new Predicate[0]));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasText(req.getPlatform())) {
|
||||||
|
predicates.add(cb.equal(root.get("platform"), req.getPlatform()));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasText(req.getPartRole())) {
|
||||||
|
// If you normalize partRole elsewhere, keep it consistent here.
|
||||||
|
predicates.add(cb.equal(cb.lower(root.get("partRole")), req.getPartRole().toLowerCase()));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.getImportStatus() != null) {
|
||||||
|
predicates.add(cb.equal(root.get("importStatus"), req.getImportStatus()));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.getVisibility() != null) {
|
||||||
|
predicates.add(cb.equal(root.get("visibility"), req.getVisibility()));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.getStatus() != null) {
|
||||||
|
predicates.add(cb.equal(root.get("status"), req.getStatus()));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.getBuilderEligible() != null) {
|
||||||
|
predicates.add(cb.equal(root.get("builderEligible"), req.getBuilderEligible()));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.getAdminLocked() != null) {
|
||||||
|
predicates.add(cb.equal(root.get("adminLocked"), req.getAdminLocked()));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasText(req.getQ())) {
|
||||||
|
String like = "%" + req.getQ().trim().toLowerCase() + "%";
|
||||||
|
|
||||||
|
Predicate name = cb.like(cb.lower(root.get("name")), like);
|
||||||
|
Predicate slug = cb.like(cb.lower(root.get("slug")), like);
|
||||||
|
|
||||||
|
// mpn/upc are nullable
|
||||||
|
Predicate mpn = cb.like(cb.lower(cb.coalesce(root.get("mpn"), "")), like);
|
||||||
|
Predicate upc = cb.like(cb.lower(cb.coalesce(root.get("upc"), "")), like);
|
||||||
|
|
||||||
|
predicates.add(cb.or(name, slug, mpn, upc));
|
||||||
|
}
|
||||||
|
|
||||||
|
return cb.and(predicates.toArray(new Predicate[0]));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- reusable small specs (optional, but nice to have) ---
|
||||||
|
|
||||||
|
public static Specification<Product> notDeleted() {
|
||||||
|
return (root, query, cb) -> cb.isNull(root.get("deletedAt"));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Specification<Product> platform(String platform) {
|
||||||
|
return (root, query, cb) -> cb.equal(root.get("platform"), platform);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Specification<Product> partRoleEquals(String partRole) {
|
||||||
|
return (root, query, cb) -> cb.equal(cb.lower(root.get("partRole")), partRole.toLowerCase());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Specification<Product> importStatus(ImportStatus status) {
|
||||||
|
return (root, query, cb) -> cb.equal(root.get("importStatus"), status);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Specification<Product> visibility(ProductVisibility visibility) {
|
||||||
|
return (root, query, cb) -> cb.equal(root.get("visibility"), visibility);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Specification<Product> status(ProductStatus status) {
|
||||||
|
return (root, query, cb) -> cb.equal(root.get("status"), status);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Specification<Product> builderEligible(Boolean builderEligible) {
|
||||||
|
return (root, query, cb) -> cb.equal(root.get("builderEligible"), builderEligible);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Specification<Product> adminLocked(Boolean adminLocked) {
|
||||||
|
return (root, query, cb) -> cb.equal(root.get("adminLocked"), adminLocked);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean hasText(String s) {
|
||||||
|
return s != null && !s.trim().isEmpty();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
package group.goforward.battlbuilder.web.admin;
|
||||||
|
|
||||||
|
import group.goforward.battlbuilder.services.admin.AdminProductService;
|
||||||
|
import group.goforward.battlbuilder.web.dto.admin.AdminProductSearchRequest;
|
||||||
|
import group.goforward.battlbuilder.web.dto.admin.ProductBulkUpdateRequest;
|
||||||
|
import group.goforward.battlbuilder.web.dto.admin.ProductAdminRowDto;
|
||||||
|
|
||||||
|
import org.springframework.data.domain.Page;
|
||||||
|
import org.springframework.data.domain.Pageable;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/v1/admin/products")
|
||||||
|
public class AdminProductController {
|
||||||
|
|
||||||
|
private final AdminProductService adminProductService;
|
||||||
|
|
||||||
|
public AdminProductController(AdminProductService adminProductService) {
|
||||||
|
this.adminProductService = adminProductService;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Admin product list (paged + filterable)
|
||||||
|
*/
|
||||||
|
@GetMapping
|
||||||
|
public Page<ProductAdminRowDto> search(
|
||||||
|
AdminProductSearchRequest request,
|
||||||
|
Pageable pageable
|
||||||
|
) {
|
||||||
|
return adminProductService.search(request, pageable);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bulk admin actions (disable, hide, lock, etc.)
|
||||||
|
*/
|
||||||
|
@PatchMapping("/bulk")
|
||||||
|
public Map<String, Object> bulkUpdate(
|
||||||
|
@RequestBody ProductBulkUpdateRequest request
|
||||||
|
) {
|
||||||
|
int updated = adminProductService.bulkUpdate(request);
|
||||||
|
return Map.of("updatedCount", updated);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
package group.goforward.battlbuilder.web.dto.admin;
|
||||||
|
|
||||||
|
import group.goforward.battlbuilder.model.ImportStatus;
|
||||||
|
import group.goforward.battlbuilder.model.ProductStatus;
|
||||||
|
import group.goforward.battlbuilder.model.ProductVisibility;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bound from query params on:
|
||||||
|
* GET /api/v1/admin/products?platform=AR-15&q=mini&visibility=PUBLIC...
|
||||||
|
*/
|
||||||
|
public class AdminProductSearchRequest {
|
||||||
|
|
||||||
|
private String q;
|
||||||
|
private String platform;
|
||||||
|
private String partRole;
|
||||||
|
|
||||||
|
private ImportStatus importStatus;
|
||||||
|
private ProductVisibility visibility;
|
||||||
|
private ProductStatus status;
|
||||||
|
|
||||||
|
private Boolean builderEligible;
|
||||||
|
private Boolean adminLocked;
|
||||||
|
|
||||||
|
// --- getters/setters ---
|
||||||
|
|
||||||
|
public String getQ() { return q; }
|
||||||
|
public void setQ(String q) { this.q = q; }
|
||||||
|
|
||||||
|
public String getPlatform() { return platform; }
|
||||||
|
public void setPlatform(String platform) { this.platform = platform; }
|
||||||
|
|
||||||
|
public String getPartRole() { return partRole; }
|
||||||
|
public void setPartRole(String partRole) { this.partRole = partRole; }
|
||||||
|
|
||||||
|
public ImportStatus getImportStatus() { return importStatus; }
|
||||||
|
public void setImportStatus(ImportStatus importStatus) { this.importStatus = importStatus; }
|
||||||
|
|
||||||
|
public ProductVisibility getVisibility() { return visibility; }
|
||||||
|
public void setVisibility(ProductVisibility visibility) { this.visibility = visibility; }
|
||||||
|
|
||||||
|
public ProductStatus getStatus() { return status; }
|
||||||
|
public void setStatus(ProductStatus status) { this.status = status; }
|
||||||
|
|
||||||
|
public Boolean getBuilderEligible() { return builderEligible; }
|
||||||
|
public void setBuilderEligible(Boolean builderEligible) { this.builderEligible = builderEligible; }
|
||||||
|
|
||||||
|
public Boolean getAdminLocked() { return adminLocked; }
|
||||||
|
public void setAdminLocked(Boolean adminLocked) { this.adminLocked = adminLocked; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,203 @@
|
|||||||
|
package group.goforward.battlbuilder.web.dto.admin;
|
||||||
|
|
||||||
|
import group.goforward.battlbuilder.model.ImportStatus;
|
||||||
|
import group.goforward.battlbuilder.model.Product;
|
||||||
|
import group.goforward.battlbuilder.model.ProductStatus;
|
||||||
|
import group.goforward.battlbuilder.model.ProductVisibility;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lightweight row for the admin products table.
|
||||||
|
* Avoids fetching offers unless you explicitly want it.
|
||||||
|
*/
|
||||||
|
public class ProductAdminRowDto {
|
||||||
|
|
||||||
|
private Integer id;
|
||||||
|
private UUID uuid;
|
||||||
|
|
||||||
|
private String name;
|
||||||
|
private String slug;
|
||||||
|
|
||||||
|
private String platform;
|
||||||
|
private String partRole;
|
||||||
|
|
||||||
|
private String brandName;
|
||||||
|
|
||||||
|
private ImportStatus importStatus;
|
||||||
|
|
||||||
|
private ProductVisibility visibility;
|
||||||
|
private ProductStatus status;
|
||||||
|
private Boolean builderEligible;
|
||||||
|
private Boolean adminLocked;
|
||||||
|
|
||||||
|
private String adminNote;
|
||||||
|
|
||||||
|
private String mainImageUrl;
|
||||||
|
|
||||||
|
private Instant createdAt;
|
||||||
|
private Instant updatedAt;
|
||||||
|
|
||||||
|
|
||||||
|
public static ProductAdminRowDto fromEntity(Product p) {
|
||||||
|
ProductAdminRowDto dto = new ProductAdminRowDto();
|
||||||
|
|
||||||
|
dto.id = p.getId();
|
||||||
|
dto.uuid = p.getUuid();
|
||||||
|
|
||||||
|
dto.name = p.getName();
|
||||||
|
dto.slug = p.getSlug();
|
||||||
|
|
||||||
|
dto.platform = p.getPlatform();
|
||||||
|
dto.partRole = p.getPartRole();
|
||||||
|
|
||||||
|
dto.brandName = (p.getBrand() != null) ? p.getBrand().getName() : null;
|
||||||
|
|
||||||
|
dto.importStatus = p.getImportStatus();
|
||||||
|
|
||||||
|
dto.visibility = p.getVisibility();
|
||||||
|
dto.status = p.getStatus();
|
||||||
|
dto.builderEligible = p.getBuilderEligible();
|
||||||
|
dto.adminLocked = p.getAdminLocked();
|
||||||
|
|
||||||
|
dto.adminNote = p.getAdminNote();
|
||||||
|
dto.mainImageUrl = p.getMainImageUrl();
|
||||||
|
|
||||||
|
dto.createdAt = p.getCreatedAt();
|
||||||
|
dto.updatedAt = p.getUpdatedAt();
|
||||||
|
|
||||||
|
return dto;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- getters/setters ---
|
||||||
|
|
||||||
|
public Integer getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setId(Integer id) {
|
||||||
|
this.id = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public UUID getUuid() {
|
||||||
|
return uuid;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setUuid(UUID uuid) {
|
||||||
|
this.uuid = uuid;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getName() {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setName(String name) {
|
||||||
|
this.name = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getSlug() {
|
||||||
|
return slug;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSlug(String slug) {
|
||||||
|
this.slug = slug;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getPlatform() {
|
||||||
|
return platform;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPlatform(String platform) {
|
||||||
|
this.platform = platform;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getPartRole() {
|
||||||
|
return partRole;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPartRole(String partRole) {
|
||||||
|
this.partRole = partRole;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getBrandName() {
|
||||||
|
return brandName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setBrandName(String brandName) {
|
||||||
|
this.brandName = brandName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ImportStatus getImportStatus() {
|
||||||
|
return importStatus;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setImportStatus(ImportStatus importStatus) {
|
||||||
|
this.importStatus = importStatus;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ProductVisibility getVisibility() {
|
||||||
|
return visibility;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setVisibility(ProductVisibility visibility) {
|
||||||
|
this.visibility = visibility;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ProductStatus getStatus() {
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setStatus(ProductStatus status) {
|
||||||
|
this.status = status;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Boolean getBuilderEligible() {
|
||||||
|
return builderEligible;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setBuilderEligible(Boolean builderEligible) {
|
||||||
|
this.builderEligible = builderEligible;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Boolean getAdminLocked() {
|
||||||
|
return adminLocked;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAdminLocked(Boolean adminLocked) {
|
||||||
|
this.adminLocked = adminLocked;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getAdminNote() {
|
||||||
|
return adminNote;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAdminNote(String adminNote) {
|
||||||
|
this.adminNote = adminNote;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getMainImageUrl() {
|
||||||
|
return mainImageUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setMainImageUrl(String mainImageUrl) {
|
||||||
|
this.mainImageUrl = mainImageUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Instant getCreatedAt() {
|
||||||
|
return createdAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCreatedAt(Instant createdAt) {
|
||||||
|
this.createdAt = createdAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Instant getUpdatedAt() {
|
||||||
|
return updatedAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setUpdatedAt(Instant updatedAt) {
|
||||||
|
this.updatedAt = updatedAt;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
package group.goforward.battlbuilder.web.dto.admin;
|
||||||
|
|
||||||
|
import group.goforward.battlbuilder.model.ProductStatus;
|
||||||
|
import group.goforward.battlbuilder.model.ProductVisibility;
|
||||||
|
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
public class ProductBulkUpdateRequest {
|
||||||
|
|
||||||
|
private Set<Integer> productIds;
|
||||||
|
|
||||||
|
private ProductVisibility visibility;
|
||||||
|
private ProductStatus status;
|
||||||
|
|
||||||
|
private Boolean builderEligible;
|
||||||
|
private Boolean adminLocked;
|
||||||
|
|
||||||
|
private String adminNote;
|
||||||
|
|
||||||
|
// --- getters/setters ---
|
||||||
|
|
||||||
|
public Set<Integer> getProductIds() { return productIds; }
|
||||||
|
public void setProductIds(Set<Integer> productIds) { this.productIds = productIds; }
|
||||||
|
|
||||||
|
public ProductVisibility getVisibility() { return visibility; }
|
||||||
|
public void setVisibility(ProductVisibility visibility) { this.visibility = visibility; }
|
||||||
|
|
||||||
|
public ProductStatus getStatus() { return status; }
|
||||||
|
public void setStatus(ProductStatus status) { this.status = status; }
|
||||||
|
|
||||||
|
public Boolean getBuilderEligible() { return builderEligible; }
|
||||||
|
public void setBuilderEligible(Boolean builderEligible) { this.builderEligible = builderEligible; }
|
||||||
|
|
||||||
|
public Boolean getAdminLocked() { return adminLocked; }
|
||||||
|
public void setAdminLocked(Boolean adminLocked) { this.adminLocked = adminLocked; }
|
||||||
|
|
||||||
|
public String getAdminNote() { return adminNote; }
|
||||||
|
public void setAdminNote(String adminNote) { this.adminNote = adminNote; }
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user