package handlers import ( "net/http" "strconv" "time" "github.com/breakpilot/ai-compliance-sdk/internal/rbac" "github.com/breakpilot/ai-compliance-sdk/internal/training" "github.com/gin-gonic/gin" "github.com/google/uuid" ) // ============================================================================ // Assignment Endpoints // ============================================================================ // ComputeAssignments computes assignments for a user based on roles // POST /sdk/v1/training/assignments/compute func (h *TrainingHandlers) ComputeAssignments(c *gin.Context) { tenantID := rbac.GetTenantID(c) var req training.ComputeAssignmentsRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } trigger := req.Trigger if trigger == "" { trigger = "manual" } assignments, err := training.ComputeAssignments( c.Request.Context(), h.store, tenantID, req.UserID, req.UserName, req.UserEmail, req.Roles, trigger, ) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{ "assignments": assignments, "created": len(assignments), }) } // ListAssignments returns assignments for the tenant // GET /sdk/v1/training/assignments func (h *TrainingHandlers) ListAssignments(c *gin.Context) { tenantID := rbac.GetTenantID(c) filters := &training.AssignmentFilters{ Limit: 50, Offset: 0, } if v := c.Query("user_id"); v != "" { if uid, err := uuid.Parse(v); err == nil { filters.UserID = &uid } } if v := c.Query("module_id"); v != "" { if mid, err := uuid.Parse(v); err == nil { filters.ModuleID = &mid } } if v := c.Query("role"); v != "" { filters.RoleCode = v } if v := c.Query("status"); v != "" { filters.Status = training.AssignmentStatus(v) } if v := c.Query("limit"); v != "" { if n, err := strconv.Atoi(v); err == nil { filters.Limit = n } } if v := c.Query("offset"); v != "" { if n, err := strconv.Atoi(v); err == nil { filters.Offset = n } } assignments, total, err := h.store.ListAssignments(c.Request.Context(), tenantID, filters) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, training.AssignmentListResponse{ Assignments: assignments, Total: total, }) } // GetAssignment returns a single assignment // GET /sdk/v1/training/assignments/:id func (h *TrainingHandlers) GetAssignment(c *gin.Context) { id, err := uuid.Parse(c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid assignment ID"}) return } assignment, err := h.store.GetAssignment(c.Request.Context(), id) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } if assignment == nil { c.JSON(http.StatusNotFound, gin.H{"error": "assignment not found"}) return } c.JSON(http.StatusOK, assignment) } // StartAssignment marks an assignment as started // POST /sdk/v1/training/assignments/:id/start func (h *TrainingHandlers) StartAssignment(c *gin.Context) { id, err := uuid.Parse(c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid assignment ID"}) return } tenantID := rbac.GetTenantID(c) if err := h.store.UpdateAssignmentStatus(c.Request.Context(), id, training.AssignmentStatusInProgress, 0); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } // Audit log userID := rbac.GetUserID(c) h.store.LogAction(c.Request.Context(), &training.AuditLogEntry{ TenantID: tenantID, UserID: &userID, Action: training.AuditActionStarted, EntityType: training.AuditEntityAssignment, EntityID: &id, }) c.JSON(http.StatusOK, gin.H{"status": "in_progress"}) } // UpdateAssignmentProgress updates progress on an assignment // POST /sdk/v1/training/assignments/:id/progress func (h *TrainingHandlers) UpdateAssignmentProgress(c *gin.Context) { id, err := uuid.Parse(c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid assignment ID"}) return } var req training.UpdateAssignmentProgressRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } status := training.AssignmentStatusInProgress if req.Progress >= 100 { status = training.AssignmentStatusCompleted } if err := h.store.UpdateAssignmentStatus(c.Request.Context(), id, status, req.Progress); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"status": string(status), "progress": req.Progress}) } // UpdateAssignment updates assignment fields (e.g. deadline) // PUT /sdk/v1/training/assignments/:id func (h *TrainingHandlers) UpdateAssignment(c *gin.Context) { id, err := uuid.Parse(c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid assignment ID"}) return } var req struct { Deadline *string `json:"deadline"` } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request body"}) return } if req.Deadline != nil { deadline, err := time.Parse(time.RFC3339, *req.Deadline) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid deadline format (use RFC3339)"}) return } if err := h.store.UpdateAssignmentDeadline(c.Request.Context(), id, deadline); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } } assignment, err := h.store.GetAssignment(c.Request.Context(), id) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } if assignment == nil { c.JSON(http.StatusNotFound, gin.H{"error": "assignment not found"}) return } c.JSON(http.StatusOK, assignment) } // CompleteAssignment marks an assignment as completed // POST /sdk/v1/training/assignments/:id/complete func (h *TrainingHandlers) CompleteAssignment(c *gin.Context) { id, err := uuid.Parse(c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid assignment ID"}) return } tenantID := rbac.GetTenantID(c) if err := h.store.UpdateAssignmentStatus(c.Request.Context(), id, training.AssignmentStatusCompleted, 100); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } userID := rbac.GetUserID(c) h.store.LogAction(c.Request.Context(), &training.AuditLogEntry{ TenantID: tenantID, UserID: &userID, Action: training.AuditActionCompleted, EntityType: training.AuditEntityAssignment, EntityID: &id, }) c.JSON(http.StatusOK, gin.H{"status": "completed"}) }