From 2215aaa458b318edb16337ab56cf658117023eb4 Mon Sep 17 00:00:00 2001 From: Peter Stone Date: Mon, 19 Jan 2026 09:11:04 -1000 Subject: Implement Unified Quick Add for Tasks tab (Phase 3 Step 8) Add Quick Add form to create Todoist tasks or Trello cards directly from the Tasks tab with optional due date support. Features: - HandleUnifiedAdd handler with due date parsing - HandleGetListsOptions for dynamic Trello list loading - Quick Add form with source toggle (Todoist/Trello) - Date picker for due dates - HX-Trigger refresh after successful creation - Pass boards to tasks-tab template for board selector Cleanup: - Remove resolved issue tracking files Co-Authored-By: Claude Opus 4.5 --- cmd/dashboard/main.go | 4 ++ internal/handlers/handlers.go | 87 +++++++++++++++++++++++++++++++++++ internal/handlers/tabs.go | 6 ++- issues/bug_001_template_rendering.md | 17 ------- issues/bug_002_tab_state.md | 31 ------------- issues/phase3_step1_trello_write.md | 78 ------------------------------- issues/phase3_step2_trello_lists.md | 21 --------- issues/phase3_step3_trello_ui.md | 36 --------------- issues/phase3_step4_todoist_write.md | 40 ---------------- web/templates/partials/tasks-tab.html | 63 ++++++++++++++++++++++++- 10 files changed, 157 insertions(+), 226 deletions(-) delete mode 100644 issues/bug_001_template_rendering.md delete mode 100644 issues/bug_002_tab_state.md delete mode 100644 issues/phase3_step1_trello_write.md delete mode 100644 issues/phase3_step2_trello_lists.md delete mode 100644 issues/phase3_step3_trello_ui.md delete mode 100644 issues/phase3_step4_todoist_write.md diff --git a/cmd/dashboard/main.go b/cmd/dashboard/main.go index e8c2fa4..30b90ab 100644 --- a/cmd/dashboard/main.go +++ b/cmd/dashboard/main.go @@ -88,6 +88,10 @@ func main() { // Unified task completion (for Tasks tab Atoms) r.Post("/complete-atom", h.HandleCompleteAtom) + // Unified Quick Add (for Tasks tab) + r.Post("/unified-add", h.HandleUnifiedAdd) + r.Get("/partials/lists", h.HandleGetListsOptions) + // Serve static files fileServer := http.FileServer(http.Dir("web/static")) r.Handle("/static/*", http.StripPrefix("/static/", fileServer)) diff --git a/internal/handlers/handlers.go b/internal/handlers/handlers.go index b3bc8e4..20095fe 100644 --- a/internal/handlers/handlers.go +++ b/internal/handlers/handlers.go @@ -3,6 +3,7 @@ package handlers import ( "context" "encoding/json" + "fmt" "html/template" "log" "net/http" @@ -727,3 +728,89 @@ func (h *Handler) HandleCompleteAtom(w http.ResponseWriter, r *http.Request) { // Return 200 OK with empty body to remove the element from DOM w.WriteHeader(http.StatusOK) } + +// HandleUnifiedAdd creates a task in Todoist or a card in Trello from the Quick Add form +func (h *Handler) HandleUnifiedAdd(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + if err := r.ParseForm(); err != nil { + http.Error(w, "Failed to parse form", http.StatusBadRequest) + return + } + + title := r.FormValue("title") + source := r.FormValue("source") + dueDateStr := r.FormValue("due_date") + + if title == "" { + http.Error(w, "Title is required", http.StatusBadRequest) + return + } + + // Parse due date if provided + var dueDate *time.Time + if dueDateStr != "" { + parsed, err := time.Parse("2006-01-02", dueDateStr) + if err == nil { + dueDate = &parsed + } + } + + switch source { + case "todoist": + _, err := h.todoistClient.CreateTask(ctx, title, "", dueDate, 1) + if err != nil { + http.Error(w, "Failed to create Todoist task", http.StatusInternalServerError) + log.Printf("Error creating Todoist task: %v", err) + return + } + // Invalidate cache so fresh data is fetched + h.store.InvalidateCache("todoist_tasks") + + case "trello": + listID := r.FormValue("list_id") + if listID == "" { + http.Error(w, "List is required for Trello", http.StatusBadRequest) + return + } + _, err := h.trelloClient.CreateCard(ctx, listID, title, "", dueDate) + if err != nil { + http.Error(w, "Failed to create Trello card", http.StatusInternalServerError) + log.Printf("Error creating Trello card: %v", err) + return + } + // Invalidate cache so fresh data is fetched + h.store.InvalidateCache("trello_boards") + + default: + http.Error(w, "Invalid source", http.StatusBadRequest) + return + } + + // Trigger a refresh of the tasks tab via HTMX + w.Header().Set("HX-Trigger", "refresh-tasks") + w.WriteHeader(http.StatusOK) +} + +// HandleGetListsOptions returns HTML options for lists in a given board +func (h *Handler) HandleGetListsOptions(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + boardID := r.URL.Query().Get("board_id") + + if boardID == "" { + http.Error(w, "board_id is required", http.StatusBadRequest) + return + } + + lists, err := h.trelloClient.GetLists(ctx, boardID) + if err != nil { + http.Error(w, "Failed to fetch lists", http.StatusInternalServerError) + log.Printf("Error fetching lists for board %s: %v", boardID, err) + return + } + + w.Header().Set("Content-Type", "text/html") + for _, list := range lists { + fmt.Fprintf(w, ``, list.ID, list.Name) + } +} diff --git a/internal/handlers/tabs.go b/internal/handlers/tabs.go index ce9c34f..74dfbe8 100644 --- a/internal/handlers/tabs.go +++ b/internal/handlers/tabs.go @@ -110,9 +110,11 @@ func (h *TabsHandler) HandleTasks(w http.ResponseWriter, r *http.Request) { // Render template data := struct { - Atoms []models.Atom + Atoms []models.Atom + Boards []models.Board }{ - Atoms: atoms, + Atoms: atoms, + Boards: boards, } if err := h.templates.ExecuteTemplate(w, "tasks-tab", data); err != nil { diff --git a/issues/bug_001_template_rendering.md b/issues/bug_001_template_rendering.md deleted file mode 100644 index 61a8022..0000000 --- a/issues/bug_001_template_rendering.md +++ /dev/null @@ -1,17 +0,0 @@ -# Bug 001: Template Error in Notes Tab - -**Status:** Resolved -**Severity:** High (Runtime Panic/Error) -**Component:** Frontend/Handlers - -## Description -The `notes-tab` template attempts to render the `error-banner` partial, which expects an `.Errors` field in the data context. However, the `HandleNotes` handler was passing an anonymous struct containing only `Notes`, causing a template execution error. - -## Root Cause -Mismatch between template expectation (`{{.Errors}}`) and handler data structure (`struct { Notes []models.Note }`). - -## Fix -Updated `HandleNotes` in `internal/handlers/tabs.go` to include `Errors []string` in the data struct passed to the template. - -## Verification -A reproduction test case `internal/handlers/template_test.go` was created to verify that the `notes-tab` template can be successfully executed with the updated data structure. diff --git a/issues/bug_002_tab_state.md b/issues/bug_002_tab_state.md deleted file mode 100644 index c8c7c09..0000000 --- a/issues/bug_002_tab_state.md +++ /dev/null @@ -1,31 +0,0 @@ -# Bug 002: Tab State Persistence (RESOLVED) - -## Status -**RESOLVED** - -## Description -When a user switches tabs (e.g., to "Notes") and refreshes the page, the dashboard resets to the default "Tasks" tab. This is a poor user experience. The application should respect the `?tab=` query parameter and update the URL when tabs are switched. - -## Root Cause -1. **Server-Side:** `HandleDashboard` does not read the `tab` query parameter or pass it to the template. -2. **Client-Side:** `index.html` hardcodes the initial active tab to "Tasks". -3. **Client-Side:** Tab buttons use `hx-push-url="false"`, so the URL doesn't update on click. - -## Resolution -1. **Model Update:** Added `ActiveTab` field to `DashboardData` struct in `internal/models/types.go`. -2. **Handler Update:** Updated `HandleDashboard` in `internal/handlers/handlers.go` to: - * Read `r.URL.Query().Get("tab")`. - * Validate the tab name (defaulting to "tasks"). - * Set `ActiveTab` in the data passed to the template. -3. **Template Update:** Updated `web/templates/index.html` to: - * Use `{{if eq .ActiveTab "..."}}` to conditionally apply the `tab-button-active` class. - * Set the initial `hx-get` for `#tab-content` to `/tabs/{{.ActiveTab}}`. - * Set `hx-push-url="?tab=..."` on all tab buttons to ensure the URL updates in the browser history. -4. **Client-Side Update:** Updated `web/static/js/app.js` to initialize `currentTab` from the URL query parameter. - -## Verification -* **Automated Test:** Created `internal/handlers/tab_state_test.go` which verifies: - * Default load (`/`) renders "tasks" as active. - * Query param load (`/?tab=notes`) renders "notes" as active. - * All valid tab names are supported. -* **Manual Verification:** Confirmed that clicking tabs updates the URL and refreshing the page preserves the active tab. diff --git a/issues/phase3_step1_trello_write.md b/issues/phase3_step1_trello_write.md deleted file mode 100644 index 8f13e47..0000000 --- a/issues/phase3_step1_trello_write.md +++ /dev/null @@ -1,78 +0,0 @@ -# Phase 3 Step 1: Trello Write Operations - -**Status:** Active -**Priority:** High -**Feature:** Interactive Dashboard (Write Ops) - -## Description -Currently, the Trello client is read-only. We need to implement `CreateCard` and `UpdateCard` to enable interactivity (adding tasks, moving cards, completing items). - -## Requirements -1. **CreateCard:** - * Method: `POST /1/cards` - * Parameters: `name`, `idList`, `desc` (optional), `due` (optional). - * Returns: Created `models.Card`. - -2. **UpdateCard:** - * Method: `PUT /1/cards/{id}` - * Parameters: Flexible map of updates (e.g., `idList` to move, `closed=true` to archive). - * Returns: Updated `models.Card` (or just error). - -## Reproduction / Test Plan -Since we cannot hit the real Trello API in tests, we will use `httptest.Server` to mock the API responses. - -### `internal/api/trello_test.go` -```go -package api - -import ( - "encoding/json" - "net/http" - "net/http/httptest" - "testing" - "time" - - "task-dashboard/internal/models" -) - -func TestTrelloClient_CreateCard(t *testing.T) { - // Mock Server - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if r.Method != "POST" { - t.Errorf("Expected POST, got %s", r.Method) - } - if r.URL.Path != "/1/cards" { - t.Errorf("Expected /1/cards, got %s", r.URL.Path) - } - - // Verify params - r.ParseForm() - if r.Form.Get("name") != "New Task" { - t.Errorf("Expected name='New Task', got %s", r.Form.Get("name")) - } - - // Return mock response - card := models.Card{ - ID: "new-card-id", - Name: "New Task", - } - json.NewEncoder(w).Encode(card) - })) - defer server.Close() - - client := &TrelloClient{ - BaseURL: server.URL, - Key: "test-key", - Token: "test-token", - Client: server.Client(), - } - - card, err := client.CreateCard("list-id", "New Task", "Description", nil) - if err != nil { - t.Fatalf("CreateCard failed: %v", err) - } - if card.ID != "new-card-id" { - t.Errorf("Expected ID 'new-card-id', got %s", card.ID) - } -} -``` diff --git a/issues/phase3_step2_trello_lists.md b/issues/phase3_step2_trello_lists.md deleted file mode 100644 index f16e226..0000000 --- a/issues/phase3_step2_trello_lists.md +++ /dev/null @@ -1,21 +0,0 @@ -# Phase 3 Step 2: Trello Lists Support - -## Description -To enable "Add Card" functionality, we need to know the available lists for each board. Currently, the `Board` model does not include lists, and the API client only fetches lists internally to map names. - -## Requirements -1. **Update Models**: Add `List` struct and `Lists` field to `Board`. -2. **Update Interface**: Add `GetLists` to `TrelloAPI`. -3. **Update Client**: - * Refactor `getLists` to return `[]models.List`. - * Update `GetBoardsWithCards` to populate `board.Lists`. - * Implement public `GetLists`. - -## Plan -1. Modify `internal/models/types.go`. -2. Modify `internal/api/interfaces.go`. -3. Modify `internal/api/trello.go`. - -## Verification -* Run `go test ./internal/api/...` to ensure no regressions. -* (Implicit) The UI will eventually use this data. diff --git a/issues/phase3_step3_trello_ui.md b/issues/phase3_step3_trello_ui.md deleted file mode 100644 index 92dd8a5..0000000 --- a/issues/phase3_step3_trello_ui.md +++ /dev/null @@ -1,36 +0,0 @@ -# Phase 3 Step 3: Trello UI & Handlers - -**Status:** Open -**Priority:** High -**Created:** 2024-05-22 - -## Description -Implement the UI and backend handlers to enable creating and completing Trello cards directly from the dashboard. - -## Requirements - -### 1. UI Updates -* **Refactor:** Extract individual board rendering into a new partial `web/templates/partials/trello-board.html`. -* **Add Card:** Add a "Quick Add" form to each board (using `
` for simplicity) that allows selecting a List and entering a Name. -* **Complete Card:** Add a checkbox to each card that marks it as complete (archives/closes it). - -### 2. Backend Handlers -* `HandleCreateCard`: - * POST `/cards` - * Params: `board_id`, `list_id`, `name` - * Action: Call `CreateCard` API. - * Response: Re-render the specific board partial with updated data. -* `HandleCompleteCard`: - * POST `/cards/complete` - * Params: `card_id` - * Action: Call `UpdateCard` API (set `closed=true`). - * Response: Empty string (removes the card from UI via HTMX). - -### 3. Routing -* Register the new routes in `cmd/dashboard/main.go`. - -## Implementation Plan -1. Create `web/templates/partials/trello-board.html`. -2. Update `web/templates/partials/trello-boards.html`. -3. Implement handlers in `internal/handlers/handlers.go`. -4. Register routes in `cmd/dashboard/main.go`. diff --git a/issues/phase3_step4_todoist_write.md b/issues/phase3_step4_todoist_write.md deleted file mode 100644 index f68a963..0000000 --- a/issues/phase3_step4_todoist_write.md +++ /dev/null @@ -1,40 +0,0 @@ -# Phase 3 Step 4: Todoist Write Operations - -## Goal -Implement write operations for Todoist (Create Task, Complete Task) and update the UI to support them. - -## Requirements - -### Backend -1. **Models**: - * Add `Project` struct to `internal/models/types.go`. - * Add `Projects []Project` to `DashboardData`. -2. **API (`internal/api/todoist.go`)**: - * Refactor `GetProjects` to return `[]models.Project`. - * Implement `CreateTask(content, projectID string)`. - * Implement `CompleteTask(taskID string)`. - * Refactor `baseURL` to be a struct field for testability. -3. **Handlers (`internal/handlers/handlers.go`)**: - * Update `aggregateData` to fetch projects and populate `DashboardData`. - * Implement `HandleCreateTask`: - * Parse form (`content`, `project_id`). - * Call `CreateTask`. - * Return updated task list partial. - * Implement `HandleCompleteTask`: - * Call `CompleteTask`. - * Return empty string (to remove from DOM). - -### Frontend -1. **Template (`web/templates/partials/todoist-tasks.html`)**: - * Add "Quick Add" form at the top. - * Input: Task content. - * Select: Project (populated from `.Projects`). - * Update Task Items: - * Add Checkbox. - * `hx-post="/tasks/complete"`. - * `hx-target="closest .todoist-task-item"`. - * `hx-swap="outerHTML"`. - -## Verification -* Unit tests for API client. -* Manual verification of UI flows. diff --git a/web/templates/partials/tasks-tab.html b/web/templates/partials/tasks-tab.html index 9379e5c..ba9aa80 100644 --- a/web/templates/partials/tasks-tab.html +++ b/web/templates/partials/tasks-tab.html @@ -1,5 +1,66 @@ {{define "tasks-tab"}} -
+
+ +
+

Quick Add

+
+
+
+ +
+
+ +
+
+ +
+ +
+ + + +
+
+

-- cgit v1.2.3