From 351587d06c75dfba88bf51c10fe63cd095c9097b Mon Sep 17 00:00:00 2001 From: Amolith Date: Sun, 21 Dec 2025 18:04:55 -0700 Subject: [PATCH] feat(stats): add TaskCounter for task counts Provides reusable helpers to count uncompleted tasks by area or goal ID with lazy loading of task list. Assisted-by: Claude Opus 4.5 via Crush --- internal/stats/stats.go | 90 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100644 internal/stats/stats.go diff --git a/internal/stats/stats.go b/internal/stats/stats.go new file mode 100644 index 0000000000000000000000000000000000000000..6cd4ebb6850a11ceba53f2a5f781e0dc4ff03d41 --- /dev/null +++ b/internal/stats/stats.go @@ -0,0 +1,90 @@ +// SPDX-FileCopyrightText: Amolith +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +// Package stats provides helpers for computing task statistics. +package stats + +import ( + "context" + "fmt" + + "git.secluded.site/go-lunatask" +) + +// TaskCounter provides methods to count tasks by various criteria. +type TaskCounter struct { + client *lunatask.Client + tasks []lunatask.Task +} + +// NewTaskCounter creates a counter that fetches tasks on first use. +func NewTaskCounter(client *lunatask.Client) *TaskCounter { + return &TaskCounter{client: client} +} + +// UncompletedInArea counts uncompleted tasks in the specified area. +func (tc *TaskCounter) UncompletedInArea(ctx context.Context, areaID string) (int, error) { + if err := tc.ensureTasks(ctx); err != nil { + return 0, err + } + + count := 0 + + for _, task := range tc.tasks { + if !isUncompleted(task) { + continue + } + + if task.AreaID != nil && *task.AreaID == areaID { + count++ + } + } + + return count, nil +} + +// UncompletedInGoal counts uncompleted tasks in the specified goal. +func (tc *TaskCounter) UncompletedInGoal(ctx context.Context, goalID string) (int, error) { + if err := tc.ensureTasks(ctx); err != nil { + return 0, err + } + + count := 0 + + for _, task := range tc.tasks { + if !isUncompleted(task) { + continue + } + + if task.GoalID != nil && *task.GoalID == goalID { + count++ + } + } + + return count, nil +} + +// ensureTasks fetches tasks if not already loaded. +func (tc *TaskCounter) ensureTasks(ctx context.Context) error { + if tc.tasks != nil { + return nil + } + + tasks, err := tc.client.ListTasks(ctx, nil) + if err != nil { + return fmt.Errorf("listing tasks: %w", err) + } + + tc.tasks = tasks + + return nil +} + +func isUncompleted(task lunatask.Task) bool { + if task.Status == nil { + return true + } + + return *task.Status != lunatask.StatusCompleted +}