diff options
| -rw-r--r-- | PHASE_2_SURGICAL_PLAN.md | 2 | ||||
| -rw-r--r-- | SESSION_STATE.md | 19 | ||||
| -rw-r--r-- | internal/handlers/tabs.go | 178 | ||||
| -rw-r--r-- | web/templates/index.html | 18 | ||||
| -rw-r--r-- | web/templates/partials/meals-tab.html | 6 | ||||
| -rw-r--r-- | web/templates/partials/planning-tab.html | 6 | ||||
| -rw-r--r-- | web/templates/partials/tasks-tab.html | 79 |
7 files changed, 287 insertions, 21 deletions
diff --git a/PHASE_2_SURGICAL_PLAN.md b/PHASE_2_SURGICAL_PLAN.md index dbac3a6..e300639 100644 --- a/PHASE_2_SURGICAL_PLAN.md +++ b/PHASE_2_SURGICAL_PLAN.md @@ -25,7 +25,7 @@ Implement mapper functions. ``` ## 2. Information Architecture: The 4-Tab Split -**Status:** [ ] Pending +**Status:** [x] Complete ```text Refactor the frontend and router to support 4 distinct tabs. diff --git a/SESSION_STATE.md b/SESSION_STATE.md index e3a498e..dd8e30f 100644 --- a/SESSION_STATE.md +++ b/SESSION_STATE.md @@ -39,6 +39,18 @@ Frontend modernization with tabs, HTMX, and Tailwind build pipeline complete. - Priority normalization (1-4 scale), brand color mapping (Trello=Blue, Todoist=Red, Obsidian=Purple, PlanToEat=Green) - Preserves raw data for future write operations - All tests passing after implementation +- **4-Tab Architecture:** Implemented unified information architecture using Atom model + - internal/handlers/tabs.go: New TabsHandler with 4 specialized methods + - HandleTasks: Unified view of Todoist + Trello cards with due dates, converted to Atoms, sorted by due date and priority + - HandlePlanning: Trello boards view for project planning + - HandleNotes: Obsidian notes view + - HandleMeals: PlanToEat meals view + - cmd/dashboard/main.go: Registered 4 tab routes (/tabs/tasks, /tabs/planning, /tabs/notes, /tabs/meals) + - web/templates/index.html: Updated navigation with 4 tabs + - web/templates/partials/tasks-tab.html: Rewritten to render unified Atom list with source icons, priorities, and brand colors + - web/templates/partials/planning-tab.html: New tab for Trello boards + - web/templates/partials/meals-tab.html: New tab for PlanToEat meals + - Clean separation of concerns: Tasks (due items), Planning (all boards), Notes (knowledge), Meals (calendar) - **Build Pipeline:** npm + PostCSS + Tailwind configuration (replaced CDN) - package.json, tailwind.config.js, postcss.config.js, Makefile - Custom design system with brand colors (Trello, Todoist, Obsidian, PlanToEat) @@ -82,10 +94,9 @@ Frontend modernization with tabs, HTMX, and Tailwind build pipeline complete. - **Decision:** Unified Atom Model - Abstract all data sources (Trello, Todoist, Obsidian, PlanToEat) into a single `models.Atom` type for consistent handling, sorting, and rendering across the UI. ## 📋 Next Steps -1. **Phase 2 Step 2:** Implement 4-Tab Split (Tasks, Planning, Notes, Meals) using the Atom model. -2. **Phase 2 Step 3:** Trello smart sorting (activity-based, modification date). -3. **Phase 2 Step 4:** Todoist "due first" sorting. -4. **Phase 2 Remaining:** Search, visual overhaul, write operations, PWA. +1. **Phase 2 Step 3:** Trello smart sorting (activity-based, modification date). +2. **Phase 2 Step 4:** Todoist "due first" sorting. +3. **Phase 2 Remaining:** Obsidian search & categorization, visual overhaul (glassmorphism), write operations, PWA. ## ⚠️ Known Blockers / Debt - None currently. diff --git a/internal/handlers/tabs.go b/internal/handlers/tabs.go new file mode 100644 index 0000000..fa60a10 --- /dev/null +++ b/internal/handlers/tabs.go @@ -0,0 +1,178 @@ +package handlers + +import ( + "html/template" + "log" + "net/http" + "sort" + "time" + + "task-dashboard/internal/models" + "task-dashboard/internal/store" +) + +// TabsHandler handles tab-specific rendering with Atom model +type TabsHandler struct { + store *store.Store + templates *template.Template +} + +// NewTabsHandler creates a new TabsHandler instance +func NewTabsHandler(store *store.Store) *TabsHandler { + // Parse templates including partials + tmpl, err := template.ParseGlob("web/templates/*.html") + if err != nil { + log.Printf("Warning: failed to parse templates: %v", err) + } + + // Also parse partials + tmpl, err = tmpl.ParseGlob("web/templates/partials/*.html") + if err != nil { + log.Printf("Warning: failed to parse partial templates: %v", err) + } + + return &TabsHandler{ + store: store, + templates: tmpl, + } +} + +// HandleTasks renders the unified Tasks tab (Todoist + Trello cards with due dates) +func (h *TabsHandler) HandleTasks(w http.ResponseWriter, r *http.Request) { + // Fetch Todoist tasks + tasks, err := h.store.GetTasks() + if err != nil { + http.Error(w, "Failed to fetch tasks", http.StatusInternalServerError) + log.Printf("Error fetching tasks: %v", err) + return + } + + // Fetch Trello boards + boards, err := h.store.GetBoards() + if err != nil { + http.Error(w, "Failed to fetch boards", http.StatusInternalServerError) + log.Printf("Error fetching boards: %v", err) + return + } + + // Convert to Atoms + atoms := make([]models.Atom, 0) + + // Convert Todoist tasks + for _, task := range tasks { + if !task.Completed { + atoms = append(atoms, models.TaskToAtom(task)) + } + } + + // Convert Trello cards with due dates + for _, board := range boards { + for _, card := range board.Cards { + if card.DueDate != nil { + atoms = append(atoms, models.CardToAtom(card)) + } + } + } + + // Sort atoms: by DueDate (earliest first), then by Priority (descending) + sort.SliceStable(atoms, func(i, j int) bool { + // Handle nil due dates (push to end) + if atoms[i].DueDate == nil && atoms[j].DueDate != nil { + return false + } + if atoms[i].DueDate != nil && atoms[j].DueDate == nil { + return true + } + + // Both have due dates, sort by date + if atoms[i].DueDate != nil && atoms[j].DueDate != nil { + if !atoms[i].DueDate.Equal(*atoms[j].DueDate) { + return atoms[i].DueDate.Before(*atoms[j].DueDate) + } + } + + // Same due date (or both nil), sort by priority (descending) + return atoms[i].Priority > atoms[j].Priority + }) + + // Render template + data := struct { + Atoms []models.Atom + }{ + Atoms: atoms, + } + + if err := h.templates.ExecuteTemplate(w, "tasks-tab", data); err != nil { + http.Error(w, "Failed to render template", http.StatusInternalServerError) + log.Printf("Error rendering tasks tab: %v", err) + } +} + +// HandlePlanning renders the Planning tab (Trello boards) +func (h *TabsHandler) HandlePlanning(w http.ResponseWriter, r *http.Request) { + // Fetch Trello boards + boards, err := h.store.GetBoards() + if err != nil { + http.Error(w, "Failed to fetch boards", http.StatusInternalServerError) + log.Printf("Error fetching boards: %v", err) + return + } + + data := struct { + Boards []models.Board + }{ + Boards: boards, + } + + if err := h.templates.ExecuteTemplate(w, "planning-tab", data); err != nil { + http.Error(w, "Failed to render template", http.StatusInternalServerError) + log.Printf("Error rendering planning tab: %v", err) + } +} + +// HandleNotes renders the Notes tab (Obsidian notes) +func (h *TabsHandler) HandleNotes(w http.ResponseWriter, r *http.Request) { + // Fetch Obsidian notes + notes, err := h.store.GetNotes(20) + if err != nil { + http.Error(w, "Failed to fetch notes", http.StatusInternalServerError) + log.Printf("Error fetching notes: %v", err) + return + } + + data := struct { + Notes []models.Note + }{ + Notes: notes, + } + + if err := h.templates.ExecuteTemplate(w, "notes-tab", data); err != nil { + http.Error(w, "Failed to render template", http.StatusInternalServerError) + log.Printf("Error rendering notes tab: %v", err) + } +} + +// HandleMeals renders the Meals tab (PlanToEat) +func (h *TabsHandler) HandleMeals(w http.ResponseWriter, r *http.Request) { + // Fetch meals for next 7 days + startDate := time.Now() + endDate := startDate.AddDate(0, 0, 7) + + meals, err := h.store.GetMeals(startDate, endDate) + if err != nil { + http.Error(w, "Failed to fetch meals", http.StatusInternalServerError) + log.Printf("Error fetching meals: %v", err) + return + } + + data := struct { + Meals []models.Meal + }{ + Meals: meals, + } + + if err := h.templates.ExecuteTemplate(w, "meals-tab", data); err != nil { + http.Error(w, "Failed to render template", http.StatusInternalServerError) + log.Printf("Error rendering meals tab: %v", err) + } +} diff --git a/web/templates/index.html b/web/templates/index.html index 2d35b37..b544ec3 100644 --- a/web/templates/index.html +++ b/web/templates/index.html @@ -31,7 +31,15 @@ hx-target="#tab-content" hx-push-url="false" onclick="setActiveTab(this)"> - 📋 Tasks & Planning + ✓ Tasks + </button> + <button + class="tab-button" + hx-get="/tabs/planning" + hx-target="#tab-content" + hx-push-url="false" + onclick="setActiveTab(this)"> + 📋 Planning </button> <button class="tab-button" @@ -41,6 +49,14 @@ onclick="setActiveTab(this)"> 📝 Notes </button> + <button + class="tab-button" + hx-get="/tabs/meals" + hx-target="#tab-content" + hx-push-url="false" + onclick="setActiveTab(this)"> + 🍽️ Meals + </button> </nav> </div> diff --git a/web/templates/partials/meals-tab.html b/web/templates/partials/meals-tab.html new file mode 100644 index 0000000..5900368 --- /dev/null +++ b/web/templates/partials/meals-tab.html @@ -0,0 +1,6 @@ +{{define "meals-tab"}} +<div class="space-y-6"> + <!-- PlanToEat Meals Section --> + {{template "plantoeat-meals" .}} +</div> +{{end}} diff --git a/web/templates/partials/planning-tab.html b/web/templates/partials/planning-tab.html new file mode 100644 index 0000000..e538578 --- /dev/null +++ b/web/templates/partials/planning-tab.html @@ -0,0 +1,6 @@ +{{define "planning-tab"}} +<div class="space-y-6"> + <!-- Trello Boards Section --> + {{template "trello-boards" .}} +</div> +{{end}} diff --git a/web/templates/partials/tasks-tab.html b/web/templates/partials/tasks-tab.html index 5678193..f528553 100644 --- a/web/templates/partials/tasks-tab.html +++ b/web/templates/partials/tasks-tab.html @@ -1,22 +1,71 @@ {{define "tasks-tab"}} -<div class="space-y-10"> - <!-- Error Messages --> - {{template "error-banner" .}} +<div class="space-y-6"> + <!-- Unified Tasks Section --> + <section> + <h2 class="section-header mb-6"> + <span class="section-accent bg-gradient-to-r from-red-500 to-blue-500"></span> + Upcoming Tasks + </h2> - <!-- Trello Boards Section --> - {{template "trello-boards" .}} + {{if .Atoms}} + <div class="space-y-4"> + {{range .Atoms}} + <div class="bg-white rounded-lg p-6 shadow-sm hover:shadow-md transition-shadow border-l-4 {{.ColorClass}}"> + <div class="flex items-start justify-between"> + <div class="flex-1"> + <div class="flex items-center gap-3 mb-2"> + <span class="text-2xl">{{.SourceIcon}}</span> + <h3 class="text-lg font-semibold text-gray-900">{{.Title}}</h3> + </div> - <!-- Todoist + PlanToEat Grid --> - <div class="grid grid-cols-1 lg:grid-cols-3 gap-6"> - <!-- Todoist (2 cols) --> - <div class="lg:col-span-2"> - {{template "todoist-tasks" .}} - </div> + {{if .Description}} + <p class="text-sm text-gray-600 mb-3">{{.Description}}</p> + {{end}} + + <div class="flex items-center gap-4 text-sm text-gray-500"> + {{if .DueDate}} + <div class="flex items-center gap-1"> + <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> + <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"></path> + </svg> + <span>{{.DueDate.Format "Jan 2, 3:04 PM"}}</span> + </div> + {{end}} + + <div class="flex items-center gap-1"> + <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> + <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z"></path> + </svg> + <span>{{if eq .Source "trello"}}Trello{{else if eq .Source "todoist"}}Todoist{{else}}{{.Source}}{{end}}</span> + </div> - <!-- PlanToEat (1 col) --> - <div> - {{template "plantoeat-meals" .}} + {{if gt .Priority 2}} + <div class="flex items-center gap-1 text-red-600 font-medium"> + <svg class="w-4 h-4" fill="currentColor" viewBox="0 0 20 20"> + <path d="M10 2a8 8 0 100 16 8 8 0 000-16zm1 11H9v-2h2v2zm0-4H9V5h2v4z"></path> + </svg> + <span>Priority {{.Priority}}</span> + </div> + {{end}} + </div> + </div> + + {{if .URL}} + <a href="{{.URL}}" target="_blank" class="ml-4 text-primary-600 hover:text-primary-800 transition-colors"> + <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> + <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"></path> + </svg> + </a> + {{end}} + </div> + </div> + {{end}} + </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> </div> - </div> + {{end}} + </section> </div> {{end}} |
