diff --git a/src/cmd/list.go b/src/cmd/list.go index 9091133..91582bf 100644 --- a/src/cmd/list.go +++ b/src/cmd/list.go @@ -11,6 +11,8 @@ import ( var lTags, lRels []string var lStatus, lPrio, lType, lNamespace, lAssignee, lMention string +var lDue bool +var lDueWithin int var listCmd = &cobra.Command{ Use: "list", Short: "List nodes", @@ -22,6 +24,11 @@ var listCmd = &cobra.Command{ } var filter service.ListFilter + filter.HasDueDate = lDue + if lDueWithin >= 0 { + n := lDueWithin + filter.DueWithin = &n + } // --tag is an alias for a label filter with no target. for _, tag := range lTags { @@ -76,4 +83,6 @@ func init() { f.StringVar(&lNamespace, "namespace", "", "filter by namespace") f.StringVar(&lAssignee, "assignee", "", "filter by assignee") f.StringVar(&lMention, "mention", "", "filter by mention") + f.BoolVar(&lDue, "due", false, "filter to nodes with a due date") + f.IntVar(&lDueWithin, "due-within", -1, "filter to nodes due within N days (includes overdue)") } diff --git a/src/serve/server.go b/src/serve/server.go index a40909b..cded2b3 100644 --- a/src/serve/server.go +++ b/src/serve/server.go @@ -6,6 +6,7 @@ import ( "axolotl/store" "encoding/json" "net/http" + "strconv" "strings" ) @@ -84,6 +85,14 @@ func (s *server) listNodes(w http.ResponseWriter, r *http.Request) { if v := q.Get("mention"); v != "" { filter.Rels = append(filter.Rels, service.RelInput{Type: models.RelMentions, Target: v}) } + if q.Get("has_due_date") == "true" { + filter.HasDueDate = true + } + if v := q.Get("due_within"); v != "" { + if n, err := strconv.Atoi(v); err == nil { + filter.DueWithin = &n + } + } nodes, err := svc.List(filter) if err != nil { writeError(w, http.StatusInternalServerError, err.Error()) diff --git a/src/service/api_client.go b/src/service/api_client.go index 3ccef2c..b0d4480 100644 --- a/src/service/api_client.go +++ b/src/service/api_client.go @@ -8,6 +8,7 @@ import ( "fmt" "net/http" "net/url" + "strconv" ) type apiClient struct { @@ -78,6 +79,12 @@ func (c *apiClient) List(filter ListFilter) ([]*models.Node, error) { q.Add("rel", string(r.Type)+":"+r.Target) } } + if filter.HasDueDate { + q.Set("has_due_date", "true") + } + if filter.DueWithin != nil { + q.Set("due_within", strconv.Itoa(*filter.DueWithin)) + } path := "/nodes" if len(q) > 0 { path += "?" + q.Encode() diff --git a/src/service/node_service.go b/src/service/node_service.go index 87779bb..5b0f766 100644 --- a/src/service/node_service.go +++ b/src/service/node_service.go @@ -56,7 +56,9 @@ type UpdateInput struct { // Tag filters (Target == "") match by rel_name prefix. // Edge filters (Target != "") are resolved to node IDs. type ListFilter struct { - Rels []RelInput + Rels []RelInput + HasDueDate bool // when true, only return nodes that have a due date set + DueWithin *int // when non-nil, only return nodes due within this many days (includes overdue) } // RelInput is a typed, directed rel with a target that may be a name or node ID. diff --git a/src/service/node_service_impl.go b/src/service/node_service_impl.go index 0ea1d58..bd3b7e4 100644 --- a/src/service/node_service_impl.go +++ b/src/service/node_service_impl.go @@ -234,6 +234,26 @@ func (s *nodeServiceImpl) List(filter ListFilter) ([]*models.Node, error) { result = append(result, n) } } + + if filter.HasDueDate || filter.DueWithin != nil { + now := time.Now().UTC().Truncate(24 * time.Hour) + filtered := result[:0] + for _, n := range result { + if n.DueDate == nil { + continue + } + if filter.DueWithin != nil { + due := n.DueDate.UTC().Truncate(24 * time.Hour) + cutoff := now.AddDate(0, 0, *filter.DueWithin) + if due.After(cutoff) { + continue + } + } + filtered = append(filtered, n) + } + result = filtered + } + return result, nil }