feat: Archive tab — role-scoped list of archived tasks with search
New /archive page (nav link for all roles) backed by GET /api/v1/archive: - admins see every archived task; - consultants see archived tasks for customers they're assigned to; - developers see archived tasks they were assigned to, claimed, or acted on. Supports full-text ?q= search; cards link to the task detail view. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -8,6 +8,7 @@ import (
|
||||
"time"
|
||||
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
"go.mongodb.org/mongo-driver/v2/mongo/options"
|
||||
|
||||
"bountyboard/internal/atomize"
|
||||
"bountyboard/internal/domain"
|
||||
@@ -23,10 +24,66 @@ func (s *Server) routesTasks(mux *http.ServeMux) {
|
||||
mux.Handle("POST /api/v1/tasks/publish", s.authedRole("consultant", s.handlePublishBulk))
|
||||
mux.Handle("POST /api/v1/tasks/{id}/archive", s.authed(s.handleArchiveTask))
|
||||
mux.Handle("POST /api/v1/tasks/archive", s.authedRole("consultant", s.handleArchiveBulk))
|
||||
mux.Handle("GET /api/v1/archive", s.requireAuth(http.HandlerFunc(s.handleArchive)))
|
||||
mux.Handle("GET /api/v1/tasks/{id}", s.requireAuth(http.HandlerFunc(s.handleTaskDetail)))
|
||||
mux.Handle("GET /api/v1/service-health", s.requireAuth(http.HandlerFunc(s.handleServiceHealth)))
|
||||
}
|
||||
|
||||
// handleArchive lists archived tasks scoped by role: admins see everything,
|
||||
// consultants see their customers' tasks, developers see tasks they were
|
||||
// assigned to or worked on. Optional ?q= full-text search.
|
||||
func (s *Server) handleArchive(w http.ResponseWriter, r *http.Request) {
|
||||
me := CurrentUser(r.Context())
|
||||
q := bson.M{"status": domain.StatusArchived}
|
||||
if !me.Roles.Admin {
|
||||
ors := []bson.M{}
|
||||
if me.Roles.Consultant {
|
||||
cs, err := s.store.ListCustomers(r.Context(), me.ID, true)
|
||||
if err != nil {
|
||||
s.internalError(w, r, "archive customers", err)
|
||||
return
|
||||
}
|
||||
ids := make([]string, 0, len(cs))
|
||||
for _, c := range cs {
|
||||
ids = append(ids, c.ID)
|
||||
}
|
||||
ors = append(ors, bson.M{"customerId": bson.M{"$in": ids}})
|
||||
}
|
||||
if me.Roles.Developer {
|
||||
ors = append(ors,
|
||||
bson.M{"assignee.userId": me.ID},
|
||||
bson.M{"claimRequests.developerId": me.ID})
|
||||
}
|
||||
// everyone also sees archived tasks they personally acted on
|
||||
ors = append(ors, bson.M{"timeline.actorId": me.ID})
|
||||
q["$or"] = ors
|
||||
}
|
||||
if search := strings.TrimSpace(r.URL.Query().Get("q")); search != "" {
|
||||
q["$text"] = bson.M{"$search": search}
|
||||
}
|
||||
cur, err := s.store.DB.Collection("tasks").Find(r.Context(), q,
|
||||
options.Find().SetSort(bson.D{{Key: "_id", Value: -1}}).SetLimit(200))
|
||||
if err != nil {
|
||||
s.internalError(w, r, "list archive", err)
|
||||
return
|
||||
}
|
||||
tasks := []domain.Task{}
|
||||
if err := cur.All(r.Context(), &tasks); err != nil {
|
||||
s.internalError(w, r, "decode archive", err)
|
||||
return
|
||||
}
|
||||
// resolve customer names for display
|
||||
names := map[string]string{}
|
||||
for _, t := range tasks {
|
||||
if t.CustomerID != "" && names[t.CustomerID] == "" {
|
||||
if c, err := s.store.CustomerByID(r.Context(), t.CustomerID); err == nil {
|
||||
names[t.CustomerID] = c.Name
|
||||
}
|
||||
}
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]any{"tasks": tasks, "customerNames": names})
|
||||
}
|
||||
|
||||
// consultantTask loads a task and authorizes the current user: admins and
|
||||
// every consultant assigned to the task's customer may manage it (§4.4).
|
||||
func (s *Server) consultantTask(w http.ResponseWriter, r *http.Request, id string) (*domain.Task, *domain.Customer, bool) {
|
||||
|
||||
Reference in New Issue
Block a user