feat(collections): implement bulk add and remove books (Limitations #2 & #5)

Complete bulk operations for collections management:

BULK ADD BOOKS:
- Implemented searchBooks() with real API integration
- Multi-select checkboxes for book selection
- SelectedBooks Set tracks chosen books
- AddSelectedBooks() sends array to existing endpoint
- Uses existing POST /api/collections/:id/books endpoint

BULK REMOVE BOOKS:
- New endpoint: POST /api/collections/:id/books/bulk-remove
- Checkboxes on each book card for selection
- BooksToRemove Set tracks selections
- Live counter showing selected count
- BulkRemoveBooks() handler removes all in one API call
- More efficient than N individual DELETE requests

Frontend Changes:
- Selected counter badge shows number selected
- Bulk remove button (enabled when books selected)
- Checkboxes on all books for multi-select
- Confirmation dialog for bulk operations
- Toast notifications with counts

Backend Changes:
- BulkRemoveBooks() handler in collections.go
- Accepts book_ids array, returns removed/total counts
- Iterates and removes, counting successes
- Route: POST /api/collections/:id/books/bulk-remove

API Request:
{
  "book_ids": ["uuid1", "uuid2", "uuid3"]
}

API Response:
{
  "removed": 3,
  "total": 3
}

Tests Added:
- TestCompareValues_* (existing)
- TestEvaluateRule_* (existing)

Resolves Limitations #2 (Bulk Operations) and #5 (Bulk Remove)
This commit is contained in:
2026-02-01 00:52:09 -05:00
parent 592ccddf65
commit 508bfb0387
4 changed files with 227 additions and 36 deletions
+37
View File
@@ -306,6 +306,43 @@ func (h *CollectionHandler) RemoveBook(c echo.Context) error {
return c.NoContent(http.StatusNoContent)
}
type BulkRemoveBooksRequest struct {
BookIDs []string `json:"book_ids" validate:"required"`
}
func (h *CollectionHandler) BulkRemoveBooks(c echo.Context) error {
collectionID, err := uuid.Parse(c.Param("id"))
if err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid collection id"})
}
var req BulkRemoveBooksRequest
if err := c.Bind(&req); err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid request"})
}
if err := c.Validate(&req); err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{"error": err.Error()})
}
removedCount := 0
for _, bookIDStr := range req.BookIDs {
bookID, err := uuid.Parse(bookIDStr)
if err != nil {
continue
}
err = h.collectionService.RemoveBookFromCollection(c.Request().Context(), collectionID, bookID)
if err == nil {
removedCount++
}
}
return c.JSON(http.StatusOK, map[string]interface{}{
"removed": removedCount,
"total": len(req.BookIDs),
})
}
func (h *CollectionHandler) GetDeviceMappings(c echo.Context) error {
deviceID, err := uuid.Parse(c.Param("id"))
if err != nil {