dynamic category mapping and updating from admin.

This commit is contained in:
2025-12-03 21:50:00 -05:00
parent 5e3f7d5044
commit 74a5c42e26
8 changed files with 208 additions and 114 deletions

View File

@@ -20,21 +20,21 @@ public class AdminCategoryController {
@GetMapping @GetMapping
public List<PartCategoryDto> listCategories() { public List<PartCategoryDto> listCategories() {
return partCategories.findAllByOrderByGroupNameAscSortOrderAscNameAsc() return partCategories
.findAllByOrderByGroupNameAscSortOrderAscNameAsc()
.stream() .stream()
.map(this::toDto) .map(this::toDto)
.toList(); .toList();
} }
private PartCategoryDto toDto(PartCategory entity) { private PartCategoryDto toDto(PartCategory entity) {
PartCategoryDto dto = new PartCategoryDto(); return new PartCategoryDto(
dto.setId(entity.getId()); entity.getId(),
dto.setSlug(entity.getSlug()); entity.getSlug(),
dto.setName(entity.getName()); entity.getName(),
dto.setDescription(entity.getDescription()); entity.getDescription(),
dto.setGroupName(entity.getGroupName()); entity.getGroupName(),
dto.setSortOrder(entity.getSortOrder()); entity.getSortOrder()
dto.setUuid(entity.getUuid()); );
return dto;
} }
} }

View File

@@ -0,0 +1,125 @@
package group.goforward.ballistic.controllers.admin;
import group.goforward.ballistic.model.AffiliateCategoryMap;
import group.goforward.ballistic.model.PartCategory;
import group.goforward.ballistic.repos.CategoryMappingRepository;
import group.goforward.ballistic.repos.PartCategoryRepository;
import group.goforward.ballistic.web.dto.admin.PartRoleMappingDto;
import group.goforward.ballistic.web.dto.admin.PartRoleMappingRequest;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.server.ResponseStatusException;
import java.util.List;
@RestController
@RequestMapping("/api/admin/category-mappings")
@CrossOrigin
public class AdminCategoryMappingController {
private final CategoryMappingRepository categoryMappingRepository;
private final PartCategoryRepository partCategoryRepository;
public AdminCategoryMappingController(
CategoryMappingRepository categoryMappingRepository,
PartCategoryRepository partCategoryRepository
) {
this.categoryMappingRepository = categoryMappingRepository;
this.partCategoryRepository = partCategoryRepository;
}
// GET /api/admin/category-mappings?platform=AR-15
@GetMapping
public List<PartRoleMappingDto> list(
@RequestParam(name = "platform", defaultValue = "AR-15") String platform
) {
List<AffiliateCategoryMap> mappings =
categoryMappingRepository.findBySourceTypeAndPlatformOrderById("PART_ROLE", platform);
return mappings.stream()
.map(this::toDto)
.toList();
}
// POST /api/admin/category-mappings
@PostMapping
public ResponseEntity<PartRoleMappingDto> create(
@RequestBody PartRoleMappingRequest request
) {
if (request.platform() == null || request.partRole() == null || request.categorySlug() == null) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "platform, partRole, and categorySlug are required");
}
PartCategory category = partCategoryRepository.findBySlug(request.categorySlug())
.orElseThrow(() -> new ResponseStatusException(
HttpStatus.BAD_REQUEST,
"Unknown category slug: " + request.categorySlug()
));
AffiliateCategoryMap mapping = new AffiliateCategoryMap();
mapping.setSourceType("PART_ROLE");
mapping.setSourceValue(request.partRole());
mapping.setPlatform(request.platform());
mapping.setPartCategory(category);
mapping.setNotes(request.notes());
AffiliateCategoryMap saved = categoryMappingRepository.save(mapping);
return ResponseEntity.status(HttpStatus.CREATED).body(toDto(saved));
}
// PUT /api/admin/category-mappings/{id}
@PutMapping("/{id}")
public PartRoleMappingDto update(
@PathVariable Integer id,
@RequestBody PartRoleMappingRequest request
) {
AffiliateCategoryMap mapping = categoryMappingRepository.findById(id)
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Mapping not found"));
if (request.platform() != null) {
mapping.setPlatform(request.platform());
}
if (request.partRole() != null) {
mapping.setSourceValue(request.partRole());
}
if (request.categorySlug() != null) {
PartCategory category = partCategoryRepository.findBySlug(request.categorySlug())
.orElseThrow(() -> new ResponseStatusException(
HttpStatus.BAD_REQUEST,
"Unknown category slug: " + request.categorySlug()
));
mapping.setPartCategory(category);
}
if (request.notes() != null) {
mapping.setNotes(request.notes());
}
AffiliateCategoryMap saved = categoryMappingRepository.save(mapping);
return toDto(saved);
}
// DELETE /api/admin/category-mappings/{id}
@DeleteMapping("/{id}")
@ResponseStatus(HttpStatus.NO_CONTENT)
public void delete(@PathVariable Integer id) {
if (!categoryMappingRepository.existsById(id)) {
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Mapping not found");
}
categoryMappingRepository.deleteById(id);
}
private PartRoleMappingDto toDto(AffiliateCategoryMap map) {
PartCategory cat = map.getPartCategory();
return new PartRoleMappingDto(
map.getId(),
map.getPlatform(),
map.getSourceValue(), // partRole
cat != null ? cat.getSlug() : null, // categorySlug
cat != null ? cat.getGroupName() : null,
map.getNotes()
);
}
}

View File

