summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--internal/api/trello.go4
-rw-r--r--internal/handlers/handlers.go43
-rw-r--r--internal/handlers/heuristic_test.go97
-rw-r--r--internal/handlers/tabs.go17
-rw-r--r--internal/models/types.go14
-rw-r--r--web/templates/partials/tasks-tab.html4
6 files changed, 169 insertions, 10 deletions
diff --git a/internal/api/trello.go b/internal/api/trello.go
index 9c18ade..7140f79 100644
--- a/internal/api/trello.go
+++ b/internal/api/trello.go
@@ -225,6 +225,10 @@ func (c *TrelloClient) GetBoardsWithCards(ctx context.Context) ([]models.Board,
// Fetch cards
cards, err := c.GetCards(ctx, boards[i].ID)
if err == nil {
+ // Set BoardName for each card
+ for j := range cards {
+ cards[j].BoardName = boards[i].Name
+ }
// It is safe to write to specific indices of the slice concurrently
boards[i].Cards = cards
}
diff --git a/internal/handlers/handlers.go b/internal/handlers/handlers.go
index c3e49ed..9ba6351 100644
--- a/internal/handlers/handlers.go
+++ b/internal/handlers/handlers.go
@@ -6,6 +6,8 @@ import (
"html/template"
"log"
"net/http"
+ "sort"
+ "strings"
"sync"
"time"
@@ -302,6 +304,47 @@ func (h *Handler) aggregateData(ctx context.Context, forceRefresh bool) (*models
wg.Wait()
+ // Filter Trello cards into tasks based on heuristic
+ var trelloTasks []models.Card
+ for _, board := range data.Boards {
+ for _, card := range board.Cards {
+ listNameLower := strings.ToLower(card.ListName)
+ isTask := card.DueDate != nil ||
+ strings.Contains(listNameLower, "todo") ||
+ strings.Contains(listNameLower, "doing") ||
+ strings.Contains(listNameLower, "progress") ||
+ strings.Contains(listNameLower, "task")
+
+ if isTask {
+ trelloTasks = append(trelloTasks, card)
+ }
+ }
+ }
+
+ // Sort trelloTasks: earliest due date first, nil last, then by board name
+ sort.Slice(trelloTasks, func(i, j int) bool {
+ // Both have due dates: compare dates
+ if trelloTasks[i].DueDate != nil && trelloTasks[j].DueDate != nil {
+ if !trelloTasks[i].DueDate.Equal(*trelloTasks[j].DueDate) {
+ return trelloTasks[i].DueDate.Before(*trelloTasks[j].DueDate)
+ }
+ // Same due date, fall through to board name comparison
+ }
+
+ // Only one has due date: that one comes first
+ if trelloTasks[i].DueDate != nil && trelloTasks[j].DueDate == nil {
+ return true
+ }
+ if trelloTasks[i].DueDate == nil && trelloTasks[j].DueDate != nil {
+ return false
+ }
+
+ // Both nil or same due date: sort by board name
+ return trelloTasks[i].BoardName < trelloTasks[j].BoardName
+ })
+
+ data.TrelloTasks = trelloTasks
+
return data, nil
}
diff --git a/internal/handlers/heuristic_test.go b/internal/handlers/heuristic_test.go
new file mode 100644
index 0000000..f76fdc0
--- /dev/null
+++ b/internal/handlers/heuristic_test.go
@@ -0,0 +1,97 @@
+package handlers
+
+import (
+ "net/http/httptest"
+ "os"
+ "strings"
+ "testing"
+ "time"
+
+ "task-dashboard/internal/models"
+ "task-dashboard/internal/store"
+)
+
+func TestHandleTasks_Heuristic(t *testing.T) {
+ // Create temp database file
+ tmpFile, err := os.CreateTemp("", "test_heuristic_*.db")
+ if err != nil {
+ t.Fatalf("Failed to create temp db: %v", err)
+ }
+ tmpFile.Close()
+ defer os.Remove(tmpFile.Name())
+
+ // Save current directory and change to project root
+ originalDir, err := os.Getwd()
+ if err != nil {
+ t.Fatalf("Failed to get working directory: %v", err)
+ }
+
+ // Change to project root (2 levels up from internal/handlers)
+ if err := os.Chdir("../../"); err != nil {
+ t.Fatalf("Failed to change to project root: %v", err)
+ }
+ defer os.Chdir(originalDir)
+
+ // Initialize store (this runs migrations)
+ db, err := store.New(tmpFile.Name())
+ if err != nil {
+ t.Fatalf("Failed to initialize store: %v", err)
+ }
+ defer db.Close()
+
+ // Seed Data
+ // Board 1: Has actionable lists
+ board1 := models.Board{ID: "b1", Name: "Work Board"}
+
+ // Card 1: Has Due Date (Should appear)
+ due := time.Now()
+ card1 := models.Card{ID: "c1", Name: "Due Task", ListID: "l1", ListName: "Backlog", DueDate: &due, BoardName: "Work Board"}
+
+ // Card 2: No Due Date, Actionable List (Should appear)
+ card2 := models.Card{ID: "c2", Name: "Doing Task", ListID: "l2", ListName: "Doing", BoardName: "Work Board"}
+
+ // Card 3: No Due Date, Non-Actionable List (Should NOT appear)
+ card3 := models.Card{ID: "c3", Name: "Backlog Task", ListID: "l1", ListName: "Backlog", BoardName: "Work Board"}
+
+ // Card 4: No Due Date, "To Do" List (Should appear)
+ card4 := models.Card{ID: "c4", Name: "Todo Task", ListID: "l3", ListName: "To Do", BoardName: "Work Board"}
+
+ board1.Cards = []models.Card{card1, card2, card3, card4}
+
+ if err := db.SaveBoards([]models.Board{board1}); err != nil {
+ t.Fatalf("Failed to save boards: %v", err)
+ }
+
+ // Create Handler
+ h := NewTabsHandler(db)
+
+ // Skip if templates are not loaded
+ if h.templates == nil {
+ t.Skip("Templates not available in test environment")
+ }
+
+ req := httptest.NewRequest("GET", "/tabs/tasks", nil)
+ w := httptest.NewRecorder()
+
+ // Execute
+ h.HandleTasks(w, req)
+
+ // Verify
+ resp := w.Body.String()
+
+ // Check for presence of expected tasks
+ if !strings.Contains(resp, "Due Task") {
+ t.Errorf("Expected 'Due Task' to be present")
+ }
+ if !strings.Contains(resp, "Doing Task") {
+ t.Errorf("Expected 'Doing Task' to be present")
+ }
+ if !strings.Contains(resp, "Todo Task") {
+ t.Errorf("Expected 'Todo Task' to be present")
+ }
+
+ // Check for absence of non-expected tasks
+ if strings.Contains(resp, "Backlog Task") {
+ t.Errorf("Expected 'Backlog Task' to be ABSENT")
+ }
+}
diff --git a/internal/handlers/tabs.go b/internal/handlers/tabs.go
index c23910d..ce9c34f 100644
--- a/internal/handlers/tabs.go
+++ b/internal/handlers/tabs.go
@@ -5,12 +5,25 @@ import (
"log"
"net/http"
"sort"
+ "strings"
"time"
"task-dashboard/internal/models"
"task-dashboard/internal/store"
)
+// isActionableList returns true if the list name indicates an actionable list
+func isActionableList(name string) bool {
+ lower := strings.ToLower(name)
+ return strings.Contains(lower, "doing") ||
+ strings.Contains(lower, "in progress") ||
+ strings.Contains(lower, "to do") ||
+ strings.Contains(lower, "todo") ||
+ strings.Contains(lower, "tasks") ||
+ strings.Contains(lower, "next") ||
+ strings.Contains(lower, "today")
+}
+
// TabsHandler handles tab-specific rendering with Atom model
type TabsHandler struct {
store *store.Store
@@ -65,10 +78,10 @@ func (h *TabsHandler) HandleTasks(w http.ResponseWriter, r *http.Request) {
}
}
- // Convert Trello cards with due dates
+ // Convert Trello cards with due dates or in actionable lists
for _, board := range boards {
for _, card := range board.Cards {
- if card.DueDate != nil {
+ if card.DueDate != nil || isActionableList(card.ListName) {
atoms = append(atoms, models.CardToAtom(card))
}
}
diff --git a/internal/models/types.go b/internal/models/types.go
index 245f25f..fab732f 100644
--- a/internal/models/types.go
+++ b/internal/models/types.go
@@ -52,12 +52,13 @@ type Board struct {
// Card represents a Trello card
type Card struct {
- ID string `json:"id"`
- Name string `json:"name"`
- ListID string `json:"list_id"`
- ListName string `json:"list_name"`
- DueDate *time.Time `json:"due_date,omitempty"`
- URL string `json:"url"`
+ ID string `json:"id"`
+ Name string `json:"name"`
+ ListID string `json:"list_id"`
+ ListName string `json:"list_name"`
+ BoardName string `json:"board_name"`
+ DueDate *time.Time `json:"due_date,omitempty"`
+ URL string `json:"url"`
}
// Project represents a Todoist project
@@ -85,6 +86,7 @@ type DashboardData struct {
Notes []Note `json:"notes"`
Meals []Meal `json:"meals"`
Boards []Board `json:"boards,omitempty"`
+ TrelloTasks []Card `json:"trello_tasks,omitempty"`
Projects []Project `json:"projects,omitempty"`
LastUpdated time.Time `json:"last_updated"`
Errors []string `json:"errors,omitempty"`
diff --git a/web/templates/partials/tasks-tab.html b/web/templates/partials/tasks-tab.html
index f528553..43afa7d 100644
--- a/web/templates/partials/tasks-tab.html
+++ b/web/templates/partials/tasks-tab.html
@@ -63,9 +63,9 @@
</div>
{{else}}
<div class="bg-gray-50 rounded-lg p-8 text-center">
- <p class="text-gray-600">No upcoming tasks with due dates.</p>
+ <p class="text-gray-600">No upcoming tasks found.</p>
</div>
{{end}}
</section>
</div>
-{{end}}
+{{end}} \ No newline at end of file