From 52b9ffd105872a3262e1b5e0b427b8b9c3c047fc Mon Sep 17 00:00:00 2001 From: Sean Date: Fri, 9 Jan 2026 10:18:56 -0500 Subject: [PATCH] fixes to admin/products --- .../admin/AdminPlatformController.java | 32 +++++ .../goforward/battlbuilder/model/Caliber.java | 69 +++++++++ .../battlbuilder/repos/CaliberRepository.java | 11 ++ .../admin/impl/AdminProductServiceImpl.java | 17 ++- .../web/admin/AdminCaliberController.java | 132 ++++++++++++++++++ .../web/dto/admin/CaliberDto.java | 13 ++ .../web/dto/admin/CaliberUpsertRequest.java | 8 ++ src/main/resources/application.properties | 14 +- 8 files changed, 290 insertions(+), 6 deletions(-) create mode 100644 src/main/java/group/goforward/battlbuilder/model/Caliber.java create mode 100644 src/main/java/group/goforward/battlbuilder/repos/CaliberRepository.java create mode 100644 src/main/java/group/goforward/battlbuilder/web/admin/AdminCaliberController.java create mode 100644 src/main/java/group/goforward/battlbuilder/web/dto/admin/CaliberDto.java create mode 100644 src/main/java/group/goforward/battlbuilder/web/dto/admin/CaliberUpsertRequest.java diff --git a/src/main/java/group/goforward/battlbuilder/controllers/admin/AdminPlatformController.java b/src/main/java/group/goforward/battlbuilder/controllers/admin/AdminPlatformController.java index 96491a5..294af44 100644 --- a/src/main/java/group/goforward/battlbuilder/controllers/admin/AdminPlatformController.java +++ b/src/main/java/group/goforward/battlbuilder/controllers/admin/AdminPlatformController.java @@ -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 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 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) {} } \ No newline at end of file diff --git a/src/main/java/group/goforward/battlbuilder/model/Caliber.java b/src/main/java/group/goforward/battlbuilder/model/Caliber.java new file mode 100644 index 0000000..881bf46 --- /dev/null +++ b/src/main/java/group/goforward/battlbuilder/model/Caliber.java @@ -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(); + } +} \ No newline at end of file diff --git a/src/main/java/group/goforward/battlbuilder/repos/CaliberRepository.java b/src/main/java/group/goforward/battlbuilder/repos/CaliberRepository.java new file mode 100644 index 0000000..73134e5 --- /dev/null +++ b/src/main/java/group/goforward/battlbuilder/repos/CaliberRepository.java @@ -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 { + List findAllByDeletedAtIsNullAndIsActiveTrueOrderByLabelAsc(); + List findAllByDeletedAtIsNullOrderByLabelAsc(); +} \ No newline at end of file diff --git a/src/main/java/group/goforward/battlbuilder/services/admin/impl/AdminProductServiceImpl.java b/src/main/java/group/goforward/battlbuilder/services/admin/impl/AdminProductServiceImpl.java index ed9ca06..9d61599 100644 --- a/src/main/java/group/goforward/battlbuilder/services/admin/impl/AdminProductServiceImpl.java +++ b/src/main/java/group/goforward/battlbuilder/services/admin/impl/AdminProductServiceImpl.java @@ -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 distinctCalibers(AdminProductSearchRequest request) { + // Prefer curated calibers if present (table-based) + List 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(); diff --git a/src/main/java/group/goforward/battlbuilder/web/admin/AdminCaliberController.java b/src/main/java/group/goforward/battlbuilder/web/admin/AdminCaliberController.java new file mode 100644 index 0000000..305f5ef --- /dev/null +++ b/src/main/java/group/goforward/battlbuilder/web/admin/AdminCaliberController.java @@ -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 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 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 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 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 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() + ); + } +} \ No newline at end of file diff --git a/src/main/java/group/goforward/battlbuilder/web/dto/admin/CaliberDto.java b/src/main/java/group/goforward/battlbuilder/web/dto/admin/CaliberDto.java new file mode 100644 index 0000000..3e4269a --- /dev/null +++ b/src/main/java/group/goforward/battlbuilder/web/dto/admin/CaliberDto.java @@ -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 +) {} \ No newline at end of file diff --git a/src/main/java/group/goforward/battlbuilder/web/dto/admin/CaliberUpsertRequest.java b/src/main/java/group/goforward/battlbuilder/web/dto/admin/CaliberUpsertRequest.java new file mode 100644 index 0000000..02291d6 --- /dev/null +++ b/src/main/java/group/goforward/battlbuilder/web/dto/admin/CaliberUpsertRequest.java @@ -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 +) {} \ No newline at end of file diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 080aafc..dbae7ff 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -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/