diff options
| -rw-r--r-- | internal/handlers/handlers_test.go | 34 | ||||
| -rw-r--r-- | internal/handlers/timeline.go | 10 | ||||
| -rw-r--r-- | web/templates/partials/timeline-tab.html | 3 |
3 files changed, 47 insertions, 0 deletions
diff --git a/internal/handlers/handlers_test.go b/internal/handlers/handlers_test.go index d16c7cf..bb383aa 100644 --- a/internal/handlers/handlers_test.go +++ b/internal/handlers/handlers_test.go @@ -2321,3 +2321,37 @@ func TestHandleCompleteAtom_DootShowsTitle(t *testing.T) { } } +func TestHandleTimeline_IncludesBudgetStatusWhenTrackedTaskExists(t *testing.T) { + h, cleanup := setupTestHandler(t) + defer cleanup() + + project, err := h.store.CreateProject("Tracked", "#111111") + if err != nil { + t.Fatal(err) + } + if err := h.store.SetProjectBudgetTracked(project.ID, true); err != nil { + t.Fatal(err) + } + due := time.Now() + if err := h.store.CreateNativeTask(models.Task{ID: "t-tracked", Content: "Tracked", ProjectID: project.ID, DueDate: &due, EstimatedMinutes: 30}); err != nil { + t.Fatal(err) + } + + req := httptest.NewRequest("GET", "/tabs/timeline", nil) + w := httptest.NewRecorder() + h.HandleTimeline(w, req) + + mock := h.renderer.(*MockRenderer) + lastCall := mock.Calls[len(mock.Calls)-1] + data, ok := lastCall.Data.(TimelineData) + if !ok { + t.Fatalf("expected TimelineData, got %T", lastCall.Data) + } + if data.BudgetStatus == nil { + t.Fatal("expected non-nil BudgetStatus") + } + if data.BudgetStatus.Today.ScheduledMinutes != 30 { + t.Errorf("Today.ScheduledMinutes = %d, want 30", data.BudgetStatus.Today.ScheduledMinutes) + } +} + diff --git a/internal/handlers/timeline.go b/internal/handlers/timeline.go index 9656d4c..9284451 100644 --- a/internal/handlers/timeline.go +++ b/internal/handlers/timeline.go @@ -101,6 +101,10 @@ type TimelineData struct { // Current time for "now" line NowHour int NowMinute int + + // Opt-in budget/availability indicator for Today -- nil when no + // budget-tracked task exists (see computeBudgetStatus). + BudgetStatus *models.BudgetStatus } // HandleTimeline renders the timeline view @@ -204,6 +208,12 @@ func (h *Handler) HandleTimeline(w http.ResponseWriter, r *http.Request) { data.TomorrowHours = append(data.TomorrowHours, h) } + budgetStatus, err := h.computeBudgetStatus(now) + if err != nil { + log.Printf("Warning: failed to compute budget status: %v", err) + } + data.BudgetStatus = budgetStatus + HTMLResponse(w, h.renderer, "timeline-tab", data) } diff --git a/web/templates/partials/timeline-tab.html b/web/templates/partials/timeline-tab.html index 36f861c..d3b3fb6 100644 --- a/web/templates/partials/timeline-tab.html +++ b/web/templates/partials/timeline-tab.html @@ -116,6 +116,9 @@ <summary class="text-lg font-semibold mb-3 flex items-center gap-2 text-white/90 cursor-pointer hover:text-white sticky top-0 bg-black/20 backdrop-blur-md py-2 z-10 rounded-lg px-2 list-none"> <span>๐
</span> {{.TodayLabel}} <span class="text-sm font-normal text-white/50">({{len .TodayItems}} items)</span> + {{if .BudgetStatus}} + <span class="text-sm font-normal text-white/50">ยท {{.BudgetStatus.Today.ScheduledMinutes}}m scheduled / {{.BudgetStatus.Today.AvailableMinutes}}m available</span> + {{end}} <svg class="w-4 h-4 ml-auto transform transition-transform group-open:rotate-180" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path> </svg> |
