package handlers import ( "net/http" "github.com/breakpilot/ai-compliance-sdk/internal/dsb" "github.com/gin-gonic/gin" "github.com/google/uuid" ) // DSBHandlers handles DSB-as-a-Service portal HTTP requests. type DSBHandlers struct { store *dsb.Store } // NewDSBHandlers creates new DSB handlers. func NewDSBHandlers(store *dsb.Store) *DSBHandlers { return &DSBHandlers{store: store} } // getDSBUserID extracts and parses the X-User-ID header as UUID. func getDSBUserID(c *gin.Context) (uuid.UUID, bool) { userIDStr := c.GetHeader("X-User-ID") if userIDStr == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "X-User-ID header is required"}) return uuid.Nil, false } userID, err := uuid.Parse(userIDStr) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid X-User-ID header: must be a valid UUID"}) return uuid.Nil, false } return userID, true } // ============================================================================ // Dashboard // ============================================================================ // GetDashboard returns the aggregated DSB dashboard. // GET /sdk/v1/dsb/dashboard func (h *DSBHandlers) GetDashboard(c *gin.Context) { dsbUserID, ok := getDSBUserID(c) if !ok { return } dashboard, err := h.store.GetDashboard(c.Request.Context(), dsbUserID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, dashboard) } // ============================================================================ // Assignments // ============================================================================ // CreateAssignment creates a new DSB-to-tenant assignment. // POST /sdk/v1/dsb/assignments func (h *DSBHandlers) CreateAssignment(c *gin.Context) { var req dsb.CreateAssignmentRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } assignment := &dsb.Assignment{ DSBUserID: req.DSBUserID, TenantID: req.TenantID, Status: req.Status, ContractStart: req.ContractStart, ContractEnd: req.ContractEnd, MonthlyHoursBudget: req.MonthlyHoursBudget, Notes: req.Notes, } if err := h.store.CreateAssignment(c.Request.Context(), assignment); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusCreated, gin.H{"assignment": assignment}) } // ListAssignments returns all assignments for the authenticated DSB user. // GET /sdk/v1/dsb/assignments func (h *DSBHandlers) ListAssignments(c *gin.Context) { dsbUserID, ok := getDSBUserID(c) if !ok { return } assignments, err := h.store.ListAssignments(c.Request.Context(), dsbUserID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{ "assignments": assignments, "total": len(assignments), }) } // GetAssignment retrieves a single assignment by ID. // GET /sdk/v1/dsb/assignments/:id func (h *DSBHandlers) 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.StatusNotFound, gin.H{"error": "assignment not found"}) return } c.JSON(http.StatusOK, gin.H{"assignment": assignment}) } // UpdateAssignment updates an existing assignment. // PUT /sdk/v1/dsb/assignments/:id func (h *DSBHandlers) 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 } assignment, err := h.store.GetAssignment(c.Request.Context(), id) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "assignment not found"}) return } var req dsb.UpdateAssignmentRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } // Apply non-nil fields if req.Status != nil { assignment.Status = *req.Status } if req.ContractEnd != nil { assignment.ContractEnd = req.ContractEnd } if req.MonthlyHoursBudget != nil { assignment.MonthlyHoursBudget = *req.MonthlyHoursBudget } if req.Notes != nil { assignment.Notes = *req.Notes } if err := h.store.UpdateAssignment(c.Request.Context(), assignment); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"assignment": assignment}) } // ============================================================================ // Hours // ============================================================================ // CreateHourEntry creates a new time tracking entry for an assignment. // POST /sdk/v1/dsb/assignments/:id/hours func (h *DSBHandlers) CreateHourEntry(c *gin.Context) { assignmentID, err := uuid.Parse(c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid assignment ID"}) return } var req dsb.CreateHourEntryRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } billable := true if req.Billable != nil { billable = *req.Billable } entry := &dsb.HourEntry{ AssignmentID: assignmentID, Date: req.Date, Hours: req.Hours, Category: req.Category, Description: req.Description, Billable: billable, } if err := h.store.CreateHourEntry(c.Request.Context(), entry); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusCreated, gin.H{"hour_entry": entry}) } // ListHours returns time entries for an assignment. // GET /sdk/v1/dsb/assignments/:id/hours?month=YYYY-MM func (h *DSBHandlers) ListHours(c *gin.Context) { assignmentID, err := uuid.Parse(c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid assignment ID"}) return } month := c.Query("month") entries, err := h.store.ListHours(c.Request.Context(), assignmentID, month) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{ "hours": entries, "total": len(entries), }) } // GetHoursSummary returns aggregated hour statistics for an assignment. // GET /sdk/v1/dsb/assignments/:id/hours/summary?month=YYYY-MM func (h *DSBHandlers) GetHoursSummary(c *gin.Context) { assignmentID, err := uuid.Parse(c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid assignment ID"}) return } month := c.Query("month") summary, err := h.store.GetHoursSummary(c.Request.Context(), assignmentID, month) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, summary) } // ============================================================================ // Tasks // ============================================================================ // CreateTask creates a new task for an assignment. // POST /sdk/v1/dsb/assignments/:id/tasks func (h *DSBHandlers) CreateTask(c *gin.Context) { assignmentID, err := uuid.Parse(c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid assignment ID"}) return } var req dsb.CreateTaskRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } task := &dsb.Task{ AssignmentID: assignmentID, Title: req.Title, Description: req.Description, Category: req.Category, Priority: req.Priority, DueDate: req.DueDate, } if err := h.store.CreateTask(c.Request.Context(), task); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusCreated, gin.H{"task": task}) } // ListTasks returns tasks for an assignment. // GET /sdk/v1/dsb/assignments/:id/tasks?status=open func (h *DSBHandlers) ListTasks(c *gin.Context) { assignmentID, err := uuid.Parse(c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid assignment ID"}) return } status := c.Query("status") tasks, err := h.store.ListTasks(c.Request.Context(), assignmentID, status) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{ "tasks": tasks, "total": len(tasks), }) } // UpdateTask updates an existing task. // PUT /sdk/v1/dsb/tasks/:taskId func (h *DSBHandlers) UpdateTask(c *gin.Context) { taskID, err := uuid.Parse(c.Param("taskId")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid task ID"}) return } // We need to fetch the existing task first. Since tasks belong to assignments, // we query by task ID directly. For now, we do a lightweight approach: bind the // update request and apply changes via store. var req dsb.UpdateTaskRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } // Fetch current task by querying all tasks and filtering. Since we don't have // a GetTask(taskID) method, we build the task from partial data and update. // The store UpdateTask uses the task ID to locate the row. task := &dsb.Task{ID: taskID} // We need to get the current values to apply partial updates correctly. // Query the task directly. row := h.store.Pool().QueryRow(c.Request.Context(), ` SELECT id, assignment_id, title, description, category, priority, status, due_date, completed_at, created_at, updated_at FROM dsb_tasks WHERE id = $1 `, taskID) if err := row.Scan( &task.ID, &task.AssignmentID, &task.Title, &task.Description, &task.Category, &task.Priority, &task.Status, &task.DueDate, &task.CompletedAt, &task.CreatedAt, &task.UpdatedAt, ); err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "task not found"}) return } // Apply non-nil fields if req.Title != nil { task.Title = *req.Title } if req.Description != nil { task.Description = *req.Description } if req.Category != nil { task.Category = *req.Category } if req.Priority != nil { task.Priority = *req.Priority } if req.Status != nil { task.Status = *req.Status } if req.DueDate != nil { task.DueDate = req.DueDate } if err := h.store.UpdateTask(c.Request.Context(), task); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"task": task}) } // CompleteTask marks a task as completed. // POST /sdk/v1/dsb/tasks/:taskId/complete func (h *DSBHandlers) CompleteTask(c *gin.Context) { taskID, err := uuid.Parse(c.Param("taskId")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid task ID"}) return } if err := h.store.CompleteTask(c.Request.Context(), taskID); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"message": "task completed"}) } // ============================================================================ // Communications // ============================================================================ // CreateCommunication creates a new communication log entry. // POST /sdk/v1/dsb/assignments/:id/communications func (h *DSBHandlers) CreateCommunication(c *gin.Context) { assignmentID, err := uuid.Parse(c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid assignment ID"}) return } var req dsb.CreateCommunicationRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } comm := &dsb.Communication{ AssignmentID: assignmentID, Direction: req.Direction, Channel: req.Channel, Subject: req.Subject, Content: req.Content, Participants: req.Participants, } if err := h.store.CreateCommunication(c.Request.Context(), comm); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusCreated, gin.H{"communication": comm}) } // ListCommunications returns all communications for an assignment. // GET /sdk/v1/dsb/assignments/:id/communications func (h *DSBHandlers) ListCommunications(c *gin.Context) { assignmentID, err := uuid.Parse(c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid assignment ID"}) return } comms, err := h.store.ListCommunications(c.Request.Context(), assignmentID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{ "communications": comms, "total": len(comms), }) }