From 8499c99721cb19d28b45a62d3cca083b9c35f0b8 Mon Sep 17 00:00:00 2001 From: M Dombaugh Date: Sun, 1 Mar 2026 22:22:19 +0000 Subject: [PATCH] feat: eisenhower matrix view --- main.py | 2 + routers/eisenhower.py | 76 ++++++++++++++++++++ static/style.css | 147 ++++++++++++++++++++++++++++++++++++++ templates/base.html | 4 ++ templates/eisenhower.html | 129 +++++++++++++++++++++++++++++++++ 5 files changed, 358 insertions(+) create mode 100644 routers/eisenhower.py create mode 100644 templates/eisenhower.html diff --git a/main.py b/main.py index 50e4b17..66cb615 100644 --- a/main.py +++ b/main.py @@ -42,6 +42,7 @@ from routers import ( processes as processes_router, calendar as calendar_router, time_budgets as time_budgets_router, + eisenhower as eisenhower_router, ) @@ -199,3 +200,4 @@ app.include_router(time_tracking_router.router) app.include_router(processes_router.router) app.include_router(calendar_router.router) app.include_router(time_budgets_router.router) +app.include_router(eisenhower_router.router) diff --git a/routers/eisenhower.py b/routers/eisenhower.py new file mode 100644 index 0000000..45dbf96 --- /dev/null +++ b/routers/eisenhower.py @@ -0,0 +1,76 @@ +"""Eisenhower Matrix: read-only 2x2 priority/urgency grid of open tasks.""" + +from fastapi import APIRouter, Request, Depends +from fastapi.templating import Jinja2Templates +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy import text + +from core.database import get_db +from core.sidebar import get_sidebar_data + +router = APIRouter(prefix="/eisenhower", tags=["eisenhower"]) +templates = Jinja2Templates(directory="templates") + + +@router.get("/") +async def eisenhower_matrix( + request: Request, + db: AsyncSession = Depends(get_db), +): + sidebar = await get_sidebar_data(db) + + result = await db.execute(text(""" + SELECT t.id, t.title, t.priority, t.status, t.due_date, + t.context, t.estimated_minutes, + p.name as project_name, + d.name as domain_name, d.color as domain_color + FROM tasks t + LEFT JOIN projects p ON t.project_id = p.id + LEFT JOIN domains d ON t.domain_id = d.id + WHERE t.is_deleted = false + AND t.status IN ('open', 'in_progress', 'blocked') + ORDER BY t.priority, t.due_date NULLS LAST, t.title + """)) + tasks = [dict(r._mapping) for r in result] + + # Classify into quadrants + from datetime import date, timedelta + today = date.today() + urgent_cutoff = today + timedelta(days=7) + + quadrants = { + "do_first": [], # Urgent + Important + "schedule": [], # Not Urgent + Important + "delegate": [], # Urgent + Not Important + "eliminate": [], # Not Urgent + Not Important + } + + for t in tasks: + important = t["priority"] in (1, 2) + urgent = ( + t["due_date"] is not None + and t["due_date"] <= urgent_cutoff + ) + + if important and urgent: + quadrants["do_first"].append(t) + elif important and not urgent: + quadrants["schedule"].append(t) + elif not important and urgent: + quadrants["delegate"].append(t) + else: + quadrants["eliminate"].append(t) + + counts = {k: len(v) for k, v in quadrants.items()} + total = sum(counts.values()) + + return templates.TemplateResponse("eisenhower.html", { + "request": request, + "sidebar": sidebar, + "quadrants": quadrants, + "counts": counts, + "total": total, + "today": today, + "page_title": "Eisenhower Matrix", + "active_nav": "eisenhower", + }) diff --git a/static/style.css b/static/style.css index 62fc5bc..315241f 100644 --- a/static/style.css +++ b/static/style.css @@ -1437,3 +1437,150 @@ a:hover { color: var(--accent-hover); } .cal-event-time { display: none; } .cal-month-label { font-size: 1rem; min-width: 140px; } } + +/* ---- Eisenhower Matrix ---- */ +.eisenhower-grid { + display: grid; + grid-template-columns: 32px 1fr 1fr; + grid-template-rows: 1fr 1fr auto; + gap: 12px; + min-height: 60vh; +} + +.eisenhower-y-label { + display: flex; + align-items: center; + justify-content: center; + writing-mode: vertical-lr; + transform: rotate(180deg); + font-size: 0.78rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.08em; + color: var(--muted); +} + +.eisenhower-x-spacer { + /* empty cell under the y-labels */ +} + +.eisenhower-x-label { + text-align: center; + font-size: 0.78rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.08em; + color: var(--muted); + padding: 4px 0; +} + +.eisenhower-quadrant { + background: var(--surface); + border: 1px solid var(--border); + border-radius: var(--radius-lg); + padding: 16px; + display: flex; + flex-direction: column; + overflow: hidden; +} + +.eisenhower-q1 { border-top: 3px solid var(--red); } +.eisenhower-q2 { border-top: 3px solid var(--accent); } +.eisenhower-q3 { border-top: 3px solid var(--amber); } +.eisenhower-q4 { border-top: 3px solid var(--muted); } + +.eisenhower-quadrant-header { + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 12px; + flex-wrap: wrap; +} + +.eisenhower-quadrant-header h3 { + margin: 0; + font-size: 0.95rem; + font-weight: 700; +} + +.eisenhower-quadrant-subtitle { + font-size: 0.72rem; + color: var(--muted); +} + +.eisenhower-task-list { + flex: 1; + overflow-y: auto; + display: flex; + flex-direction: column; + gap: 2px; +} + +.eisenhower-task { + display: flex; + align-items: center; + gap: 8px; + padding: 6px 8px; + border-radius: var(--radius); + text-decoration: none; + color: var(--text); + font-size: 0.82rem; + transition: background var(--transition); +} + +.eisenhower-task:hover { + background: var(--surface2); + color: var(--text); +} + +.eisenhower-task-title { + flex: 1; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.eisenhower-task-due { + font-size: 0.72rem; + color: var(--muted); + white-space: nowrap; +} + +.eisenhower-task-due.overdue { + color: var(--red); + font-weight: 600; +} + +.eisenhower-task-project { + font-size: 0.68rem; + color: var(--muted); + background: var(--surface2); + padding: 1px 6px; + border-radius: var(--radius-sm); + white-space: nowrap; +} + +.eisenhower-empty { + color: var(--muted); + font-size: 0.82rem; + padding: 12px; + text-align: center; +} + +.badge-red { background: var(--red); color: #fff; } +.badge-amber { background: var(--amber); color: #fff; } +.badge-accent { background: var(--accent); color: #fff; } +.badge-muted { background: var(--muted); color: #fff; } + +@media (max-width: 768px) { + .eisenhower-grid { + grid-template-columns: 1fr; + grid-template-rows: auto; + min-height: auto; + } + .eisenhower-y-label, + .eisenhower-x-label, + .eisenhower-x-spacer { display: none; } + .eisenhower-quadrant { min-height: 120px; } +} diff --git a/templates/base.html b/templates/base.html index ff1e648..1889a7d 100644 --- a/templates/base.html +++ b/templates/base.html @@ -64,6 +64,10 @@ Calendar + + + Eisenhower + Decisions diff --git a/templates/eisenhower.html b/templates/eisenhower.html new file mode 100644 index 0000000..8c0b195 --- /dev/null +++ b/templates/eisenhower.html @@ -0,0 +1,129 @@ +{% extends "base.html" %} +{% block content %} + + +
+ +
+ Important +
+ + +
+ + +
+
+

Schedule

+ Not Urgent & Important + {{ counts.schedule }} +
+ +
+ + +
+ Not Important +
+ + +
+
+

Delegate

+ Urgent & Not Important + {{ counts.delegate }} +
+ +
+ + +
+
+

Eliminate

+ Not Urgent & Not Important + {{ counts.eliminate }} +
+ +
+ + +
+
Urgent
+
Not Urgent
+
+ +{% endblock %}