feat: eisenhower matrix view

This commit is contained in:
2026-03-01 22:22:19 +00:00
parent d792f89fe6
commit 8499c99721
5 changed files with 358 additions and 0 deletions

View File

@@ -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)

76
routers/eisenhower.py Normal file
View File

@@ -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",
})

View File

@@ -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; }
}

View File

@@ -64,6 +64,10 @@
<svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="4" width="18" height="18" rx="2" ry="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/><rect x="7" y="14" width="3" height="3" rx="0.5"/><rect x="14" y="14" width="3" height="3" rx="0.5"/></svg>
Calendar
</a>
<a href="/eisenhower" class="nav-item {{ 'active' if active_nav == 'eisenhower' }}">
<svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="8" height="8" rx="1"/><rect x="13" y="3" width="8" height="8" rx="1"/><rect x="3" y="13" width="8" height="8" rx="1"/><rect x="13" y="13" width="8" height="8" rx="1"/></svg>
Eisenhower
</a>
<a href="/decisions" class="nav-item {{ 'active' if active_nav == 'decisions' }}">
<svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 22c5.523 0 10-4.477 10-10S17.523 2 12 2 2 6.477 2 12s4.477 10 10 10z"/><path d="M9 12l2 2 4-4"/></svg>
Decisions

129
templates/eisenhower.html Normal file
View File

@@ -0,0 +1,129 @@
{% extends "base.html" %}
{% block content %}
<div class="page-header">
<h1>Eisenhower Matrix</h1>
<span class="text-muted">{{ total }} open tasks classified by priority &amp; urgency</span>
</div>
<div class="eisenhower-grid">
<!-- Axis labels -->
<div class="eisenhower-y-label">
<span>Important</span>
</div>
<!-- Q1: Urgent + Important -->
<div class="eisenhower-quadrant eisenhower-q1">
<div class="eisenhower-quadrant-header">
<h3>Do First</h3>
<span class="eisenhower-quadrant-subtitle">Urgent &amp; Important</span>
<span class="badge badge-red">{{ counts.do_first }}</span>
</div>
<div class="eisenhower-task-list">
{% for task in quadrants.do_first %}
<a href="/tasks/{{ task.id }}" class="eisenhower-task">
<span class="priority-dot priority-{{ task.priority }}"></span>
<span class="eisenhower-task-title">{{ task.title }}</span>
{% if task.due_date %}
<span class="eisenhower-task-due {% if task.due_date < today %}overdue{% endif %}">
{{ task.due_date.strftime('%b %-d') }}
</span>
{% endif %}
{% if task.project_name %}
<span class="eisenhower-task-project">{{ task.project_name }}</span>
{% endif %}
</a>
{% else %}
<div class="eisenhower-empty">No tasks</div>
{% endfor %}
</div>
</div>
<!-- Q2: Not Urgent + Important -->
<div class="eisenhower-quadrant eisenhower-q2">
<div class="eisenhower-quadrant-header">
<h3>Schedule</h3>
<span class="eisenhower-quadrant-subtitle">Not Urgent &amp; Important</span>
<span class="badge badge-accent">{{ counts.schedule }}</span>
</div>
<div class="eisenhower-task-list">
{% for task in quadrants.schedule %}
<a href="/tasks/{{ task.id }}" class="eisenhower-task">
<span class="priority-dot priority-{{ task.priority }}"></span>
<span class="eisenhower-task-title">{{ task.title }}</span>
{% if task.due_date %}
<span class="eisenhower-task-due">{{ task.due_date.strftime('%b %-d') }}</span>
{% endif %}
{% if task.project_name %}
<span class="eisenhower-task-project">{{ task.project_name }}</span>
{% endif %}
</a>
{% else %}
<div class="eisenhower-empty">No tasks</div>
{% endfor %}
</div>
</div>
<!-- Second row y-label -->
<div class="eisenhower-y-label">
<span>Not Important</span>
</div>
<!-- Q3: Urgent + Not Important -->
<div class="eisenhower-quadrant eisenhower-q3">
<div class="eisenhower-quadrant-header">
<h3>Delegate</h3>
<span class="eisenhower-quadrant-subtitle">Urgent &amp; Not Important</span>
<span class="badge badge-amber">{{ counts.delegate }}</span>
</div>
<div class="eisenhower-task-list">
{% for task in quadrants.delegate %}
<a href="/tasks/{{ task.id }}" class="eisenhower-task">
<span class="priority-dot priority-{{ task.priority }}"></span>
<span class="eisenhower-task-title">{{ task.title }}</span>
{% if task.due_date %}
<span class="eisenhower-task-due {% if task.due_date < today %}overdue{% endif %}">
{{ task.due_date.strftime('%b %-d') }}
</span>
{% endif %}
{% if task.project_name %}
<span class="eisenhower-task-project">{{ task.project_name }}</span>
{% endif %}
</a>
{% else %}
<div class="eisenhower-empty">No tasks</div>
{% endfor %}
</div>
</div>
<!-- Q4: Not Urgent + Not Important -->
<div class="eisenhower-quadrant eisenhower-q4">
<div class="eisenhower-quadrant-header">
<h3>Eliminate</h3>
<span class="eisenhower-quadrant-subtitle">Not Urgent &amp; Not Important</span>
<span class="badge badge-muted">{{ counts.eliminate }}</span>
</div>
<div class="eisenhower-task-list">
{% for task in quadrants.eliminate %}
<a href="/tasks/{{ task.id }}" class="eisenhower-task">
<span class="priority-dot priority-{{ task.priority }}"></span>
<span class="eisenhower-task-title">{{ task.title }}</span>
{% if task.due_date %}
<span class="eisenhower-task-due">{{ task.due_date.strftime('%b %-d') }}</span>
{% endif %}
{% if task.project_name %}
<span class="eisenhower-task-project">{{ task.project_name }}</span>
{% endif %}
</a>
{% else %}
<div class="eisenhower-empty">No tasks</div>
{% endfor %}
</div>
</div>
<!-- X-axis labels -->
<div class="eisenhower-x-spacer"></div>
<div class="eisenhower-x-label">Urgent</div>
<div class="eisenhower-x-label">Not Urgent</div>
</div>
{% endblock %}