fixes to admin/products

This commit is contained in:
2026-01-09 10:18:56 -05:00
parent d7408d41a3
commit 52b9ffd105
8 changed files with 290 additions and 6 deletions

View File

@@ -9,6 +9,7 @@ import org.springframework.web.bind.annotation.*;
import java.time.OffsetDateTime;
import java.util.List;
import org.springframework.web.server.ResponseStatusException;
@RestController
@RequestMapping("/api/platforms")
@@ -102,4 +103,35 @@ public class AdminPlatformController {
public ResponseEntity<Void> deleteLegacy(@PathVariable Integer id) {
return delete(id);
}
/**
* Update platform (toggle active)
* PATCH /api/platforms/{id}
* PUT /api/platforms/{id}
* Body: { "isActive": true|false }
*/
@PatchMapping("/{id}")
@PutMapping("/{id}")
public ResponseEntity<PlatformDto> update(@PathVariable Integer id, @RequestBody PlatformUpdateRequest req) {
Platform platform = platformRepository.findById(id)
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Platform not found"));
if (req != null && req.isActive() != null) {
platform.setIsActive(req.isActive());
}
platform.setUpdatedAt(OffsetDateTime.now());
Platform saved = platformRepository.save(platform);
return ResponseEntity.ok(new PlatformDto(
saved.getId(),
saved.getKey(),
saved.getLabel(),
saved.getCreatedAt(),
saved.getUpdatedAt(),
saved.getIsActive()
));
}
public record PlatformUpdateRequest(Boolean isActive) {}
}

View File

@@ -0,0 +1,69 @@
package group.goforward.battlbuilder.model;
import jakarta.persistence.*;
import java.time.OffsetDateTime;
@Entity
@Table(name = "calibers")
public class Caliber {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
@Column(nullable = false, unique = true)
private String key;
@Column(nullable = false)
private String label;
@Column(name = "is_active", nullable = false)
private Boolean isActive = true;
@Column(name = "created_at", nullable = false)
private OffsetDateTime createdAt;
@Column(name = "updated_at", nullable = false)
private OffsetDateTime updatedAt;
@Column(name = "deleted_at")
private OffsetDateTime deletedAt;
// --- getters/setters ---
public Integer getId() { return id; }
public void setId(Integer id) { this.id = id; }
public String getKey() { return key; }
public void setKey(String key) { this.key = key; }
public String getLabel() { return label; }
public void setLabel(String label) { this.label = label; }
public Boolean getIsActive() { return isActive; }
public void setIsActive(Boolean active) { isActive = active; }
public OffsetDateTime getCreatedAt() { return createdAt; }
public void setCreatedAt(OffsetDateTime createdAt) { this.createdAt = createdAt; }
public OffsetDateTime getUpdatedAt() { return updatedAt; }
public void setUpdatedAt(OffsetDateTime updatedAt) { this.updatedAt = updatedAt; }
public OffsetDateTime getDeletedAt() { return deletedAt; }
public void setDeletedAt(OffsetDateTime deletedAt) { this.deletedAt = deletedAt; }
// --- lifecycle hooks (prevents null timestamp issues) ---
@PrePersist
protected void onCreate() {
OffsetDateTime now = OffsetDateTime.now();
if (createdAt == null) createdAt = now;
if (updatedAt == null) updatedAt = now;
if (isActive == null) isActive = true;
}
@PreUpdate
protected void onUpdate() {
updatedAt = OffsetDateTime.now();
}
}

View File

@@ -0,0 +1,11 @@
package group.goforward.battlbuilder.repos;
import group.goforward.battlbuilder.model.Caliber;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;
public interface CaliberRepository extends JpaRepository<Caliber, Integer> {
List<Caliber> findAllByDeletedAtIsNullAndIsActiveTrueOrderByLabelAsc();
List<Caliber> findAllByDeletedAtIsNullOrderByLabelAsc();
}

View File

