All checks were successful
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-school (push) Successful in 28s
CI / test-go-edu-search (push) Successful in 27s
CI / test-python-klausur (push) Successful in 1m45s
CI / test-python-agent-core (push) Successful in 16s
CI / test-nodejs-website (push) Successful in 21s
- edu-search-service von breakpilot-pwa nach breakpilot-lehrer kopiert (ohne vendor) - opensearch + edu-search-service in docker-compose.yml hinzugefuegt - voice-service aus docker-compose.yml entfernt (jetzt in breakpilot-core) - geo-service aus docker-compose.yml entfernt (nicht mehr benoetigt) - CI/CD: edu-search-service zu Gitea Actions und Woodpecker hinzugefuegt (Go lint, test mit go mod download, build, SBOM) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
315 lines
9.0 KiB
Go
315 lines
9.0 KiB
Go
package handlers
|
|
|
|
import (
|
|
"net/http"
|
|
"strconv"
|
|
|
|
"github.com/breakpilot/edu-search-service/internal/orchestrator"
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
// AudienceHandler handles audience-related HTTP requests
|
|
type AudienceHandler struct {
|
|
repo orchestrator.AudienceRepository
|
|
}
|
|
|
|
// NewAudienceHandler creates a new audience handler
|
|
func NewAudienceHandler(repo orchestrator.AudienceRepository) *AudienceHandler {
|
|
return &AudienceHandler{repo: repo}
|
|
}
|
|
|
|
// CreateAudienceRequest represents a request to create an audience
|
|
type CreateAudienceRequest struct {
|
|
Name string `json:"name" binding:"required"`
|
|
Description string `json:"description"`
|
|
Filters orchestrator.AudienceFilters `json:"filters"`
|
|
CreatedBy string `json:"created_by"`
|
|
}
|
|
|
|
// UpdateAudienceRequest represents a request to update an audience
|
|
type UpdateAudienceRequest struct {
|
|
Name string `json:"name" binding:"required"`
|
|
Description string `json:"description"`
|
|
Filters orchestrator.AudienceFilters `json:"filters"`
|
|
IsActive bool `json:"is_active"`
|
|
}
|
|
|
|
// CreateExportRequest represents a request to create an export
|
|
type CreateExportRequest struct {
|
|
ExportType string `json:"export_type" binding:"required"` // csv, json, email_list
|
|
Purpose string `json:"purpose"`
|
|
ExportedBy string `json:"exported_by"`
|
|
}
|
|
|
|
// ListAudiences returns all audiences
|
|
func (h *AudienceHandler) ListAudiences(c *gin.Context) {
|
|
activeOnly := c.Query("active_only") == "true"
|
|
|
|
audiences, err := h.repo.ListAudiences(c.Request.Context(), activeOnly)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to list audiences", "details": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"audiences": audiences,
|
|
"count": len(audiences),
|
|
})
|
|
}
|
|
|
|
// GetAudience returns a single audience
|
|
func (h *AudienceHandler) GetAudience(c *gin.Context) {
|
|
idStr := c.Param("id")
|
|
id, err := uuid.Parse(idStr)
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid audience ID"})
|
|
return
|
|
}
|
|
|
|
audience, err := h.repo.GetAudience(c.Request.Context(), id)
|
|
if err != nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "Audience not found", "details": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, audience)
|
|
}
|
|
|
|
// CreateAudience creates a new audience
|
|
func (h *AudienceHandler) CreateAudience(c *gin.Context) {
|
|
var req CreateAudienceRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request body", "details": err.Error()})
|
|
return
|
|
}
|
|
|
|
audience := &orchestrator.Audience{
|
|
Name: req.Name,
|
|
Description: req.Description,
|
|
Filters: req.Filters,
|
|
CreatedBy: req.CreatedBy,
|
|
IsActive: true,
|
|
}
|
|
|
|
if err := h.repo.CreateAudience(c.Request.Context(), audience); err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create audience", "details": err.Error()})
|
|
return
|
|
}
|
|
|
|
// Update the member count
|
|
count, _ := h.repo.UpdateAudienceCount(c.Request.Context(), audience.ID)
|
|
audience.MemberCount = count
|
|
|
|
c.JSON(http.StatusCreated, audience)
|
|
}
|
|
|
|
// UpdateAudience updates an existing audience
|
|
func (h *AudienceHandler) UpdateAudience(c *gin.Context) {
|
|
idStr := c.Param("id")
|
|
id, err := uuid.Parse(idStr)
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid audience ID"})
|
|
return
|
|
}
|
|
|
|
var req UpdateAudienceRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request body", "details": err.Error()})
|
|
return
|
|
}
|
|
|
|
audience := &orchestrator.Audience{
|
|
ID: id,
|
|
Name: req.Name,
|
|
Description: req.Description,
|
|
Filters: req.Filters,
|
|
IsActive: req.IsActive,
|
|
}
|
|
|
|
if err := h.repo.UpdateAudience(c.Request.Context(), audience); err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update audience", "details": err.Error()})
|
|
return
|
|
}
|
|
|
|
// Update the member count
|
|
count, _ := h.repo.UpdateAudienceCount(c.Request.Context(), audience.ID)
|
|
audience.MemberCount = count
|
|
|
|
c.JSON(http.StatusOK, audience)
|
|
}
|
|
|
|
// DeleteAudience soft-deletes an audience
|
|
func (h *AudienceHandler) DeleteAudience(c *gin.Context) {
|
|
idStr := c.Param("id")
|
|
id, err := uuid.Parse(idStr)
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid audience ID"})
|
|
return
|
|
}
|
|
|
|
if err := h.repo.DeleteAudience(c.Request.Context(), id); err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete audience", "details": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{"deleted": true, "id": idStr})
|
|
}
|
|
|
|
// GetAudienceMembers returns members matching the audience filters
|
|
func (h *AudienceHandler) GetAudienceMembers(c *gin.Context) {
|
|
idStr := c.Param("id")
|
|
id, err := uuid.Parse(idStr)
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid audience ID"})
|
|
return
|
|
}
|
|
|
|
// Parse pagination
|
|
limit := 50
|
|
offset := 0
|
|
if l := c.Query("limit"); l != "" {
|
|
if parsed, err := strconv.Atoi(l); err == nil && parsed > 0 && parsed <= 500 {
|
|
limit = parsed
|
|
}
|
|
}
|
|
if o := c.Query("offset"); o != "" {
|
|
if parsed, err := strconv.Atoi(o); err == nil && parsed >= 0 {
|
|
offset = parsed
|
|
}
|
|
}
|
|
|
|
members, totalCount, err := h.repo.GetAudienceMembers(c.Request.Context(), id, limit, offset)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get members", "details": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"members": members,
|
|
"count": len(members),
|
|
"total_count": totalCount,
|
|
"limit": limit,
|
|
"offset": offset,
|
|
})
|
|
}
|
|
|
|
// RefreshAudienceCount recalculates the member count
|
|
func (h *AudienceHandler) RefreshAudienceCount(c *gin.Context) {
|
|
idStr := c.Param("id")
|
|
id, err := uuid.Parse(idStr)
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid audience ID"})
|
|
return
|
|
}
|
|
|
|
count, err := h.repo.UpdateAudienceCount(c.Request.Context(), id)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to refresh count", "details": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"audience_id": idStr,
|
|
"member_count": count,
|
|
})
|
|
}
|
|
|
|
// PreviewAudienceFilters previews the result of filters without saving
|
|
func (h *AudienceHandler) PreviewAudienceFilters(c *gin.Context) {
|
|
var filters orchestrator.AudienceFilters
|
|
if err := c.ShouldBindJSON(&filters); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request body", "details": err.Error()})
|
|
return
|
|
}
|
|
|
|
// Return the filters for now - preview functionality can be expanded later
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"filters": filters,
|
|
"message": "Preview functionality requires direct repository access",
|
|
})
|
|
}
|
|
|
|
// CreateExport creates a new export for an audience
|
|
func (h *AudienceHandler) CreateExport(c *gin.Context) {
|
|
idStr := c.Param("id")
|
|
id, err := uuid.Parse(idStr)
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid audience ID"})
|
|
return
|
|
}
|
|
|
|
var req CreateExportRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request body", "details": err.Error()})
|
|
return
|
|
}
|
|
|
|
// Get the member count for the export
|
|
_, totalCount, err := h.repo.GetAudienceMembers(c.Request.Context(), id, 1, 0)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get members", "details": err.Error()})
|
|
return
|
|
}
|
|
|
|
export := &orchestrator.AudienceExport{
|
|
AudienceID: id,
|
|
ExportType: req.ExportType,
|
|
RecordCount: totalCount,
|
|
ExportedBy: req.ExportedBy,
|
|
Purpose: req.Purpose,
|
|
}
|
|
|
|
if err := h.repo.CreateExport(c.Request.Context(), export); err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create export", "details": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusCreated, export)
|
|
}
|
|
|
|
// ListExports lists exports for an audience
|
|
func (h *AudienceHandler) ListExports(c *gin.Context) {
|
|
idStr := c.Param("id")
|
|
id, err := uuid.Parse(idStr)
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid audience ID"})
|
|
return
|
|
}
|
|
|
|
exports, err := h.repo.ListExports(c.Request.Context(), id)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to list exports", "details": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"exports": exports,
|
|
"count": len(exports),
|
|
})
|
|
}
|
|
|
|
// SetupAudienceRoutes configures audience API routes
|
|
func SetupAudienceRoutes(r *gin.RouterGroup, h *AudienceHandler) {
|
|
audiences := r.Group("/audiences")
|
|
{
|
|
// Audience CRUD
|
|
audiences.GET("", h.ListAudiences)
|
|
audiences.GET("/:id", h.GetAudience)
|
|
audiences.POST("", h.CreateAudience)
|
|
audiences.PUT("/:id", h.UpdateAudience)
|
|
audiences.DELETE("/:id", h.DeleteAudience)
|
|
|
|
// Members
|
|
audiences.GET("/:id/members", h.GetAudienceMembers)
|
|
audiences.POST("/:id/refresh", h.RefreshAudienceCount)
|
|
|
|
// Exports
|
|
audiences.GET("/:id/exports", h.ListExports)
|
|
audiences.POST("/:id/exports", h.CreateExport)
|
|
|
|
// Preview (no audience required)
|
|
audiences.POST("/preview", h.PreviewAudienceFilters)
|
|
}
|
|
}
|