1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
|
package handlers
import (
"errors"
"net/http"
"strconv"
"strings"
"github.com/go-chi/chi/v5"
"task-dashboard/internal/models"
"task-dashboard/internal/store"
)
// BuildBucketSummaries returns every maintenance bucket with its full pool
// (dormant and active items both). Backs the Settings page's bucket
// management section.
func BuildBucketSummaries(s *store.Store) ([]models.BucketSummary, error) {
buckets, err := s.GetBuckets()
if err != nil {
return nil, err
}
summaries := make([]models.BucketSummary, 0, len(buckets))
for _, bucket := range buckets {
items, err := s.GetBucketItems(bucket.ID)
if err != nil {
return nil, err
}
summaries = append(summaries, models.BucketSummary{Bucket: bucket, Items: items})
}
return summaries, nil
}
// HandleBucketsCreate creates a new maintenance bucket from the Settings page's form.
func (h *Handler) HandleBucketsCreate(w http.ResponseWriter, r *http.Request) {
if err := r.ParseForm(); err != nil {
JSONError(w, http.StatusBadRequest, "Failed to parse form", err)
return
}
name := strings.TrimSpace(r.FormValue("name"))
cycleDays, err1 := strconv.Atoi(r.FormValue("cycle_days"))
pickN, err2 := strconv.Atoi(r.FormValue("pick_n"))
if name == "" || err1 != nil || err2 != nil || cycleDays <= 0 || pickN <= 0 {
JSONError(w, http.StatusBadRequest, "Name, a positive cycle_days, and a positive pick_n are required", nil)
return
}
if _, err := h.store.CreateBucket(name, cycleDays, pickN); err != nil {
JSONError(w, http.StatusInternalServerError, "Failed to create bucket", err)
return
}
h.renderBucketsSection(w, r)
}
// HandleBucketItemAdd creates a new native task from the form's title and
// assigns it to the bucket's pool in one step -- the Settings page's
// "type a title to add to the pool" flow. To add a task that already
// exists elsewhere, use the /api/widget/buckets/{id}/items endpoint with
// its id directly (unchanged, still task_id-based).
func (h *Handler) HandleBucketItemAdd(w http.ResponseWriter, r *http.Request) {
bucketID := chi.URLParam(r, "id")
if err := r.ParseForm(); err != nil {
JSONError(w, http.StatusBadRequest, "Failed to parse form", err)
return
}
title := strings.TrimSpace(r.FormValue("title"))
if title == "" {
JSONError(w, http.StatusBadRequest, "title is required", nil)
return
}
task := models.Task{ID: newID(), Content: title, Priority: 1}
if err := h.store.CreateNativeTask(task); err != nil {
JSONError(w, http.StatusInternalServerError, "Failed to create task", err)
return
}
if err := h.store.AddBucketItem(bucketID, task.ID); err != nil {
JSONError(w, http.StatusInternalServerError, "Failed to add task to bucket", err)
return
}
h.renderBucketsSection(w, r)
}
// HandleBucketItemRemove clears a task's bucket membership from the
// Settings page (the task itself is left in place, just unbucketed).
func (h *Handler) HandleBucketItemRemove(w http.ResponseWriter, r *http.Request) {
if err := r.ParseForm(); err != nil {
JSONError(w, http.StatusBadRequest, "Failed to parse form", err)
return
}
taskID := r.FormValue("task_id")
if err := h.store.RemoveBucketItem(taskID); err != nil {
if errors.Is(err, store.ErrNativeTaskNotFound) {
JSONError(w, http.StatusNotFound, "task not found", err)
return
}
JSONError(w, http.StatusInternalServerError, "Failed to remove bucket item", err)
return
}
h.renderBucketsSection(w, r)
}
// HandleBucketDelete removes a bucket, returning its member tasks to being
// plain unbucketed tasks.
func (h *Handler) HandleBucketDelete(w http.ResponseWriter, r *http.Request) {
id := chi.URLParam(r, "id")
if err := h.store.DeleteBucket(id); err != nil {
if errors.Is(err, store.ErrNativeTaskNotFound) {
JSONError(w, http.StatusNotFound, "bucket not found", err)
return
}
JSONError(w, http.StatusInternalServerError, "Failed to delete bucket", err)
return
}
h.renderBucketsSection(w, r)
}
// renderBucketsSection re-renders the "buckets-section" partial after any
// bucket mutation, so the Settings page's htmx targets can swap in the
// fresh pool contents without a full page reload.
func (h *Handler) renderBucketsSection(w http.ResponseWriter, r *http.Request) {
summaries, err := BuildBucketSummaries(h.store)
if err != nil {
JSONError(w, http.StatusInternalServerError, "Failed to load buckets", err)
return
}
data := struct {
Buckets []models.BucketSummary
}{Buckets: summaries}
if err := h.renderer.Render(w, "buckets-section", data); err != nil {
JSONError(w, http.StatusInternalServerError, "Failed to render buckets", err)
}
}
|