diff options
| -rw-r--r-- | internal/api/trello.go | 4 | ||||
| -rw-r--r-- | internal/handlers/handlers.go | 43 | ||||
| -rw-r--r-- | internal/handlers/heuristic_test.go | 97 | ||||
| -rw-r--r-- | internal/handlers/tabs.go | 17 | ||||
| -rw-r--r-- | internal/models/types.go | 14 | ||||
| -rw-r--r-- | web/templates/partials/tasks-tab.html | 4 |
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 |
