roadmap_handlers.go (740 LOC) → roadmap_handlers.go, roadmap_item_handlers.go, roadmap_import_handlers.go academy/store.go (683 LOC) → store_courses.go, store_enrollments.go cmd/server/main.go (681 LOC) → internal/app/app.go (Run+buildRouter) + internal/app/routes.go (registerXxx helpers) main.go reduced to 7 LOC thin entrypoint calling app.Run() All files under 410 LOC. Zero behavior changes, same package declarations. go vet passes on all directly-split packages. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
259 lines
6.7 KiB
Go
259 lines
6.7 KiB
Go
package handlers
|
|
|
|
import (
|
|
"net/http"
|
|
"time"
|
|
|
|
"github.com/breakpilot/ai-compliance-sdk/internal/roadmap"
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
// ============================================================================
|
|
// RoadmapItem CRUD
|
|
// ============================================================================
|
|
|
|
// CreateItem creates a new roadmap item
|
|
// POST /sdk/v1/roadmaps/:id/items
|
|
func (h *RoadmapHandlers) CreateItem(c *gin.Context) {
|
|
roadmapID, err := uuid.Parse(c.Param("id"))
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid roadmap ID"})
|
|
return
|
|
}
|
|
|
|
var input roadmap.RoadmapItemInput
|
|
if err := c.ShouldBindJSON(&input); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
item := &roadmap.RoadmapItem{
|
|
RoadmapID: roadmapID,
|
|
Title: input.Title,
|
|
Description: input.Description,
|
|
Category: input.Category,
|
|
Priority: input.Priority,
|
|
Status: input.Status,
|
|
ControlID: input.ControlID,
|
|
RegulationRef: input.RegulationRef,
|
|
GapID: input.GapID,
|
|
EffortDays: input.EffortDays,
|
|
AssigneeName: input.AssigneeName,
|
|
Department: input.Department,
|
|
PlannedStart: input.PlannedStart,
|
|
PlannedEnd: input.PlannedEnd,
|
|
Notes: input.Notes,
|
|
}
|
|
|
|
if err := h.store.CreateItem(c.Request.Context(), item); err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
// Update roadmap progress
|
|
h.store.UpdateRoadmapProgress(c.Request.Context(), roadmapID)
|
|
|
|
c.JSON(http.StatusCreated, gin.H{"item": item})
|
|
}
|
|
|
|
// ListItems lists items for a roadmap
|
|
// GET /sdk/v1/roadmaps/:id/items
|
|
func (h *RoadmapHandlers) ListItems(c *gin.Context) {
|
|
roadmapID, err := uuid.Parse(c.Param("id"))
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid roadmap ID"})
|
|
return
|
|
}
|
|
|
|
filters := &roadmap.RoadmapItemFilters{
|
|
SearchQuery: c.Query("search"),
|
|
Limit: 100,
|
|
}
|
|
|
|
if status := c.Query("status"); status != "" {
|
|
filters.Status = roadmap.ItemStatus(status)
|
|
}
|
|
if priority := c.Query("priority"); priority != "" {
|
|
filters.Priority = roadmap.ItemPriority(priority)
|
|
}
|
|
if category := c.Query("category"); category != "" {
|
|
filters.Category = roadmap.ItemCategory(category)
|
|
}
|
|
if controlID := c.Query("control_id"); controlID != "" {
|
|
filters.ControlID = controlID
|
|
}
|
|
|
|
items, err := h.store.ListItems(c.Request.Context(), roadmapID, filters)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"items": items,
|
|
"total": len(items),
|
|
})
|
|
}
|
|
|
|
// GetItem retrieves a roadmap item
|
|
// GET /sdk/v1/roadmap-items/:id
|
|
func (h *RoadmapHandlers) GetItem(c *gin.Context) {
|
|
id, err := uuid.Parse(c.Param("id"))
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid ID"})
|
|
return
|
|
}
|
|
|
|
item, err := h.store.GetItem(c.Request.Context(), id)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
if item == nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "item not found"})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{"item": item})
|
|
}
|
|
|
|
// UpdateItem updates a roadmap item
|
|
// PUT /sdk/v1/roadmap-items/:id
|
|
func (h *RoadmapHandlers) UpdateItem(c *gin.Context) {
|
|
id, err := uuid.Parse(c.Param("id"))
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid ID"})
|
|
return
|
|
}
|
|
|
|
item, err := h.store.GetItem(c.Request.Context(), id)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
if item == nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "item not found"})
|
|
return
|
|
}
|
|
|
|
var input roadmap.RoadmapItemInput
|
|
if err := c.ShouldBindJSON(&input); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
// Update fields
|
|
item.Title = input.Title
|
|
item.Description = input.Description
|
|
if input.Category != "" {
|
|
item.Category = input.Category
|
|
}
|
|
if input.Priority != "" {
|
|
item.Priority = input.Priority
|
|
}
|
|
if input.Status != "" {
|
|
item.Status = input.Status
|
|
}
|
|
item.ControlID = input.ControlID
|
|
item.RegulationRef = input.RegulationRef
|
|
item.GapID = input.GapID
|
|
item.EffortDays = input.EffortDays
|
|
item.AssigneeName = input.AssigneeName
|
|
item.Department = input.Department
|
|
item.PlannedStart = input.PlannedStart
|
|
item.PlannedEnd = input.PlannedEnd
|
|
item.Notes = input.Notes
|
|
|
|
if err := h.store.UpdateItem(c.Request.Context(), item); err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
// Update roadmap progress
|
|
h.store.UpdateRoadmapProgress(c.Request.Context(), item.RoadmapID)
|
|
|
|
c.JSON(http.StatusOK, gin.H{"item": item})
|
|
}
|
|
|
|
// UpdateItemStatus updates just the status of a roadmap item
|
|
// PATCH /sdk/v1/roadmap-items/:id/status
|
|
func (h *RoadmapHandlers) UpdateItemStatus(c *gin.Context) {
|
|
id, err := uuid.Parse(c.Param("id"))
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid ID"})
|
|
return
|
|
}
|
|
|
|
var req struct {
|
|
Status roadmap.ItemStatus `json:"status"`
|
|
}
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
item, err := h.store.GetItem(c.Request.Context(), id)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
if item == nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "item not found"})
|
|
return
|
|
}
|
|
|
|
item.Status = req.Status
|
|
|
|
// Set actual dates
|
|
now := time.Now().UTC()
|
|
if req.Status == roadmap.ItemStatusInProgress && item.ActualStart == nil {
|
|
item.ActualStart = &now
|
|
}
|
|
if req.Status == roadmap.ItemStatusCompleted && item.ActualEnd == nil {
|
|
item.ActualEnd = &now
|
|
}
|
|
|
|
if err := h.store.UpdateItem(c.Request.Context(), item); err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
// Update roadmap progress
|
|
h.store.UpdateRoadmapProgress(c.Request.Context(), item.RoadmapID)
|
|
|
|
c.JSON(http.StatusOK, gin.H{"item": item})
|
|
}
|
|
|
|
// DeleteItem deletes a roadmap item
|
|
// DELETE /sdk/v1/roadmap-items/:id
|
|
func (h *RoadmapHandlers) DeleteItem(c *gin.Context) {
|
|
id, err := uuid.Parse(c.Param("id"))
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid ID"})
|
|
return
|
|
}
|
|
|
|
item, err := h.store.GetItem(c.Request.Context(), id)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
if item == nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "item not found"})
|
|
return
|
|
}
|
|
|
|
roadmapID := item.RoadmapID
|
|
|
|
if err := h.store.DeleteItem(c.Request.Context(), id); err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
// Update roadmap progress
|
|
h.store.UpdateRoadmapProgress(c.Request.Context(), roadmapID)
|
|
|
|
c.JSON(http.StatusOK, gin.H{"message": "item deleted"})
|
|
}
|