package handlers // folders.go — Chat folder CRUD (v0.23.1) // // Folders are user-scoped groupings for personal (direct) chats. // Routes: // GET /api/v1/folders // POST /api/v1/folders // PUT /api/v1/folders/:id // DELETE /api/v1/folders/:id import ( "net/http" "strings" "time" "github.com/gin-gonic/gin" "git.gobha.me/xcaliber/chat-switchboard/database" ) type FolderHandler struct{} func NewFolderHandler() *FolderHandler { return &FolderHandler{} } type folderRow struct { ID string `json:"id"` Name string `json:"name"` ParentID *string `json:"parent_id,omitempty"` SortOrder int `json:"sort_order"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` } func (h *FolderHandler) List(c *gin.Context) { userID := getUserID(c) rows, err := database.DB.QueryContext(c.Request.Context(), database.Q(` SELECT id, name, parent_id, sort_order, created_at, updated_at FROM folders WHERE user_id = $1 ORDER BY sort_order, name `), userID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to list folders"}) return } defer rows.Close() var folders []folderRow for rows.Next() { var f folderRow if err := rows.Scan(&f.ID, &f.Name, &f.ParentID, &f.SortOrder, &f.CreatedAt, &f.UpdatedAt); err != nil { continue } folders = append(folders, f) } if folders == nil { folders = []folderRow{} } c.JSON(http.StatusOK, gin.H{"folders": folders}) } func (h *FolderHandler) Create(c *gin.Context) { userID := getUserID(c) var req struct { Name string `json:"name" binding:"required"` SortOrder int `json:"sort_order"` } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } req.Name = strings.TrimSpace(req.Name) if req.Name == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "name is required"}) return } var f folderRow err := database.DB.QueryRowContext(c.Request.Context(), database.Q(` INSERT INTO folders (user_id, name, sort_order) VALUES ($1, $2, $3) RETURNING id, name, parent_id, sort_order, created_at, updated_at `), userID, req.Name, req.SortOrder).Scan( &f.ID, &f.Name, &f.ParentID, &f.SortOrder, &f.CreatedAt, &f.UpdatedAt) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to create folder"}) return } c.JSON(http.StatusCreated, gin.H{"folder": f}) } func (h *FolderHandler) Update(c *gin.Context) { userID := getUserID(c) folderID := c.Param("id") var req struct { Name string `json:"name"` SortOrder *int `json:"sort_order"` } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } if req.Name != "" { req.Name = strings.TrimSpace(req.Name) } res, err := database.DB.ExecContext(c.Request.Context(), database.Q(` UPDATE folders SET name = COALESCE(NULLIF($3, ''), name), sort_order = COALESCE($4, sort_order) WHERE id = $1 AND user_id = $2 `), folderID, userID, req.Name, req.SortOrder) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to update folder"}) return } if n, _ := res.RowsAffected(); n == 0 { c.JSON(http.StatusNotFound, gin.H{"error": "folder not found"}) return } c.JSON(http.StatusOK, gin.H{"ok": true}) } func (h *FolderHandler) Delete(c *gin.Context) { userID := getUserID(c) folderID := c.Param("id") // Unassign chats before deleting folder _, _ = database.DB.ExecContext(c.Request.Context(), database.Q(` UPDATE channels SET folder_id = NULL WHERE folder_id = $1 AND user_id = $2 `), folderID, userID) res, err := database.DB.ExecContext(c.Request.Context(), database.Q(` DELETE FROM folders WHERE id = $1 AND user_id = $2 `), folderID, userID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to delete folder"}) return } if n, _ := res.RowsAffected(); n == 0 { c.JSON(http.StatusNotFound, gin.H{"error": "folder not found"}) return } c.JSON(http.StatusOK, gin.H{"ok": true}) }