feat(backend): update admin custom services handler
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
@@ -50,19 +50,18 @@ type CustomServiceListResponse struct {
|
||||
Total int64 `json:"total"`
|
||||
Page int `json:"page"`
|
||||
PerPage int `json:"per_page"`
|
||||
NextCursor *string `json:"next_cursor,omitempty"`
|
||||
}
|
||||
|
||||
// parseCursor splits a "createdAt|id" cursor string into its components.
|
||||
|
||||
func GetCustomServices(w http.ResponseWriter, r *http.Request) {
|
||||
pageStr := r.URL.Query().Get("page")
|
||||
perPageStr := r.URL.Query().Get("per_page")
|
||||
q := r.URL.Query().Get("q")
|
||||
popularStr := r.URL.Query().Get("popular")
|
||||
cursorStr := r.URL.Query().Get("cursor")
|
||||
|
||||
page := 1
|
||||
perPage := 20
|
||||
if p, err := strconv.Atoi(pageStr); err == nil && p > 0 {
|
||||
page = p
|
||||
}
|
||||
if pp, err := strconv.Atoi(perPageStr); err == nil && pp > 0 && pp <= 100 {
|
||||
perPage = pp
|
||||
}
|
||||
@@ -73,7 +72,7 @@ func GetCustomServices(w http.ResponseWriter, r *http.Request) {
|
||||
n = 3
|
||||
}
|
||||
rows, err := db.DB.Query(r.Context(), `
|
||||
SELECT id, name, description, price, duration_minutes, minimum_age_required, notes, created_at, created_by, usage_count, last_used_at
|
||||
SELECT id, name, price, duration_minutes, minimum_age_required, created_at, usage_count
|
||||
FROM custom_services
|
||||
WHERE usage_count > 0
|
||||
ORDER BY usage_count DESC, last_used_at DESC NULLS LAST
|
||||
@@ -88,23 +87,9 @@ func GetCustomServices(w http.ResponseWriter, r *http.Request) {
|
||||
var services []CustomService
|
||||
for rows.Next() {
|
||||
var cs CustomService
|
||||
var desc, notes, createdBy sql.NullString
|
||||
var lastUsedAt sql.NullTime
|
||||
if err := rows.Scan(&cs.ID, &cs.Name, &desc, &cs.Price, &cs.DurationMinutes, &cs.MinimumAgeRequired, ¬es, &cs.CreatedAt, &createdBy, &cs.UsageCount, &lastUsedAt); err != nil {
|
||||
if err := rows.Scan(&cs.ID, &cs.Name, &cs.Price, &cs.DurationMinutes, &cs.MinimumAgeRequired, &cs.CreatedAt, &cs.UsageCount); err != nil {
|
||||
continue
|
||||
}
|
||||
if desc.Valid {
|
||||
cs.Description = &desc.String
|
||||
}
|
||||
if notes.Valid {
|
||||
cs.Notes = ¬es.String
|
||||
}
|
||||
if createdBy.Valid {
|
||||
cs.CreatedBy = &createdBy.String
|
||||
}
|
||||
if lastUsedAt.Valid {
|
||||
cs.LastUsedAt = &lastUsedAt.Time
|
||||
}
|
||||
services = append(services, cs)
|
||||
}
|
||||
|
||||
@@ -116,40 +101,38 @@ func GetCustomServices(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
offset := (page - 1) * perPage
|
||||
|
||||
var countQuery string
|
||||
var countArgs []interface{}
|
||||
var dataQuery string
|
||||
var dataArgs []interface{}
|
||||
|
||||
if q != "" {
|
||||
countQuery = "SELECT COUNT(*) FROM custom_services WHERE name ILIKE $1 OR description ILIKE $1"
|
||||
countArgs = []interface{}{"%" + q + "%"}
|
||||
dataQuery = `
|
||||
SELECT id, name, description, price, duration_minutes, minimum_age_required, notes, created_at, created_by, usage_count, last_used_at
|
||||
SELECT id, name, price, duration_minutes, minimum_age_required, created_at, usage_count
|
||||
FROM custom_services
|
||||
WHERE name ILIKE $1 OR description ILIKE $1
|
||||
ORDER BY usage_count DESC, created_at DESC
|
||||
LIMIT $2 OFFSET $3
|
||||
`
|
||||
dataArgs = []interface{}{"%" + q + "%", perPage, offset}
|
||||
} else {
|
||||
countQuery = "SELECT COUNT(*) FROM custom_services"
|
||||
dataQuery = `
|
||||
SELECT id, name, description, price, duration_minutes, minimum_age_required, notes, created_at, created_by, usage_count, last_used_at
|
||||
FROM custom_services
|
||||
ORDER BY usage_count DESC, created_at DESC
|
||||
LIMIT $1 OFFSET $2
|
||||
`
|
||||
dataArgs = []interface{}{perPage, offset}
|
||||
}
|
||||
dataArgs = []interface{}{"%" + q + "%"}
|
||||
|
||||
var total int64
|
||||
if err := db.DB.QueryRow(r.Context(), countQuery, countArgs...).Scan(&total); err != nil {
|
||||
http.Error(w, "Failed to count custom services: "+err.Error(), http.StatusInternalServerError)
|
||||
dataQuery += " ORDER BY created_at DESC, id DESC LIMIT $" + strconv.Itoa(len(dataArgs)+1)
|
||||
dataArgs = append(dataArgs, perPage+1)
|
||||
} else {
|
||||
dataQuery = `
|
||||
SELECT id, name, price, duration_minutes, minimum_age_required, created_at, usage_count
|
||||
FROM custom_services
|
||||
`
|
||||
|
||||
// Cursor-based pagination
|
||||
if cursorStr != "" {
|
||||
cursorCreatedAt, cursorID, err := validators.ParseCursor(cursorStr)
|
||||
if err != nil {
|
||||
http.Error(w, "Invalid cursor: "+err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
dataQuery += " WHERE (created_at, id) < ($1, $2)"
|
||||
dataArgs = append(dataArgs, cursorCreatedAt, cursorID)
|
||||
}
|
||||
dataQuery += " ORDER BY created_at DESC, id DESC LIMIT $" + strconv.Itoa(len(dataArgs)+1)
|
||||
dataArgs = append(dataArgs, perPage+1)
|
||||
}
|
||||
|
||||
rows, err := db.DB.Query(r.Context(), dataQuery, dataArgs...)
|
||||
if err != nil {
|
||||
@@ -159,28 +142,36 @@ func GetCustomServices(w http.ResponseWriter, r *http.Request) {
|
||||
defer rows.Close()
|
||||
|
||||
var services []CustomService
|
||||
var total int64
|
||||
|
||||
// Compute total from whichever count query ran above.
|
||||
if q != "" {
|
||||
var countTotal int64
|
||||
db.DB.QueryRow(r.Context(), "SELECT COUNT(*) FROM custom_services WHERE name ILIKE $1 OR description ILIKE $1", "%"+q+"%").Scan(&countTotal)
|
||||
total = countTotal
|
||||
} else {
|
||||
var countTotal int64
|
||||
db.DB.QueryRow(r.Context(), "SELECT COUNT(*) FROM custom_services").Scan(&countTotal)
|
||||
total = countTotal
|
||||
}
|
||||
|
||||
for rows.Next() {
|
||||
var cs CustomService
|
||||
var desc, notes, createdBy sql.NullString
|
||||
var lastUsedAt sql.NullTime
|
||||
if err := rows.Scan(&cs.ID, &cs.Name, &desc, &cs.Price, &cs.DurationMinutes, &cs.MinimumAgeRequired, ¬es, &cs.CreatedAt, &createdBy, &cs.UsageCount, &lastUsedAt); err != nil {
|
||||
if err := rows.Scan(&cs.ID, &cs.Name, &cs.Price, &cs.DurationMinutes, &cs.MinimumAgeRequired, &cs.CreatedAt, &cs.UsageCount); err != nil {
|
||||
continue
|
||||
}
|
||||
if desc.Valid {
|
||||
cs.Description = &desc.String
|
||||
}
|
||||
if notes.Valid {
|
||||
cs.Notes = ¬es.String
|
||||
}
|
||||
if createdBy.Valid {
|
||||
cs.CreatedBy = &createdBy.String
|
||||
}
|
||||
if lastUsedAt.Valid {
|
||||
cs.LastUsedAt = &lastUsedAt.Time
|
||||
}
|
||||
services = append(services, cs)
|
||||
}
|
||||
|
||||
// nextCursor is set only when we fetched perPage+1 items, proving a next page exists.
|
||||
var nextCursor *string
|
||||
if len(services) > perPage {
|
||||
services = services[:perPage]
|
||||
last := services[len(services)-1]
|
||||
cursor := last.CreatedAt.Format(time.RFC3339) + "|" + last.ID
|
||||
nextCursor = &cursor
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
if services == nil {
|
||||
services = []CustomService{}
|
||||
@@ -188,8 +179,8 @@ func GetCustomServices(w http.ResponseWriter, r *http.Request) {
|
||||
json.NewEncoder(w).Encode(CustomServiceListResponse{
|
||||
Services: services,
|
||||
Total: total,
|
||||
Page: page,
|
||||
PerPage: perPage,
|
||||
NextCursor: nextCursor,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -261,8 +261,7 @@ func TestCustomServices_List_Pagination(t *testing.T) {
|
||||
|
||||
handler := http.HandlerFunc(GetCustomServices)
|
||||
|
||||
// First page with 2 per page
|
||||
w := makeAdminRequest(handler, "GET", "/api/admin/custom-services?page=1&per_page=2", nil)
|
||||
w := makeAdminRequest(handler, "GET", "/api/admin/custom-services?per_page=2", nil)
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("expected status 200, got %d. body: %s", w.Code, w.Body.String())
|
||||
}
|
||||
@@ -272,10 +271,6 @@ func TestCustomServices_List_Pagination(t *testing.T) {
|
||||
t.Fatalf("failed to parse response: %v", err)
|
||||
}
|
||||
|
||||
if resp.Page != 1 {
|
||||
t.Errorf("expected page 1, got %d", resp.Page)
|
||||
}
|
||||
|
||||
if resp.PerPage != 2 {
|
||||
t.Errorf("expected per_page 2, got %d", resp.PerPage)
|
||||
}
|
||||
@@ -285,7 +280,11 @@ func TestCustomServices_List_Pagination(t *testing.T) {
|
||||
}
|
||||
|
||||
if len(resp.Services) != 2 {
|
||||
t.Errorf("expected 2 services on page 1, got %d", len(resp.Services))
|
||||
t.Errorf("expected 2 services on first page, got %d", len(resp.Services))
|
||||
}
|
||||
|
||||
if resp.NextCursor == nil {
|
||||
t.Fatal("expected next_cursor when items remain")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user