@@ -1,7 +1,9 @@
package group.goforward.battlbuilder.services.admin.impl;
import group.goforward.battlbuilder.model.Caliber;
import group.goforward.battlbuilder.model.Product;
import group.goforward.battlbuilder.model.enums.PartRoleSource;
import group.goforward.battlbuilder.repos.CaliberRepository;
import group.goforward.battlbuilder.repos.ProductRepository;
import group.goforward.battlbuilder.services.admin.AdminProductService;
import group.goforward.battlbuilder.domain.specs.ProductSpecifications;
@@ -36,9 +38,11 @@ import java.time.Instant;
public class AdminProductServiceImpl implements AdminProductService {
private final ProductRepository productRepository;
private final CaliberRepository caliberRepository;
public AdminProductServiceImpl(ProductRepository productRepository) {
public AdminProductServiceImpl(ProductRepository productRepository, CaliberRepository caliberRepository) {
this.productRepository = productRepository;
this.caliberRepository = caliberRepository;
}
@Override
@@ -241,6 +245,17 @@ public class AdminProductServiceImpl implements AdminProductService {
@Override
@Transactional(readOnly = true)
public List<String> distinctCalibers(AdminProductSearchRequest request) {
// Prefer curated calibers if present (table-based)
List<Caliber> curated = caliberRepository.findAllByDeletedAtIsNullAndIsActiveTrueOrderByLabelAsc();
if (curated != null && !curated.isEmpty()) {
return curated.stream()
.map(Caliber::getLabel)
.filter(s -> s != null && !s.trim().isEmpty())
.map(String::trim)
.toList();
}
// Fallback (legacy): compute distinct calibers from products using the same filter spec
var spec = ProductSpecifications.adminSearch(request);
CriteriaBuilder cb = em.getCriteriaBuilder();

View File

@@ -0,0 +1,132 @@
package group.goforward.battlbuilder.web.admin;
import group.goforward.battlbuilder.model.Caliber;
import group.goforward.battlbuilder.repos.CaliberRepository;
import group.goforward.battlbuilder.web.dto.admin.CaliberDto;
import group.goforward.battlbuilder.web.dto.admin.CaliberUpsertRequest;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.time.OffsetDateTime;
import java.util.List;
@RestController
@RequestMapping("/api/v1/admin/calibers")
public class AdminCaliberController {
private final CaliberRepository caliberRepository;
public AdminCaliberController(CaliberRepository caliberRepository) {
this.caliberRepository = caliberRepository;
}
@GetMapping
public List<CaliberDto> list(
@RequestParam(name = "activeOnly", defaultValue = "true") boolean activeOnly
) {
var list = activeOnly
? caliberRepository.findAllByDeletedAtIsNullAndIsActiveTrueOrderByLabelAsc()
: caliberRepository.findAllByDeletedAtIsNullOrderByLabelAsc();
return list.stream()
.map(this::toDto)
.toList();
}
@PostMapping
public ResponseEntity<CaliberDto> create(@RequestBody CaliberUpsertRequest req) {
Caliber c = new Caliber();
if (req == null || req.key() == null || req.key().trim().isEmpty()) {
return ResponseEntity.badRequest().build();
}
String key = normalizeKey(req.key());
c.setKey(key);
String label = (req.label() == null || req.label().trim().isEmpty())
? key
: req.label().trim();
c.setLabel(label);
c.setIsActive(req.isActive() == null ? true : req.isActive());
c.setCreatedAt(OffsetDateTime.now());
c.setUpdatedAt(OffsetDateTime.now());
Caliber saved = caliberRepository.save(c);
return ResponseEntity.status(HttpStatus.CREATED).body(toDto(saved));
}
@PutMapping("/{id}")
public ResponseEntity<CaliberDto> update(@PathVariable Integer id, @RequestBody CaliberUpsertRequest req) {
var existingOpt = caliberRepository.findById(id);
if (existingOpt.isEmpty()) return ResponseEntity.notFound().build();
Caliber c = existingOpt.get();
if (c.getDeletedAt() != null) return ResponseEntity.notFound().build();
if (req.key() != null && !req.key().trim().isEmpty()) {
c.setKey(normalizeKey(req.key()));
}
if (req.label() != null) {
String label = req.label().trim();
c.setLabel(label.isEmpty() ? c.getKey() : label);
}
if (req.isActive() != null) {
c.setIsActive(req.isActive());
}
c.setUpdatedAt(OffsetDateTime.now());
Caliber saved = caliberRepository.save(c);
return ResponseEntity.ok(toDto(saved));
}
@PatchMapping("/{id}/active")
public ResponseEntity<CaliberDto> setActive(@PathVariable Integer id, @RequestParam boolean active) {
var existingOpt = caliberRepository.findById(id);
if (existingOpt.isEmpty()) return ResponseEntity.notFound().build();
Caliber c = existingOpt.get();
if (c.getDeletedAt() != null) return ResponseEntity.notFound().build();
c.setIsActive(active);
c.setUpdatedAt(OffsetDateTime.now());
Caliber saved = caliberRepository.save(c);
return ResponseEntity.ok(toDto(saved));
}
@DeleteMapping("/{id}")
public ResponseEntity<Void> delete(@PathVariable Integer id) {
var existingOpt = caliberRepository.findById(id);
if (existingOpt.isEmpty()) return ResponseEntity.notFound().build();
Caliber c = existingOpt.get();
if (c.getDeletedAt() != null) return ResponseEntity.noContent().build();
c.setDeletedAt(OffsetDateTime.now());
c.setUpdatedAt(OffsetDateTime.now());
caliberRepository.save(c);
return ResponseEntity.noContent().build();
}
private String normalizeKey(String raw) {
return raw == null ? null : raw.trim().toUpperCase();
}
private CaliberDto toDto(Caliber c) {
return new CaliberDto(
c.getId(),
c.getKey(),
c.getLabel(),
c.getIsActive(),
c.getCreatedAt(),
c.getUpdatedAt()
);
}
}

View File

@@ -0,0 +1,13 @@
// web/dto/admin/CaliberDto.java
package group.goforward.battlbuilder.web.dto.admin;
import java.time.OffsetDateTime;
public record CaliberDto(
Integer id,
String key,
String label,
Boolean isActive,
OffsetDateTime createdAt,
OffsetDateTime updatedAt
) {}

View File

@@ -0,0 +1,8 @@
// web/dto/admin/CaliberUpsertRequest.java
package group.goforward.battlbuilder.web.dto.admin;
public record CaliberUpsertRequest(
String key,
String label,
Boolean isActive
) {}

View File

@@ -5,17 +5,13 @@ spring.datasource.username=postgres
spring.datasource.password=cul8rman
spring.datasource.driver-class-name=org.postgresql.Driver
# Hibernate properties
#spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
#spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect
security.jwt.secret=ballistic-test-secret-key-1234567890-ABCDEFGHIJKLNMOPQRST
security.jwt.access-token-minutes=2880
# Hibernate properties
#spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
#spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect
@@ -23,6 +19,14 @@ spring.jpa.show-sql=true
logging.level.org.hibernate.SQL=off
logging.level.org.hibernate.type.descriptor.sql.BasicBinder=warn
logging.level.org.hibernate.orm.jdbc.bind=OFF
logging.level.org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping=DEBUG
# expose actuator endpoints you want
management.endpoints.web.exposure.include=health,info,mappings
# (optional) nicer output + shows details
management.endpoint.mappings.enabled=true
management.endpoint.health.show-details=always
# JSP Configuration
spring.mvc.view.prefix=/WEB-INF/views/