package handlers import ( "net/http" "time" "github.com/gin-gonic/gin" "git.gobha.me/xcaliber/chat-switchboard/database" ) // ── Workflow Assignment Handler ───────────── // Manages the assignment queue for human review stages. type WorkflowAssignmentHandler struct{} func NewWorkflowAssignmentHandler() *WorkflowAssignmentHandler { return &WorkflowAssignmentHandler{} } type assignmentRow struct { ID string `json:"id"` ChannelID string `json:"channel_id"` Stage int `json:"stage"` TeamID string `json:"team_id"` AssignedTo *string `json:"assigned_to"` Status string `json:"status"` CreatedAt time.Time `json:"created_at"` ClaimedAt *time.Time `json:"claimed_at"` CompletedAt *time.Time `json:"completed_at"` } // ListForTeam returns unassigned + claimed assignments for a team. // GET /api/v1/teams/:teamId/assignments func (h *WorkflowAssignmentHandler) ListForTeam(c *gin.Context) { teamID := c.Param("teamId") status := c.DefaultQuery("status", "unassigned") rows, err := database.DB.QueryContext(c.Request.Context(), database.Q(` SELECT id, channel_id, stage, team_id, assigned_to, status, created_at, claimed_at, completed_at FROM workflow_assignments WHERE team_id = $1 AND status = $2 ORDER BY created_at ASC `), teamID, status) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to list assignments"}) return } defer rows.Close() var result []assignmentRow for rows.Next() { var a assignmentRow if err := rows.Scan(&a.ID, &a.ChannelID, &a.Stage, &a.TeamID, &a.AssignedTo, &a.Status, &a.CreatedAt, &a.ClaimedAt, &a.CompletedAt); err != nil { continue } result = append(result, a) } if result == nil { result = []assignmentRow{} } c.JSON(http.StatusOK, gin.H{"data": result}) } // ListForUser returns assignments claimed by the current user. // GET /api/v1/workflow-assignments/mine func (h *WorkflowAssignmentHandler) ListMine(c *gin.Context) { userID := c.GetString("user_id") rows, err := database.DB.QueryContext(c.Request.Context(), database.Q(` SELECT id, channel_id, stage, team_id, assigned_to, status, created_at, claimed_at, completed_at FROM workflow_assignments WHERE assigned_to = $1 AND status = 'claimed' ORDER BY claimed_at DESC `), userID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to list assignments"}) return } defer rows.Close() var result []assignmentRow for rows.Next() { var a assignmentRow if err := rows.Scan(&a.ID, &a.ChannelID, &a.Stage, &a.TeamID, &a.AssignedTo, &a.Status, &a.CreatedAt, &a.ClaimedAt, &a.CompletedAt); err != nil { continue } result = append(result, a) } if result == nil { result = []assignmentRow{} } c.JSON(http.StatusOK, gin.H{"data": result}) } // Claim assigns a workflow to the current user via optimistic lock. // POST /api/v1/workflow-assignments/:id/claim func (h *WorkflowAssignmentHandler) Claim(c *gin.Context) { assignmentID := c.Param("id") userID := c.GetString("user_id") now := time.Now().UTC() res, err := database.DB.ExecContext(c.Request.Context(), database.Q(` UPDATE workflow_assignments SET assigned_to = $1, status = 'claimed', claimed_at = $2 WHERE id = $3 AND status = 'unassigned' `), userID, now, assignmentID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to claim assignment"}) return } n, _ := res.RowsAffected() if n == 0 { c.JSON(http.StatusConflict, gin.H{"error": "assignment already claimed or not found"}) return } c.JSON(http.StatusOK, gin.H{"claimed": true, "assignment_id": assignmentID}) } // Complete marks an assignment as completed. // POST /api/v1/workflow-assignments/:id/complete func (h *WorkflowAssignmentHandler) Complete(c *gin.Context) { assignmentID := c.Param("id") now := time.Now().UTC() res, err := database.DB.ExecContext(c.Request.Context(), database.Q(` UPDATE workflow_assignments SET status = 'completed', completed_at = $1 WHERE id = $2 AND status = 'claimed' `), now, assignmentID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to complete assignment"}) return } n, _ := res.RowsAffected() if n == 0 { c.JSON(http.StatusConflict, gin.H{"error": "assignment not in claimed state"}) return } c.JSON(http.StatusOK, gin.H{"completed": true, "assignment_id": assignmentID}) }