summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPeter Stone <thepeterstone@gmail.com>2026-01-19 09:11:04 -1000
committerPeter Stone <thepeterstone@gmail.com>2026-01-19 09:11:04 -1000
commit2215aaa458b318edb16337ab56cf658117023eb4 (patch)
tree73dc62cb8ed385e8ab5d255825b03ffb8845b27e
parent791034f1b588bf679f45a0f89168515fcbde66d5 (diff)
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 <noreply@anthropic.com>
-rw-r--r--cmd/dashboard/main.go4
-rw-r--r--internal/handlers/handlers.go87
-rw-r--r--internal/handlers/tabs.go6
-rw-r--r--issues/bug_001_template_rendering.md17
-rw-r--r--issues/bug_002_tab_state.md31
-rw-r--r--issues/phase3_step1_trello_write.md78
-rw-r--r--issues/phase3_step2_trello_lists.md21
-rw-r--r--issues/phase3_step3_trello_ui.md36
-rw-r--r--issues/phase3_step4_todoist_write.md40
-rw-r--r--web/templates/partials/tasks-tab.html63
10 files changed, 157 insertions, 226 deletions
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, `<option value="%s">%s</option>`, 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 `<details>` 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"}}
-<div class="space-y-6">
+<div class="space-y-6"
+ hx-get="/tabs/tasks"
+ hx-trigger="refresh-tasks from:body"
+ hx-target="#tab-content"
+ hx-swap="innerHTML">
+ <!-- Quick Add Form -->
+ <section class="bg-white rounded-lg p-4 shadow-sm">
+ <h3 class="text-lg font-semibold text-gray-900 mb-3">Quick Add</h3>
+ <form hx-post="/unified-add"
+ hx-swap="none"
+ hx-on::after-request="if(event.detail.successful) { this.reset(); document.getElementById('trello-fields').style.display = 'none'; }">
+ <div class="flex flex-wrap gap-2 items-end">
+ <div class="flex-1 min-w-[200px]">
+ <input type="text"
+ name="title"
+ placeholder="Task name..."
+ class="w-full border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-primary-500 focus:border-primary-500"
+ required>
+ </div>
+ <div>
+ <input type="date"
+ name="due_date"
+ class="border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-primary-500">
+ </div>
+ <div>
+ <select name="source"
+ class="border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-primary-500"
+ onchange="document.getElementById('trello-fields').style.display = this.value === 'trello' ? 'flex' : 'none'">
+ <option value="todoist">Todoist</option>
+ <option value="trello">Trello</option>
+ </select>
+ </div>
+ <button type="submit"
+ class="bg-primary-600 hover:bg-primary-700 text-white px-4 py-2 rounded-lg transition-colors font-medium">
+ Add
+ </button>
+ </div>
+
+ <!-- Trello Fields (Hidden by default) -->
+ <div id="trello-fields" class="flex flex-wrap gap-2 mt-3" style="display: none;">
+ <div>
+ <select name="board_id"
+ class="border border-gray-300 rounded-lg px-3 py-2 text-sm"
+ hx-get="/partials/lists"
+ hx-target="#list-select"
+ hx-trigger="change"
+ hx-swap="innerHTML">
+ <option value="">Select Board...</option>
+ {{range .Boards}}
+ <option value="{{.ID}}">{{.Name}}</option>
+ {{end}}
+ </select>
+ </div>
+ <div>
+ <select id="list-select" name="list_id" class="border border-gray-300 rounded-lg px-3 py-2 text-sm">
+ <option value="">Select List...</option>
+ </select>
+ </div>
+ </div>
+ </form>
+ </section>
+
<!-- Unified Tasks Section -->
<section>
<h2 class="section-header mb-6">