@@ -0,0 +1,35 @@
package group.goforward.ballistic.controllers.admin;
import group.goforward.ballistic.model.PartCategory;
import group.goforward.ballistic.repos.PartCategoryRepository;
import group.goforward.ballistic.web.dto.admin.PartCategoryDto;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/admin/part-categories")
@CrossOrigin // keep it loose for now, you can tighten origins later
public class PartCategoryAdminController {
private final PartCategoryRepository partCategories;
public PartCategoryAdminController(PartCategoryRepository partCategories) {
this.partCategories = partCategories;
}
@GetMapping
public List<PartCategoryDto> list() {
return partCategories.findAllByOrderByGroupNameAscSortOrderAscNameAsc()
.stream()
.map(pc -> new PartCategoryDto(
pc.getId(),
pc.getSlug(),
pc.getName(),
pc.getDescription(),
pc.getGroupName(),
pc.getSortOrder()
))
.toList();
}
}

View File

@@ -8,18 +8,17 @@ public class AffiliateCategoryMap {
@Id @Id
@GeneratedValue(strategy = GenerationType.IDENTITY) @GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id", nullable = false)
private Integer id; private Integer id;
// e.g. "PART_ROLE" // e.g. "PART_ROLE", "RAW_CATEGORY", etc.
@Column(name = "source_type", nullable = false) @Column(name = "source_type", nullable = false)
private String sourceType; private String sourceType;
// e.g. "suppressor" // the value were mapping from (e.g. "suppressor", "TRIGGER")
@Column(name = "source_value", nullable = false) @Column(name = "source_value", nullable = false)
private String sourceValue; private String sourceValue;
// e.g. "AR-15", nullable // optional platform ("AR-15", "PRECISION", etc.)
@Column(name = "platform") @Column(name = "platform")
private String platform; private String platform;
@@ -30,6 +29,8 @@ public class AffiliateCategoryMap {
@Column(name = "notes") @Column(name = "notes")
private String notes; private String notes;
// --- getters / setters ---
public Integer getId() { public Integer getId() {
return id; return id;
} }

View File

@@ -3,18 +3,27 @@ package group.goforward.ballistic.repos;
import group.goforward.ballistic.model.AffiliateCategoryMap; import group.goforward.ballistic.model.AffiliateCategoryMap;
import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;
import java.util.Optional; import java.util.Optional;
public interface CategoryMappingRepository extends JpaRepository<AffiliateCategoryMap, Integer> { public interface CategoryMappingRepository extends JpaRepository<AffiliateCategoryMap, Integer> {
// Match by source_type + source_value + platform (case-insensitive)
Optional<AffiliateCategoryMap> findBySourceTypeAndSourceValueAndPlatformIgnoreCase( Optional<AffiliateCategoryMap> findBySourceTypeAndSourceValueAndPlatformIgnoreCase(
String sourceType, String sourceType,
String sourceValue, String sourceValue,
String platform String platform
); );
// Fallback: match by source_type + source_value when platform is null/ignored
Optional<AffiliateCategoryMap> findBySourceTypeAndSourceValueIgnoreCase( Optional<AffiliateCategoryMap> findBySourceTypeAndSourceValueIgnoreCase(
String sourceType, String sourceType,
String sourceValue String sourceValue
); );
// Used by AdminCategoryMappingController: list mappings for a given source_type + platform
List<AffiliateCategoryMap> findBySourceTypeAndPlatformOrderById(
String sourceType,
String platform
);
} }

View File

@@ -1,35 +1,10 @@
package group.goforward.ballistic.web.dto.admin; package group.goforward.ballistic.web.dto.admin;
import java.util.UUID; public record PartCategoryDto(
Integer id,
public class PartCategoryDto { String slug,
private Integer id; String name,
private String slug; String description,
private String name; String groupName,
private String description; Integer sortOrder
private String groupName; ) {}
private Integer sortOrder;
private UUID uuid;
// getters + setters
public Integer getId() { return id; }
public void setId(Integer id) { this.id = id; }
public String getSlug() { return slug; }
public void setSlug(String slug) { this.slug = slug; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getDescription() { return description; }
public void setDescription(String description) { this.description = description; }
public String getGroupName() { return groupName; }
public void setGroupName(String groupName) { this.groupName = groupName; }
public Integer getSortOrder() { return sortOrder; }
public void setSortOrder(Integer sortOrder) { this.sortOrder = sortOrder; }
public UUID getUuid() { return uuid; }
public void setUuid(UUID uuid) { this.uuid = uuid; }
}

View File

@@ -1,69 +1,10 @@
package group.goforward.ballistic.web.dto.admin; package group.goforward.ballistic.web.dto.admin;
public class PartRoleMappingDto { public record PartRoleMappingDto(
private Integer id; Integer id,
private String platform; String platform,
private String partRole; String partRole,
private String categorySlug; String categorySlug,
private String categoryName; String groupName,
private String groupName; String notes
private String notes; ) {}
// getters + setters...
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
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 getCategorySlug() {
return categorySlug;
}
public void setCategorySlug(String categorySlug) {
this.categorySlug = categorySlug;
}
public String getCategoryName() {
return categoryName;
}
public void setCategoryName(String categoryName) {
this.categoryName = categoryName;
}
public String getGroupName() {
return groupName;
}
public void setGroupName(String groupName) {
this.groupName = groupName;
}
public String getNotes() {
return notes;
}
public void setNotes(String notes) {
this.notes = notes;
}
}

View File

@@ -0,0 +1,8 @@
package group.goforward.ballistic.web.dto.admin;
public record PartRoleMappingRequest(
String platform,
String partRole,
String categorySlug,
String notes
) {}