package handlers import ( "net/http" "strconv" "github.com/breakpilot/ai-compliance-sdk/internal/academy" "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" ) // ============================================================================ // Deadline / Escalation Endpoints // ============================================================================ // GetDeadlines returns upcoming deadlines // GET /sdk/v1/training/deadlines func (h *TrainingHandlers) GetDeadlines(c *gin.Context) { tenantID := rbac.GetTenantID(c) limit := 20 if v := c.Query("limit"); v != "" { if n, err := strconv.Atoi(v); err == nil { limit = n } } deadlines, err := h.store.GetDeadlines(c.Request.Context(), tenantID, limit) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, training.DeadlineListResponse{ Deadlines: deadlines, Total: len(deadlines), }) } // GetOverdueDeadlines returns overdue assignments // GET /sdk/v1/training/deadlines/overdue func (h *TrainingHandlers) GetOverdueDeadlines(c *gin.Context) { tenantID := rbac.GetTenantID(c) deadlines, err := training.GetOverdueDeadlines(c.Request.Context(), h.store, tenantID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, training.DeadlineListResponse{ Deadlines: deadlines, Total: len(deadlines), }) } // CheckEscalation runs the escalation check // POST /sdk/v1/training/escalation/check func (h *TrainingHandlers) CheckEscalation(c *gin.Context) { tenantID := rbac.GetTenantID(c) results, err := training.CheckEscalations(c.Request.Context(), h.store, tenantID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } overdueAll, _ := h.store.ListOverdueAssignments(c.Request.Context(), tenantID) c.JSON(http.StatusOK, training.EscalationResponse{ Results: results, TotalChecked: len(overdueAll), Escalated: len(results), }) } // ============================================================================ // Audit / Stats Endpoints // ============================================================================ // GetAuditLog returns the training audit trail // GET /sdk/v1/training/audit-log func (h *TrainingHandlers) GetAuditLog(c *gin.Context) { tenantID := rbac.GetTenantID(c) filters := &training.AuditLogFilters{ Limit: 50, Offset: 0, } if v := c.Query("action"); v != "" { filters.Action = training.AuditAction(v) } if v := c.Query("entity_type"); v != "" { filters.EntityType = training.AuditEntityType(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 } } entries, total, err := h.store.ListAuditLog(c.Request.Context(), tenantID, filters) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, training.AuditLogResponse{ Entries: entries, Total: total, }) } // GetStats returns training dashboard statistics // GET /sdk/v1/training/stats func (h *TrainingHandlers) GetStats(c *gin.Context) { tenantID := rbac.GetTenantID(c) stats, err := h.store.GetTrainingStats(c.Request.Context(), tenantID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, stats) } // VerifyCertificate verifies a certificate // GET /sdk/v1/training/certificates/:id/verify func (h *TrainingHandlers) VerifyCertificate(c *gin.Context) { id, err := uuid.Parse(c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid certificate ID"}) return } valid, assignment, err := training.VerifyCertificate(c.Request.Context(), h.store, id) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "certificate not found"}) return } c.JSON(http.StatusOK, gin.H{ "valid": valid, "assignment": assignment, }) } // ============================================================================ // Certificate Endpoints // ============================================================================ // GenerateCertificate generates a certificate for a completed assignment // POST /sdk/v1/training/certificates/generate/:assignmentId func (h *TrainingHandlers) GenerateCertificate(c *gin.Context) { assignmentID, err := uuid.Parse(c.Param("assignmentId")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid assignment ID"}) return } tenantID := rbac.GetTenantID(c) assignment, err := h.store.GetAssignment(c.Request.Context(), assignmentID) 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 } if assignment.Status != training.AssignmentStatusCompleted { c.JSON(http.StatusBadRequest, gin.H{"error": "assignment is not completed"}) return } if assignment.QuizPassed == nil || !*assignment.QuizPassed { c.JSON(http.StatusBadRequest, gin.H{"error": "quiz has not been passed"}) return } // Generate certificate ID certID := uuid.New() if err := h.store.SetCertificateID(c.Request.Context(), assignmentID, certID); 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.AuditActionCertificateIssued, EntityType: training.AuditEntityCertificate, EntityID: &certID, Details: map[string]interface{}{ "assignment_id": assignmentID.String(), "user_name": assignment.UserName, "module_title": assignment.ModuleTitle, }, }) // Reload assignment with certificate_id assignment, _ = h.store.GetAssignment(c.Request.Context(), assignmentID) c.JSON(http.StatusOK, gin.H{ "certificate_id": certID, "assignment": assignment, }) } // DownloadCertificatePDF generates and returns a PDF certificate // GET /sdk/v1/training/certificates/:id/pdf func (h *TrainingHandlers) DownloadCertificatePDF(c *gin.Context) { certID, err := uuid.Parse(c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid certificate ID"}) return } assignment, err := h.store.GetAssignmentByCertificateID(c.Request.Context(), certID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } if assignment == nil { c.JSON(http.StatusNotFound, gin.H{"error": "certificate not found"}) return } // Get module for title module, _ := h.store.GetModule(c.Request.Context(), assignment.ModuleID) courseName := assignment.ModuleTitle if module != nil { courseName = module.Title } score := 0 if assignment.QuizScore != nil { score = int(*assignment.QuizScore) } issuedAt := assignment.UpdatedAt if assignment.CompletedAt != nil { issuedAt = *assignment.CompletedAt } // Use academy PDF generator pdfBytes, err := academy.GenerateCertificatePDF(academy.CertificateData{ CertificateID: certID.String(), UserName: assignment.UserName, CourseName: courseName, Score: score, IssuedAt: issuedAt, ValidUntil: issuedAt.AddDate(1, 0, 0), }) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "PDF generation failed: " + err.Error()}) return } c.Header("Content-Disposition", "attachment; filename=zertifikat-"+certID.String()[:8]+".pdf") c.Data(http.StatusOK, "application/pdf", pdfBytes) } // ListCertificates returns all certificates for a tenant // GET /sdk/v1/training/certificates func (h *TrainingHandlers) ListCertificates(c *gin.Context) { tenantID := rbac.GetTenantID(c) certificates, err := h.store.ListCertificates(c.Request.Context(), tenantID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{ "certificates": certificates, "total": len(certificates), }) }