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"}) }